Skip to content

smarthead/SmartHead.Essentials

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

38 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

SmartHead.Essentials

НугСт ΠΏΠ°ΠΊΠ΅Ρ‚ с Ρ‚ΠΈΠΏΠΎΠ²Ρ‹ΠΌΠΈ Ρ€Π΅ΡˆΠ΅Π½ΠΈΡΠΌΠΈ для ускорСния Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ.

SmartHead.Essentials.Abstractions

Π‘ΠΎΠ΄Π΅Ρ€ΠΆΠΈΡ‚ Π² сСбС Π±Π°Π·ΠΎΠ²Ρ‹Π΅ классы, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΡ‹ ΠΏΡ€ΠΈ Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ΅ ΠΏΠΎ DDD + CQRS + Event Sourcing (immediate consistency). Π‘ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° совмСстима со спСцификациями ΠΈΠ· open source Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΈ Force (https://github.com/hightechgroup/force). Π˜ΠΌΠ΅Π΅Ρ‚ прямыС зависимости ΠΊ Entity Framework.

DDD: Entity, ValueObject.

Π Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Π½Ρ‹ ΠΏΠΎ ΠΏΡ€ΠΈΠ½Ρ†ΠΈΠΏΡƒ "is a", Π° Π½Π΅ "can do".

ИспользованиС:

public class Animal : Entity
public class Address : ValueObject

CQRS + Event Sourcing

Π Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Π½Ρ‹ с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΈ 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();

SmartHead.Essentials.Implementation

InMemoryBus

Π“Π»ΠΎΠ±Π°Π»ΡŒΠ½Π°Ρ шина для функционирования MediatR. НастроСн Π½Π° сохранСниС всСх наслСдников Event Π² EventStore, ΠΊΡ€ΠΎΠΌΠ΅ DomainNotification.

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);

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);
}

DomainNotificationHandler

ΠžΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ, с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ³ΠΎ построСна Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π° ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ ошибок Ρ‡Π΅Ρ€Π΅Π· 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();
}

EventStore

Π¨ΠΈΠ½Π° для ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ наслСдников Event, для ΠΏΠΎΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΠΈΠ΅Π³ΠΎ сохранСния Π² Π±Π°Π·Ρƒ. Π‘ΠΎΠ΄Π΅Ρ€ΠΆΠΈΡ‚ Π² сСбС Π°Π³Π³Ρ€Π΅Π³Π°Ρ‚, Ρ‚ΠΈΠΏ, врСмя, ΠΈ Ρ‚Π΅Π»ΠΎ события Π² сСриализованном Π²ΠΈΠ΄Π΅. Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ Π² InMemoryBus.

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);
    }

CommandHandlerBase

Π‘Π°Π·ΠΎΠ²Ρ‹ΠΉ класс ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠ° ΠΊΠΎΠΌΠ°Π½Π΄, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ содСрТит Π² сСбС Π±Π°Π·ΠΎΠ²Ρ‹Π΅ зависимости, Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΡ‹Π΅ для ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ Π΄ΠΎΠΌΠ΅Π½Π½Ρ‹Ρ… ошибок ΠΈ взаимодСйствия с Π±Π°Π·ΠΎΠΉ Π΄Π°Π½Π½Ρ‹Ρ…. Π Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Π½Π½Ρ‹Π΅ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ Commit() ΠΈ CommitAsync() Π½Π΅ позволят Π·Π°ΠΏΠΈΡΠ°Ρ‚ΡŒ Π² Π±Π°Π·Ρƒ, Ссли найдутся Π΄ΠΎΠΌΠ΅Π½Π½Ρ‹Π΅ ошибки. Π’Π°ΠΊΠΆΠ΅ ΡƒΠΌΠ΅ΡŽΡ‚ Π²Ρ‹Π±Ρ€Π°ΡΡ‹Π²Π°Ρ‚ΡŒ свои ошибки ΠΏΡ€ΠΈ Π½Π°Π»ΠΈΡ‡ΠΈΠΈ ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΉ Π²ΠΎ врСмя записи, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΌΠΎΠΆΠ½ΠΎ Π² Π±ΡƒΠ΄ΡƒΡ‰Π΅ΠΌ Π°Π³Π³Ρ€Π΅Π³ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΈ Π΄ΠΎΡΡ‚Π°Π²ΠΈΡ‚ΡŒ Π² Ρ‚Π΅Π»ΠΎ Bad Request ΠΈΡ‚Π΄. Π‘ΠΎΠ΄Π΅Ρ€ΠΆΠΈΡ‚ зависимости IMediatorHandler, DomainNotificationHandler, IUnitOfWork.

UnitOfWork

Класс, Ρ€Π΅Π°Π»ΠΈΠ·ΡƒΡŽΡ‰ΠΈΠΉ ΠΏΠ°Ρ‚Ρ‚Π΅Ρ€Π½ Unit Of Work. Π Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Π½Ρ‹ Π²ΠΈΡ€Ρ‚ΡƒΠ°Π»ΡŒΠ½Ρ‹Π΅ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ Commit() ΠΈ CommitAsync() с Π»ΠΎΠ³ΠΈΠΊΠΎΠΉ ΠΎΡ€Π±Π°Π±ΠΎΡ‚ΠΊΠΈ интСрфСйсов IHasCreationTime ΠΈ IHasModificationTime. ΠŸΡ€ΠΈ нСобходимости ΠΌΠΎΠΆΠ½ΠΎ Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ свою Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΡŽ, наслСдовавшись ΠΎΡ‚ класса ΠΈ ΠΏΠ΅Ρ€Π΅Π·Π°ΠΏΠΈΡΠ°Ρ‚ΡŒ ΠΌΠ΅Ρ‚ΠΎΠ΄Ρ‹ Commit(), CommitAsync().

SmartHead.Essentials.Application

Набор инструмСнтов, ΡƒΡΠΊΠΎΡ€ΡΡŽΡ‰ΠΈΡ… Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΡƒ 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)

Response Formatter

Π˜Π½ΡΡ‚Ρ€ΡƒΠΌΠ΅Π½Ρ‚, Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΡ‹ΠΉ для форматирования ΠΎΡ‚Π²Π΅Ρ‚Π° прилоТСния ΠΈ привСдСния ΠΎΡ‚Π²Π΅Ρ‚Π° Π² Π΅Π΄ΠΈΠ½ΡƒΡŽ стилистику.

ΠŸΠΎΠ»ΠΎΠΆΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹ΠΉ Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ ΠΎΡ‚Π²Π΅Ρ‚Π°:

{
  "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 Response

Π£ΠΏΡ€ΠΎΡ‰Π΅Π½ΠΈΠ΅ Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ swagger Π΄ΠΎΠΊΡƒΠΌΠ΅Π½Ρ‚Π°Ρ†ΠΈΠΈ. АвтоматичСски дополняСт ΠΎΡ‚Π²Π΅Ρ‚ ошибкой 500, Π° Ρ‚Π°ΠΊΠΆΠ΅ 401 ΠΈ 403 Ссли ΠΌΠ΅Ρ‚ΠΎΠ΄ ΠΏΠΎΠΊΡ€Ρ‹Ρ‚ Π°Π²Ρ‚ΠΎΡ€ΠΈΠ·Π°Ρ†ΠΈΠ΅ΠΉ. Для упрощСния докумСнтирования Ρ€Π°Π·Π»ΠΈΡ‡Π½Ρ‹Ρ… ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ² присутствуСт Π½Π°Π±ΠΎΡ€ ΡˆΠ°Π±Π»ΠΎΠ½Π½Ρ‹Ρ… сообщСний SwaggerResponseMessages. 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)

Seed

ΠœΠ΅Ρ‚ΠΎΠ΄Ρ‹ для ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ Π΄Π°Π½Π½Ρ‹Ρ… Π² Π‘Π”.

  • 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, Ρ‚Π°ΠΊ ΠΊΠ°ΠΊ сначала Π΄ΠΎΠ»ΠΆΠ½Π° ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒΡΡ Π°ΠΊΡ‚ΡƒΠ°Π»ΡŒΠ½Π°Ρ схСма, Π° ΠΏΠΎΡ‚ΠΎΠΌ ΡƒΠΆΠ΅ всС ΠΎΡΡ‚Π°Π»ΡŒΠ½ΠΎΠ΅.

About

Nuget package for quick development.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •  

Languages