NuGet пакет с типовыми решениями для ускорения разработки.
Содержит в себе базовые классы, которые необходимы при разработке по DDD + CQRS + Event Sourcing (immediate consistency). Библиотека совместима со спецификациями из open source библиотеки Force (https://github.com/hightechgroup/force). Имеет прямые зависимости к Entity Framework.
Реализованы по принципу "is a", а не "can do".
Использование:
public class Animal : Entity
public class Address : ValueObject
Реализованы с помощью библиотеки MediatR.
public class AddProductCommand : Command
public class ProductAddedEvent : Event
Startup.cs
services.AddMediatR(typeof(Startup));
// Хранилище Events
services.AddScoped<IEventStore, EventStore>();
// Шина обработки событий
services.AddScoped<IMediatorHandler, InMemoryBus>();
// Domain - Events
services.AddScoped<INotificationHandler<ProductAddedEvent>, ProductAddedEventHandler>();
// Domain - Commands
services.AddScoped<IRequestHandler<AddProductCommand, bool>, ProductCommandHandler>();
ProductCommandHandler.cs
public class ProductCommandHandler : CommandHandlerBase, IRequestHandler<ProductAddCommand, bool>
...
public async Task<bool> Handle(ProductAddCommand command, CancellationToken ct)
{
bool isValidOperation;
// Валидация
if (!isValidOperation)
{
await Mediator.RaiseEventAsync(
new DomainNotification(nameof(DomainNotification), Resources.NotValidOperation), ct);
return false;
}
// Бизнес логика
if (!await CommitAsync())
return false;
await _mediator.RaiseEventAsync(
new ProductAddedEvent(
product.Id,
product.Price,
// Другие поля
), ct);
return true;
ProductsController.cs
var command = _mapper.Map<ProductAddCommand>(model);
await Mediator.SendCommandAsync(command);
if (!IsValidOperation())
return BadRequest(Errors);
return Ok();
Глобальная шина для функционирования MediatR. Настроена на сохранение всех наследников Event
в EventStore
, кроме DomainNotification
, который предназначен для хранения ошибок во время выполнения бизнес логики.
InMemoryBus.cs
public virtual async Task RaiseEventAsync<T>(T @event, CancellationToken ct = default)
where T : Event
{
if (!@event.MessageType.Equals("DomainNotification"))
// Записываем наследников Event и с типом не DomainNotification.
await EventStore.SaveAsync(@event, ct);
// Паблишим ивенты. Они будут доступны при реализации INotificationHandler<T>, где T = Event
await Mediator.Publish(@event, ct);
}
Startup.cs
services.AddScoped<IMediatorHandler, InMemoryBus>();
ProductsController.cs
public class ProductsController : FormattedApiControllerBase
{
private readonly IMediatorHandler _mediator;
public ProductsController(IMediatorHandler mediator)
{
_mediator = mediator;
}
...
[HttpGet]
public IActionResult Post(AddProduct model)
{
...
await _mediator.SendCommandAsync(command);
Обработчик, с помощью которого построена архитектура обработки ошибок через INotification библиотеки MediatR.
Startup.cs
services.AddScoped<INotificationHandler<DomainNotification>, DomainNotificationHandler>();
ProductCommandHandler.cs
public class ProductCommandHandler : CommandHandlerBase, IRequestHandler<ProductAddCommand, bool>
...
public async Task<bool> Handle(ProductAddCommand command, CancellationToken ct)
{
bool isValidOperation;
// Валидация
if (!isValidOperation)
{
await Mediator.RaiseEventAsync(
new DomainNotification(nameof(DomainNotification), Resources.InvalidOperation), ct);
// Ошибка типа DomainNotification попала в InMemoryBus и сохранилась в памяти.
// Теперь она доступна через DomainNotificationHandler для дальнейшей обработки.
return false;
}
ApiControllerBase.cs
public abstract class ApiControllerBase : FormattedApiControllerBase
{
private readonly DomainNotificationHandler _notifications;
protected readonly IMediatorHandler Mediator;
protected ApiControllerBase(IMediatorHandler mediator, INotificationHandler<DomainNotification> notifications)
{
Mediator = mediator;
_notifications = (DomainNotificationHandler)notifications;
}
protected IEnumerable<DomainNotification> Notifications
=> _notifications.Notifications();
protected IEnumerable<string> Errors
=> Notifications.Select(x => x.Value);
protected bool IsValidOperation()
=> !_notifications.HasNotifications();
}
Репозиторий для обработки наследников Event
, для последующиего сохранения в базу. Содержит в себе аггрегат, тип, время, и тело события в сериализованном виде. Служит для хранения историчности аггрегата. Используется в InMemoryBus как await EventStore.SaveAsync(@event, ct)
.
Startup.cs
services.AddScoped<IEventStore, EventStore>();
InMemoryBus.cs
public class InMemoryBus : IMediatorHandler
{
protected readonly IMediator Mediator;
protected readonly IEventStore EventStore;
...
public virtual async Task RaiseEventAsync<T>(T @event, CancellationToken ct = default)
where T : Event
{
if (!@event.MessageType.Equals("DomainNotification"))
await EventStore.SaveAsync(@event, ct);
await Mediator.Publish(@event, ct);
}
Базовый класс обработчика команд, который содержит в себе базовые зависимости, необходимые для обработки доменных ошибок и взаимодействия с базой данных. Реализованные методы Commit()
и CommitAsync()
не позволят записать в базу, если найдутся доменные ошибки. Также умеют выбрасывать свои ошибки при наличии исключений во время записи в базу, которые можно в будущем аггрегировать и завернуть в тело Bad Request итд. Содержит зависимости IMediatorHandler
, DomainNotificationHandler
, IUnitOfWork
.
Класс, реализующий паттерн Unit Of Work, предлагает единую точку коммита транзакции в базу данных. Реализованы виртуальные методы Commit()
и CommitAsync()
с логикой орбаботки интерфейсов IHasCreationTime
и IHasModificationTime
. При необходимости можно добавить свою реализацию, наследовавшись от класса и перезаписать методы Commit()
, CommitAsync()
.
Набор инструментов, ускоряющих разработку Application
слоя приложения.
Набор атрибутов для работы с файлами в REST Api.
- AllowedExtensionsAttribute
- MaxFileSize
- HasValidFileName
Пример.
[HasValidFileName]
[MaxFileSize(500 * 1024 * 1024)] // 500 mb
[AllowedExtensions(new[] {".jpg", ".png", ".mp4", ".jpeg"})]
public IFormFile File { get; set; }
- DevelopmentOnly
Атрибут, который позволяет выключать метод в не Development окружении. Пример.
[HttpDelete]
[DevelopmentOnly]
[ApiExplorerSettings(IgnoreApi = true)]
public async Task<IActionResult> Delete([FromQuery] DeleteRequest request)
Инструмент, необходимый для форматирования ответа приложения и приведения ответа в единую стилистику.
Положительный формат ответа:
{
"content": {
"key": "value"
},
"debugData": "string"
}
Отрицательный формат ответа:
{
"subStatus": "string",
"errorContent": [
"string"
],
"debugData": "string"
}
Необходимо зарегистрировать следующим образом.
Startup.cs
services
.AddControllers()
.SetCompatibilityVersion(CompatibilityVersion.Latest)
.AddResponseOutputFormatter();
services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory =
actionContext =>
InvalidModelStateResponseFactory.CreateFrom(Resources.InvalidModel, actionContext.ModelState);
});
ApiControllerBase.cs
public abstract class ApiControllerBase : FormattedApiControllerBase
{
Использование.
/// <summary>
/// Вывод списка продуктов.
/// </summary>
[HttpGet]
[SwaggerResponse(200, SwaggerResponseMessages.Ok + " Возвращается список продуктов.",
typeof(SwaggerSuccessApiResponse<PagedResponse<ProductItemModel>>))]
public IActionResult Get([FromQuery] PagingQueryModel query)
{
var products = _context
.Set<Domain.Entities.Products>()
.OrderByDescending(x => x.Rating)
.ProjectTo<ProductItemModel>(_mapper.ConfigurationProvider)
.Paginate(query.Page, query.Size);
return Ok(products);
}
Ответ.
{
"pagination": {
"itemsTotal": 0,
"page": 0,
"total": 0,
"size": 0,
"hasPrevious": true,
"hasNext": true
},
"items": [
{
"id": 0,
"name": "string",
"price": 0,
"rating": 0
}
]
}
Упрощение разработки swagger документации. Автоматически дополняет ответ ошибкой 500, а также 401 и 403 если метод покрыт авторизацией. Для упрощения документирования различных ответов присутствует набор шаблонных сообщений SwaggerResponseMessages
. Как референс для текстов ошибок использовалась официальная документация Mozilla. SwaggerErrorApiResponse
и SwaggerSuccessApiResponse
добавлены для построения тел ответов на swagger странице при использовании в связке с Response Formatter.
Регистрация.
services.AddSwaggerGen(options =>
{
// Ваш конфиг
options.OperationFilter<ResponseOperationFilter>();
}
);
Использование. 500, 403, 401 ошибки добавились автоматически.
/// <summary>
/// Удаление продукта.
/// </summary>
[HttpDelete("{id}")]
[Authorize]
[SwaggerResponse(204, SwaggerResponseMessages.NoContent, typeof(void))]
[SwaggerResponse(400, SwaggerResponseMessages.BadRequest, typeof(SwaggerErrorApiResponse<IEnumerable<string>>))]
public async Task<IActionResult> Delete(long id)
Методы для инициализации данных в БД.
- MigrationsInitializer - используется для автомиграции при старте.
- DataInitializerBase - базовый метод для инициализации данных в базу. Имеет фабричный метод InitializeAsync, который необходимо реализовать.
Использование.
Program.cs
public static async Task Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
await host.InitAsync();
await host.RunAsync();
}
Startup.cs
services.AddAsyncInitializer<MigrationsInitializer>();
services.AddAsyncInitializer<AdminsInitializer>();
В начале советуется запускать MigrationsInitializer, так как сначала должна инициализироваться актуальная схема, а потом уже все остальное.