Как стать автором
Обновить

Комментарии 24

А пример какой… чтобы в сорцы не лезть )))

Да, пример, не выложили…
Это по сути для «пустого» сервиса. Полноценный пример будет позже в репозитории.
Program.cs:
class Program
{ 
	public static void Main(string[] args)
	{
		CompanyHostBuilder.Create()
                                  .UseServer((b, c) => { b.UseKestrel(); })
                                  .BuildWebHost(args)
                                  .Run()
	}
}

Конфиг conf/appsettings.json:
{
  "webApiConfiguration": {
	"portNumber": 80
  }
}
ViennaNET.Mediator.*
Такой подход позволяет сократить количество DI-инъекций до одной, например, в контроллерах.

Это про то, что теперь вместо пяти разных сервисов, можно заинжектить какой-нибудь IMediator и юзать его?


Если да, то есть мнение, что это тот же service locator.

Это про то, что теперь вместо пяти разных сервисов, можно заинжектить какой-нибудь IMediator и юзать его?
— Да

Если да, то есть мнение, что это тот же service locator.
— Нет. Медиатор — это внутренняя шина, по которой можно отправлять сообщения или команды, на которые будут реагировать обработчики. Таким образом проще соблюдать SRP, ведь каждый обработчик отвечает только за обработку одной конкретной команды.

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

— Нет. Медиатор — это внутренняя шина, по которой можно отправлять сообщения или команды, на которые будут реагировать обработчики. Таким образом проще соблюдать SRP, ведь каждый обработчик отвечает только за обработку одной конкретной команды.


Ох как мне эта идея не нравится… вместо того чтобы вызвать понятный метод у понятной зависимости мы теперь посылаем сообщения в надежде, что их кто-то, как-то обработает? А эти зависимости в транзакциях участвовать умеют? И зачем вообще прятать заморачиваться с IMediator?

Плюсы медиатра в том, что очень легко решать cross cutting concerns, у marshinov хорошие статьи были на эту тему.

Сейчас используем такой извращённый интересный подход:


Какая-нибудь 'фича' или app service
namespace Vendor.Features.Statistics
{
  public class GetAutomationStatisticsQuery : IRequest<GetAutomationStatisticsResult>
  {
    public DateTime? FromDate { get; set; }
    public DateTime? ToDate { get; set; }
    public string TaskArn { get; set; }
  }

  public class GetAutomationStatisticsResult
  {
    public long TotalAutomations { get; set; }
    public long ErrorsReported { get; set; }
    public long WarningsReported { get; set; }
  }

  internal class GetAutomationStatisticsHandler :
    IRequestHandler<GetAutomationStatisticsQuery, GetAutomationStatisticsResult>
  {
    private readonly GetAutomationsLogTotalCount _getAutomationsLogTotalCount;
    private readonly GetAutomationsTotalCount _getAutomationsTotalCount;

    public GetAutomationStatisticsHandler(
      // Это делегаты на другие фичи (этакая инкапсуляция над медиатром)
      GetAutomationsLogTotalCount getAutomationsLogTotalCount,
      GetAutomationsTotalCount getAutomationsTotalCount)
    {
      _getAutomationsLogTotalCount = getAutomationsLogTotalCount;
      _getAutomationsTotalCount = getAutomationsTotalCount;
    }

    public async Task<GetAutomationStatisticsResult> Handle(GetAutomationStatisticsQuery query, CancellationToken ct)
    {
      // some logic
    }
  }

  public delegate Task<GetAutomationStatisticsResult> GetAutomationStatistics(
    GetAutomationStatisticsQuery query, CancellationToken ct = default);
}

В каждом файле "фичи" лежат классы для реквеста, респонса, хендлера и делегат (GetAutomationStatistics в примере).


Затем этот делегат через рефлексию регистрируется в IoC с помощью такого класса:


MediatorForwarderForDelegates
    private sealed class MediatorForwarderForDelegates<TRequest, TResponse> where TRequest : IRequest<TResponse>
    {
      private readonly IMediator _mediator;

      public MediatorForwarderForDelegates(IMediator mediator)
      {
        _mediator = mediator;
      }

      public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken)
      {
        return _mediator.Send(request, cancellationToken);
      }
    }

Ну и эти делегаты потом уже можно инжектировать в другие хендлеры/сервисы.

Кстати, как впечатления от работы, в сравнении с расположением /Controllers
/…
/Models
/…
/Views
/…
?

В целом положительные.
Но у нас не как у Джими Богарда с его vertical features.


У нас "почти clean" — сборка с апп сервисами и в ней свои фичи, сборка презентейшн — либо напрямую юзает классы (дто) из апп сервисов, либо добавляет свои фичи (например, для graphql).


Кроме размещения фич в одном файле, не стесняемся пользоваться nested классами — меньше проблем с придумыванием имён и меньше нейм-конфликтов. Если класс получается большим из-за nested-классов, то делаем класс partial и выносим в отдельный файл.


Раньше вообще делали так, тоже неплохо было, но не прижилось почему-то:


Заголовок спойлера
  public class CreateAccount
  {
    public class Command : MediatR.IRequest<Result>
    {
      public string Name { get; }
    }

    public class Result
    {
      public Account Account { get; set; }
    }

    // dto
    public class Account
    {
    }

    public class Mappings : Profile
    {
    }

    public class Validation : AbstractValidator<Command>
    {
    }

    public class Handler : IRequestHandler<Command, Result>
    {
    }
  }

Вопрос - а зачем тут IMediator? Можно же вот так написать:

namespace Vendor.Features.Statistics
{
  public class GetAutomationStatisticsResult
  {
    public long TotalAutomations { get; set; }
    public long ErrorsReported { get; set; }
    public long WarningsReported { get; set; }
  }

  public class GetAutomationStatisticsHandler 
  {
    private readonly GetAutomationsLogTotalCount _getAutomationsLogTotalCount;
    private readonly GetAutomationsTotalCount _getAutomationsTotalCount;

    public GetAutomationStatisticsHandler(
      // Это делегаты на другие фичи (этакая инкапсуляция над медиатром)
      GetAutomationsLogTotalCount getAutomationsLogTotalCount,
      GetAutomationsTotalCount getAutomationsTotalCount)
    {
      _getAutomationsLogTotalCount = getAutomationsLogTotalCount;
      _getAutomationsTotalCount = getAutomationsTotalCount;
    }

    public async Task<GetAutomationStatisticsResult> Handle(DateTime? FromDate, DateTime? ToDate, string TaskArn, CancellationToken ct)
    {
      // some logic
    }
  }

//не уверен что он нужен но пусть будет
  public delegate Task<GetAutomationStatisticsResult> GetAutomationStatistics(DateTime? FromDate, DateTime? ToDate, string TaskArn, CancellationToken ct = default);
}

В таком виде мы явно видим что вызываем, понятны параметры вызова. Намного проще переходить по коду. Я честно не могу понять какую пользу приносит именно IMediator в реализацию такого хендлера. Да я понимаю что есть валидация, транзакции, логирование в пайплайне. Если именно для этого - то понятно. Но если без пайплайна - то зачем?

Думаю, похожий набор библиотек есть в каждой крупной компании. Минус у них общий — кастомность.
Хочешь заинжектить вторую реализацию интерфейса — хрен тебе, наш DI контейнер это не поддерживает, так что страдай. Хочешь два http клиента с разной авторизацией — хрен тебе, страдай. Хочешь… ну вы поняли.
Разумеется все эти хотелки рано или поздно попадут в либу… Но они нужны сейчас, а не поздно или даже рано. И в итоге ты стоишь перед выбором — или отказаться от либы (т.е. ВСЕЙ инфраструктуры) или страдать и костылять.
При построении нашего набора библиотек мы учитывали расширяемость. Есть и клиенты с разной авторизацией (тип авторизации указывается в конфиге), для одного интерфейса мессаджинга есть разные реализации (IBM, rabbit, кафка, опять же, тип реализации указывается в конфиге), для общего набора интерфейсов ORM есть поддержка 5 СУБД.

Выбор взять всё как есть или отказаться — не стоит, можно начать с нуля и набрать нужный набор реализаций. Например если моё приложение использует ViennaNET.ORM и его реализацию для MsSQL, то чтобы поменять базу на PostgresQL, мне нужно просто зареференсить другую реализацию и в конфиге поменять тип базы. Более того, можно зареференсить обе реализации, указать два подключения и на уровне регистрации Entity указать, с какого коннекта его доставать — так делали при миграции данных из одной БД в другую.

Сейчас этого в репозитории нет, но позже выкатим.
Сложно комментировать то, чего еще нет… Но вот вам сходу возможная боль.

Допустим, я решил использовать ViennaNET.Logging. Везде, во всем проекте, у меня ILog. И тут проекту потребовался EF Core. Без ViennaNET.ORM (которой пока нет и не уверен, что там этой проблемы не будет) и… и получаю факап, потому потому что EF Core для подключения логгирования запросов хочет классический ILogger. И теперь мне нужно пилить свой ILoggerFactory и пытаться как-то натянуть сову на глобус, т.е. ILog на ILogger.
Совсем не обязательно ждать, когда что-то появится непосредственно в библиотеке. Можно сделать свою реализацию конфигуратора, которая использует абстракции корневого билдера, завернуть в свой пакет, и подключать во все необходимые сервисы.

Немного дополню по поводу Http-клиентов. Вот, пример реализации для JWT-клиента: github.com/Raiffeisen-DGTL/ViennaNET/blob/master/ViennaNET.WebApi.Configurators.HttpClients.Jwt/JwtHttpClientsConfigurator.cs. По сути здесь мы просто добавляем хэндлер для переотправки входящего токена.

В подобных конфигураторах можно сделать более сложную логику, добавить другие хэндлеры, считывать дополнительные параметры из файла конфигурации. Ну и затем, через свой метод расширения, подключить:
Скрытый текст
CompanyHostBuilder.Create()
                  ...
                  .UseCustomJwtClient()
                  ...
                  .BuildWebHost(args)
                  .Run()



Да, HttpClient у вас весьма гибкий) А если, например, мне нужно сказать сваггеру игнорировать поля запросов, помеченные определенным атрибутом — я смогу это сделать?
Насколько я понимаю, это делается в настройках SwaggerGen. Его можно сконфигурировать через метод расширения UseSwaggerWithOptions() из конфигуратора
github.com/Raiffeisen-DGTL/ViennaNET/blob/master/ViennaNET.WebApi.Configurators.Swagger/SwaggerConfigurator.cs
НЛО прилетело и опубликовало эту надпись здесь
Вместо сервисов — нет. Да и разве можно это полноценно реализовать через него? Он не для этого. Использовали только по прямому назначению — для отложенного запуска задач.
Наш фреймворк делался для запуска рестовых сервисов, а не для бэкграунд воркеров.
НЛО прилетело и опубликовало эту надпись здесь

Кажется вы немного загоняетесь по поводу причин написания. В любой мало-мальски большой конторе со временем возникает желание унифицировать какие-то практики/методики, чтоб не заниматься копипастой и не втаскивать в каждый проект зависимости от конкретных реализаций инфраструктурных компонентов. То же логирование, метрики, валидация — они во всех проектах одинаковые и если со временем надо что-то поменять в способе их доставки, то что — ползать по всем проектам и править тонны разношерстных велосипедов или всё же поправить одну либу и пнуть всем командам "обновитесь"? Т.е. причина появления этого


Да, по сути это boilerplate. Фреймворком особо не назвать и применимость его за пределами компании-разработчика — нулевая. Ну реально никто это не будет себе тащить в проект, потому что подобных фреймворков тьма. Каждый второй погромист в желании избавить себя от ctrl+c/v пишет нечто подобное. И каждому первому именно эта реализация не подойдёт по причине недостаточной гибкости.
Суть-то в чём: есть фреймворк, ну тот же aspnet core. Он набит достаточно базовыми компонентами, чтоб их можно было комбинировать и управлять ими, но при этом имеет достаточно много готовых "полных" реализаций, цель которых именно в уменьшении ручной работы. Т.е. есть и готовые однострочники с кучей дефолтов и есть возможность всё настроить руками.


А приведённый в статье вариант — это ещё один однострочник но с другими дефолтами. И если тебе нужто что-то, отличное от дефолтов — ты опять собираешь руками. И смысл этого фреймворка испаряется.
Т.е. всё это хорошо, пока ты внутри компании пишешь кодик по внутренним гайдлайнам, завязанный на внутреннюю инфраструктуру. За пределами компании это труп. Ну т.е. это должен быть либо прям вот неимоверно исчерпывающий набор, представляющий из себя реально другую и более удобную модель, чем уже существует (и тогда у него могут найтись какие-то поклонники), либо это просто ещё один набор дефолт-обвязок, который тащить в паблик бессмысленно. Даже с точки зрения самопиара — посмотрев на такой код фиг кто загорится "Хочу кодить в райфе!"


зы: умилил ребрендинг-коммит
ззы: блин, да вы даже валидацию собственную навелосипедили, НОЗАЧЕМ? логирование запилили на собственном куцем интерфейсе, запиленном под конкретного мастодонта log4net. дефолтовые абстракции ILogger вам чем не угодили?


в общем смысла выкладки вот этого в гит, а соответственно и поста не вижу вообще.

Разве станет хуже, если будет много фреймворков хороших и разных, чтобы можно выбрать или что-то позаимствовать… Интересно было бы увидеть подобные инфраструктурные наработки других банков, что если в них будет больше общего чем разного? Да и по нынешним меркам, настолько ли внутренняя инфраструктура будет отличаться от внешней, те же докеры, куберы, кэши и т.п.
На будущее совет — свои ошибки надо наследовать от ApplicationException а не от глобального Exception и уж тем более не кидать в своем коде стандартные ошибки .net фреймворка а бросать свои унаследованные от ApplicationException.
Мне кажется это не совсем так
Заголовок спойлера
User applications, not the common language runtime, throw custom exceptions derived from the ApplicationException class. The ApplicationException class differentiates between exceptions defined by applications versus exceptions defined by the system.

If you are designing an application that needs to create its own exceptions, you are advised to derive custom exceptions from the Exception class. It was originally thought that custom exceptions should derive from the ApplicationException class; however in practice this has not been found to add significant value. For more information, see Best Practices for Handling Exceptions

ApplicationException Class
Зарегистрируйтесь на Хабре, чтобы оставить комментарий