Search code examples
c#inversion-of-controlsimple-injector

SimpleInjector force Lifestyle.Transient for Unregistered-Type


I have a wrapper class as below:

// this is not used for IoC purposes, but by code to cast object to known T
internal interface ITaskCompletionSourceWrapper
{
    void SetResult(object result);
    /* snip */
}

internal class TaskCompletionSourceGenericWrapper<TResult> : ITaskCompletionSourceWrapper
{
    readonly TaskCompletionSource<TResult> tcs;
    public TaskCompletionSourceGenericWrapper() =>        
        this.tcs = new TaskCompletionSource<TResult>(TaskCreationOptions.RunContinuationsAsynchronously);
    public void SetResult(object result) => tcs.SetResult((TResult)result);
    /* snip */
}

Where i currently instantiate using Activator:

var responseType = request
                     .GetType()
                     .GetInterfaces()
                     .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IRequest<>))
                     GetGenericArguments()[0];

var wrapperType = typeof(TaskCompletionSourceGenericWrapper<>).MakeGenericType(responseType);
var tcsWrapper = this.container.GetInstance(wrapperType) as ITaskCompletionSourceWrapper;

I switched this to SimpleInjector (as may use logging and other IOC goodness) and thought all was well until I found an issue (which was due to lifestyle being shared):

/* snip responseType code from above */
var wrapperType = typeof(TaskCompletionSourceGenericWrapper<>).MakeGenericType(responseType);
var tcsWrapper = this.container.GetInstance(wrapperType) as ITaskCompletionSourceWrapper;

// i can see the type has been registered as scoped, but we need it to be unique
[30] = ServiceType = TaskCompletionSourceGenericWrapper<Object>, Lifestyle = Async Scoped

The above is being called within a AsyncScopedLifestyle.BeginScope block.

I would like to instantiate this wrapper at runtime using SimpleInjector (but ensuring Lifestyle.Transient):

The types for the wrapper are pretty much arbitrary from int, string, bool etc but could also be simple POCO classes.

This is similar to a case of open generic registration but I would rather avoid having other developers specify assemblies for the simple types (as for sure they will forget, and their classes are simple anyways - command/query POCOs and the like).

How can I achieve the above please or am I forced to register collection?

Or is this deeper with lifestyle mismatches - I hope not, since the wrapper has no dependencies (and will not other than ILogger perhaps) is only doing something instantiation of a simple class (which I want to replace Activator rather than decouple components IoC style).


Solution

  • Based on their docs Batch-Registration / Auto-Registration and experience, you need to register the implementation of the interface when using dependency injection.

    var container = new Container();
    // If related classes exist in a different assembly then add them here
    var assemblies = new[] { typeof(TaskCompletionSourceGenericWrapper<>).Assembly };
    
    container.Collection.Register(typeof(TaskCompletionSourceGenericWrapper<>), assemblies, Lifestyle.Transient);
    
    DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(container));
    

    One hack that I can think of but haven't tested is to create a wrapper class that returns a deep copy a.k.a clone of your wrapper. Returning a clone of an object is similar to transient as it returns a new instance every time you request for it but the downside on this approach is that you can only do it for serializable classes so you may need to add [Serializable()]. Refer to Deep cloning objects for more info.

    public interface IUglyDuckling
    {
        ITaskCompletionSourceWrapper GetITaskCompletionSourceWrapper(Type type);
    }
    public class UglyDuckling : IUglyDuckling
    {
        public ITaskCompletionSourceWrapper GetITaskCompletionSourceWrapper(Type type)
        {
            var wrapperType = typeof(TaskCompletionSourceGenericWrapper<>).MakeGenericType(type);
            var tcsWrapper = this.container.GetInstance(wrapperType) as ITaskCompletionSourceWrapper;
            return ObjectCopier.Clone<ITaskCompletionSourceWrapper>(tcsWrapper);
        }
    }