Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign upDecorated services poc #93
Conversation
| return serviceDescriptor.ImplementationInstance.GetType(); | ||
|
|
||
| if (serviceDescriptor.ImplementationFactory != null) | ||
| return serviceDescriptor.ImplementationFactory.GetType().GenericTypeArguments[1]; |
PawelGerr
Aug 13, 2019
The generic type arg is always Object because the signature of the factory is Func<IServiceProvider,object>.
public interface IFoo { }
public class Foo : IFoo { }
var descriptor = ServiceDescriptor.Transient(typeof(IFoo), prov => new Foo());
var type = descriptor.GetImplementationType(); // return Object
Having a factory we cannot determine the implementation type because there can be multiple and some of them might be disposable and some are not:
public class Foo2 : IFoo, IDisposable
{
public void Dispose() { }
}
var descriptor1 = ServiceDescriptor.Transient(typeof(IFoo), prov =>
{
var foo = new Random().Next() == 42
? (IFoo) new Foo()
: new Foo2();
return foo;
});
The generic type arg is always Object because the signature of the factory is Func<IServiceProvider,object>.
public interface IFoo { }
public class Foo : IFoo { }
var descriptor = ServiceDescriptor.Transient(typeof(IFoo), prov => new Foo());
var type = descriptor.GetImplementationType(); // return Object
Having a factory we cannot determine the implementation type because there can be multiple and some of them might be disposable and some are not:
public class Foo2 : IFoo, IDisposable
{
public void Dispose() { }
}
var descriptor1 = ServiceDescriptor.Transient(typeof(IFoo), prov =>
{
var foo = new Random().Next() == 42
? (IFoo) new Foo()
: new Foo2();
return foo;
});
andrewlock
Aug 13, 2019
•
Author
Contributor
The generic type arg is always Object because the signature of the factory is Func<IServiceProvider,object>.
Actually, that;s not the case, as GetType() gets the runtime type. However it still doesn't work for other reasons 🙂 In the example you've provided, the factory is of type Func<IServiceProvider, IFoo>, so descriptor.GetImplementationType() returns IFoo - not object as you suggest, but also not Foo as intended!
Having a factory we cannot determine the implementation type because there can be multiple and some of them might be disposable and some are not.
Hmmm, yeah that's a very good point, I think the specific approach in this PR falls down fundamentally because of that.
The generic type arg is always
Objectbecause the signature of the factory isFunc<IServiceProvider,object>.
Actually, that;s not the case, as GetType() gets the runtime type. However it still doesn't work for other reasons Func<IServiceProvider, IFoo>, so descriptor.GetImplementationType() returns IFoo - not object as you suggest, but also not Foo as intended!
Having a factory we cannot determine the implementation type because there can be multiple and some of them might be disposable and some are not.
Hmmm, yeah that's a very good point, I think the specific approach in this PR falls down fundamentally because of that.
| @@ -287,8 +287,11 @@ private static bool TryDecorateDescriptors(this IServiceCollection services, Typ | |||
| { | |||
| var index = services.IndexOf(descriptor); | |||
|
|
|||
| // replace original descriptor with implementation type | |||
| services[index] = ReplaceWithImplementation(descriptor); | |||
PawelGerr
Aug 13, 2019
Replacing a descriptor with the implementation type may break existing registrations
// Before registration of the decorator
ServiceCollection services = ...;
services.AddTransient<Foo, Foo2>();
services.AddTransient<IFoo, Foo>();
Resolve<IFoo> -> Foo
Resolve<Foo> -> Foo2
Now we will decorate IFoo with FooDecorator
// the decoration will replace
services.AddTransient<IFoo, Foo>();
// with
services.AddTransient<Foo, Foo>();
// this breaks the following registration
services.AddTransient<Foo, Foo2>();
Resolve<Foo> => Foo instead of Foo2
Replacing a descriptor with the implementation type may break existing registrations
// Before registration of the decorator
ServiceCollection services = ...;
services.AddTransient<Foo, Foo2>();
services.AddTransient<IFoo, Foo>();
Resolve<IFoo> -> Foo
Resolve<Foo> -> Foo2
Now we will decorate IFoo with FooDecorator
// the decoration will replace
services.AddTransient<IFoo, Foo>();
// with
services.AddTransient<Foo, Foo>();
// this breaks the following registration
services.AddTransient<Foo, Foo2>();
Resolve<Foo> => Foo instead of Foo2
andrewlock
Aug 13, 2019
Author
Contributor
Yeah, I was aware that's a potential breaking change, though I'd argue two things:
- The example that you describe seems like it would be an edge case, that's some strange registrations going on there right?
- It wouldn't actually break. Last registration wins, so
Resolve<Foo> => Foo2 still. The difference is Resolve<IEnumerable<Foo>> returns both Foo and Foo2, instead of just Foo I think.
For me, I think that change is worth it. #91 is preventing me use the decoration entirely, the registration caveats are a necessary evil I think. I take it you don't agree? 🙂
Yeah, I was aware that's a potential breaking change, though I'd argue two things:
- The example that you describe seems like it would be an edge case, that's some strange registrations going on there right?
- It wouldn't actually break. Last registration wins, so
Resolve<Foo> => Foo2still. The difference isResolve<IEnumerable<Foo>>returns bothFooandFoo2, instead of justFooI think.
For me, I think that change is worth it. #91 is preventing me use the decoration entirely, the registration caveats are a necessary evil I think. I take it you don't agree?
PawelGerr
Aug 13, 2019
- DI lib must be reliable. There will be different kinds of registrations in the wild, especially if we trying to decorate a complex 3rd party lib (e.g. EF Core / ASP.NET Core).
- It does :P
try the following
var services = new ServiceCollection();
services.AddTransient<Foo, Foo2>();
services.AddTransient<IFoo, Foo>();
//services.Decorate<IFoo,FooDecorator>();
var provider = services.BuildServiceProvider();
without Decorate
provider.GetRequiredService<IFoo>().Dump(); // Foo
provider.GetRequiredService<Foo>().Dump(); // Foo2
with Decorate
provider.GetRequiredService<IFoo>().Dump(); // FooDecorator+Foo
provider.GetRequiredService<Foo>().Dump(); // Foo instead of Foo2
- DI lib must be reliable. There will be different kinds of registrations in the wild, especially if we trying to decorate a complex 3rd party lib (e.g. EF Core / ASP.NET Core).
- It does :P
try the following
var services = new ServiceCollection();
services.AddTransient<Foo, Foo2>();
services.AddTransient<IFoo, Foo>();
//services.Decorate<IFoo,FooDecorator>();
var provider = services.BuildServiceProvider();
without Decorate
provider.GetRequiredService<IFoo>().Dump(); // Foo
provider.GetRequiredService<Foo>().Dump(); // Foo2
with Decorate
provider.GetRequiredService<IFoo>().Dump(); // FooDecorator+Foo
provider.GetRequiredService<Foo>().Dump(); // Foo instead of Foo2
andrewlock
Aug 13, 2019
Author
Contributor
- Absolutely agreed, my argument is that the current behaviour isn't "reliable" - disposable services aren't disposed by the container when you use the current implementation.
- True, but you cheated in that example and moved the
Foo2 registration up before the decorate call, it was after it previously 😛
I still agree that there's too many sharp edges for this to be acceptable. I think it's worth pursuing though. 🙂
- Absolutely agreed, my argument is that the current behaviour isn't "reliable" - disposable services aren't disposed by the container when you use the current implementation.
- True, but you cheated in that example and moved the
Foo2registration up before the decorate call, it was after it previously😛
I still agree that there's too many sharp edges for this to be acceptable. I think it's worth pursuing though.
|
There's definitely some issues like you've outlined @PawelGerr. I'll have another think and see if there's a way round them. |
|
You may try a different approach that lets the DI do work for us.
Put all "manually" created instances into
That way the disposables are attached to correct The transient registration of |
|
Nice @PawelGerr - I was trying to achieve something similar with generics but that is much simpler! And as far as I can see, it works without the limitations you pointed out previously |
| @@ -282,16 +283,15 @@ private static bool TryDecorateDescriptors(this IServiceCollection services, Typ | |||
| { | |||
| return false; | |||
| } | |||
|
|
|||
| services.TryAddTransient<DecoratedServiceDisposer>(); | |||
khellang
Aug 14, 2019
•
Owner
Registering and resolving IDisposable transients from the top-level provider is almost guaranteed a memory leak... 🙈
Registering and resolving IDisposable transients from the top-level provider is almost guaranteed a memory leak...
PawelGerr
Aug 14, 2019
Resolution of DecoratedServiceDisposer from root happens
- for singletons only and that's correct because singletons are resolved from root as well.
- for non-singleton that are explicitly resolved from the root
In both cases the decoratee, the decorator and DecoratedServiceDisposer are captured by the root scope and must be disposed of by the root (only).
Can you provide an example so I understand your concerns?
Resolution of DecoratedServiceDisposer from root happens
- for singletons only and that's correct because singletons are resolved from root as well.
- for non-singleton that are explicitly resolved from the root
In both cases the decoratee, the decorator and DecoratedServiceDisposer are captured by the root scope and must be disposed of by the root (only).
Can you provide an example so I understand your concerns?
As described in #91