aspnet / DependencyInjection

[Archived] Contains common DI abstractions that ASP.NET Core and Entity Framework Core use. Project moved to https://github.com/aspnet/Extensions

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Proposal] Resolve Services on "this" Constructor or Static Constructor

RandyBuchholz opened this issue · comments

Internal DI System Scope

DI is something of an all or nothing deal - if your object wasn't created by the container you have no access to the DI services. Once you "new" something or use things like Activator.Create() you are out of the DI scope. So is everything downstream, limiting the use of DI more towards upstream and business service classes, when it can be just as valuable to all classes.

As a result it creates cases where you may need to use static or instance classes when you should be able to use DI. A common case is utilities. Many utilities are implemented as static classes and methods. This makes them tightly coupled and difficult to change and test. Rewriting them as utility services and injecting them would be great, but unless the classes that use them are part of the DI hierarchy you can't resolve them.

This proposal provides a way to use the internal DI system with any class. This extends DI beyond top-level business service classes all the way down to small runtime objects.

Example

Say I have a set of features defined by IFooProcesser that accept Foo as a parameter, and manipulate Foo's.

public interface IFooProcessor{
    Foo Reverse(Foo inFoo);
    void Transform(ref Foo foo);
}

IFooProcessor provides "utility services" and not business services, so they may be needed in many places, both business and "data". I also want to be able to swap them out. Foo's are runtime objects, and the type Bar can contain them.

public class Bar {
   public List<Foo> foos { get; set; }
}

I can use my IFooProcessor on my Foo's.

public class Bar{
    private readonly IFooProcessor _fooProc;
    
    public Bar(IFooProcessor fp){
        _fooProc = fp;
    }
    
    public List<Foo> foos { get; set; }

    public Foo Oof(Foo foo) => _fooProc.Reverse(foo);
    
    public void ChangeThem(){
        foreach(var f in foos){
            _fooProc.Transform(ref f);
        }
}

The problem is Bar is also a runtime object, and Bar participates in things like serialization and Activator.Create() that require a public parameterless constructor. Built-in DI only allows a single constructor. I need to be able to do var f = new Bar(). Once I do though, I loose my Services. I have to implement public Bar() { } so it becomes

public class Bar{
   private readonly IFooProcessor _fooProc;
   public Bar() {
    ...
   }
}

and now my service IFooProcessor can't resolve.

IFooProcessor is a basic utility-type service. These are often implemented as static classes so they are easy to use anywhere. Going that route though doesn't allow implementing the interface (public static class X : IX is not valid). If I go with an instance class I'm newing or resorting to "creator" approaches, when I should just register it as a DI singleton and be done. Many IFooProc type of classes are either "in" or "out" of DI, - they can't easily participate in both. Having a class be able to participate in DI and be "newed" would be valuable.

The ability to have multiple "DI Execution Roots" would also be valuable. These would share the same Composition Root, (Though multiple composition trees could be supported.) but can be placed anywhere in the system. Essentially, a class I new x(), Activator.Create(), or similar can be tagged as being another "root' in a DI tree/chain. I can see two ways to do this.

The "this" Constructor

This approach modifies the way "this" works on a constructor to resolve services from there. The syntax could be like the current syntax and call another constructor. This is like "Poor Mans Injection". DI would just inspect the "this" part of the constructor as part of its process and the parameterless constructor is there for other needs.

public class Bar{
    private readonly IFooProcessor _fooProc;

    public Bar() : this( IFooProcessor fooProcessor)

    private Bar(IFooProcessor fooProcessor){
        _fooProc = fooProcessor;
   }
}

This provides both the ability to "new" and inject at the same time, making it easy to create and inject utility services. This has also created a new "Service Root" on Bar. If the FooProcessor implementation has dependencies they will inject just like normal. This will work fine for

public class FooProcessor : IFooProcessor {
    private readonly IServiceDependency _sdp;

    public FooProcessor(IServiceDependency depend) {
        _sdp = depend;
    }
}

The new Service Root on Bar extends down to FooProcessor and beyond.

A more explicit syntax might be better. This would be nice candy even for regular DI.

public class Bar{
    private readonly IFooProcessor _fooProc;

    public Bar() : this ( IFooProcessor => _fooProc )
        or
    public Bar() : this ( _fooProc <= IFooProcessor )
}

Static Constructor

This is a deep change that effects language fundamentals. It is more of a thought/logical approach than an actual suggestion. With this change, static constructors would be able to accept parameters and set readonly fields. Parameters would be limited to DI-resolvable items.

An implementation approach could just defer the actions to the instance constructor and not actually change the underlying implementation of the static constructor.

Considering two things about constructor injection DI it's logical for DI to be implemented at this (creation) level. Putting the injected items in the static constructor makes clear what the class needs to function. If we accept/assume that most injected classes are essential to the class (part of the class composition) they may belong in the static constructor, at least conceptually. It also provides a level of structural vs business/functional separation. Core composition structure is static, functional/business is instance.

Using the shorthand syntax it could look like this.

public class Bar{
    private readonly _fooProc;
    
    // "Structural" construction
    static Bar( IFooProcessor => _fooProc) { }
    
   // Cleaner "functional" constructors
   public Bar() { }
   public Bar(sting name){ }
}

Again, the static constructor approach may more conceptual sense than language sense.

Built-in DI only allows a single constructor

It doesn't, not sure where you got that premise from. The built in DI container prefers the constructor with the most resolvable services. See https://github.com/aspnet/DependencyInjection/blob/rel/1.1.1/src/Microsoft.Extensions.DependencyInjection.Specification.Tests/DependencyInjectionSpecificationTests.cs#L640 for details.

It seems like what you are suggesting requires new language syntax and potentially runtime features. Was this more of an "out there" brainstorming type suggestion? (just trying to understand so I can formulate the appropriate reply).

No this is a not just an out of the blue thing, but something I've struggled with many times. It's sort of a bottom-up problem. So at the heart, say I have low-level utility classes that are static. I have a class that uses the utility classes - Project. The utilities should be "replaceable" so I abstract them to interfaces, and implement instance class. Now I can program the consuming class using the interfaces and manually add private IUtilty _util = new Utility(); to Project. In my "parent" class Portfolio I do var p = new Project() and everything works. So far, so good. But I have newed-up Utiltiy in Project and every other consumer class. Maybe DI is the way to go. I register the interface and instance, modify Project

public class Project {
    private IUtility _util;   
    public Project ( IUtility util ){
        _util = util;
    }
}

Of course now var p = new Project() fails - it wants a parameter. var p = new Project(new Utility()); Why? Because I'm not connected to the ServiceProvider. This is because Portfolio is not connected to a provider. It seems ServiceProvider isn't pervasive across my application, where it will magically resolve any constructor, but is part of a hierarchy. It will only resolve if the parent was resolved. So I convert my classes all the way up the chain until I reach one in the hierarchy. Now everything works and resolves. As long as I stay in the chain - don't use new (), that breaks the chain. This is my understanding and experience with DI so far.

The problems start when you have to "new" something. In my case I have to have an instance of a generic type created when I create a containing type. This is for model binding. I create the instance using Activator.Create(). This is where things fall apart. Activator.Create() will take parameters, but it doesn't (seem to) work with DI. I get an instance, but none of the injections resolve.

The idea was making DI more pervasive by having an approach that will let DI recognize it is wanted from anywhere. In the case I seem to encounter I want DI for business services (top down) and I want DI for shared utilities (bottom-up), and still be able to new/actvator/deserialize my operating classes (that utilize injected utilities).

Hope that made sense, or maybe I'm missing something basic here.

Of course now var p = new Project() fails - it wants a parameter. var p = new Project(new Utility()); Why? Because I'm not connected to the ServiceProvider. This is because Portfolio is not connected to a provider.

It doesn't need to fail, you can have multiple ctors but you still won't be able to use the DI container if you don't create instances from the service provider. There's no getting around that.

It seems ServiceProvider isn't pervasive across my application, where it will magically resolve any constructor, but is part of a hierarchy

There's no magic to it. The only reason it seems magical is because you're using a framework that's doing the heavy lifting on your behalf. If you're in a place where the framework doesn't control activation (which is the case for lots of utility code then you're the one that needs to call GetService).

The problems start when you have to "new" something. In my case I have to have an instance of a generic type created when I create a containing type. This is for model binding. I create the instance using Activator.Create(). This is where things fall apart. Activator.Create() will take parameters, but it doesn't (seem to) work with DI. I get an instance, but none of the injections resolve.

Right, the DI container is supposed to "new" it up for you, it's basically all or nothing (as you've eluded to).

The problems start when you have to "new" something. In my case I have to have an instance of a generic type created when I create a containing type. This is for model binding. I create the instance using Activator.Create(). This is where things fall apart. Activator.Create() will take parameters, but it doesn't (seem to) work with DI. I get an instance, but none of the injections resolve.

You can use a helper to get injection: ActivatorUtilities.CreateInstance(serviceProvider, ...). It's basically a DI aware version of Activator.CreateInstance

The idea was making DI more pervasive by having an approach that will let DI recognize it is wanted from anywhere. In the case I seem to encounter I want DI for business services (top down) and I want DI for shared utilities (bottom-up), and still be able to new/actvator/deserialize my operating classes (that utilize injected utilities).

I understand your request but I just don't understand concretely how it would work. It's more of an interesting idea but I don't think it's feasible without more radical changes to other parts of the stack. What you're asking for would basically be possible if the runtime (or compiler) had a hook that would give up control of any object creation and maybe the DI system could hook into it. Might be something to propose on http://github.com/dotnet/coreclr. Even with that hook the DI container would need to be ephemerally available so that the generate code could call into it from a utility class.

@davidfowl I've looked at constructor overloading and DI and know where I got the single constructor perception from. As you said, DI resolves to the most resolvable constructor. The link to the testing implies it should look for the "least" resolvable - i.e., longest resolvable signature. It is currently resolving to the "most resolvable", so if you have a parameterless constructor it will resolve to that.

So while the class can have multiple constructors, from a DI perspective there is only one - the one with the minimum signature.

I have a repo here to show the problem.
https://github.com/Randy-Buchholz/DIOverload/tree/master/DIOverload

I have opened an issue on this
Resolves to Least Complex Signature, not Most Complex #557

This issue was moved to dotnet/aspnetcore#2339