Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decorated services poc #93

Draft
wants to merge 5 commits into
base: master
from

Conversation

@andrewlock
Copy link
Contributor

@andrewlock andrewlock commented Jun 15, 2019

As described in #91

return serviceDescriptor.ImplementationInstance.GetType();

if (serviceDescriptor.ImplementationFactory != null)
return serviceDescriptor.ImplementationFactory.GetType().GenericTypeArguments[1];

This comment has been minimized.

@PawelGerr

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

This comment has been minimized.

@andrewlock

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.

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

This comment has been minimized.

@PawelGerr

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

This comment has been minimized.

@andrewlock

andrewlock Aug 13, 2019
Author Contributor

Yeah, I was aware that's a potential breaking change, though I'd argue two things:

  1. The example that you describe seems like it would be an edge case, that's some strange registrations going on there right?
  2. 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? 🙂

This comment has been minimized.

@PawelGerr

PawelGerr Aug 13, 2019

  1. 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).
  2. 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

This comment has been minimized.

@andrewlock

andrewlock Aug 13, 2019
Author Contributor

  1. 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.
  2. 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. 🙂

@andrewlock
Copy link
Contributor Author

@andrewlock andrewlock commented Aug 13, 2019

There's definitely some issues like you've outlined @PawelGerr. I'll have another think and see if there's a way round them.

@PawelGerr
Copy link

@PawelGerr PawelGerr commented Aug 14, 2019

You may try a different approach that lets the DI do work for us.

  1. Introduce a transient type like
services.TryAddTransient<MyDisposable>()

class MyDisposable : IDisposable
{
      private IDisposable _disposable;

     void Set(IDisposable d)
     {
         _disposable = d;
     }

   void Dispose()
   {
       _disposable.Dispose();
   }
}

Put all "manually" created instances into MyDisposable

object MyDecoratorFactory(IServiceProvider prov)
{
   var instanceToDecorate = CreateInstance(....);

   if(instanceToDecorate is IDisposable d)
      prov.Resolve<MyDisposable>().Set(d);

  return CreateDecorator(...., instanceToDecorate);
}

That way the disposables are attached to correct IServiceScope, i.e. transient/scoped types to the most inner scope they are resolved from, singleton to the root scope.

The transient registration of MyDisposable guarantees correct "dispose order".

@andrewlock
Copy link
Contributor Author

@andrewlock andrewlock commented Aug 14, 2019

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

This comment has been minimized.

@khellang

khellang Aug 14, 2019
Owner

Registering and resolving IDisposable transients from the top-level provider is almost guaranteed a memory leak... 🙈

This comment has been minimized.

@PawelGerr

PawelGerr Aug 14, 2019

Resolution of DecoratedServiceDisposer from root happens

  1. for singletons only and that's correct because singletons are resolved from root as well.
  2. 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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

3 participants
You can’t perform that action at this time.