[Из песочницы] Моя модернизация Byndyusoft.Infrastructure | DDD + CQRS + WebApi
→ Ссылка на реализацию
Немного модернизации и я получил вполне универсальный рабочий шаблон.
Для всех, кто не знаком с DDD можно начать с wiki.
В конце мы получим связку с DDD + CQRS + Entity Framework + OData + WebApi + Log4Net + Castle Windsor + Kendo UI.
Звучит громоздко, но сугубо лично, в результате получаем вполне легко масштабируемую систему.
Итак начнем…
Создаем папку Domain и Infrastrcutre. В папке Domain создаем 3 проекта (class library):
- Domain.Commands
- Domain.Database
- Domain.Model
В папке Infrastrcuture создаем 4 проекта (class library):
- Infrastrcuture.Web
- Infrastrcuture.Domain
- Infrastrcuture.EntityFramework
- Infrastrcuture.Logging
И само веб-приложение (ASP MVC5), назовём его Web (с шаблоном MVC). И последний проект (class library) Web.Application.
А теперь по каждому по подробнее:
CQRS (Command Query Responsibility Segregation)
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):
namespace Web.Application
{
using System.Linq;
using System.Web.Http.OData;
using Infrastructure.Domain;
using Infrastructure.EntityFramework;
public class ReadODataControllerBase : ODataController
where TEntity : class, IEntity
{
private readonly IRepository _repository;
public ReadODataControllerBase(IRepository repository)
{
_repository = repository;
}
public IQueryable Get()
{
return _repository.Query();
}
}
}
И базовый form контроллер:
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 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().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 form)
where TForm : IFileForm
{
var formHanlderFactory = ResolveFormHandlerFactory();
formHanlderFactory.Create().Execute(form);
return File(form.FileContent, System.Net.Mime.MediaTypeNames.Application.Octet, form.FileName);
}
private JsonResult JsonError(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();
}
private IUnitOfWork ResolveUnitOfWork()
{
return GetContainer().Resolve();
}
private ILogger ResolveLogger()
{
return GetContainer().Resolve();
}
private IWindsorContainer GetContainer()
{
var containerAccessor = HttpContext.ApplicationInstance as IContainerAccessor;
return containerAccessor.Container;
}
private ICurrentUserKeeper ResolveCurrentUserKeeper()
{
return GetContainer().Resolve();
}
#endregion
#region CurrentUserAccessor Memebers
public ApplicationUser CurrentUser
{
get
{
var currentUserKeeper = ResolveCurrentUserKeeper();
return currentUserKeeper.GetCurrentUser();
}
}
#endregion
}
}
В результате для считывания данных с базы мы просто создаем класс и наследуем его от класса ReadODataController и просто переходим на localhost:12345/odata/Stations. Весь запрос вместо нас пишет OData:
namespace Web.Application.Station
{
using Domain.Model.Station;
using Infrastructure.EntityFramework;
public class StationsController : ReadODataControllerBase
{
public StationsController(IRepository repository) : base(repository)
{
}
}
}
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
Комментарии (1)
27 апреля 2017 в 14:12
+1↑
↓
Несколько вопросов: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?