NSouth
NSouth

Reputation: 5276

Can Prism use .NET Core's built in Dependency Injection?

I'd like to start a WPF app using .NET Core 3.1
Can Prism make use of .Net Core's built-in DI (IServiceCollection) or do I have to use something like Unity?
If Prism cannot use the built-in DI, can they exist side-by-side?

Upvotes: 3

Views: 4036

Answers (3)

Hakan Fıstık
Hakan Fıstık

Reputation: 19421

Can Prism make use of .Net Core's built-in DI

Short Answer, NO

Here is a comment by @brianlagunas (The creator of Prism)

As I mentioned, we can't use IServiceProvider as we are in netstandard 1.0. The ServiceProvider and IServiceCollection is in netstandard 2.0. Also, there are a number of features that Prism needs that are to limited in the IServiceCollection implementation. Such as named instances and registrations, as well as a mutable container.

here is a comment by @dansiegel

I have spent a lot of time discussing this issue, and ultimately we cannot directly rely on IServiceProvider and IServiceCollection for a variety of reasons that extend beyond whether or not they are available.

here is the another comment also by @brianlagunas

Upvotes: 0

Alex
Alex

Reputation: 893

Can Prism make use of .Net Core's built-in DI

From what I've seen you can't really replace Prism's DryIot with the ASP.NET Core build-in one. Mainly DryIot is more feature-full than the ServiceCollection API. There is this opensource package I've found that has an IContainerExtension implementation using ServiceCollection, but per the developer's own words this is more of a proof of concept rather than sable solution.

If Prism cannot use the built-in DI, can they exist side-by-side?

Yes, they can. With a caveat - you cannot simply register a service in ServiceCollection and expect to be able to inject that service directly in your App, Modules and ViewModels. This will fail because those files are managed by the Prism framework and thus injection will only work for services you have registered using the IContainerRegistry interface.

Benefits

Why would you do it? As the build-in IoT container the ServiceCollection API is well-known, thus it will be simpler for .Net developers. Furthermore you can structure you non-WPF projects to register services using the default container thus allowing them to be completely decoupled from your Desktop project. This is very good for more complex architectures like Domain-Driven Design.

Let's consider the following project structure:

solution
-- Desktop // Prism WPF application, containing only views and models
-- Application // Class library, containing operational logic.

Let's say that as a part of the Application project you need an IUserService which holds information about the current user that has to be populated in-memory when the user authenticates in the Desktop app. A registration method would look like this:

public IServiceCollection AddServices(this IServiceCollection services)
{
    services.AddSingleton<IUserService, UserService>()
}

So now we need to inject it inside the Desktop project. I suggest two methods:

  1. Simple
  2. Seemless

Simple

This approach requires very simple startup configuration. The caveat is that you will not be able to inject your services directly in the constructor, but through the IServiceProvider interface.

  1. Reference Microsoft.Extensions.DependencyInjection

  2. Call your service registration method in App:

     protected override void RegisterTypes(IContainerRegistry container)
     {
         // Build service provider using the ServiceCollection API
         var provider = new ServiceCollection()
             .AddServices()
             .BuildServiceProvider();
    
         // Register IServiceProvider within DryIot and provide 
         // a factory method to retrieve the service instance
         container.Register<IServiceProvider>(() => provider);
     }
    
  3. Inject IServiceProvider where you need IUserService. For this example I'll use a Prism Module:

     public class Module : IModule
     {
         private readonly IUserService userService;
    
         public Module(IServiceProvider serviceProvider)
         {
             this.userService = serviceProvider.GetService<IUserService>();
         }
         ...
         private void Authenticate()
         {
             this.userService.IsAuthenticated = true;
         }
     }
    

That's it, you can now use your ServiceCollection registered dependency wherever you can access the IServiceProvider through Prism injection. This is the approach I recommend, because we are simply wrapping the .Net container in Prism's.

Seemless

This is where it gets a bit more interesting. Full disclaimer - you might encounter problems using this approach. I have not yet tested this beyond the most basic use-case. The only advantage this method offers is that you will be able to directly inject services in the constructor, instead of having to go through the IServiceProvider.

In its essence this method is simply looping through the ServiceCollection and registering all services directly in the Prism container. If we take a look at the implementation of ServiceCollection - it is simply a list of ServiceDescriptors. Upon further inspection we observe that ServiceDescriptior contains a bunch of constructors. We'll ignore those and focus on the properties:

  1. ServiceType - the type that will be used when injecting
  2. ImplementationType - type of the implementation to be injected
  3. ImplementationInstance - instance of the implementation type
  4. ImplementationFactory - factory delegate that returns an instance of the implementation type
  5. LifeTime - Singleton, Scoped or Transient type

Let's now inspect the IContainerRegistry interface. We'll see that there are a lot of overloads of Register that accept Types, object and delegates.

Using that knowledge we can now create an adapter from ServiceDescriptor to registration of IContainerRegistry. The below implementation will only focus on Transient services, but the difference between service lifetimes is simply which registry method we call - Register for a Transient and RegisterSingleton for well Singletons.

  1. Create and Adapter class with static method that accepts IContainerRegistry and ServiceDescriptor arguments:

     public static void Register(IContainerRegistry container, ServiceDescriptor service)
     {
         // In case an implementation instance is provided we simply need to register it
         if (service.ImplementationInstance != null)
         {
             containter.Register(service.ServiceType, service.ImplementationInstance);
         }
         // In case a factory is provided we have a bit more work. 
         // We need to modify it in order for it to be usable by the DryIot container
         else if (service.ImlementationFactory != null)
         {
             var factory = service.ImplementationFactory;
             var dryIotFactory = dryIotProvider =>
             {
                 var provider = dryIotProvider.Resolve<IServiceProvider>();
                 return factory(provider);
             };
    
             container.Register(service.ServiceType, dryIotFactory);
         }
         // If no implementation or factory is provided then we simply register the
         // types and let the container create the implementation instance on its own.
         else
         {
             container.Register(service.ServiceType, service.ImplementationType);
         }
     }
    

The most tricky part here is the factory. To better understand factories in service-registration know that sometimes you may need access to other services to provide the correct implementation instance. For example if IHttpClient is registered you need to provide the IAuthorizationSerice with HttpAuthorizationService implementation instead of DesktopAuthorizationService.

Essentially we wrap the original factory method with a DryIot-compatible factory (accepts instance of DryIot container) that can supply the original factory with IServiceProvider instance.

  1. Reference Microsoft.Extensions.DependencyInjection

  2. Call your service registration method in App:

     protected override void RegisterTypes(IContainerRegistry container)
     {
         var services = new ServiceCollection().AddServices()
    
         foreach (var service in services)
         {
             Adapter.Register(container, service);
         }
     }
    
  3. Inject IUserService directly in the module constructor:

     public class Module : IModule
     {
         private readonly IUserService userService;
    
         public Module(IUserService userService)
         {
             this.userService = userService;
         }
     }
    

Final thoughts

Again, I recommend the simple approach. Simplicity means lower learning curve and less room for errors. The inconvenience is minor in comparison.

Another fair warning - this is not production ready code. Especially the seemless method. I have yet to "battle-test" this implementation, but it might point you in the right direction.

If anyone has feedback/opinions I would be glad to read about it :)

Upvotes: 6

Haukinger
Haukinger

Reputation: 10863

do I have to use something like Unity?

The ServiceCollection is "something like Unity". And, yes, you can use it with prism:

  1. Create an IContainerExtension implementation that redirects to ServiceCollection
  2. Derive from PrismApplicationBase and return your container extension from CreateContainerExtension

Upvotes: 1

Related Questions