Back-end на основе Microsoft Azure

в 10:59, , рубрики: .net, Microsoft Azure, mobile services, server-side

Хочу рассказать ещё об одной теме, по которой материалов пока что чрезвычайно мало. Речь пойдёт о разработке back-end'а на основе Microsoft Azure Mobile Services. Хотя вводных статей на эту тему немало, легко заметить, что традиционный пример с TodoItems (которым подавляющее большинство введений ограничиваются) содержит потенциальные проблемы для большого проекта.

Самый главный минус демонстрационного проекта заложен в особенностях EntityDomainManager, который вынуждает отправлять через JSON те же классы, что используются в ORM (допустим, используем Entity Framework). Во-первых, сериализуемый класс должен наследоваться от EntityData, получается, что в базе данных оказываются не всегда нужные и удобные поля (например, он идентифицируется строкой, но хорошо ли строить индексы всегда на строках?). Во-вторых, EF располагает к наследованию класса только для схемы code first, не предусматривающей в текущей версии mapping'а на хранимые процедуры (вновь вопрос о быстродействии БД). И, в конце концов, а где тогда слой логики? Ведь структура БД не обязательно тождественна внешнему интерфейсу.

По этим причинам рассмотрим другой метод. Оговорю также тот факт, что введения в основы здесь не будет, предполагается, что краткое введение читателю уже известно.

Итак, для преодоления указанных выше недостатков заменяем EntityDomainManager на MappedEntityDomainManager. Последний ориентирован на использование модуля AutoMapper, который легко найти через NuGet, к тому ж этот модуль входит в заготовку проекта Azure Mobile Services. Использовать его функции надо будет в файле WebApiConfig.cs.

Для примера преобразуем всё тот ж проект TodoItems и будем полагать, что работа с БД осуществляется через EF на основе model first. Соответствующая ORM-модель позволяет отображать классы сущностей на представления и хранимые процедуры, делать вызовы хранимых процедур. При работе с EF даже простая выборка данных через хранимые процедуры происходит минимум в два раза быстрее при прочих равных. Рассмотрение особенностей БД и EF выходит за рамки этого описания Micrsoft Azure, в данном случае будет использоваться созданный в проекте контекст, но который будет заменяемым.

Обратим внимание на это факт, что класс MappedEntityDomainManager в отличие от EntityDomainManager является абстрактным, поэтому нет возможности просто заменить строку:

DomainManager = new EntityDomainManager<TodoItem>(context, Request, Services);

на создание экземпляра класса MappedEntityDomainManager. Поэтому создадим класс TodoItemsDomainManager. Но перед этим обозначим класс преобразования данных (или класс сериализации) названием TodoItemDTO во избежание путаницы классов различных слоёв приложения. Другим generic-параметром наследуемого класса будет класс mapping'а БД, в котором для идентификации будем использовать числовое значение, как оптимальный вариант для индексов БД. В итоге класс будет выглядеть следующим образом:

    public class TodoItemsDomainManager : MappedEntityDomainManager<TodoItemDTO, TodoItemDA>
    {
        private readonly AzureBackendDemoContext _context;

        public TodoItemsDomainManager(AzureBackendDemoContext context, HttpRequestMessage request, ApiServices services)
            : base(context, request, services)
        {
            _context = context;
        }

        public override System.Threading.Tasks.Task<bool> DeleteAsync(string id)
        {
            return base.DeleteItemAsync(id);
        }

        public override System.Web.Http.SingleResult<TodoItemDTO> Lookup(string id)
        {
            return base.LookupEntity(c => c.Id.ToString() == id);
        }

        public override System.Threading.Tasks.Task<TodoItemDTO> UpdateAsync(string id, System.Web.Http.OData.Delta<TodoItemDTO> patch)
        {
            return base.UpdateEntityAsync(patch, id);
        }
    }

Здесь и далее приведён простейший пример реализации, но, в отличие от исходного варианта, он гораздо более приспособлен к дальнейшему развитию. Остаётся такой момент, что TodoItemsDomainManager пока не знает, как сопоставлять классы TodoItemDTO и TodoItemDA. Поэтому находим класс WebApiConfig и добавляем в метод Register строки:

            Mapper.Initialize(cfg =>
            {
                cfg.CreateMap<TodoItemDA, TodoItemDTO>().
                    ForMember(dst => dst.Id, map => map.MapFrom(c => c.Id.ToString()))
                    .ForMember(dst => dst.Text, map => map.MapFrom(c => c.Text))
                    .ForMember(dst => dst.Complete, map => map.MapFrom(c => c.Complete));

                cfg.CreateMap<TodoItemDTO, TodoItemDA>().
                    ForMember(dst => dst.Id, map => map.MapFrom(c => int.Parse(c.Id)))
                    .ForMember(dst => dst.Text, map => map.MapFrom(c => c.Text))
                    .ForMember(dst => dst.Complete, map => map.MapFrom(c => c.Complete));                
            });

Обратите внимание, что отображение классов не обязательно выполняется один к одному. Подробности использования AutoMapper здесь рассматриваться не будут, поскольку на эту тему материалов немало.

Теперь заменим в контроллере таблицы строку создания диспетчера доменов:

DomainManager = new TodoItemsDomainManager(context, Request, Services);

Также в классе контекста и всех связанных с ним классах требуется заменить TodoItemDTO на TodoItemDA и зарегистрировать этот класс в внутри метода OnModelCreating:

modelBuilder.Entity<TodoItemDA>();

Сделаю небольшую оговорку — поскольку в этом примере не создавалась реальная модель данных, был использован исходный класс AzureBackendDemoInitializer, наследуемый от DropCreateDatabaseIfModelChanges. В реальном проекте, который подключается к базе данных через строку соединения, рекомендую реализовывать интерфейс IDatabaseInitializer, который в этом случае выглядел б так:

    public class DatabaseModelInitializer : IDatabaseInitializer<SomeDatabaseContext>
    {
        public void InitializeDatabase(SomeDatabaseContext context)
        {
            context.Set<TodoItemDA>().Load();
        }
    }

Запустим проект с исходным примером тестовых данных и добавим к адресу строку /tables/TodoItem. В итоге увидим результат запроса:

[{"id":"1","complete":false,"text":"First item"},{"id":"2","complete":false,"text":"Second item"}]

В качестве небольшого дополнения приведу пример front-end'а к проекту. Для упрощения примера будет использована версия под Windows на основе WPF. Но реализация возможна также для мобильных устройств на операционных системах Android, iOS, Windows Phone и Windows RT. При этом WPF-проект не считается совместимым с Azure Mobile Services, хотя в реальности совместимость присутствует, поэтому в консоли NuGet введём:

Install-Package WindowsAzure.MobileServices

Затем просто добавим эти ссылки в проект. Создадим класс для получения данных. Для запроса по имени класса он должен будет называться TodoItem, тогда обращение будет к соответствующему контроллеру. Но такое название может относиться и к модели данных, и к классу локальной сериализации для выгрузки данных и т. п., поэтому применим DataTableAttribute и объявим класс так:

    [DataTable("TodoItem")]
    public class TodoItemDTO
    {
        [CreatedAt]
        public DateTimeOffset? CreatedAt { get; set; }

        public string Id { get; set; }

        [UpdatedAt]
        public DateTimeOffset? UpdatedAt { get; set; }

        [Version]
        public byte[] Version { get; set; }

        public string Text { get; set; }

        public bool Complete { get; set; }
    }

Тогда строки запроса будут следующими:

        public async Task Load()
        {
            MobileServiceClient client = new MobileServiceClient("http://localhost:1146/");
            try
            {
                IEnumerable<TodoItemDTO> items = await client.GetTable<TodoItemDTO>().ToEnumerableAsync();
                _items = items.Select(slo => new TodoItemModel
                {
                    Complete = slo.Complete,
                    Text = slo.Text
                }).ToList();
            }
            catch (MobileServiceInvalidOperationException ex)
            {
                OnGotError();
            }
            catch (TaskCanceledException ex)
            {
                OnGotError();
            }
        }

Здесь не был рассмотрен слой серверной логики, но само использование наследования от абстрактного класса указывает на возможность вызова в соответствующих методах необходимых логических объектов. Для примера было показано, как заменить строковый идентификатор на числовой. Не будем также забывать про ApiController.

Естественно, само название говорит о назначении back-end'а не для desktop, а для мобильных устройств, но эти же строки можно выполнять в PCL для упомянутых выше операционных систем. На этом пример завершаю, для большей понятности что к чему хочу предоставить файл исходных кодов примера (поправьте меня, если его можно предоставить как-то лучше), ибо я сам не люблю tutorial'ы без полных исходных кодов примера.

Автор: Ilya81

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js