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

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

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

Для всех, кто не знаком с 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
Поделиться с друзьями
-->

Комментарии (6)


  1. SergeyVoyteshonok
    27.04.2017 14:12
    +5

    Несколько вопросов:

    1. Объясните смысл конструкции:

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

    Все три кэтча ведут в одно место, причем последний обобщающий, зачем?

    2. Зачем оборачивать DbContext в IUnitOfWork, он ведь и так им является, и он не привязан к конкретной реализации БД, абстракция ради абстракции?

    3. (это уже моя вкусовщина) Не думали про логирование в аспектом стиле, чтобы не загружать код логированием?

    4. Зачем использовать шаблон MVC, если приложение WebAPI?

    5. Почему не используете для клиентского кода grunt/gulp/webpack?


    1. LiveToWin
      28.04.2017 15:35

      1. Проект не идеален, но все еще в процессе модернизации, но пока не могу вспомнить, почему именно так написал.
      2. Да, действительно, абстракция ради абстракции =) Честно, пока еще разбираюсь в этом и сам иногда задавался вопросом «Почему?».
      3. Спасибо за Вашу вкусовщину. Пойду по читаю про «логгирование в аспектном стиле». Звучит интересно.
      4. Шаблон, правильнее будет, не я выбирал. Я просто чуточку модернизировал то, что автор этой архитектуры создал.
      5. Про клиентскую часть вообще пока не задумывался, так как этим постом я хотел показать интересную реализацию архитектуры, которая меня удивила.

      Спасибо большое за замечания. В свободное время буду дополнять, допиливать и может и получится «Идеальная архитекура» для проектов средней сложности


      1. SergeyVoyteshonok
        28.04.2017 15:46

        Вот простой пример про логирование


  1. oxidmod
    27.04.2017 19:40
    +3

    Почему доменные ексепшены в инфраструктуре? ОО
    В моем понимании внутри домена леат интерфейсы для всех необходимых домену внешних сервисов. Объекты-значения и агрегаты. Доменные события и ексепшены. Ну и самое главное, доменные сервисы, оперирующие этими самыми агрегатами интерфейсами.
    Реализации же лежат в слое приложения/инфраструктуры


    1. LiveToWin
      28.04.2017 15:42

      Это не только доменные ексепшны, а общие, Business Exceptions, которая пригодится еще в дальнейшем, к примеру, в других сервисах.


      1. oxidmod
        28.04.2017 18:18
        +1

        Имхо, Business Exceptions не длолжны быть в инфраструктуре. Для этого есть Shared Kernel