Maui: [Spec] Microsoft.Extensions.Hosting and/or Microsoft.Extensions.DependencyInjection

Created on 18 May 2020  ·  21Comments  ·  Source: dotnet/maui

Bake the features of Microsoft.Extensions.Hosting into .NET MAUI

https://montemagno.com/add-asp-net-cores-dependency-injection-into-xamarin-apps-with-hostbuilder/

Utilize the Generic Host structure that's setup with .netcore 3.0 to initialize .NET MAUI applications.

This will provide users with a very Microsoft experience and will bring a lot of our code in line with ASP.NET core

Deeply root IServiceProvider into .NET MAUI

Replace all instances of Activator.CreateInstance(Type) with IServiceProvider.Get()

For example if we change this out on ElementTemplate
https://github.com/dotnet/maui/blob/1a380f3c1ddd9ba76d1146bb9f806a6ed150d486/Xamarin.Forms.Core/ElementTemplate.cs#L26

Then any DataTemplate specified via type will take advantage of being created via constructor injection.

Examples

```C#
Host.CreateDefaultBuilder()
.ConfigureHostConfiguration(c =>
{
c.AddCommandLine(new string[] { $"ContentRoot={FileSystem.AppDataDirectory}" });
c.AddJsonFile(fullConfig);
})
.ConfigureServices((c, x) =>
{
nativeConfigureServices(c, x);
ConfigureServices(c, x);
})
.ConfigureLogging(l => l.AddConsole(o =>
{
o.DisableColors = true;
}))
.Build();

static void ConfigureServices(HostBuilderContext ctx, IServiceCollection services)
{
if (ctx.HostingEnvironment.IsDevelopment())
{
var world = ctx.Configuration["Hello"];
}

services.AddHttpClient();
services.AddTransient<IMainViewModel, MainViewModel>();
services.AddTransient<MainPage>();
services.AddSingleton<App>();

}

### Shell Examples

Shell is already string based and just uses types to create everything so we can easily hook into DataTemplates and provide ServiceCollection extensions 

```C#
static void ConfigureServices(HostBuilderContext ctx, IServiceCollection services)
{
     services.RegisterRoute(typeof(MainPage));
     services.RegisterRoute(typeof(SecondPage));
}

If all the DataTemplates are wired up through the IServiceProvider users could specify Interfaces on DataTemplates

<ShellContent ContentTemplate="{DataTemplate view:MainPage}"/ShellContent>
<ShellContent ContentTemplate="{DataTemplate view:ISecondPage}"></ShellContent>

Baked in constructor injection

```C#
public class App
{
public App()
{
InitializeComponent();
MainPage = ServiceProvider.GetService();
}
}
public partial class MainPage : ContentPage
{
public MainPage(IMainViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}

public class MainViewModel
{
public MainViewModel(ILogger logger, IHttpClientFactory httpClientFactory)
{
var httpClient = httpClientFactory.CreateClient();
logger.LogCritical("Always be logging!");
Hello = "Hello from IoC";
}
}

### This will allow Shell to also have baked in Constructor Injection

```C#
Routing.RegisterRoute("MainPage", MainPage)

GotoAsync("MainPage") // this will use the ServiceProvider to create the type

All the ContentTemplates specified as part of Shell will be created via the IServiceProvider

    <ShellContent
        x:Name="login"
        ContentTemplate="{DataTemplate MainPage}"
        Route="login" />

Implementation Details to consider

Use Microsoft.Build to facilitate the startup pipeline

Pull in the host features to articulate a specific startup location where things are registered
https://montemagno.com/add-asp-net-cores-dependency-injection-into-xamarin-apps-with-hostbuilder/

This has the benefit of letting us tie into implementations of IoC containers that already work against asp.net core

Pros: This gives .NET developers a consistent experience.
Cons: Performance? Is this overkill for mobile?

DI Container options

Deprecate DependencyService in favor of Microsoft.Extensions.DependencyInjection

Xamarin.Forms currently has a very simple home grown dependency service that doesn't come with a lot of features. Growing the features of this service in the face of already available options doesn't make much sense. In order to align ourselves more appropriately with other Microsoft Platforms we can switch over to the container inside Microsoft.Extensions.DependencyInjection.

Automatic registration of the DependencyService will be tied to the new registrar. If the user has opted in for the registrar to do assembly scanning than this will trigger the DependencyService to scan for assembly level attributes

One of the caveats of using this container is that types can't be registered on the fly once the app has started. You can only register types as part of the startup process and then once the IServicePRovider is constructed that's it. Registration is closed for business

Pros: It's a full featured container
Cons: Performance?

Convert our DependencyService to use IServiceCollection as an internal container and have it implement IServiceProvider

This would allow us to use a very slimmed down no featured container if people just want the best performance. We could probably use this as a default and then people could opt in for the more featured one if they want.

```C#
public class DependencyService : IServiceProvider
{
}

public static ServiceCollectionExtensions
{
public static DependencyService Create(this IServiceCollection);
}
```

Considerations

Is this overall useful for a new users app experience? Do we really want to add the overhead of understanding a the build host startup loop for new users? It would probably be useful to just have a default setup for all of this that just uses Init and then new users can just easily do what they need to without having to setup settings files/configureservices/etc..

Performance

In my tests limited tests it takes about 25 ms for the Microsoft Hosting bits to startup. We'll probably want to dive deeper into those 25 ms to see if we can get around it or if that cost is already part of a different startup cost we will already incur

Backward Compatibility

  • The current DependencyService scans for assembly level attributes which we will most likely shift to being opt in for .NET MAUI. The default will require you to register things via the Service Collection explicitly

Difficulty : Medium/Large

Existing work:
https://github.com/xamarin/Xamarin.Forms/pull/8220

breaking proposal-open

Most helpful comment

Something to consider when implementing this is using the Microsoft.Extensions.DependencyInjection.Abstractions project as the main dependency across the implementation in Maui. By reducing the footprint of Microsoft.Extensions.DependencyInjection to only be used on creating the container it will enable 3rd party libraries and frameworks to be agnostic of the container.

This enables a developer or framework the option to use a different container than the one provided by Maui. By using the abstractions project the work required by the developer or framework is just implementing the interfaces in the abstractions project. Where all of the Maui code will just use the interfaces. This will provide a massive extension point for everyone as we re-work how Dependency Injection works in the platform.

All 21 comments

Something to consider when implementing this is using the Microsoft.Extensions.DependencyInjection.Abstractions project as the main dependency across the implementation in Maui. By reducing the footprint of Microsoft.Extensions.DependencyInjection to only be used on creating the container it will enable 3rd party libraries and frameworks to be agnostic of the container.

This enables a developer or framework the option to use a different container than the one provided by Maui. By using the abstractions project the work required by the developer or framework is just implementing the interfaces in the abstractions project. Where all of the Maui code will just use the interfaces. This will provide a massive extension point for everyone as we re-work how Dependency Injection works in the platform.

@ahoefling Yea that's how Maui will consume everything. The only question is if we need to use the already implemented container for the default implementation or roll our own that's a bit more mobile minded as far as performance

@PureWeen The current default container implementation performs well for mobile. The wad of dependencies that the hostbuilder brings in is staggering though.

@PureWeen this is great to hear!

As far as I understand the extensions project container performance is pretty solid. I would recommend that we start with the default container and we can iterate if performance tests don't meet our expectations. I put together a little code sample of what I was thinking to handle some of the extension points.

This allows us to easily swap out the container by developers, frameworks or the Maui platform if we want to use a different container.

Maybe the System.Maui.Application could look something like this:
```c#
public class Application : Element, IResourcesProvider, IApplicationController, IElementConfiguration
{
public IServiceProvider Container { get; }

protected virtual IServiceProvider CreateContainer()
{
    var serviceCollection = new ServiceCollection();
    RegisterDependencies(serviceCollection);
    return serviceCollection.BuildServiceProvider();
}

// This should be implemented in the app code
protected virtual void RegisterDependencies(IServiceCollection serviceCollection)
{
    // TODO - Register dependencies in app code
}

public Application()
{
    Container = CreateContainer();

    // omited code
}

// omitted code

}
```

@ahoefling can we use this to allow people to create their own IServiceProvider?

https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder.useserviceproviderfactory?view=dotnet-plat-ext-3.1

That's what things like AutoFac hook into

As far as I understand the Host Builder provides a rich API to add more default dependencies such as logging out of the box. Where my suggested code is much more lean. I don't think either of the approaches are more 'right' than the other, as they solve different problems with Dependency Injection.

Host Builder
If we want to create the opinions that console logging will happen a certain way and let's say HttpClientFactory will work a certain way than the Host Builder is probably going to be our best option.

Lean ServiceCollection
If we want our DI system to be as lean as possible and let the app developer decide what will work for them and what won't, then I think my suggested code or similar would be the way to implement it.

The way I am looking at the problem is what is the least invasive way to swap out the DependencyService. If the end goal is to follow the patterns in place with asp.net core and the host builder model it'll probably best to create a Startup.cs class that utilizes the host builder.

The code in the OP is problematic as it's unclear from the types involved how ServiceProvider.GetService<MainPage>() works, and how MainViewModel gets discovered. It also involves a null which should trigger a NRT warning. This all seems like a way to circumvent the type system. ASP.Net is not a shining example of a clean API.

It's important that users should be able to avoid use of this capability, as they can now, and specify the objects in a type-safe way. And that by avoiding it, users also avoid paying the performance penalty.

type App() =
    inherit Application()
    let vm = someMainViewModel(...)
    let mainPage = MainPage(vm)
    do  base.MainPage <- mainPage

If people want to get a viewmodel in a reflection/convention/annotation-based way, does MAUI really need to provide explicit support? Can't they do this anyway? The original link suggests they can.

@ahoefling

Host Builder vs Lean ServiceCollection

Right, figuring out the benefit of one vs the other is definitely a big part of this!

At first I was just looking at Lean Service Collections but decided to go all in.
Part of the advantage of using the builder direction is that it just ties into existing netcore implementations. For example with AutoFac you can just use the same startup loop syntax opposed to having to invent our own thing. The domain knowledge is transferrable.

@PureWeen This makes sense and is a good idea to keep with the unification of .NET 5+ and the transferable skills between the different tools.

I think we should create a Startup.cs that sits right next to the App.cs or App.xaml.cs that handles all the startup code. This will unify the Dependency Injection story in Maui with other projects in the .NET Ecosystem. When I implemented Dependency Injection in DNN Modules last year I did something similar which allowed for developers to transfer their skills quite easily.

think we should create a Startup.cs that sits right next to the App.cs or App.xaml.cs that handles all the startup code.

New users won't want to figure out all of this bootstrapping stuff. In my opinion, this is the last thing you want especially since MAUI will alleviate all of the boilerplate around AppDelegate, MainActivity, etc. 1 startup/app.xaml to rule them all.

@aritchie agreed

I'd like to enable people as much as possible but for new users most of this can just stay hidden.
But then once they are comfortable they can start extending things.

I think there are scenarios here where users can get advantages here without having to buy into everything.

For example with Shell we could just introduce a RegisterRoute syntax like

Shell.RegisterRoute<TBindingContext, TPage>(String routeName);
or just
Shell.RegisterRoute<TType>();

That under the hood uses all of this stuff to create the types. Then if users want an HttpContext that'll get injected in etc..

The other part of this proposal is to try and create all DataTemplates via the IServiceProvider as well which enables some fun scenarios.

I was playing with some variations with Shell here based on James's sample

https://github.com/PureWeen/AllExtensions-DI-IoC/tree/shell_ioc

@charlesroddie

If people want to get a viewmodel in a reflection/convention/annotation-based way, does MAUI really need to provide explicit support?

The plan currently isn't to provide anything through reflection because of performance considerations. There might be some areas we can simplify registration without taking a performance hit but still need to explore these some.

I've also updated the original comment with registration syntax

@PureWeen

I would recommend having some sort of route extensions off the IServiceCollection so things like Prism can plugin to the model. I've been using XF for years and I don't use Shell.

ie. services.UseRoute(optional name)

On another note, Shiny has extensive amounts of use around the Microsoft DI mechanics if you need other ideas. I ended up rejecting the hostbuilder model (in its current form) because of the amount of things it brought in which lead to a horrid fight with the linker. I'd be happy to share these experiences on a call at some point.

Perhaps Source Generators could be used to alleviate the performance issues? Then you can still use the custom attributes but do all the (default) registration in a generated source file.

@aritchie sounds good!

once we're on the other side of build fun let's chat!

@rogihee

Perhaps Source Generators

Yea!! We're wanting to restructure the current renderer registration pre Maui and we're looking at the Source Generators for that work. If we end up going that route we could definitely leverage that for this

Would be great to have DI in Xamarin Forms, but I don't read anything about DI scopes. Using DI scopes is a great way to manage the lifetime of components. It's especially useful when working with EF core's database contexts.

It would be great to have a DI-aware ICommand implementation that creates a scope and requests a command-handler object on this scope.

Using dependency injection in WPF applications is also hard, so maybe team up with the WPF team to allow using DI in WPF in a similar way.

The only question is if we need to use the already implemented container for the default implementation or roll our own that's a bit more mobile minded as far as performance

I would say to implement a container for the Maui from scratch. As you can see here, the DependecyService implemented in the Xamarin.Forms is the faster one. (I know... That article is a little old, but I don't think that values change too much).
Also it is way better - in terms of performance - to create our own DI Container. But my concerns are for a world without .NET 5. With .NET 5 and the source code generator, we may have another solution to this problem.
And, for mobile, if you need a heavy DI you are doing something wrong.

I came to this repository to propose making MAUI applications support the generic host (Microsoft.Extensions.Hosting), preferable by default (although some people might want to protect), as it's very natural to have DI in a UI application. It would mean that people need to understand less, if they are used to dependency injection for asp.net core than it will also work with MAUI. For beginners it might be good to keep the configuration very minimalistic.

Instead of being the first, I found this issue, and think it's a good discussion!

Having applications which provide some services and a small UI (status, configuration etc.) is still a common thing. Every time I build a small application which provides some service, I keep finding the need to extend this with a UI.

I already build something to make Windows Forms and WPF applications run on the generic host, as can be seen in this project: https://github.com/dapplo/Dapplo.Microsoft.Extensions.Hosting
I was hoping to use this for Greenshot, but currently I haven't released anything with it yet.
In the samples there are WPF projects which use ReactiveUI, MahApps and Caliburn.Micro.

Please don't make me register the views manually in DI. Take the best from blazor, its great! In blazor I can easily pass down parameters or inject the service in the component. No need to register the component it self. I only need to register the services. MVVM is great for pages, but I have never felt more productive creating these self hosted component views. I hate starting a new Xamarin, WPF, UWP project because of the things which is not there out of the box. Once again, look at Blazor, its a true blueprint for all future GUI-frameworks.

I can't understand why anyone would ever create these two concecutive statements:

    services.AddTransient<IMainViewModel, MainViewModel>();
    services.AddTransient<MainPage>();

What is wrong with injecting the service to the views code behind? Its what you want 99.99 % of the time.

Also I like very much the Property Injection in blazor instead of constructor injection, just because its easier.

I actually just released a library that does this for Xamarin.Forms https://github.com/hostly-org/hostly . It uses a custom implementation of IHost, that can be easily set up in the native entry points. e.g. in MainActivity you would replace:

LoadApplication(new App());`

with:

new XamarinHostBuilder().UseApplication<App>()
   .UseStartup<Startup>()
   .UsePlatform(this)
   .Start();

it also has some extra goodies built in like support for registering middleware with navigation.

I’m in favour of using a DI system that is outside the System.Maui namespace so that code can be shared with both MAUI and non-MAUI projects.

Where I am less certain is regarding using Microsoft.Extensions.DependencyInjection or something based on it as that DI system. I won’t pretend to be an expert on this – I certainly haven’t used multiple modern DI systems myself. However, I wonder if others have read the second edition of “Dependency Injection. Principles, Practices and Patterns” by Steven van Deursen and Mark Seemann. They devote a section in the second edition to looking at Autofac, Simple Injector, and Microsoft.Extensions.DependencyInjection, providing pros & cons, as well as their own opinions/conclusions. Whilst I can see some benefits of using Microsoft.Extensions.DependencyInjection (primarily that it’s used in other Microsoft scenarios) in the MAUI world, I wonder if anybody here with experience of multiple DI systems could comment on the authors’ conclusions and to what degree they relate to the MAUI world of mobile and desktop usage?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

PureWeen picture PureWeen  ·  9Comments

qcjxberin picture qcjxberin  ·  5Comments

mhrastegary77 picture mhrastegary77  ·  3Comments

probonopd picture probonopd  ·  50Comments

Suplanus picture Suplanus  ·  4Comments