Giter VIP home page Giter VIP logo

Comments (5)

dotnetjunkie avatar dotnetjunkie commented on May 20, 2024

When using ASP.NET 5 (vNext), don't try to integrate Simple Injector completely into the ASP.NET pipeline. There is no compelling reason to do so, and it leads to all sorts of complications, because ASP.NET implements the Conforming Container anti-pattern and forces the used DI container into an API that is not compatible with Simple Injector's API.

So instead of trying to replace the default IServiceProvider with Simple Injector, keep them separate and let ASP.NET resolve its own services for its own purpose, and use Simple Injector for where its really good at: resolving the types that belong to your application.

This basically means you just have to do a few tiny things in your Startup class:

  1. In the ConfigureServices method, you replace ASP.NET's default IControllerActivator with a custom version for Simple Injector that intercepts the creation of ASP.NET controllers. In most applications, the creation of controllers is the only thing you need to intercept.
  2. In the Configure method, you configure Simple Injector by registering your own components and your controllers. If your application requires any scoped registrations, you need to apply scoping to ASP.NET's request pipeline as well. If your components depend on any ASP.NET specific abstraction, this is the time to register those as well. This would be as simple as calling container.Register(app.ApplicationServices.GetRequiredService<[AspNetAbstraction]>).

The following is an example of how the startup class would typically look like:

public class Startup
{
    private Container container = new SimpleInjector.Container();
    public Startup(IHostingEnvironment env) {
        // ASP.NET default stuff here
    }

    // This method gets called by the runtime.
    public void ConfigureServices(IServiceCollection services) {
        // ASP.NET default stuff here

        services.AddInstance<IControllerActivator>(
            new SimpleInjectorControllerActivator(this.container));
    }

    // Configure is called after ConfigureServices is called.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory fac) {
        InitializeContainer(app);
        RegisterControllers(app);

        this.container.Verify();

        // Wrap requests in a execution context scope. This allows
        // scoped instances to be resolved from the container.
        app.Use(async (context, next) => {
            using (this.container.BeginExecutionContextScope()) {
                await next();
            }
        });

        // ASP.NET default stuff here
    }

    private void InitializeContainer(IApplicationBuilder app) {
        this.container.Options.DefaultScopedLifestyle = new ExecutionContextScopeLifestyle();

        // For instance: 
        container.Register<IUserRepository, SqlUserRepository>(Lifestyle.Scoped);
    }

    // In the future, this RegisterControllers method will be moved to an integration package.
    private void RegisterControllers(IApplicationBuilder app) {
        // Register ASP.NET controllers
        var provider = app.ApplicationServices.GetRequiredService<IControllerTypeProvider>();
        var controllerTypes = provider.ControllerTypes.Select(t => t.AsType());
        foreach (Type type in controllerTypes) {
            var registration = Lifestyle.Transient.CreateRegistration(type, container);
            container.AddRegistration(type, registration);
            registration.SuppressDiagnosticWarning(DiagnosticType.DisposableTransientComponent, 
                "ASP.NET disposes controllers.");
        }
    }
}

internal sealed class SimpleInjectorControllerActivator : IControllerActivator {
    private readonly Container container;
    public SimpleInjectorControllerActivator(Container container) { this.container = container; }

    [DebuggerStepThrough]
    public object Create(ActionContext context, Type type) => this.container.GetInstance(type);
}

For more detailed discussion, please see the following articles:

from simpleinjector.

mrchief avatar mrchief commented on May 20, 2024

Thanks for the beautiful explanation and sample code! There are so many goodies in this answer and the code, I simply love it!!

I'm gonna give it a shot and will let you know (although I'm pretty sure this will work)! This is going to be very helpful for folks like me who come looking for ways to migrate to vNext and still keep SimpleInjector as their DI of choice!

from simpleinjector.

davidfowl avatar davidfowl commented on May 20, 2024

When using ASP.NET 5 (vNext), don't try to integrate Simple Injector completely into the ASP.NET pipeline. There is no compelling reason to do so, and it leads to all sorts of complications, because ASP.NET implements the Conforming Container anti-pattern and forces the used DI container into an API that is not compatible with Simple Injector's API.

Not exactly. Most of the our framework depends on minimal DI behaviors (we try to avoid the service locator pattern ourselves) that are available everywhere but there's nothing stopping end users from using the container to its full ability.

So instead of trying to replace the default IServiceProvider with Simple Injector, keep them separate and let ASP.NET resolve its own services for its own purpose, and use Simple Injector for where its really good at: resolving the types that belong to your application.

That's actually a fair point but I don't see why it wouldn't work with the DI system. Is there some feature that Simple Injector doesn't support?

from simpleinjector.

dotnetjunkie avatar dotnetjunkie commented on May 20, 2024

Hi David,

Thank you for showing an interest in this topic, I appreciate it.

It is my understanding that application developers, external parties and ASP.NET framework developers all make registrations to what becomes a list of ServiceDescriptor instances (by appending to and modifying this list using methods such as Add, AddTransient, TryAdd and Remove). As such, it is this list; the maintenance of this list; and the way instances are processed that all form the registration API of the built-in vNext container. This registration API defines certain expectations (both explicit and implicit) on anyone (e.g. application developers and 3rd party container developer) who wishes to take ownership of the management and maintenance of the runtime list of registrations. These individual expectations of the vNext runtime all form the API's specifications. Here is a small subset of the specifications that any adapter implementer should be aware of:

  • A registration for a closed generic type of some service will always be resolved before a registration of an open generic type for that same type. In other words, the open registration will always act as a fallback registration when there is no applicable closed registration. This fallback behavior will occur regardless of the order the two registrations appear in the list.
  • Registrations for collections are handled by appending registrations for the service type one after the other to the registrations list.
  • If a collection is requested using GetRequiredServices<T>(), the underlying container is expected to return all registered implementations of T, even if the IList<ServiceDescriptor> only contains a single registration for T.
  • If a single instance is requested using GetRequiredService<T>(), the container is expected to return the last registration for T, in case there are multiple registrations for T.
  • If a collection is requested using GetRequiredServices<T>() or injected into a constructor, the order in which the registrations are made in the IList<ServiceDescriptor> is preserved.
  • When creating scopes using the IServiceScopeFactory abstraction, scopes are expected to be explicit and should be able to resolve from an outer scope, even if that outer scope is called after an inner scope is created. This means, for example, that scopes are not expected to work in the same ambient way as the TransactionScope class does. This behavior is specified in the NestedScopedServiceCanBeResolvedWithNoFallbackProvider test.
  • IServiceCollection allows registration of struct types.

Without a doubt there are many more of these explicit and implicit specifications that application developers, third party library developers and developers from within Microsoft will come to rely on over time; these are just a few that I noticed while figuring out how the new DI system works. Whether or not your team decides to make some of the implicit requirements explicit is irrelevant; hundreds of components and literally millions of developers will slowly begin to depend on each characteristic of the API. Any change made to any part of the contract (whether it is implicit or explicit) can break existing applications and third party components.

Each specification (requirement or restriction) defines a contract of what to expect from anyone who wishes to replace the built-in container with their own. This is what Mark Seemann’s defined as a Conforming Container. According to Mark's definition, the 'conforming container' is not the library that conforms or adapts to the framework, but rather the abstraction or required behavior defined by the framework is called the 'conforming container'. Whether or not there is a central IContainer interface for registration is irrelevant to the existence of such a Conforming Container. It's about having an API with some sort of guarantee that it will behave in a certain way, regardless of the underlying container implementation.

Now here’s the catch: There is no single DI library for .NET (other than vNext's built-in DI library itself) that adheres to all of the implicit and explicit requirements. I’m not just talking about Simple Injector here; there is no library that exactly conforms to the behavior specified in vNext. To give you an idea of what application developers and adapter builders will face, take a look at the following incompatibilities:

  • Simple Injector v3 will not treat open generic registrations as fallback registration by default, while the vNext API consumers will expect fallback behavior.
  • Castle Windsor always resolves the first element in a collection if you ask for one implementation; vNext consumers will assume the last to be resolved.
  • Ninject will throw an exception if you request exactly one instance when multiple are registered.
  • Registration of collections in Simple Injector are explicit and separate from other registrations (a bit like how Ninject prevents mixing those). See below for a more detailed discussion.
  • Unity forces multiple registrations of the same service (collections) to be named.
  • Autofac does not guarantee the order a registration will appear inside a resolved collection; vNext consumers will assume strict ordering.
  • Scopes in Castle Windsor and Simple Injector are ambient, just like .NET’s the TransactionScope. You can never resolve from the scope, you always resolve from the container and the container "knows" within which active scope it is currently running. Passing scopes around is useless.
  • The author of StructureMap explained to me that the registration API of StructureMap is so different, that implementing an adapter causes big performance problems for StructureMap users.
  • Many containers (like Simple Injector) do not allow structs to be registered in the container.

I don't see why it wouldn't work with the DI system. Is there some feature that Simple Injector doesn't support?

One of the most obvious differences between Simple Injector and the majority of DI libraries is, as already mentioned, the way users are expected to register collections. It is quite common for containers to allow users to make multiple distinct registrations for the same service type and the container will stack those registrations up as a collection (just as the vNext API specifies). Having witnessed this as a common source of configuration errors, Simple Injector forces the user to separate registrations of collections from 'normal' one-to-one mappings. The full reasoning for this is described here. This separation of registrations is something that doesn't marry with the model that vNext defines. So it’s not that Simple Injector “doesn’t support” some feature, but it simply has a different view on how a registration API should work and behave.

In vNext, the application is supplied with the list of registrations (i.e. an IList<ServiceDescriptor>). Both application developers, external libraries and other parts of the ASP.NET framework are expected to append, remove and replace registrations from this list. At the end of this process, vNext will integrate this configuration into its internal container. In the scenario that someone wants to replace the built-in container he needs to use the list of ServiceDescriptor instances and transform them into native container registrations. But as described above, this list comes with certain expectations that the adapter writer must fully understand and adhere to. Not adhering to these expectations, could cause applications to be wired incorrectly and external framework components to break directly, or break when a future version of such framework component or third party tool is released. This especially holds when this adapter is published on NuGet as a reusable library.

To adhere to the vNext specifications, an ASP.NET vNext adapter for Simple Injector must, among other things, make sure that any group of registrations that form a collection, are correctly registered in the Simple Injector container. Unfortunately, it is impossible for the adapter to confidently detect whether certain registrations are part of a collection or not. Although one might think that the adapter can easily group registrations by their service type, and register them as collection in case there is more than one registration in a group, this will not work. Take ASP.NET's IActionDescriptorProvider for instance; this abstraction will be resolved by framework code as a collection, but in its default configuration there is only one registration. Every ASP.NET application would immediately break when this approach is used.

Another idea would be to group the list of registrations by their service type and register each group always as collection (even if there is only one registration in the group), and in case the group contains just one registration, make the one-to-one mapping as well. Problem with this approach is that this would fundamentally change how Simple Injector behaves and this will confuse Simple Injector users. We lose the verification possibilities that separating collections brings and we would therefore lose some of the unique abilities that developers use and love about Simple Injector. And this the main point here: although it would be technically possible to create an adapter for Simple Injector; by doing so the adapter would change the behavior of Simple Injector in such radical way that it becomes a totally different library. It would become a library that loses some of its unique and compelling features. Mark Seemann terms this it stifles innovation.

But again, this –not only– holds for Simple Injector, this more or less holds for every DI library. Since no single DI library is 100% compatible with the vNext API, every adapter will have to do some transformation. With each transformation, the behavior of the adapted library is changed. Changed in a way that will confuse its users; changed in a way that will make it lose its uniqueness and usefulness. Your team has asked container builders to create and maintain adapters for the DI system, but this like opening a can of worms. In the end, everybody will lose.

But all is not lost! As I described in my previous comment, it is very easy to have the two worlds live side-by-side. Not only is it possible, I would say that it is actually preferable to use your custom (application) container side-by-side with the framework’s container. Mixing framework registrations and application registrations in a single container stems from the idea that application components need to be built-up using framework components. But doing this is a violation of the Dependency Inversion Principle which states that "the abstracts are owned by the upper/policy layers". Forcing application components to directly depend on framework types (even if they are abstractions) violates this principle. Instead of injecting framework types into application components, application components should depend solely on application abstractions. When required, small, specific adapter implementations can act as the gateway to framework types and methods.

To conclude, my main grievance is not so much the existence of the vNext DI system itself, it is with how the ASP.NET team suggests that existing DI libraries should be integrated with the new vNext model. It is my opinion that this is the wrong message. Instead of using adapters, application developers would be better advised to keep these things separate. Let vNext use its container internally and the vNext container is there for you to use if you need it (e.g. if you have never used a container before) and it will undoubtedly guide some developers towards the SOLID principles.

My hope is that the ASP.NET team will stop suggesting that adapters are needed to integrate with the internal DI system, and instead start showing examples of how easy it is to plug a custom library into the pipeline, just as I demonstrated in my earlier comment.

from simpleinjector.

davidfowl avatar davidfowl commented on May 20, 2024

@dotnetjunkie

To give you an idea of what application developers and adapter builders will face, take a look at the following incompatibilities

I'm not sure application developers will have to deal with anything. I do think adapter creators do though.

These individual expectations of the vNext runtime all form the API's specifications. Here is a small subset of the specifications that any adapter implementer should be aware of:

This is a great list and yes we do have some default expectations, or rather a "spec" on how we think the container should behave when these registrations are added, but we don't really have a registration API per se, nor do we prevent you from taking advantage of specific container you choose to use. That's one of the things in my mind that a conforming container does and we absolutely do not do that.

Each specification (requirement or restriction) defines a contract of what to expect from anyone who wishes to replace the built-in container with their own. This is what Mark Seemann’s defined as a Conforming Container

Now if you're saying that if we assume anything about how the DI system behaves without being a conforming container then you got us 😄 but that's not how I read it originally.

It's about having an API with some sort of guarantee that it will behave in a certain way, regardless of the underlying container implementation

If that's the case then yes, we have a conforming container. One that expects a base set of behavior and one that also lets you use a specific DI container to it's full ability. Autofac for example actually works pretty cleanly with ASP.NET 5 in general without changes to it. The end user can also use Autofac APIs directly to take advantage of their features. So in this case the "Conforming Container" lets you do more than you can do with the base assumptions, that's a win win IMO.

My hope is that the ASP.NET team will stop suggesting that adapters are needed to integrate with the internal DI system, and instead start showing examples of how easy it is to plug a custom library into the pipeline, just as I demonstrated in my earlier comment.

I think there can be a balance between the 2 worlds. You can absolutely replace the DI system and we should talk about how to do it and advertise the containers that are compatible and "conforming". We can also document well known composition roots so that non conforming containers can be used in very specific and narrow ways (IControllerActivator etc.). Heck, you can even use Pure DI if that floats your boat.

But again, this –not only– holds for Simple Injector, this more or less holds for every DI library. Since no single DI library is 100% compatible with the vNext API, every adapter will have to do some transformation. With each transformation, the behavior of the adapted library is changed. Changed in a way that will confuse its users; changed in a way that will make it lose its uniqueness and usefulness. Your team has asked container builders to create and maintain adapters for the DI system, but this like opening a can of worms. In the end, everybody will lose.

People will make compatible adapters since pretty much every DI library supports the list of features that our "conforming container" requires. There are quirks, but they can be analyzed and ironed out.

But doing this is a violation of the Dependency Inversion Principle which states that "the abstracts are owned by the upper/policy layers". Forcing application components to directly depend on framework types (even if they are abstractions) violates this principle.

I understand this statement but I wholeheartedly disagree with it. To each his own. I'm a huge fan of framework types being injected into user code.

from simpleinjector.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.