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

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

Выглядит, как переизобретенная концепция Middleware… Или я что-то упустил?

Скорее, как переиспользованная там, где ее раньше не использовали. По крайней мере я об этом не слышал.
Autofac умеет это "из коробки".

код
interface IService1
	{
		void Do();
	}

	class Service1:IService1
	{
		public void Do()
		{
			Console.WriteLine("Service1:In do method");
		}
	}

	class ServiceDecorator1:IService1
	{
		private readonly IService1 _decoratedService;
		
		public ServiceDecorator1(IService1 decoratedService)
		{
			this._decoratedService = decoratedService;
		}

		public void Do()
		{
			Console.WriteLine("Start decorator1");
			_decoratedService.Do();
			Console.WriteLine("Finish decorator1");
		}
	}

	class ServiceDecorator2 : IService1
	{
		private readonly IService1 _decoratedService;

		public ServiceDecorator2(IService1 decoratedService)
		{
			this._decoratedService = decoratedService;
		}

		public void Do()
		{
			Console.WriteLine("Start decorator2");
			_decoratedService.Do();
			Console.WriteLine("Finish decorator2");
		}
	}

	class Program
	{
		static void Main(string[] args)
		{
			var containerBuilder = new ContainerBuilder();

			containerBuilder.RegisterType<Service1>().As<IService1>();
			containerBuilder.RegisterDecorator<ServiceDecorator1, IService1>();
			containerBuilder.RegisterDecorator<ServiceDecorator2, IService1>();

			var container = containerBuilder.Build();

			var service1 = container.Resolve<IService1>();

			service1.Do();

			Console.WriteLine("Done");
			Console.ReadKey();
		}
	}



Вывод
Start decorator2
Start decorator1
Service1:In do method
Finish decorator1
Finish decorator2


И плюс всякие плюшки типа условного декорирования и т.д.
Я, наверное, очень невнимателен, но не нашел в документации про указание времени жизни декораторов.
The lifetime of a decorator is tied to the lifetime of the thing it decorates.

Т.е. по умолчанию какой тип времени жизни у сервиса, такой же и у его декоратора.
Но если требуется задавать разные периоды жизни декораторов, то можно воспользоваться Proxy объектами.
Код
interface IService1
	{
		void Do();
	}
	
	class Service1:IService1
	{
		/// <summary>Initializes a new instance of the <see cref="T:System.Object" /> class.</summary>
		public Service1()
		{
			
		}

		public void Do()
		{
			Console.WriteLine($"Service1 In Do method");
		}
	}

	class ServiceDecorator1:IService1
	{
		private static int Seed;

		private readonly int Id;

		private readonly IService1 _decoratedService;
		
		public ServiceDecorator1(IService1 decoratedService)
		{
			this._decoratedService = decoratedService;
			Id = ++Seed;
		}

		public void Do()
		{
			Console.WriteLine($"Start decorator1 'per dependency'' - {Id}" );
			_decoratedService.Do();
			Console.WriteLine($"Finish decorator1 'per dependency'' - {Id}");
		}
	}

	class ServiceDecorator2Proxy
	{
		private static int Seed;

		private readonly int Id;

		public ServiceDecorator2Proxy()
		{
			Id = ++Seed;
		}

		public void Do(IService1 service1)
		{
			Console.WriteLine($"Start decorator2 'singleton' - {Id}");
			service1.Do();
			Console.WriteLine($"Start decorator2 'singleton' - {Id}");
		}
	}

	class ServiceDecorator2 : IService1
	{
		private readonly ServiceDecorator2Proxy _decoratorProxy;

		private readonly IService1 _decoratedService;

		public ServiceDecorator2(ServiceDecorator2Proxy decoratorProxy, IService1 decoratedService)
		{
			this._decoratorProxy = decoratorProxy;
			_decoratedService = decoratedService;
		}

		public void Do()
		{
			_decoratorProxy.Do(_decoratedService);
		}
	}

	class Program
	{
		static void Main(string[] args)
		{
			var containerBuilder = new ContainerBuilder();

			containerBuilder.RegisterType<Service1>().As<IService1>().InstancePerDependency();
			containerBuilder.RegisterType<ServiceDecorator2Proxy>().AsSelf().SingleInstance();
			containerBuilder.RegisterDecorator<ServiceDecorator1, IService1>();
			containerBuilder.RegisterDecorator<ServiceDecorator2, IService1>();

			var container = containerBuilder.Build();

			var service1 = container.Resolve<IService1>();
			var service2 = container.Resolve<IService1>();

			service1.Do();
			service2.Do();

			Console.WriteLine("Done");
			Console.ReadKey();
		}
	}



Тут как раз получается, что ServiceDecorator1 — время жизни такое как и у Service1, а у ServiceDecorator2 (точнее тот Proxy класс метод которого он вызывает) — singleton.
К моему великому стыду, я не знал, что автофак умеет декораторы. Но исполнение мне не очень нравится.
Во-первых, мне не нравится, что в подходе с декораторами приходится плодить столько классов. Возникают вопросы, как лучше организовать структуру проекта, чтобы не утонуть в этом. А тут еще и прокси добавляются.
Во-вторых, инъекция через конструктор не позволяет управлять временем жизни. Предложенным прокси вы оборачиваете синглтон в трансиент, но не сможете обернуть трансиент в синглтон. В моем же инструменте это возможно. Более того, он и разрабатывался так, чтобы обойти это ограничение. Именно поэтому у меня NextDelegate вместо Next.
столько классов.

Это в случае если требуется разный lifitime, но я с трудом представляю когда это может понадобится.
сможете обернуть трансиент в синглтон.

Autofac тоже это позволяет. вот код.

Код
interface IService1
	{
		void Do();
	}
	
	class Service1:IService1
	{
		/// <summary>Initializes a new instance of the <see cref="T:System.Object" /> class.</summary>
		public Service1()
		{
			
		}

		public void Do()
		{
			Console.WriteLine($"Service1 In Do method");
		}
	}

	class ServiceDecorator1:IService1
	{
		private static int Seed;

		private readonly int Id;

		private readonly IService1 _decoratedService;
		
		public ServiceDecorator1(IService1 decoratedService)
		{
			this._decoratedService = decoratedService;
			Id = ++Seed;
		}

		public void Do()
		{
			Console.WriteLine($"Start decorator1 'singleton'' - {Id}" );
			_decoratedService.Do();
			Console.WriteLine($"Finish decorator1 'singleton'' - {Id}");
		}
	}

	class ServiceDecorator2Proxy
	{
		private static int Seed;

		private readonly int Id;

		public ServiceDecorator2Proxy()
		{
			Id = ++Seed;
		}

		public void Do(IService1 service1)
		{
			Console.WriteLine($"Start decorator2 'per dependency' - {Id}");
			service1.Do();
			Console.WriteLine($"Finish decorator2 'per dependency' - {Id}");
		}
	}

	class ServiceProxyFabric<TService>
	{
		private readonly IComponentContext _context;

		/// <summary>Initializes a new instance of the <see cref="T:System.Object" /> class.</summary>
		public ServiceProxyFabric(IComponentContext context)
		{
			_context = context;
		}

		public TService GetService()
		{
			return _context.Resolve<TService>();
		}
	}

	class ServiceDecorator2 : IService1
	{
		private readonly ServiceProxyFabric<ServiceDecorator2Proxy> _serviceProxyFabric;

		private readonly IService1 _decoratedService;

		public ServiceDecorator2(ServiceProxyFabric<ServiceDecorator2Proxy> serviceProxyFabric, IService1 decoratedService)
		{
			this._serviceProxyFabric = serviceProxyFabric;
			_decoratedService = decoratedService;
		}

		public void Do()
		{
			_serviceProxyFabric.GetService().Do(_decoratedService);
		}
	}

	class Program
	{
		static void Main(string[] args)
		{
			var containerBuilder = new ContainerBuilder();
			
			containerBuilder.RegisterGeneric(typeof(ServiceProxyFabric<>));

			//containerBuilder.RegisterType<Service1>().As<IService1>().InstancePerDependency();
			//containerBuilder.RegisterType<ServiceDecorator2Proxy>().AsSelf().SingleInstance();

			containerBuilder.RegisterType<Service1>().As<IService1>().SingleInstance();
			containerBuilder.RegisterType<ServiceDecorator2Proxy>().AsSelf().InstancePerDependency();

			containerBuilder.RegisterDecorator<ServiceDecorator1, IService1>();
			containerBuilder.RegisterDecorator<ServiceDecorator2, IService1>();
			

			var container = containerBuilder.Build();

			var service1 = container.Resolve<IService1>();
			var service2 = container.Resolve<IService1>();

			service1.Do();
			service2.Do();

			Console.WriteLine("Done");
			Console.ReadKey();
		}
	}



моем же инструменте это возможно

Вот именно — это ваш инструмент, пока я не увидел кейсов, где стандартными средствами (пускай и с некоторым бойлерплейтом) нельзя решить ту или иную проблему.
пока я не увидел кейсов, где стандартными средствами (пускай и с некоторым бойлерплейтом) нельзя решить ту или иную проблему

Ну например, у меня ASP NET Core приложение и я использую другую IoC реализацию, потому что она умеет то, что не умеют другие. И если я перейду на автофак ради декораторов, потеряю другие плюшки. Использовать 2 различные реализации, ну такое себе, к тому же их еще надо подружить. Мое же решение использует базовые абстракции, на которых построен DI в ASP NET Core и оно не зависит от реализации под капотом. Как вам кейс?

А что вообще понимается под "обернуть трансиент в синглтон"?


Так-то Func<...> в автофаке тоже можно запрашивать, более того — он эту фабрику автоматически создаст.

делают инъекции в ваше приложение в нужные места, внешне похожи на магию 80 уровня и, зачастую, имеют проблемы с типизацией и отладкой.

Третий закон Кларка гласит "Любая достаточно развитая технология неотличима от магии".
C DI движками действительно может быть непросто порой в отладке, но в целом это весьма мощная и развитая технология, которую нужно осваивать, иначе так и останется магией, которой весьма просто по неумению выстрелить себе в ногу.


Разумеется, явное гораздо удобнее неявного, поэтому DI движки критиковать легко… там действительно сложно разобраться почему так, а не иначе.

Что вы что вы! Я ни в коем случае не критикую DI. Более того, я сам его активно использую, как видите. Я про инъекцию IL говорил.
Иметь возможность в юнит тестах тестировать только БЛ. Причем я не люблю делать 100500 моков, чтобы отключить весь вспомогательный функционал. 2-3 — еще ладно, но больше не хочу.

Есть AutoMocker, чтобы не делать 100500 моков всяких логгеров.
ну моки всяких там логгеров можно просто заранее определить. Хоть в статике. И использовать их. Я же говорю про случаи, когда ваш конструктор принимает 10 аргументов, но в тестируемом методе используется только один. Иди еще разберись, нужен ли именно этот сервис в тестируемом методе. Это тот случай, когда сервис нужно декомпозировать. Но как декомпозировать сервис, который помимо полезной нагрузки должен еще чего то куда то записывать, открывать закрывать транзакции, отправлять СМС и майнить криптовалюту? Ответ тут только один. Он и не должен. Но вот разгрузить его бывает проблематично.

Или FakeItEasy

Есть чудесная библиотека Scrutor в которой есть метод Decorate, похоже на него.

Круто. Сразу умеет декорировать на стороне, но я все еще не понял, как задать время жизни декораторов. Похоже, мы с авторами предложенных выше библиотек преследовали разные цели.

В каких случаях необходимо задавать разные времена жизни декоратора и декорируемого?

Ну, например, сервис использует DbContext, который по умолчанию Scoped. Его можно зарегать и с другим временем жизни, но по ряду причин это делать не следует. Получается, ваш сервис уже не может быть синглтоном. А зачем вы хотели синглтон? Ну, скажем, ваш сервис долго инициализируется (какой нибудь вспомогательный функционал, который сложно вынести наружу). И вот, у вас появились декораторы. Вы выносите этот вспомогательный функционал в декоратор, и вам контекст базы ни к чему. Так почему бы не зарегать его (декоратор) синглтоном? У вас запрос ускорится с 2 секунд до 100 мс (цифры с потолка, есессно). Вы бы этого хотели?

Еще пример, ваш декоратор стейтфул.

Тут вопрос в том, к какому скоупу должен быть привязан DbContext. Если речь идет о скоупе запроса — то ваше решение содержит ошибку, поскольку в ASP.NET Core нет способа "восстановить" потеряный скоуп.


Если же декоратор сам волен создавать скоуп — то ваш вызов NextDelegate() всё равно содержит ошибку, потому что кто вызывать Dispose() скоупу будет?


Если вам понадобилось декорировать Scoped синглтоном — у вас что-то сильно не так с архитектурой. И лучше бы сначала это "что-то" исправить, а уже потом обходные пути искать.

Если речь идет о скоупе запроса — то ваше решение содержит ошибку, поскольку в ASP.NET Core нет способа «восстановить» потеряный скоуп.
Не совсем понял. Я не пытаюсь искать никакой скоуп. Корка создает его во время запроса.

Если же декоратор сам волен создавать скоуп — то ваш вызов NextDelegate() всё равно содержит ошибку, потому что кто вызывать Dispose() скоупу будет?
И тут не понял. Вызывает диспоз в 99% случаев тот, кто создает IDisposable(). Если кто-то в декораторе решит поиграться со скопами на свой страх и риск для каких-то нетривиальных задач, то он же его и задиспозит. В общем я не понимаю описанную проблему.

Если вам понадобилось декорировать Scoped синглтоном — у вас что-то сильно не так с архитектурой. И лучше бы сначала это «что-то» исправить, а уже потом обходные пути искать.
Тут я не согласен, это больше похоже на предположени основанное на техническом ограничении инжекта сервиса с меньшим временем жизни в большее. Но это именно техническое ограничение, а не логическое. Да и что лучше сделать сначала решать менеджерам. Скорее всего, они выберут «че побыстрее».
Не совсем понял. Я не пытаюсь искать никакой скоуп. Корка создает его во время запроса.

Вот его-то, созданный коркой во время запроса скоуп, вы и теряете в синглтоне.


И тут не понял. Вызывает диспоз в 99% случаев тот, кто создает IDisposable().

Тот, кто создал IDisposable, не знает в какой момент он перестанет быть вам нужен. У потребителя должен быть способ уведомить об этом поставщика, и ваш NextDelegate этого способа не предоставляет.


Да и что лучше сделать сначала решать менеджерам.

С каких пор менеджеры решают вопросы архитектуры?

Вот его-то, созданный коркой во время запроса скоуп, вы и теряете в синглтоне.
Не. Там же делегат, который является фабрикой и возвращает следующий экземпляр. И он все еще выполняется в скоупе. Вы воспроизвели то, о чем сказали? Если да, стоит подумать над решением. Если нет, я завтра могу попробовать либо найти использование, которое уже работает. Но навскидку проблем не вижу.

Тот, кто создал IDisposable, не знает в какой момент он перестанет быть вам нужен. У потребителя должен быть способ уведомить об этом поставщика, и ваш NextDelegate этого способа не предоставляет.
Я опять не понял.
using(var disp = new MyDisposable())
{
    // disp usage
} // dispose


С каких пор менеджеры решают вопросы архитектуры?
С тех пор как они управляют деньгами. Я либо в ограниченном бюджете и сам не полезу в глубокий рефактор, если нет на это средств, или я знаю, что есть возможность расширить бюджет и иду к мэнеджеру с предложением, описываю проблему, предлагаю решения, менеджер выбирает.
Там же делегат, который является фабрикой и возвращает следующий экземпляр. И он все еще выполняется в скоупе.

А откуда делегат узнает про скоуп?


Я опять не понял.

foo.NextDelegate = () => {
    using(var disp = new MyDisposable())
    {
        return disp; // Не работает :-(
    }
}
А откуда делегат узнает про скоуп?

Мы как будто на разных языках говорим :)
Делегат не знает про скоуп, он внутри него выполняется. И внутри него вызывает GetService()
Скоуп создается перед вызовом метода контроллера и диспозится после его окончания. Все что тянется из контейнера, зареганное как скоуп, тянется из этого скоупа. И вот этот скоуп как раз ничего не знает про то место, откуда он используется. Из синглтона или еще откуда.

// Не работает :-(

Так ясное дело оно не работает, вы диспозите объект, который хотите вернуть. А зачем? В мой делегат он попадает из контейнера. Контейнер его создает, он же и диспозит, по окончании времени жизни.
Делегат не знает про скоуп, он внутри него выполняется.

Что вы вкладываете в понятие "он внутри него выполняется"? Скоуп — это объект, делегат не может выполняться внутри объекта.


Если у делегата есть ссылка на объект-скоуп — то он может создать заресолвить сервис в этой скоупе, это я и называю "знанием про скоуп". Если у делегата нет ссылки на скоуп, то всё что он может сделать — это создать новый скоуп, но в этом случае возникает проблема освобождения скоупа.


Так всё-таки, у вашего делегата ссылка на скоуп есть или её нет? Если есть — то откуда? Если нет — то кто вызывает Dispose?


Так ясное дело оно не работает, вы диспозите объект, который хотите вернуть. А зачем? В мой делегат он попадает из контейнера. Контейнер его создает, он же и диспозит, по окончании времени жизни.

А контейнер откуда знает, когда его диспозить?

Что вы вкладываете в понятие «он внутри него выполняется»? Скоуп — это объект, делегат не может выполняться внутри объекта.
Делегат тоже объект, но это объект, который выполняется. Бредово звучит, согласен. :)
У нас ООП, поэтому у нас все является объектами. Транзакция тоже объект, например, который описывает операцию, в рамках которой что-то выполняется. Скоуп, кстати — «рамки» в переводе с инглиша. Я в этом смысле.

Если у делегата есть ссылка на объект-скоуп — то он может создать заресолвить сервис в этой скоупе, это я и называю «знанием про скоуп». Если у делегата нет ссылки на скоуп, то всё что он может сделать — это создать новый скоуп, но в этом случае возникает проблема освобождения скоупа.

Тут все немного сложнее. Фабрика для резольва сервиса выглядит как Func<IServiceProvider, TService>. Вопрос, откуда берется IServiceProvider, который корка передает в эту фабрику. Ответ — из скоупа. А откуда берется скоуп? Корка создает его на основе рута, перед резольвом контейнера.
Отсюда ответ на следующий вопрос.

Так всё-таки, у вашего делегата ссылка на скоуп есть или её нет? Если есть — то откуда? Если нет — то кто вызывает Dispose?

Она мне не нужна.

А контейнер откуда знает, когда его диспозить?

Так я же время жизни задаю. Все что Scoped и ниже, диспозится вместе со скопом, синглтоны диспозятся вместе с контейнером.

Но, если я сохраню результат NextDelegate() в переменную в синглтоне и буду использовать его, то вот тут будут проблемы. Поэтому я и добавил базовый класс декоратора, в котором NextDelegate выполняется каждый раз при попытке получить следующий сервис. Остальное на совести разработчика. Но даже если ему не получится объяснить почему, для начала можно просто предложить запомнить.
Тут все немного сложнее. Фабрика для резольва сервиса выглядит как Func<IServiceProvider, TService>. Вопрос, откуда берется IServiceProvider, который корка передает в эту фабрику. Ответ — из скоупа.

А вот нифига не так. Если ваш сервис — синглтон, то IServiceProvider в этой фабрике не будет привязан ни к одному скоупу. А значит, ваш Scoped-сервис также будет иметь время жизни как синглтон. В частности, ваш NextDelegate будет возвращать каждый раз одно и то же значение.


Ну и зачем в таком случае весь огород был?

Если ваш сервис — синглтон, то IServiceProvider в этой фабрике не будет привязан ни к одному скоупу.
Почему это? Сам синглтон будет создан той же самой фабрикой, что и скопд.

А значит, ваш Scoped-сервис также будет иметь время жизни как синглтон.
Нет, я же специально для этого сделал делегат-фабрику для сервиса вместо просто сервиса. Более того, сама эта фабрика Transient. В описанном мной коде это видно.

Ну и зачем в таком случае весь огород был?

Вы явно не поняли задумку.
Нет, я же специально для этого сделал делегат-фабрику для сервиса вместо просто сервиса.

А дальше, дальше-то что? Эта фабрика не может работать как задумывалось, просто по построению.


В описанном мной коде это видно.

Было бы видно — я бы не спорил.

Предложенное мной решение выглядит элементарным, на первый взгляд. Но уверяю вас, там все устроено сложнее. Посмотрите внимательно на методы расширения, которые регистрируют сервис с его декораторами.

Возможно, мне следовало снабдить код подробными комментариями.

Происходит вот что.
Вы пытаетесь заинжектить сервис.
Создается фабрика, которая резольвит верхний декоратор. В эту фабрику передается сервис провайдер, созданный коркой из скоупа. Далее этот сервис провайдер передается везде по замыканию. Эта фабрика так же создает фабрику, которая зарезольвит следующий уровень декоратора (либо сам сервис), положит эту фабрику в NextDelegate и вернет этот декоратор. Каждая последующая фабрика по рекурсии сделает то же самое.

Предположим, у нас такая расстановка. Декоратор синглтон, декоратор скопд, сервис трансиент.
Вы инжектите в своем коде сервис. Отрабатывает фабрика и возвразщает вам синглтон декоратор, которому назначает фабрику на следующий уровень.
После этого вы выполняете фабрику. Она (тк сервис провайдер находится в рамках скоупа) создает новый экземпляр скопд декоратора и назначает ему фабрику для следующего уровня (это уже трансиент сервис).
В скопд декораторе вы выполняете делегат и получаете трансиент сервис.

Далее, вы в рамках того же скоупа снова инжектите сервис.
Вновь создается фабрика (она ведь трансиент) и резольвит верхний декоратор. Так как он синглтон, то снова не создается (лайфтайм саксесс :)).
Далее эта фабрика назначает ему фабрику следующего уровня. Хоть она уже и была назначена ранее, это ни на что не повлияет.
Вы выполняете делегат. Делегат, используя сервис провайдер, резольвит следующий уровень. Но он ведь скоупд и в рамках данного скоупа (запроса) уже создан. Возвращается тот же самый экземпляр, что был в предыдущей итерации. Но мы этого и хотели добиться, зарегистрировав его как скоупд (лайфтайм саксесс :)).
В этом декораторе мы фабрикой уже резольвим сам сервис, но он трансиент, он создается по новой (лайфтайм саксесс :)).

Далее, в следующем запросе, а значит в новом скоупе, мы получим новый экземпляр скопд декоратора.
После этого вы выполняете фабрику. Она (тк сервис провайдер находится в рамках скоупа)

Каким образом он находится в рамках скоупа, если это корневой сервис провайдер? (А если не корневой — то это ошибка, которую надо репортить на гитхаб)


Вновь создается фабрика (она ведь трансиент) и резольвит верхний декоратор. Так как он синглтон, то снова не создается (лайфтайм саксесс :)).
Далее эта фабрика назначает ему фабрику следующего уровня. Хоть она уже и была назначена ранее, это ни на что не повлияет.

Вы хотите сказать, что заменяете NextDelegate на каждый запрос? Это ж такой трындец...


Хорошо если оно и правда ни на что не повлияет (но тогда у вас ничего не будет работать как задумывалось). Если же на этом ваш механизм и строится — то у вашего веб-приложения будут проблемы с обработкой нескольких запросов одновременно.

Каким образом он находится в рамках скоупа, если это корневой сервис провайдер?

Вот, теперь я, хотя бы, понимаю о чем весь спор.

Нет, сервис провайдер передается в фабрику через замыкание и он используется на всей рекурсии. А так как фабрика зарегана на уровне жизни меньшем, чем синглтон (в моем случае трансиент), провайдер берется из скоупа. В противном случае, через него не удалось бы создать ни один скоупд сервис (будет экзепшн при попытке создать скоупд сервис из рута). А они создаются. Код рабочий и уже в проде.

В таком случае у вас в каждом из синглтонов — гонка, и при одновременных запросах NextDelegate может вернуть сервис не из скоупа текущего запроса, а из соседнего.


Это настолько глупое решение, что я даже не мог предположить что его кто-то сделает.


Код рабочий и уже в проде.

Сочувствую.

В таком случае у вас в каждом из синглтонов — гонка
Да, я пока все это писал, уже понял, что гонка.

Это настолько глупое решение, что я даже не мог предположить что его кто-то сделает.
Воу воу, палехче. Тут не надо ничего предполагать, код то перед глазами. Оказалось, в него надо было посмотреть чуть внимательнее и мне и вам. В итоге, в результате дискуссии, был найден изъян и это замечательно. Подумаю, как устранить гонку, главное, что она локализована.

Сочувствую.
Слава богу, никто до сих пор не регал синглтон декораторы. А понижение со скоупд на трансиент не грозит гонкой в данном случае. :)
код то перед глазами

Но не весь, DecorationBuilder-то не приведен.


Подумаю, как устранить гонку, главное, что она локализована.

Не менять NextDelegate после создания объекта. И использовать строго провайдер самого объекта для резолва его зависимостей, а не какой-то внешний.


Слава богу, никто до сих пор не регал синглтон декораторы.

А ведь именно синглтонами было обосновано такое странное решение с NextDelegate. Для Scoped и Transient-обёрток достаточно простого Next.

DecorationBuilder-то не приведен.
Кстати да. Хоть в нем и 5 строк полезного кода, надо его добавить.

Не менять NextDelegate после создания объекта. И использовать строго провайдер самого объекта для резолва его зависимостей, а не какой-то внешний.
Но тогда возникнет описанная вами ранее проблема. Провайдер для синглтона будет из рута.

А ведь именно синглтонами было обосновано такое странное решение с NextDelegate. Для Scoped и Transient-обёрток достаточно простого Next.
Да, интересная ситуация. Это потому, что необходимый вспомогательный функционал с долгой инициализацией, зареганный как синглтон, подключается через конструктор, а не как декоратор. :D
Но тогда возникнет описанная вами ранее проблема. Провайдер для синглтона будет из рута.

Ага, но это естественное ограничение вашего интерфейса декоратора. Чтобы исправить его, не нарушая архитектуры ASP.NET Core — надо исправлять интерфейс.


К примеру, можно взять класс Owned из Autofac и заставить делегат возвращать Owned<T> вместо простого T. В таком случае вы пойдете по пути создания своих скоупов синглтоном.


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

И это наиболее простое и правильное решение! Вынести нужную часть из декоратора в синглтон гораздо проще, чем "правильно" пользоваться NextDelegate.

По сравнению с AOP очень много бойлерплейта.
Допустим, у меня сервис с 15 методами, в каждом из которых 5-6 параметров.
Чтобы его обернуть в логгер, придётся писать логгер-враппер с теми же 15 методами.

А с AOP можно сдедать универсальный логгер, который можно навесить абсолютно к любому сервису одной строчкой (атрибут на класс-реализацию сервиса). Единственное требование: все параметры и результат должны уметь делать адекватный ToString(), чтобы красиво попасть в лог.
Верно. Но в большинстве случаев, если у вас такой большой сервис, его лучше декомпозировать, так как он выполняет слишком много задач. God сервис такой получается.

А с AOP можно сдедать универсальный логгер, который можно навесить абсолютно к любому сервису одной строчкой (атрибут на класс-реализацию сервиса)
Да да, и при этом надо позаботиться, чтобы в ваш сквозной метод пришли все необходимые параметры. Будем принимать массив и разбирать по ходу? Явно нет. Создадим метод, который будет 1 в 1 как требуемый? Ну так вам придется поддерживать сразу в нескольких местах. При изменении определения метода, все сквозные надо будет так же не забыть поменять, а если забудете, то узнаете об этом в рантайме и если повезет, то сразу. Причем еще потребуется магия с инжектом в IL. Да еще и с отладкой бывают интересные штуки. А как обстоят дела с DI в классы, где будут ваши методы АОПовские?
Верно. Но в большинстве случаев, если у вас такой большой сервис, его лучше декомпозировать, так как он выполняет слишком много задач. God сервис такой получается.
Да абсолютно неважно. Пусть будет 3 метода и 5 классов, или 1 метод, но 15 классов — бойлерплейта столько же.
вно нет. Создадим метод, который будет 1 в 1 как требуемый? Ну так вам придется поддерживать сразу в нескольких местах. При изменении определения метода, все сквозные надо будет так же не забыть поменять, а если забудете, то узнаете об этом в рантайме и если повезет, то сразу
Это не так работает. Обёртка получает параметры в виде массива, чтобы например по ним проийтись и вывести в лог, возможно, модифицировать и получает лямбду Func<object, object[]>, которую надо вызвать вместо исходного метода. Ошибиться невозможно.
Допустим, был такой код:
[Logging]
int test(int a, string b) {  ... какой-то код ... }
Кодогенератор развернёт этот атрибут так:
int test(int a, string b)
{ 
    return (int)LoggingAttribute.OnInvoke(
        new object[] {a,b}, 
        (object[] p) => (object)test_original((int)p[0], (string)p[1]));
 }
int test_original(int a, string b) { ... какой-то код ... }

Внутри нашего атрибута мы пишем:
object OnInvoke(object[] p,  Func<object, object[]> original)
{
    foreach (var x in p) { Console.WriteLine("param is " + x); }
    var s = new Stopwatch(); s.Start();
    var result = original(p);
    Console.WriteLine("result is " + result + ", time is " + s.ElapsedMilliseconds);
    return result;
}

Да еще и с отладкой бывают интересные штуки
Идеально всё отлаживается. Можно сделать, чтобы step into заходил в OnInvoke и там можно вручную дойти до строчки original(p), и step into на ней зайдёт в ф-цию test, либо можно пометить этот метод атрибутом, и тогда step into прошагает весь инфраструктурный код, и начнёт сразу с тела оригинальной ф-ции test.
А как обстоят дела с DI в классы, где будут ваши методы АОПовские?
Тут AOP вообще никак не влияет. Если инъекция через поля-свойства, код не меняется, если через конструктор, все аргументы прозрачно пробрасываются (и то, если захотим оборачивать конструктор, а вообще конструктор как правило не оборачивается, нет смысла)
object OnInvoke(object[] p, Func<object, object[]> original)

Отлично. После этого я меняю определение метода на такое
int test(int a, string c, string b = null)

и по запаре или так как я впервые вижу этот проект забываю изменить код АОПовской функции. Никакие связи никогда не приведут меня в OnInvoke. В вашем примере, конечно, это ничего не испортит, но так ведь никто не логирует. У нас же бизнес задача, и лог должен быть соответствующий, каждый параметр несет в себе какую-то информацию, а не просто голый текст и числа. То есть у нас будет логирование через индексацию (p[0], p[1] вместо foreach). Вместо параметра b в p[1] теперь упадет c, который имеет тот же тип данных. Логи мы обычно читаем, когда что-то идет не так и спустя полгода (или сколько там ваше приложение работает без сбоев) вы обнаружите, что логи то не информативные. И, казалось бы, что тут такого. Меняю OnInvoke, перезапускаю, вижу верные логи, профит, ничего страшного. Но что если это не просто логи, а аудит безопасности? И залезли вы в них, чтобы узнать кто из пользователей мог слить инсайдерскую информацию.

Кстати, я видел реализацию АОП и поинтереснее, у которой проблем с типизацией не было. Она требует определять сквозные методы с теми же параметрами, что и основной метод. В этом случае вы получите исключение уже при запуске приложения.

Да абсолютно неважно. Пусть будет 3 метода и 5 классов, или 1 метод, но 15 классов — бойлерплейта столько же.
Важно. 3 из 5 классов вам вообще не придется оборачивать, там не нужно никакого логирования. Зато нужна какая-нибудь другая обертка. В случае с 1 же классом у вас было бы 2 обертки, у которых действительно очень много неиспользуемых методов.
А если будет 15 классов по 1 методу, так тут вообще все точечно. Вы ведь точно знаете, какие методы чем должны быть обернуты, так что декораторов будет ровно столько, сколько необходимо.

Да и к тому же, неиспользуемые методы дают разве что визуальный мусор. Они не внесут никаких непоняток.Разработчик увидит, что обертка для транзакций не открывает транзакцию на чтение данных из БД и у него не возникнет вопросов.
Так же, они не создадут проблем безопасности, которые я описал выше.

А как обстоят дела с DI в классы, где будут ваши методы АОПовские?
Тут AOP вообще никак не влияет. Если инъекция через поля-свойства, код не меняется, если через конструктор, все аргументы прозрачно пробрасываются (и то, если захотим оборачивать конструктор, а вообще конструктор как правило не оборачивается, нет смысла)
Что значит прозрачно пробрасываются? Вот вы в предложенном методе просто пишите в консоль. Измените метод так, чтобы лог писался через ILogger.Log. И покажите, откуда вы возьмете экземпляр логгера.
по запаре или так как я впервые вижу этот проект забываю изменить код АОПовской функции. Никакие связи никогда не приведут меня в OnInvoke
Не понял, какой «АОПовской функции»? Метод test генерируется автоматически каждый раз при билде, а старый test автоматически переименовывается в IL-коде в test_original.
У нас же бизнес задача, и лог должен быть соответствующий, каждый параметр несет в себе какую-то информацию, а не просто голый текст и числа. То есть у нас будет логирование через индексацию (p[0], p[1] вместо foreach)
АОП-подход для инфраструктурного кода, который принципиально не зависит от сигнатур функций. Если мы хотим как-то по особому логировать каждую ф-цию, то конечно такой подход не применим и надо писать отдельную обёртку на каждый метод. Это уже бизнес-код, а не инфраструктурный.

Другой пример — стартовать транзакцию при входе и коммитить (роллбечить) при выходе. Тут не должно быть отдельных подходов к каждой отдельной ф-ции.

АОП-подход хорош, когда наружу выставлено 100500 методов и нам достаточно видеть в логе их вызовы с параметрами, типа
test(1, "abc")->1234 [32ms]
или
test(1, "abc")->ApplicationException("Some Error") [130ms] ... stack trace
при этом не напрягая никого написанием логгеров. Можно в OnInvoke передать массив названий параметров, если нужно логировать параметры с их названиями.

Измените метод так, чтобы лог писался через ILogger.Log
Тут только 2 варианта:
1) статический экземпляр логгера (при этом, в OnInvoke можно передать тип класса, в котором находится ф-ция test),
2) хранить ссылку на логгер в классе и передавать в OnInvoke.
Например, так
public interface IAspectLoggable { ILogger logger { get; } }
public class MyService : IAspectLoggable
{
    public ILogger logger { get; set; } // заполняется DI
    int test(...)
}

Который развернётся в
int test(int a, string b)
{ 
    return (int)LoggingAttribute.OnInvoke(
        new object[] {a,b}, // параметры
        (object[] p) => (object)test_original((int)p[0], (string)p[1]),
        "test", // имя вызываемой ф-ции. удобнее передавать RuntimeTypeHandle на метод, из которого вытащим полную сигнатуру
        typeof(MyService), // из какого класса
        this // экземпляр, для static-методов пусть будет null
        );
}

И дальше в OnInvoke
if (instance is IAspectLoggable loggable) { loggable.logger.log(...); }

Но конечно это имеет недостаток — вносим в класс какие-то лишние поля.

Конечно, вы сейчас активно выискиваете недостатки. Нельзя каждому методу дать свой ILogger, мне это не подходит, до свидания, лучше напишу бойлерплейт, зато по фен-шую. Но на практике, первый способ, со статическим логгером (возможно, для каждого класса своим, т.е. где-то в web.config можно давать разные уровни логирования для разных классов), вполне работает и не требует почти никаких усилий.
Тут только 2 варианта:
1) статический экземпляр логгера (при этом, в OnInvoke можно передать тип класса, в котором находится ф-ция test),
2) хранить ссылку на логгер в классе и передавать в OnInvoke.

Есть ещё третий — сделать ILogger зависимостью того класса, в котором вызывается метод OnInvoke. Да, это потребует доработок AOP-инфраструктуры, но ничего фундаментально невозможного тут нет.

Метод, в котором вызывается OnInvoke — это метод класса MyService. Нежелательно в него добавлять что-то инфраструктурное. Но я такой пример и нарисовал в п.2

Или подразумевается, что AOP-фреймворк должен автоматически расширить MyService и внести в него новую зависимость? Сложно, но реализуемо.

Если подразумевается, кто зависеть должен класс LoggingAttribute, то проблема в том, что метод OnInvoke — static (в момент вызова мы откуда возьмём экземпляр?)

Можно же добавить аргумент в конструктор.

Тогда может сломаться код, где конструкторы вызываются явно. Точнее, такой код вообще нельзя будет написать: если logger передавать в конструктор, то на момент компиляции этого параметра ещё нет. После компиляции он появляется только в IL-коде.

Тут хорошо будет работать DI через property injection.

Если обработка аспектов идёт не отдельно от компиляции, а плагином к roslyn (вроде бы скоро так можно будет делать) — то и проблем с явным вызовом конструктора не будет.


Но при необходимости явным вызовом конструктора можно и пожертвовать.

Конечно, вы сейчас активно выискиваете недостатки.
Это вы, кстати, делаете.
Но в вашем подходе их и искать не надо, они сразу на виду.

И дальше в OnInvoke
if (instance is IAspectLoggable loggable) { loggable.logger.log(...); }

Скажите, если ваш сервис ILoggable, IAuditable, IЕщеЧтоНибудь, то зачем вам все эти навороты? Делайте просто явный код, это будет проще и читаемо. И никакой АОП тут не нужен.

АОП-подход для инфраструктурного кода, который принципиально не зависит от сигнатур функций.
Я не вижу в аббревиатуре АОП букву И(инфраструктура). Следовательно использую понятие аспектов на всю катушку. Я не вижу причин, по которым я не могу связать аспекты с предметной областью. Точно так же как я связываю с предметкой объекты.

т.е. где-то в web.config можно давать разные уровни логирования для разных классов
Веб конфиг вообще не должен ничего знать про ваши классы. Как и администратор системы, который настраивает логирование.

Я ведь писал в статье, что хочу как разработчик и как проектировщик. А вы пытаетесь мне сказать что я не это хочу. И что я вовсе не это хочу.

Попытайтесь понять, что все эти декораторы находятся как бы сбоку и отдельно разруливается порядок их выполнения. То есть, когда я пишу БЛ, я не хочу знать, что чего то там еще будет логироваться. А завтра еще навешают замеры скорости. А послезавтра… И самое главное, все эти декораторы можно писать одновременно, не боясь напороться на конфликты при слиянии. И за все это приходится платить парой лишних методов в декораторах, которые просто указывают на следующий слой и ничего не делают.

И еще, я не знаю, кто именно меня минусует. Если вдруг это вы, пожалуйста не делайте это. Такими вещами должен заниматься сторонний наблюдатель. Я ведь не совсем откровенную чушь пишу, я надеюсь :)
Я не минусую. Вот плюсанул комменты выше для восстановления баланса
Удивительно, сколько минусов накидали автору в обсуждении. Такое впечатление, что не поняв идею комментаторы начали нахваливать свои частные практики.

Да, для большого проекта требуется закладывать такой функционал на этапе проектирования. И многие высказывания верны.

Но, представьте, что вам достался проект, в котором IoC отличен от Autofac и нет магии AOP. Вам нужно добавить логирование к одному конкретному сервису. В этом случае предложенное решение – отличный вариант, т.к. не тянет за собой дополнительных зависимостей и реализуется практически поверх любого IoC.

Ага, отличный вариант, если только NextDelegate на просто Next заменить, а лучше исправить на внедрение через конструктор. И переписать методы расширения. И забытый автором DelegateBuilder написать самостоятельно, там же ничего сложного.


В общем, готовое решение, только его заново переписать нужно.

Тут сама идея хороша. А реализация… «на вкус и цвет все фломастеры разные». Вы же не программируете методом copy/past, а включаете мозг. Там действительно ничего сложного :)

Так идею-то не автор придумал, идея и без него была известна...

Так я и не утверждаю, что автор открыл что-то новое. Решение является композицией известных шаблонов проектирования Proxy и Chain of Responsibility.

Мой пост был о том, что на слова автора «смотрите как можно решить задачу», он словил минусов с пожеланиями пересмотра архитектуры всего приложения (на мой взгляд, для замены IoC-конетйнера или внедрение AOP на уже работающем в проде приложении должно быть очень веское обоснование). Добрее надо быть…

Так ведь стандартный контейнер на любой другой заменяется без особых проблем, Microsoft об этом позаботились.

Вообще то нет. Если вам потребовалось менять контейнер, скорее всего, вы хотите пользоваться расширенным функционалом. И не так то просто взять и заменить многочисленные регистрации и целые фабрики сервисов на синтаксис нового контейнера.

А зачем его менять, когда можно просто дописать новый код в новое место? Метод ConfigureServices при использовании IServiceProviderFactory всё ещё работает же, просто в Startup.cs появляется метод ConfigureContainer(Action<...>).

На самом деле я ее придумал. Правда, в итоге оказалось, что я заново изобрел велосипед :)

Но, в свое оправдание могу сказать, что когда я искал инфу о том, как писать логи, аудиты и другие сквозные шутчки, я попадал на АОП.

Еще хочу заметить, что мой подход не требует менять текущую библиотеку IoC.
Я уже добавил DelegateBuilder.

Позднее заменю NextDelegate на NextFactory и создам полноценную фабрику для следующего декоратора/сервиса.

Если заменю просто на Next, потеряю возможность понижать время жизни.

Да и не переписать его нужно, а дописать. Причем немного.
Зачем столько негатива в комментариях? Вы помогли разобраться в некоторых вещах, и за это большое вам спасибо. Вы и на SO мне немало помогали. Я много в чем сумел разобраться на старте своей карьеры, благодаря вашим и еще кое-чьим ответам. Более того, научился разбираться самостоятельно. Не надо сейчас это портить.
Экспертное мнение принесет больше пользы и меньше отторжения, если оно будет без эмоций, преувеличений и содержать только факты.

Когда пишешь одно и то же разными словами пятый раз — без негатива уже не получается.


Если заменю просто на Next, потеряю возможность понижать время жизни.

Вы же сами сказали, что без декораторов-синглтонов вы как-то всё это время обходились, так что ничего вы не потеряете.


Зато код всех существующих декораторов с Next будет заметно проще, чем с NextFactory. Ну вот сравните:


public void Foo()
{
    Next.Foo();
}

public void Foo()
{
    using next = NextFactory();
    next.value.Foo();
}

Кроме того, даже с NextFactory вы не сможете "восстановить" созданный ASP.NET скоуп, который "потеряется" при вызове синглтона что бы вы ни делали. Разве что можно IServiceScope параметром в каждый метод принимать, но это же ещё больше усложнит сервисы; плюс в Scoped-сервисах такой параметр будет лишним.

Вы же сами сказали, что без декораторов-синглтонов вы как-то всё это время обходились, так что ничего вы не потеряете.
но не без декораторов скопед над трансиент сервисами. Такое уже используется и рефакторить не хочется.

Зато код всех существующих декораторов с Next будет заметно проще, чем с NextFactory. Ну вот сравните:
Верно, но ответ на прошлое утверждение не позволяет этого.

Кроме того, даже с NextFactory вы не сможете «восстановить» созданный ASP.NET скоуп, который «потеряется» при вызове синглтона что бы вы ни делали.
Я не уверен на все 100, но можно попробовать через рутовый провайдер получить IHttpContextAccessor (который, кстати, синглтон) вытянуть текущий контекст и из него получить провайдер текущего запроса, который, если я все правильно понял, порожден из текущего скоупа запроса.
но не без декораторов скопед над трансиент сервисами. Такое уже используется и рефакторить не хочется.

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


Я не уверен на все 100, но можно попробовать через рутовый провайдер получить IHttpContextAccessor (который, кстати, синглтон) вытянуть текущий контекст и из него получить провайдер текущего запроса, который, если я все правильно понял, порожден из текущего скоупа запроса.

Да, так можно, но это не будет работать в за пределами ASP.NET, то есть для тестов или фоновых задач придётся прикручивать дальнейшие костыли.


И вообще, не просто так доступ к HttpContext в Core запрятали в интерфейс.

Transient же не означает, что сервис нужно обязательно создавать
Наверное да. Но я чет уже боюсь делать поспешные выводы, как следует не обсосав это со всех сторон.

Да, так можно, но это не будет работать в за пределами ASP.NET
а мне и не надо. Самое главное здесь — IDecorator, а фабрика — это уже инфраструктура. Для нового типа приложения фабрику можно и переопределить. Можно же будет скоупами вручную управлять. Во всяком случае я сейчас не могу угадать применение скоупов в другом типе приложения. Оно может быть различным. Соответственно фабрика должна будет это все учесть.

то есть для тестов или фоновых задач придётся прикручивать дальнейшие костыли.
Не костыли, а новые архитектурные решения :)
С тестами проблем нет и не предвидится. В юнит тестах я просто мокаю NextDelegate. Слава богу это свойство с публичным сеттером. В юнит тестах вообще DI не нужен я считаю. Единственная неприятность, что не имея аргументов конструктора, я не узнаю сразу, что мне нужно эту фабрику мокать. Но щито поделать.
В интеграционных тестах у меня поднимается сервер на основе приложения с возможностью переопределить стартап и еще кое что, после чего на основе него же поднимается клиент. А значит идут полноценные запросы, а значит у меня есть HttpContext.
А для фоновых задач да, придется немного запотеть. В первую очередь перед началом фоновой задачи надо будет создать скоуп. Что делать в фабрике, пока не придумал. Надо что-то сделать после проверки контекста на null (если он null).

И вообще, не просто так доступ к HttpContext в Core запрятали в интерфейс.
Если бы его хотели запрятать, запрятали бы подальше. Вообще, особых предостережений по его использованию я не нашел. Пока что я вижу причину выноса его в интерфейс для возможности подменить реализацию, а так же из-за вариативности возвращаемых значений.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации