[.NET, C#] Сквозной функционал через обертки
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
При разработке мы не редко сталкиваемся с ситуацией, когда при выполнении какой-либо бизнес-логики требуется записать логи, аудиты, разослать оповещения. В общем реализовать некоторый сквозной функционал.
Когда масштабы производства небольшие, можно особо не усердствовать и все это делать прямо в методах. Постепенно, конструктор сервиса начинает обрастать входящими сервисами для выполнения БЛ и сквозного функционала. А это уже ILogger, IAuditService, INotifiesSerice.
Не знаю как Вы, а я не люблю много инъекций и большие методы, которые выполняют много действий за раз.
Можно накрутить на код какую либо реализацию АОП. В стеке .NET такие реализации делают инъекции в ваше приложение в нужные места, внешне похожи на магию 80 уровня и, зачастую, имеют проблемы с типизацией и отладкой.
Я попытался найти золотую середину. Если данные проблемы не обошли вас стороной, добро пожаловать под кат.
Спойлер. На самом деле, мне удалось решить чуть больше проблем, чем я описал выше. Например, я могу отдать разработку БЛ одному разработчику, а навешивание сквозного функционала и даже валидации входящих данных — другому одновременно.
И помогли мне в этом декораторы и надстройка над DI. Кто-то далее скажет, что это прокси, с радостью обсужу это в комментах.
Итак, что я хочу как разработчик?
- При реализации БЛ не отвлекаться на левый функционал.
- Иметь возможность в юнит тестах тестировать только БЛ. Причем я не люблю делать 100500 моков, чтобы отключить весь вспомогательный функционал. 2-3 — еще ладно, но больше не хочу.
- Понимать, что происходит, не имея 7 пядей во лбу. :)
- Иметь возможность управлять временем жизни сервиса и каждой его обертки ОТДЕЛЬНО!
Что я хочу, как проектировщик и лидер команды?
- Иметь возможность декомпозировать задачи наиболее оптимально и с наименьшей связностью, чтобы одновременно можно было задействовать как можно больше разработчиков на разные задачи и при этом чтобы они тратили как можно меньше времени на исследование (если разработчику надо разработать БЛ, а параллельно думать, что и как залогировать, он потратит больше времени на исследование. И так с каждым куском БЛ. Куда проще взяться за записи аудитов и распихать их по всему проекту).
- Оправлять порядком выполнения кода отдельно от его разработки.
Поможет мне в этом вот такой интерфейс.
/// <summary>
/// Обертка для сервиса.
/// </summary>
/// <typeparam name="T"> Класс сервиса. </typeparam>
public interface IDecorator<T>
{
/// <summary>
/// Делегат для работы декоратора.
/// </summary>
Func<T> NextDelegate { get; set; }
}
Можно использовать как то так
SPL
interface IService
{
Response Method(Request request);
}
class Service : IService
{
public Response Method(Request request)
{
// BL
}
}
class Wrapper : IDecorator<IService>, IService
{
public Func<IService> NextDelegate { get; set; }
public Response Method(Request request)
{
// code before
var result = NextDelegate().Method(request);
// code after
return result;
}
}
Таким образом, действие у нас будет уходить в глубину.
wrapper1
wrapper2
service
end wrapper2
end wrapper1
Но, постойте. Это же уже есть в ООП и называется наследование. :D
class Service {}
class Wrapper1: Service {}
class Wrapper2: Wrapper1 {}
Я как представил, что появится дополнительный сквозной функционал, который придется внедрять по всему приложению в середину или менять местами уже имеющиеся, так у меня на спине волосы дыбом встали.
Но моя лень — это не уважительная причина. Уважительная причина в том, что будут большие проблемы при модульном тестировании функционала в классах Wrapper1 и Wrapper2, тогда как в моем примере NextDelegate можно просто замокать. Более того, у сервиса и каждой обертки свой собственный набор инструментов, которые инжектятся в конструктор, тогда как при наследовании последняя обертка обязана иметь ненужные инструменты, чтобы передать их родителям.
Итак, подход принят, осталось придумать, где, как и когда назначать NextDelegate.
Я решил, что самым логичным решением будет делать это там, где я регистрирую сервисы. (Startup.sc, по умолчанию).
Вот как это выглядит в базовом варианте
SPL
services.AddScoped<Service>();
services.AddTransient<Wrapper1>();
services.AddSingleton<Wrapper2>();
services.AddSingleton<IService>(sp =>
{
var wrapper2 = sp.GetService<Wrapper2>();
wrapper2.NextDelegate = () =>
{
var wrapper1 = sp.GetService<Wrapper1>();
wrapper1.NextDelegate = () =>
{
return sp.GetService<Service>();
};
return wrapper1;
};
return wrapper2;
});
В целом, все требования выполнены, но появилась другая проблема — вложенность.
Эту проблему можно решить перебором или рекурсией. Но под капотом. Внешне все должно выглядеть просто и понятно.
Вот чего мне удалось добиться
SPL
services.AddDecoratedScoped<IService, Service>(builder =>
{
builder.AddSingletonDecorator<Wrapper1>();
builder.AddTransientDecorator<Wrapper2>();
builder.AddScopedDecorator<Wrapper3>();
});
А помогли мне в этом вот эти методы расширения
А помогли мне в этом вот эти методы расширения
SPL
/// <summary>
/// Методы расширения для декораторов.
/// </summary>
public static class DecorationExtensions
{
/// <summary>
/// Метод регистрации декорируемого сервиса.
/// </summary>
/// <typeparam name="TDefinition"> Интерфейс сервиса. </typeparam>
/// <typeparam name="TImplementation"> Реализация сервиса. </typeparam>
/// <param name="lifeTime"></param>
/// <param name="serviceCollection"> Коллекция сервисов. </param>
/// <param name="decorationBuilder"> Построитель декораций. </param>
/// <returns> Коллекцию сервисов после регистрации декораторов. </returns>
public static IServiceCollection AddDecorated<TDefinition, TImplementation>(
this IServiceCollection serviceCollection, ServiceLifetime lifeTime,
Action<DecorationBuilder<TDefinition>> decorationBuilder)
where TImplementation : TDefinition
{
var builder = new DecorationBuilder<TDefinition>();
decorationBuilder(builder);
var types = builder.ServiceDescriptors.Select(k => k.ImplementationType).ToArray();
var serviceDescriptor = new ServiceDescriptor(typeof(TImplementation), typeof(TImplementation), lifeTime);
serviceCollection.Add(serviceDescriptor);
foreach (var descriptor in builder.ServiceDescriptors)
{
serviceCollection.Add(descriptor);
}
var resultDescriptor = new ServiceDescriptor(typeof(TDefinition),
ConstructServiceFactory<TDefinition>(typeof(TImplementation), types), ServiceLifetime.Transient);
serviceCollection.Add(resultDescriptor);
return serviceCollection;
}
/// <summary>
/// Метод регистрации декорируемого сервиса с временем жизни Scoped.
/// </summary>
/// <typeparam name="TDefinition"> Интерфейс сервиса. </typeparam>
/// <typeparam name="TImplementation"> Реализация сервиса. </typeparam>
/// <param name="serviceCollection"> Коллекция сервисов. </param>
/// <param name="decorationBuilder"> Построитель декораций. </param>
/// <returns> Коллекцию сервисов после регистрации декораторов. </returns>
public static IServiceCollection AddDecoratedScoped<TDefinition, TImplementation>(
this IServiceCollection serviceCollection,
Action<DecorationBuilder<TDefinition>> decorationBuilder)
where TImplementation : TDefinition
{
return serviceCollection.AddDecorated<TDefinition, TImplementation>(ServiceLifetime.Scoped,
decorationBuilder);
}
/// <summary>
/// Метод регистрации декорируемого сервиса с временем жизни Singleton.
/// </summary>
/// <typeparam name="TDefinition"> Интерфейс сервиса. </typeparam>
/// <typeparam name="TImplementation"> Реализация сервиса. </typeparam>
/// <param name="serviceCollection"> Коллекция сервисов. </param>
/// <param name="decorationBuilder"> Построитель декораций. </param>
/// <returns> Коллекцию сервисов после регистрации декораторов. </returns>
public static IServiceCollection AddDecoratedSingleton<TDefinition, TImplementation>(
this IServiceCollection serviceCollection,
Action<DecorationBuilder<TDefinition>> decorationBuilder)
where TImplementation : TDefinition
{
return serviceCollection.AddDecorated<TDefinition, TImplementation>(ServiceLifetime.Singleton,
decorationBuilder);
}
/// <summary>
/// Метод регистрации декорируемого сервиса с временем жизни Transient.
/// </summary>
/// <typeparam name="TDefinition"> Интерфейс сервиса. </typeparam>
/// <typeparam name="TImplementation"> Реализация сервиса. </typeparam>
/// <param name="serviceCollection"> Коллекция сервисов. </param>
/// <param name="decorationBuilder"> Построитель декораций. </param>
/// <returns> Коллекцию сервисов после регистрации декораторов. </returns>
public static IServiceCollection AddDecoratedTransient<TDefinition, TImplementation>(
this IServiceCollection serviceCollection,
Action<DecorationBuilder<TDefinition>> decorationBuilder)
where TImplementation : TDefinition
{
return serviceCollection.AddDecorated<TDefinition, TImplementation>(ServiceLifetime.Transient,
decorationBuilder);
}
/// <summary>
/// Метод
/// </summary>
/// <typeparam name="TService"></typeparam>
/// <param name="implType"></param>
/// <param name="next"></param>
/// <returns></returns>
private static Func<IServiceProvider, TService> ConstructDecorationActivation<TService>(Type implType,
Func<IServiceProvider, TService> next)
{
return x =>
{
var service = (TService) x.GetService(implType);
if (service is IDecorator<TService> decorator)
decorator.NextDelegate = () => next(x);
else
throw new InvalidOperationException("Ожидался декоратор");
return service;
};
}
/// <summary>
/// Создание фабрики для декорируемого сервиса.
/// </summary>
/// <typeparam name="TDefinition"> Тип контракта сервиса. </typeparam>
/// <param name="serviceType"> Тип реализации сервиса. </param>
/// <param name="decoratorTypes"> Типы делегатов в требуемом порядке. </param>
/// <returns> Фабрику создания сервиса через DI. </returns>
private static Func<IServiceProvider, object> ConstructServiceFactory<TDefinition>(Type serviceType,
Type[] decoratorTypes)
{
return sp =>
{
Func<IServiceProvider, TDefinition> currentFunc = x =>
(TDefinition) x.GetService(serviceType);
foreach (var decorator in decoratorTypes)
{
currentFunc = ConstructDecorationActivation(decorator, currentFunc);
}
return currentFunc(sp);
};
}
}
Теперь немного сахара для функциональщиков
Теперь немного сахара для функциональщиков
SPL
/// <summary>
/// Базовый класс декоратора.
/// </summary>
/// <typeparam name="T"> Тип декорируемого сервиса. </typeparam>
public class DecoratorBase<T> : IDecorator<T>
{
/// <summary>
/// Делегат для получения следующего декоратора или сервиса.
/// </summary>
public Func<T> NextDelegate { get; set; }
/// <summary>
/// Выполнить код декоратора с вызовом следующего декоратора.
/// </summary>
/// <typeparam name="TResult"> Тип возвращаемого значения. </typeparam>
/// <param name="lambda"> Выполняемый код. </param>
/// <returns></returns>
protected Task<TResult> ExecuteAsync<TResult>(Func<T, Task<TResult>> lambda)
{
return lambda(NextDelegate());
}
/// <summary>
/// Выполнить код декоратора с вызовом следующего декоратора.
/// </summary>
/// <param name="lambda"> Выполняемый код. </param>
/// <returns></returns>
protected Task ExecuteAsync(Func<T, Task> lambda)
{
return lambda(NextDelegate());
}
}
Имея такой базовый класс, в декораторе, который его наследует, можно писать как то так
public Task<Response> MethodAsync(Request request)
{
return ExecuteAsync(async next =>
{
// code before
var result = await next.MethodAsync(request);
// code after
return result;
});
}
А если конкретный метод не надо оборачивать текущим декоратором, можно просто написать так
public Task<Response> MethodAsync(Request request)
{
return ExecuteAsync(next => next.MethodAsync(request));
}
Немного магии все же осталось. А именно — назначение свойства NextDelegate. Сходу не понятно, что это и как использовать, но опытный программист найдет, а неопытному надо 1 раз объяснить. Это как DbSet'ы в DbContext :)
Я не выкладывал это на гит хаб. Кода немного, он уже обобщенный, так что можно дергать прямо отсюда.
В заключении хочу ничего не говорить :)
===========
Источник:
habr.com
===========
Похожие новости:
- [.NET, Open source, Компьютерное железо, Разработка под Windows] LINKa смотри. Система выбора карточек при помощи айтрекера и не только
- [.NET, C#, Информационная безопасность] Медуза, паспорта и говнокод — почему номера паспортов всех участников интернет-голосования попали в Интернет
- [Visual Basic for Applications] Excel VBA — создаем свою панель инструментов
- [.NET, C#, Анализ и проектирование систем, Программирование] Применение CQRS & Event Sourcing в создании платформы для проведения онлайн-аукционов
- [.NET, C#, Высокая производительность, Параллельное программирование] System.Threading.Channels — высокопроизводительный производитель-потребитель и асинхронность без алокаций и стэк дайва
- [Разработка мобильных приложений, Разработка под iOS] Бюджетный DI на антипаттернах
- [.NET, C#, Программирование] Самые простые конечные автоматы или стейт-машины в три шага
- [.NET, C#, Программирование] Магические сигнатуры методов в C# (перевод)
- [Amazon Web Services, Kubernetes] Изоляция и бункер (Silos) для хранилищ данных в мультиарендных (multitenant) решениях
- [.NET, ASP, Логические игры, Разработка веб-сайтов, Разработка игр] Игра на WebAssembly, часть 2: уровни и опыт, админка
Теги для поиска: #_.net, #_c#, #_.net_core, #_.net, #_dependency_injection, #_aop, #_.net, #_c#
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:54
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
При разработке мы не редко сталкиваемся с ситуацией, когда при выполнении какой-либо бизнес-логики требуется записать логи, аудиты, разослать оповещения. В общем реализовать некоторый сквозной функционал. Когда масштабы производства небольшие, можно особо не усердствовать и все это делать прямо в методах. Постепенно, конструктор сервиса начинает обрастать входящими сервисами для выполнения БЛ и сквозного функционала. А это уже ILogger, IAuditService, INotifiesSerice. Не знаю как Вы, а я не люблю много инъекций и большие методы, которые выполняют много действий за раз. Можно накрутить на код какую либо реализацию АОП. В стеке .NET такие реализации делают инъекции в ваше приложение в нужные места, внешне похожи на магию 80 уровня и, зачастую, имеют проблемы с типизацией и отладкой. Я попытался найти золотую середину. Если данные проблемы не обошли вас стороной, добро пожаловать под кат. Спойлер. На самом деле, мне удалось решить чуть больше проблем, чем я описал выше. Например, я могу отдать разработку БЛ одному разработчику, а навешивание сквозного функционала и даже валидации входящих данных — другому одновременно. И помогли мне в этом декораторы и надстройка над DI. Кто-то далее скажет, что это прокси, с радостью обсужу это в комментах. Итак, что я хочу как разработчик?
Что я хочу, как проектировщик и лидер команды?
Поможет мне в этом вот такой интерфейс. /// <summary>
/// Обертка для сервиса. /// </summary> /// <typeparam name="T"> Класс сервиса. </typeparam> public interface IDecorator<T> { /// <summary> /// Делегат для работы декоратора. /// </summary> Func<T> NextDelegate { get; set; } } Можно использовать как то такSPLinterface IService
{ Response Method(Request request); } class Service : IService { public Response Method(Request request) { // BL } } class Wrapper : IDecorator<IService>, IService { public Func<IService> NextDelegate { get; set; } public Response Method(Request request) { // code before var result = NextDelegate().Method(request); // code after return result; } } Таким образом, действие у нас будет уходить в глубину. wrapper1 wrapper2 service end wrapper2 end wrapper1 Но, постойте. Это же уже есть в ООП и называется наследование. :D class Service {}
class Wrapper1: Service {} class Wrapper2: Wrapper1 {} Я как представил, что появится дополнительный сквозной функционал, который придется внедрять по всему приложению в середину или менять местами уже имеющиеся, так у меня на спине волосы дыбом встали. Но моя лень — это не уважительная причина. Уважительная причина в том, что будут большие проблемы при модульном тестировании функционала в классах Wrapper1 и Wrapper2, тогда как в моем примере NextDelegate можно просто замокать. Более того, у сервиса и каждой обертки свой собственный набор инструментов, которые инжектятся в конструктор, тогда как при наследовании последняя обертка обязана иметь ненужные инструменты, чтобы передать их родителям. Итак, подход принят, осталось придумать, где, как и когда назначать NextDelegate. Я решил, что самым логичным решением будет делать это там, где я регистрирую сервисы. (Startup.sc, по умолчанию). Вот как это выглядит в базовом вариантеSPLservices.AddScoped<Service>();
services.AddTransient<Wrapper1>(); services.AddSingleton<Wrapper2>(); services.AddSingleton<IService>(sp => { var wrapper2 = sp.GetService<Wrapper2>(); wrapper2.NextDelegate = () => { var wrapper1 = sp.GetService<Wrapper1>(); wrapper1.NextDelegate = () => { return sp.GetService<Service>(); }; return wrapper1; }; return wrapper2; }); В целом, все требования выполнены, но появилась другая проблема — вложенность. Эту проблему можно решить перебором или рекурсией. Но под капотом. Внешне все должно выглядеть просто и понятно. Вот чего мне удалось добитьсяSPLservices.AddDecoratedScoped<IService, Service>(builder =>
{ builder.AddSingletonDecorator<Wrapper1>(); builder.AddTransientDecorator<Wrapper2>(); builder.AddScopedDecorator<Wrapper3>(); }); А помогли мне в этом вот эти методы расширения А помогли мне в этом вот эти методы расширенияSPL/// <summary>
/// Методы расширения для декораторов. /// </summary> public static class DecorationExtensions { /// <summary> /// Метод регистрации декорируемого сервиса. /// </summary> /// <typeparam name="TDefinition"> Интерфейс сервиса. </typeparam> /// <typeparam name="TImplementation"> Реализация сервиса. </typeparam> /// <param name="lifeTime"></param> /// <param name="serviceCollection"> Коллекция сервисов. </param> /// <param name="decorationBuilder"> Построитель декораций. </param> /// <returns> Коллекцию сервисов после регистрации декораторов. </returns> public static IServiceCollection AddDecorated<TDefinition, TImplementation>( this IServiceCollection serviceCollection, ServiceLifetime lifeTime, Action<DecorationBuilder<TDefinition>> decorationBuilder) where TImplementation : TDefinition { var builder = new DecorationBuilder<TDefinition>(); decorationBuilder(builder); var types = builder.ServiceDescriptors.Select(k => k.ImplementationType).ToArray(); var serviceDescriptor = new ServiceDescriptor(typeof(TImplementation), typeof(TImplementation), lifeTime); serviceCollection.Add(serviceDescriptor); foreach (var descriptor in builder.ServiceDescriptors) { serviceCollection.Add(descriptor); } var resultDescriptor = new ServiceDescriptor(typeof(TDefinition), ConstructServiceFactory<TDefinition>(typeof(TImplementation), types), ServiceLifetime.Transient); serviceCollection.Add(resultDescriptor); return serviceCollection; } /// <summary> /// Метод регистрации декорируемого сервиса с временем жизни Scoped. /// </summary> /// <typeparam name="TDefinition"> Интерфейс сервиса. </typeparam> /// <typeparam name="TImplementation"> Реализация сервиса. </typeparam> /// <param name="serviceCollection"> Коллекция сервисов. </param> /// <param name="decorationBuilder"> Построитель декораций. </param> /// <returns> Коллекцию сервисов после регистрации декораторов. </returns> public static IServiceCollection AddDecoratedScoped<TDefinition, TImplementation>( this IServiceCollection serviceCollection, Action<DecorationBuilder<TDefinition>> decorationBuilder) where TImplementation : TDefinition { return serviceCollection.AddDecorated<TDefinition, TImplementation>(ServiceLifetime.Scoped, decorationBuilder); } /// <summary> /// Метод регистрации декорируемого сервиса с временем жизни Singleton. /// </summary> /// <typeparam name="TDefinition"> Интерфейс сервиса. </typeparam> /// <typeparam name="TImplementation"> Реализация сервиса. </typeparam> /// <param name="serviceCollection"> Коллекция сервисов. </param> /// <param name="decorationBuilder"> Построитель декораций. </param> /// <returns> Коллекцию сервисов после регистрации декораторов. </returns> public static IServiceCollection AddDecoratedSingleton<TDefinition, TImplementation>( this IServiceCollection serviceCollection, Action<DecorationBuilder<TDefinition>> decorationBuilder) where TImplementation : TDefinition { return serviceCollection.AddDecorated<TDefinition, TImplementation>(ServiceLifetime.Singleton, decorationBuilder); } /// <summary> /// Метод регистрации декорируемого сервиса с временем жизни Transient. /// </summary> /// <typeparam name="TDefinition"> Интерфейс сервиса. </typeparam> /// <typeparam name="TImplementation"> Реализация сервиса. </typeparam> /// <param name="serviceCollection"> Коллекция сервисов. </param> /// <param name="decorationBuilder"> Построитель декораций. </param> /// <returns> Коллекцию сервисов после регистрации декораторов. </returns> public static IServiceCollection AddDecoratedTransient<TDefinition, TImplementation>( this IServiceCollection serviceCollection, Action<DecorationBuilder<TDefinition>> decorationBuilder) where TImplementation : TDefinition { return serviceCollection.AddDecorated<TDefinition, TImplementation>(ServiceLifetime.Transient, decorationBuilder); } /// <summary> /// Метод /// </summary> /// <typeparam name="TService"></typeparam> /// <param name="implType"></param> /// <param name="next"></param> /// <returns></returns> private static Func<IServiceProvider, TService> ConstructDecorationActivation<TService>(Type implType, Func<IServiceProvider, TService> next) { return x => { var service = (TService) x.GetService(implType); if (service is IDecorator<TService> decorator) decorator.NextDelegate = () => next(x); else throw new InvalidOperationException("Ожидался декоратор"); return service; }; } /// <summary> /// Создание фабрики для декорируемого сервиса. /// </summary> /// <typeparam name="TDefinition"> Тип контракта сервиса. </typeparam> /// <param name="serviceType"> Тип реализации сервиса. </param> /// <param name="decoratorTypes"> Типы делегатов в требуемом порядке. </param> /// <returns> Фабрику создания сервиса через DI. </returns> private static Func<IServiceProvider, object> ConstructServiceFactory<TDefinition>(Type serviceType, Type[] decoratorTypes) { return sp => { Func<IServiceProvider, TDefinition> currentFunc = x => (TDefinition) x.GetService(serviceType); foreach (var decorator in decoratorTypes) { currentFunc = ConstructDecorationActivation(decorator, currentFunc); } return currentFunc(sp); }; } } Теперь немного сахара для функциональщиков Теперь немного сахара для функциональщиковSPL/// <summary>
/// Базовый класс декоратора. /// </summary> /// <typeparam name="T"> Тип декорируемого сервиса. </typeparam> public class DecoratorBase<T> : IDecorator<T> { /// <summary> /// Делегат для получения следующего декоратора или сервиса. /// </summary> public Func<T> NextDelegate { get; set; } /// <summary> /// Выполнить код декоратора с вызовом следующего декоратора. /// </summary> /// <typeparam name="TResult"> Тип возвращаемого значения. </typeparam> /// <param name="lambda"> Выполняемый код. </param> /// <returns></returns> protected Task<TResult> ExecuteAsync<TResult>(Func<T, Task<TResult>> lambda) { return lambda(NextDelegate()); } /// <summary> /// Выполнить код декоратора с вызовом следующего декоратора. /// </summary> /// <param name="lambda"> Выполняемый код. </param> /// <returns></returns> protected Task ExecuteAsync(Func<T, Task> lambda) { return lambda(NextDelegate()); } } Имея такой базовый класс, в декораторе, который его наследует, можно писать как то так public Task<Response> MethodAsync(Request request)
{ return ExecuteAsync(async next => { // code before var result = await next.MethodAsync(request); // code after return result; }); } А если конкретный метод не надо оборачивать текущим декоратором, можно просто написать так public Task<Response> MethodAsync(Request request)
{ return ExecuteAsync(next => next.MethodAsync(request)); } Немного магии все же осталось. А именно — назначение свойства NextDelegate. Сходу не понятно, что это и как использовать, но опытный программист найдет, а неопытному надо 1 раз объяснить. Это как DbSet'ы в DbContext :) Я не выкладывал это на гит хаб. Кода немного, он уже обобщенный, так что можно дергать прямо отсюда. В заключении хочу ничего не говорить :) =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:54
Часовой пояс: UTC + 5