REST API macOS. Server Application.

В статье коротко описано клиент/серверное приложение, реализующее REST API на macOS 10.14.2 (Mojave). Серверная часть выполнена средствами .NET Core для Ubuntu и содержит SQL Server. Но почему выбран инструментарий от Microsoft? По двум причинам. Во первых, интересно посмотреть, как он выглядит на macOS, во вторых, на LINQ любой запрос к базе данных можно реализовать в несколько строк.
Итак, подробнее. Для разработки программ на сервере применяется язык C#. LINQ и Entity Framework использованы для создания базы данных и формирования запросов к ней. В качестве контейнера использован Docker. Для разработки кода на C# установлена среда Visual Studio Code. Для формирования запросов к Web Server на этапе отладки в качестве клиента использован Postman. Для визуального управления базой данных применяется SQL Operation Studio. Статья не содержит описания установки перечисленных программных средств, это хорошо сделано здесь.
Пример сервера приложения можно свободно загрузить. Для запуска проекта достаточно распаковать архив и запустить его в VS Code.
Цель проекта состояла в том, чтобы оценить трудоемкость реализации REST API на клиентской части приложения под macOS. Клиент разработан в XCode на языке Swift 4.2. Описание и тексты будут опубликованы во второй части статьи.
1. Структура базы данных ExampleForMac.
Структура базы данных выбрана простой и содержит пару таблиц c именами: Employees и Jobs.

Обратите внимание на поле Photo и его тип, оно содержит фотографии сотрудников. А это поля таблицы Jobs:

Таблицы Employees и Jobs связаны по полям EmployeID и AssignedToEmployeId соответственно, по типу "один-ко-многим". Создание базы данных ExampleForMac, указанных выше таблиц, записей в них, а так-же установка связей между записями выполняется в файле Startup.cs

2. Формирование запросов к Web серверу Kestrel.
Вот так выглядит сервер приложения, запущенный в VS Code.

Исходный текст файла WebServerController.cs
WebServerController.cs

using System;

using Microsoft.AspNetCore.Mvc;

using System.Collections.Generic;

using System.Linq;

using ExampleWebServer.Models;


/*

Раскомментировать для загрузки файла в режиме form-data в Postman

using System.Threading.Tasks;

using System.IO;

using Microsoft.AspNetCore.Http;

*/


namespace ExampleWebServer.Controllers

{

    [Route(template: "/")]

    [ApiController]


    public class WebServerController : ControllerBase

    {

        private readonly ExampleWebServerContext _context;


         // Конструктор класса WebServerController

        public WebServerController(ExampleWebServerContext  context)

        {

            _context = context;    

            Console.WriteLine("Конструктор WebServerController .......");

        }

       

        /* 

         https://localhost:5001/job/Int

         Get запрос для таблицы Job, поиск по полю JobId

         Запрос выводит поля одной записи Job и AssignedTo для EmployeID

        */

        [HttpGet("job/{JobId:int}", Name = "GetJobId")]

        public ActionResult<Job> GetByJobId(int JobId)

        {

            Console.WriteLine("Запрос для таблицы Job по полю JobId " + JobId.ToString()); 

            

            var query = from j in _context.Jobs

                    join e in _context.Employees on   j.AssignedTo.EmployeId equals e.EmployeId 

                    where (j.JobId == JobId)

                    select new { 

                        jobId       = j.JobId,

                        titleJob    = j.TitleJob,

                        dueDate     = j.DueDate,

                        isComplete  = j.IsComplete,

                        assignedTo  = e.EmployeId

                    }; 

            if (query != null) return Ok(query.First()); else  return NotFound();

        }


        /* 

        https://localhost:5001/employe/Int

        Get запрос для таблицы Employe по полю EmployeID

        Запрос выводит одну запись Employe и связанные записи в виде массива Job 

        */

        [HttpGet("employe/{EmployeId:int}", Name = "GetEmployeId")]

        public ActionResult<Employe> GetByEmployeId(int EmployeId)

        {

            Console.WriteLine("Запрос для таблицы Employe по полю EmployeID " + EmployeId.ToString());


            var query = from e in _context.Employees

                    where (e.EmployeId == EmployeId)

                    select new { 

                    e.EmployeId, e.FirstName, e.SecondName, e.LastName, e.Phone, e.Photo, e.Jobs};

            if (query == null) {return NotFound();}

            return Ok(query.First());

        }


        /* 

        https://localhost:5001/job/all

        // Get запрос для всех записей таблицы Job  

        // Запрос выводит массив записей Job и EmployeId для Employe 

        */  

        [HttpGet("job/all", Name= "GetJobAll")]

        public ActionResult GetAllJob()

        {

            Console.WriteLine("Все записи из таблицы Job ..........");

            var query = from j in _context.Jobs

                    join e in _context.Employees on   j.AssignedTo.EmployeId equals e.EmployeId 

                    select new {

                        jobId       = j.JobId,

                        titleJob    = j.TitleJob,

                        dueDate     = j.DueDate,

                        isComplete  = j.IsComplete,

                        assignedTo  = e.EmployeId

                    };

            if (query != null) return Ok(query); else  return NotFound();

        }


        /* 

        https://localhost:5001/employe/all

        Get запрос для всех записей таблицы Employe со связанными Jobs 

        Запрос выводит все записи таблицы Employe и связанные с ними Job

        */  

        [HttpGet("employe/all", Name = "GetEmployeAll")]

        public ActionResult<Employe> GetAllEmploye()

        {

            Console.WriteLine("Все записи из таблицы Employe ..........");

            var query = from e in _context.Employees

                    select new { 

                    e.EmployeId, e.FirstName, e.SecondName, e.LastName, e.Phone, e.Photo, e.Jobs};

            if (query == null) {return NotFound();}

            return Ok(query);

        }

        


        /* 

        https://localhost:5001/all 

        Get запрос для всех записей из таблиц Employe + Job.

        Запрос выводит все записи Employe и присоединеные к ним поля Job

        в виде единого массива. Форма выода отличается от https://localhost:5001/employe/all

        */

        [HttpGet("all")]

        public ActionResult<Employe> GetAll()

        {

            Console.WriteLine("Все записи из таблиц Employe и Job ......");

            var query = from j in _context.Jobs

                    join e in _context.Employees on j.AssignedTo.EmployeId equals e.EmployeId 

                    select new {e.EmployeId,e.FirstName,e.SecondName,e.LastName, e.Photo, e.Phone,

                    j.JobId,j.TitleJob, j.DueDate, j.IsComplete};                

            if (query != null) return Ok(query); else  return NotFound();

        }

        


        /* 

        https://localhost:5001/?firstname=иван&secondname=иваныч&lastname=иванов

        Get запрос для таблицы Employe по параметрам: FirstName, SecondName, LastName.

        Запрос выводит массив записей Employe и массив связанных с записями Job

        */

        [HttpGet]

        public ActionResult<Employe> Get([FromQuery(Name = "FirstName")]    string FirstName,

                                         [FromQuery(Name = "SecondName")]   string SecondName,

                                         [FromQuery(Name = "Lastname")]     string LastName)

        {    

            Console.WriteLine("Get запрос Employe по параметрам: " + "FirstName: " + FirstName + 

            " SecondName: " +  SecondName + " LastName: " + LastName );


            bool firstParam     = string.IsNullOrEmpty(FirstName);

            bool secondParam    = string.IsNullOrEmpty(SecondName);

            bool lastParam      = string.IsNullOrEmpty(LastName);


            // Если все три параметра не равны null or empty

            if (!firstParam  && !secondParam  && !lastParam )

                {

                var query = from t in _context.Employees

                    where (t.FirstName.Equals(FirstName) && t.LastName.Equals(LastName) && t.SecondName.Equals(SecondName))

                    select new {t.EmployeId, t.FirstName, t.SecondName, t.LastName, t.Phone, t.Photo, t.Jobs};

                if ((query == null) || (query.Count() == 0 )) {

                    Console.WriteLine("Get запрос по парметрам равен null");

                    return NotFound(); 

                    }

                return Ok(query);         

                }

            else

            {

                Console.WriteLine("Get запрос по парметрам: не все параметры определны");

                return NotFound();

            }         

        }



        /* 

        Post запрос: https://localhost:5001/employe

        Добавление новой записи в таблицу Employe. 

        Значение Job = null

        {

            "firstName": "Матвей",

            "secondName": "Алексеевич",

            "lastName": "Шмидт",

            "phone": "921-215-45-72",

            "photo": "Байтовый массив",

            "jobs": null

        }  

        */       

        [HttpPost("employe/")]

        public IActionResult CreateEmploye(Employe item)

        {

            Console.WriteLine("Employe добавление записи ........" + item.ToString());

            _context.Employees.Add(item);

            _context.SaveChanges();

            // Выводит последнюю созданную запись в таблице Employe

            return CreatedAtRoute("GetEmployeId", new { EmployeId = item.EmployeId }, item);

        }



        /* 

        https://localhost:5001/job/Int

        Post запрос - добавление новой записи в таблицу Job и привязка ее к Employe

        int EmployeId -  к какой записи привязывать, Job item - содержимое записи Job.

        Запрос выводит объединеный массив записей EmployeId  + Job  для выбранного EmployeId 

        */

        [HttpPost("job/{EmployeId:int}")]

        public IActionResult CreateJob(int EmployeId, Job item )

        {

            Console.WriteLine("Job добавление записи ........" + item.ToString());

            // Найдем запись в таблице Employe

            var record = _context.Employees.Find(EmployeId);

            if (record== null){return NotFound();}

            // Установим связь

            item.AssignedTo = record;

            // Добавим и сохраним

            _context.Jobs.Add(item);

            _context.SaveChanges();  


            // Сфоромируем запрос по обоим таблицам - какие привязки есть у EmployeId

            var query = from j in _context.Jobs

                    join e in _context.Employees on   j.AssignedTo.EmployeId equals e.EmployeId 

                    where (e.EmployeId == EmployeId)

                    select new {e.EmployeId,e.FirstName,e.SecondName,e.LastName,e.Photo,e.Phone,

                    j.JobId,j.TitleJob, j.DueDate}; 


             if (query != null) return Ok(query); else  return NotFound();

        }

  

        /* 

        PUT запрос: https://localhost:5001/employe/Int

        Обновление записи в таблице Employe  по номеру EmployeId 

        Employe item - содержимое для обновления

        При запросе: https://localhost:5001/employe/1

        и содержимом:

        {

            "firstName": "Владимир",

            "secondName": "Алексеевич",

            "lastName": "Petrovs",

            "phone": "921-215-45-72",

            "jobs": [{

                "titleJob": "Job 1234",

                "dueDate": "2018-09-12T14:22:54.485596",

                "isComplete": true

                }]

        }

        обновляется первая запись в таблице Employe, добавляется запись в Job

        и привязывается к первой записив Employee. Если job = null записи не

        добавляются.

        Выводится обновленная запись и массив присоединенных Job

        */ 

        [HttpPut("employe/{EmployeId:int}")]

        public IActionResult UpdateEmploye(int EmployeId, Employe item)

        {

            Console.WriteLine("Обновление записи в таблице Employe ..........");

           // Найдем запись

            var record = _context.Employees.Find(EmployeId);

            if (record == null){return NotFound();}

            

            // Обновим переменные

            record.FirstName  = item.FirstName;

            record.SecondName = item.SecondName;

            record.LastName   = item.LastName;

            record.Phone      = item.Phone;

            record.Photo      = item.Photo;

           // record.Jobs       = item.Jobs;  // раскомментировать для массива


            // Сохраним

            _context.Employees.Update(record);

            _context.SaveChanges(); 


            /* 

             Раскомментировать для массива   

            // Вывод массива Employe + Job

            var query = from j in _context.Jobs

                    join e in _context.Employees on   j.AssignedTo.EmployeId equals e.EmployeId 

                    where (e.EmployeId == EmployeId)

                    select new {e.EmployeId,e.FirstName,e.SecondName,e.LastName,e.Phone,e.Photo,

                    j.JobId,j.TitleJob, j.DueDate, j.IsComplete}; 

            

            if (query  != null) return Ok(query ); else  return NotFound();

            */


            // Выведем одну запись

             var updateRecord = _context.Employees.Find(EmployeId);

             if (updateRecord != null) return Ok(updateRecord); else  return NotFound();

        }


        /* 

        PUT запрос: https://localhost:5001/job/Int

        Обновление одной записи в таблице Job.

        int JobId - номер обновляемой записи, Job item - обновляемые данные

        Выводится одна запись Job.


        При запросе вида:

        https://localhost:5001/job/1 и содержимом в Postam:

        {

            "titleJob": "Job 10",

            "dueDate": "2018-09-12T14:22:54.485596",

            "isComplete": true,

            "assignedTo": {

                "firstName": "1",

                "secondName": "Иваныч",

                "lastName": "Иванов",

                 "phone": "921-215-45-70"

        } 

        Будет обновлена первая запись в таблице Job и создана новая запись в таблице Employe 

        к которой будет привязана обновленная запись 1.

        */


        [HttpPut("job/{JobId:int}")]

        public IActionResult UpdateJob(int JobId, Job item)

        {

            Console.WriteLine("Обновление записи в таблице Job..........");

           // Найдем запись

            var record = _context.Jobs.Find(JobId);

            if (record == null){return NotFound();}

        

            // Обновим данные

            record.TitleJob     = item.TitleJob;

            record.DueDate      = item.DueDate;

            record.IsComplete   = item.IsComplete;

            record.AssignedTo   = item.AssignedTo;

            

            _context.Jobs.Update(record);

            _context.SaveChanges();

            /* 

            Раскомментировать для создания новой записи Employe и вывода массива.

            var query = from j in _context.Jobs

                    join e in _context.Employees on  j.AssignedTo.EmployeId equals e.EmployeId 

                    select new {e.EmployeId,e.FirstName,e.SecondName,e.LastName,e.Phone,

                    j.JobId,j.TitleJob, j.DueDate}; 

            

            if (query != null) return Ok(query); else  return NotFound();

            */

            var updateRecord = _context.Jobs.Find(JobId);

            if (updateRecord != null) return Ok(updateRecord); else  return NotFound();

        }


        /* 

        Delete запрос: https://localhost:5001/employe/Int

        Удаление Int записи в таблице Employe и обнуление связей в таблице Job

        Выводится удаленная запись.

        Можно вывести все оставшиеся записи - раскомментировать строку

        */

        [HttpDelete("employe/{EmployeId:int}")]

        public IActionResult DeleteEmploye(int EmployeId)

        {

            Console.WriteLine("Удаление записи EmployeId: " + EmployeId.ToString() +  " в Employe ........ ");          

            // Найдем запись в Employe

            var record= _context.Employees.Find(EmployeId);

            // Если такой нет - уйдем

            if (record == null){return NotFound();}

            // Загрузим запись из Job 

            _context.Entry(record).Collection(c => c.Jobs).Load();

            // Удалим Employe, связь в поле Assigned таблицы Job обнулится          

            _context.Employees.Remove(record);

            // Сохраним

            _context.SaveChanges();

           return CreatedAtRoute("GetEmployeId", new { EmployeId = record.EmployeId }, record);

            // var records = _context.Employees;

            // if (records != null) return Ok(records); else  return NotFound();

        }


        /* 

        Delete запрос: https://localhost:5001/job/5

        Удаление, например, пятой записи в таблице Job

        Возвращает удаленную запись.

        */

        [HttpDelete("job/{JobId:int}", Name = "DeleteJobId")]

        public IActionResult DeleteJob(int JobId)

        {

            Console.WriteLine("Удаление записи JobId: " + JobId.ToString() +  " в Job ........ ");          

            // Найдем запись в Employe

            var record= _context.Jobs.Find(JobId);

            // Если такой нет - уйдем

            if (record == null){return NotFound();}    

            // Удалим       

            _context.Jobs.Remove(record);

            // Сохраним

            _context.SaveChanges();

           return Ok(record);

            // var records = _context.Job;

            // if (records != null) return Ok(records); else  return NotFound();

        }


        /*

        Get запрос https://localhost:5001/JobId/EmployeID

        Привязка записи Job к записи в Employee.

        Привязка осуществляется и при создании новой записи в Job.

        Выводится объединенный массив Employe для  EmployeID

         */

        [HttpGet("{JobId:int}/{EmployeId:int}")]

        public IActionResult AssignTo(int JobId, int EmployeID)

        {

           Console.WriteLine("Переназначение записи JobId: " + JobId.ToString() +  " для EmployeID " +  EmployeID.ToString());     

            var jobRecord           = _context.Jobs.Find(JobId);

            var exampleRecord       = _context.Employees.Find(EmployeID);

            jobRecord.AssignedTo    = exampleRecord;


            _context.SaveChanges();

            var query = from j in _context.Jobs

                    join e in _context.Employees on   j.AssignedTo.EmployeId equals e.EmployeId 

                    where (e.EmployeId == EmployeID)

                    select new {e.EmployeId,e.FirstName,e.SecondName,e.LastName,e.Phone,

                    j.JobId,j.TitleJob, j.DueDate}; 


            return Ok(query);

        }


        /* Пример простой загрузки файла на серер в режиме form-data в Postman

        [HttpPost("employe/")]

        public async Task<IActionResult> LoadFile(IFormFile file)

        {        

             using (var sr = new StreamReader(file.OpenReadStream()))

            {

                var content = await sr.ReadToEndAsync();               

                var filePath = Path.GetTempFileName();

                Console.WriteLine( filePath);

                return Ok(content);

            }

        }

        */

    }

}





3. HTTP запросы.
Файл WebServerController.cs содержит болee десятка HTTP запросов различных типов: Get,Put,Post,Delete. Есть пример запроса AssignedTo, который переназначает связи в основной и подчиненной таблицах базы данных. Каждый запрос имеет подробный комментарий в тексте файла.

4. Впечатления от инструментария.
Docker работает стабильно, замедлений и сбоев не замечено.
Комплекс программ для macOS от Microsoft - на данном этапе замечаний нет.
Postman - очень полезная утилита.
SQL Operation Studio - программа находится в стадии развития (версия 0.32.9), но функции свои выполняет.

Всего доброго, Евгений Вересов.
10.01.2019 года.