简体   繁体   中英

Is there any way to validate the lifetime in DI?

I'm looking for some way to enforce the checking (runtime, of course) of the proper lifetime registration of dependency-injection services, in.Net Core or higher.

Let's say I have a stateful service like this one:

public class MyStatefulService
{
    private object _state;
}

However, by mistake, I could register it with the wrong lifetime:

services.AddTransient<MyStatefulService>();

So, I won't be alerted, but the actual behavior is not what I'd expect: the service is created at every request, and the state won't be preserved.

I wonder if there is a way to reinforce this pattern. For instance, it would be nice if I could decorate the class with an attribute like this:

[Singleton]
public class MyStatefulService
{
    private object _state;
}

at this point, possibily at startup or at the very first request, the framework should throw if the registration is different than AddSingleton (along its overloads).

Similarly, the subject could apply for transient-only services, which shouldn't registered as singletons.

The only solution came in my mind is rather naive, and I don't like so much:

//line of principle code
public static class MySingletonChecker
{
    private static HashSet<Type> _set = new HashSet<Type>();

    public static void Validate(Type type)
    {
        if (_set.Contains(type))
        {
            throw new Exception();
        }
        else
        {
            _set.Add(type);
        }
    }
}


public class MyStatefulService
{
    public MyStatefulService()
    {
        MySingletonChecker.Validate(this.GetType());
    }

    private object _state;
}

Is there any better solution, hack or anything that helps to prevent errors?

If you want to idiot-proof your code, you can provide an extension method to IServiceCollection that registers your service exactly the way it should be.

public static void AddMyStatefulService(this IServiceCollection services) 
{
    services.AddSingleton<MyStatefulService>();
}

Then in your services configuration section, the developer would type:

services.AddMyStatefulService();

To move DI composition to the declaration of a class you could work with some self-made marker interfaces like IRegisterTransientServiceAs<T> and add it to your class like this:

public class MyStatefulService : IMyStatefulService, IRegisterSingletonServiceAs<IMyStatefulService>

Within your composition you have your own extension method that iterates through all loaded types within the application domain, searches for the given generic interface and register them with the declarated lifetime from the given interface.

Maybe this is not real DI, but as you already mentioned, mostly the lifetime of a service is baked into the code of the service itself and it happens very rarely that a specific service will be used in different project with different lifetimes. So IMHO it is okay to bake the desired lifetime into the class itself, instead of making the decision outside.

Another way to validate lifetimes, is by inspecting the entire DI container in the ConfigureServices clause.

Each member of a IServiceCollection has a LifeTime property which gives you the information you're looking for: https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicedescriptor.lifetime?view=dotnet-plat-ext-5.0#Microsoft_Extensions_DependencyInjection_ServiceDescriptor_Lifetime

Here's a way like I was pointing out in the comments

Let's have some empty types:

public class ScopedService {}
public class TransientService {}

Let's have a real service that derives from one of the empty types:

public class RealService: ScopedService, IRealService {
  //impl
}

And a generic registration method helper:

static void MyAddScoped<TService, TImplementation>(IServiceCollection services)
    where TService : class
    where TImplementation : ScopedService, TService 
{
    services.AddScoped<TService, TImplementation>();
}

Let's register our realservice using the helper:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        MyAddScoped<IRealService, RealService>(services);
    }

RealService is a ScopedService - think of this as "how you decorate a service to insist it be added as scoped"

Suppose the developer changes the service to be a transient:

class RealService: TransientService, IRealService {

Now you get a compiler error from the helper method:

The type 'YourApplication.RealService' cannot be used as type parameter 'TImplementation' in the generic type or method 'Startup.MyAddScoped<TService, TImplementation>(IServiceCollection)'. There is no implicit reference conversion from 'WebApplication1.RealService' to 'WebApplication1.ScopedService'.

Your "one person who looks after registration" can know that the developer is indicating the service is no longer registerable as Scoped and can change it (and the build will be broken until it is changed, which is a good way of preventing accidental release of incorrect code)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM