Моя модернизация Byndyusoft.Infrastructure | DDD + CQRS + WebApi +12


Всем привет! Я часто ищу в просторах интернета «идеальную архитектуру» и несколько месяцев назад наткнулся на интересную реализацию и хотел бы поделится немного дополнив его.

> Ссылка на реализацию

Немного модернизации и я получил вполне универсальный рабочий шаблон.

Для всех, кто не знаком с DDD можно начать с wiki.

В конце мы получим связку с DDD + CQRS + Entity Framework + OData + WebApi + Log4Net + Castle Windsor + Kendo UI.

Звучит громоздко, но сугубо лично, в результате получаем вполне легко масштабируемую систему.

Конечный результат примерно будет таким
Картинка кликабельная (для полного экрана)

image

Итак начнем…

Создаем папку Domain и Infrastrcutre. В папке Domain создаем 3 проекта (class library):

  1. Domain.Commands
  2. Domain.Database
  3. Domain.Model

В папке Infrastrcuture создаем 4 проекта (class library):

  1. Infrastrcuture.Web
  2. Infrastrcuture.Domain
  3. Infrastrcuture.EntityFramework
  4. Infrastrcuture.Logging

И само веб-приложение (ASP MVC5), назовём его Web (с шаблоном MVC). И последний проект (class library) Web.Application.

А теперь по каждому по подробнее:

CQRS (Command Query Responsibility Segregation)

Немного о Commands and Queries
Queries: Методы возвращают результат, не изменяя состояние объекта. Другими словами у Query не никаких side effects.
Commands: Методы изменяют состояние объекта, не возвращая значение. На самом деле более корректно называть эти методы modifiers или mutators, но так исторически сложилось, что они называются командами.

В проекте Domain.Commands мы будем хранить команды которые будут менять состояние объекта и нашу бизнес-логику.

Это у нас и будет Command. А в качестве Query у нас будет служить OData.

В проекте Command.Database мы будем хранить схему базы данных (я обычно использую PowerDesigner для этого) и Seed-скрипты.

Все сущности храним в проекте Domain.Model.

Теперь папка Infrastrcuture.
Infrastrcuture.Domain — мы храним все доменные helpers, command builders, exceptions, которые нужны будут для Доменной модели.
Infrastrcuture.EntityFramework — это наш ORM.
Infrastrcuture.Logging — логгирование.
Infrastrcuture.Web — веб helpers, extensions, form handlers.

В проекте Web.Application. Создаем базовый класс для считывания (OData):

ReadODataControllerBase.cs
namespace Web.Application
{
    using System.Linq;
    using System.Web.Http.OData;
    using Infrastructure.Domain;
    using Infrastructure.EntityFramework;

    public class ReadODataControllerBase<TEntity> : ODataController
        where TEntity : class, IEntity
    {
        private readonly IRepository<TEntity> _repository;

        public ReadODataControllerBase(IRepository<TEntity> repository)
        {
            _repository = repository;
        }

        public IQueryable<TEntity> Get()
        {
            return _repository.Query();
        }
    }
}


И базовый form контроллер:

FormControllerBase.cs
namespace Web.Application
{
    using System;
    using System.Net;
    using System.Web.Mvc;
    using Castle.Core.Logging;
    using Castle.Windsor;
    using Infrastrcuture.Web.Forms;
    using Infrastructure.Domain.Exceptions;
    using Infrastructure.EntityFramework;
    using Services.Account;
    using Services.Account.Models;

    public class FormControllerBase : Controller, ICurrentUserAccessor
    {
        public JsonResult Form<TForm>(TForm form)
            where TForm : IForm
        {
            var formHanlderFactory = ResolveFormHandlerFactory();
            var unitOfWork = ResolveUnitOfWork();
            var logger = ResolveLogger();

            try
            {
                logger.Info($"Begin request of <{CurrentUser.DisplayNameWithNk}> with form <{ form.GetType().Name }>.");

                formHanlderFactory.Create<TForm>().Execute(form);

                unitOfWork.SaveChanges();

                logger.Info($"Complete request of <{CurrentUser.DisplayNameWithNk}> with form <{ form.GetType().Name }>.");

                return Json(new { form });
            }
            catch (BusinessException be)
            {
                return JsonError(form, be, logger);
            }
            catch (FormHandlerException fhe)
            {
                return JsonError(form, fhe, logger);
            }
            catch (Exception e)
            {
                return JsonError(form, e, logger);
            }
        }

        //Add exception logging
        public FileResult FileForm<TForm>(TForm form)
            where TForm : IFileForm
        {
            var formHanlderFactory = ResolveFormHandlerFactory();

            formHanlderFactory.Create<TForm>().Execute(form);

            return File(form.FileContent, System.Net.Mime.MediaTypeNames.Application.Octet, form.FileName);

        }

        private JsonResult JsonError<TForm>(TForm form, Exception e, ILogger logger)
        {
            logger.Error($"Rollback request of <{CurrentUser.DisplayNameWithNk}> with form <{ form.GetType().Name }>.", e);

            Response.TrySkipIisCustomErrors = true;
            Response.StatusCode = (int)HttpStatusCode.InternalServerError;

            return Json(new
            {
                form,
                exceptionMessage = e.Message
            });
        }

        #region Dependency resolution

        private IFormHandlerFactory ResolveFormHandlerFactory()
        {
            return GetContainer().Resolve<IFormHandlerFactory>();
        }

        private IUnitOfWork ResolveUnitOfWork()
        {
            return GetContainer().Resolve<IUnitOfWork>();
        }

        private ILogger ResolveLogger()
        {
            return GetContainer().Resolve<ILogger>();
        }

        private IWindsorContainer GetContainer()
        {
            var containerAccessor = HttpContext.ApplicationInstance as IContainerAccessor;
            return containerAccessor.Container;
        }

        private ICurrentUserKeeper ResolveCurrentUserKeeper()
        {
            return GetContainer().Resolve<ICurrentUserKeeper>();
        }

        #endregion

        #region CurrentUserAccessor Memebers

        public ApplicationUser CurrentUser
        {
            get
            {
                var currentUserKeeper = ResolveCurrentUserKeeper();
                return currentUserKeeper.GetCurrentUser();
            }
        }

        #endregion
    }
}


В результате для считывания данных с базы мы просто создаем класс и наследуем его от класса ReadODataController и просто переходим на localhost:12345/odata/Stations. Весь запрос вместо нас пишет OData:

StationsController.cs

namespace Web.Application.Station
{
    using Domain.Model.Station;
    using Infrastructure.EntityFramework;

    public class StationsController : ReadODataControllerBase<Station>
    {
        public StationsController(IRepository<Station> repository) : base(repository)
        {
        }
    }
}


ODataConfig.cs

ODataConfig.cs

namespace Web
{
    using System.Linq;
    using System.Web.Http;
    using System.Web.Http.OData.Builder;
    using System.Web.Http.OData.Extensions;
    using Domain.Model.Station;
    using Infrastrcuture.Web.Extensions;
    using Microsoft.Data.Edm;

    public class ODataConfig
    {
        public static void Register(HttpConfiguration config)
        {
            var builder = new ODataConventionModelBuilder();

            config.Routes.MapODataServiceRoute("odata", "odata", GetEdmModel(builder));
        }

        public static IEdmModel GetEdmModel(ODataConventionModelBuilder builder)
        {
            var entityTypes = typeof (Station).Assembly.GetTypes().Where(x => x.IsClass && !x.IsNested);

            var method = builder.GetType().GetMethod("EntitySet");

            foreach (var entityType in entityTypes)
            {
                var genericMethod = method.MakeGenericMethod(entityType);

                genericMethod.Invoke(builder, new object[]
                {
                    entityType.Name.Pluralize()
                });
            }

            return builder.GetEdmModel();
        }
    }
}


Данный шаблон уже протестирован и сейчас один из наших проектов в продакшне нормально, без каких либо проблем работает.

Ссылка на проект: NTemplate
-->


К сожалению, не доступен сервер mySQL