依赖注入问题
The container is locked after the first call to resolve
When an application makes its first call to GetInstance, GetAllIntances or Verify, the container locks itself to prevent any future changes being made by explicit registrations. This strictly separates the configuration of the container from its use and forces the user to configure the container in one single place. This design decision is inspired by the following design principle:
In most situations it makes little sense to change the configuration once the application is running. This only makes the application more complex, whereas dependency injection as a pattern is meant to lower the total complexity of a system. By strictly separating the registration/startup phase from the phase where the application is in a running state, it is easier to determine how the container will behave and it is easier to verify the container’s configuration. The locking behavior of Simple Injector exists to protect the user from defining invalid and/or confusing combinations of registrations.
Allowing to alter the DI configuration while the application is running could easily cause strange, hard to debug, and hard to verify behavior. This may also mean the application has numerous hard references to the container and this is something you should strive to prevent. Attempting to alter the configuration when running a multi-threaded application would lead to nondeterministic behavior, even if the container itself is thread-safe.
Imagine the scenario where you want to replace some FileLogger component for a different implementation with the same ILogger interface. If there’s a component that directly or indirectly depends on ILogger, replacing the ILogger implementation might not work as you would expect. If the consuming component is registered as singleton, for example, the container should guarantee that only one instance of this component will be created. When you are allowed to change the implementation of ILogger after a singleton instance already holds a reference to the “old” registered implementation, the container has two choices—neither of which are correct:
- Return the cached instance of the consuming component that has a reference to the “wrong” ILogger implementation.
- Create and cache a new instance of that component and, in doing so, break the promise of the type being registered as a singleton and the guarantee that the container will always return the same instance.
The description above is a simple to grasp example of dealing with the runtime replacement of services. But adding new registrations can also cause things to go wrong in unexpected ways. An example would be where the container has previously supplied the object graph with a default implementation resolved through unregistered type resolution.
Problems with thread-safety can easily emerge when the user changes a registration during a web request. If the container allowed such registration change during a request, other requests could directly be impacted by those changes (because, in general, there should only be one Container instance per AppDomain). Depending on things such as the lifestyle of the registration, the use of factories, and how the object graph is structured, it could be a real possibility that another request gets both the old and the new registration. Take for instance a transient registration that is replaced with a different one. If this is done while an object graph for a different thread is being resolved while the service is injected into multiple points within the graph—the graph would contain different instance of that abstraction with different lifetimes at the same time in the same request—and this is bad.
Because we consider it good practice to lock the container, we were able to greatly optimize performance of the container and adhere to the Fast by default principle.
Do note that container lockdown still allows runtime registrations. A few common ways to add registrations to the container are:
- Using unregistered type resolution the container will be able to at a later time resolve new types.
- The Lifestyle.CreateProducer overloads can be called at any point in time to create new InstanceProducer instances that allow building new registrations.
All these options provide users with a safe way to add registrations at a later point in time, without the risks described above.
Fast by default
For most applications the performance of the DI library is not an issue; I/O is usually the bottleneck. You will find, however, that certain DI libraries are very sensitive to different configurations and you will need to monitor the container for potential performance problems. Most performance problems can be fixed by changing the configuration (changing registrations to singleton, adding caching, etc), no matter which library you use. At that point however it can get really complicated to configure certain libraries.
Making Simple Injector fast by default removes any concerns regarding the performance of the construction of object graphs. Instead of having to monitor Simple Injector’s performance and make ugly tweaks to the configuration when object construction is too slow, you are free to worry about more important things.
Fast by default means that the performance of object instantiation from any of the registration features that the library supplies out-of-the-box will be comparable to the performance of object instantiation done by hand in plain C#.
Unregistered type resolution
Unregistered-type resolution is the ability to get notified by the container when a type that is currently unregistered in the container, is requested for the first time. This gives the user (or extension point) the chance of registering that type. Simple Injector supports this scenario with the ResolveUnregisteredType event. Unregistered type resolution enables many advanced scenarios.
For more information about how to use this event, please take a look at the ResolveUnregisteredType event documentation in the reference library.