dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
22.05k stars 1.73k forks source link

[Spec] Microsoft.Extensions.Hosting and/or Microsoft.Extensions.DependencyInjection #24

Closed PureWeen closed 3 years ago

PureWeen commented 4 years ago

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

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

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

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

public class MainViewModel
{
        public MainViewModel(ILogger<MainViewModel> 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

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.

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

Difficulty : Medium/Large

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

SkyeHoefling commented 4 years ago

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.

PureWeen commented 4 years ago

@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

aritchie commented 4 years ago

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

SkyeHoefling commented 4 years ago

@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:

public class Application : Element, IResourcesProvider, IApplicationController, IElementConfiguration<Application>
{
    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
}
PureWeen commented 4 years ago

@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

SkyeHoefling commented 4 years ago

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.

charlesroddie commented 4 years ago

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.

PureWeen commented 4 years ago

@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.

SkyeHoefling commented 4 years ago

@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.

aritchie commented 4 years ago

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.

PureWeen commented 4 years ago

@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

PureWeen commented 4 years ago

@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

aritchie commented 4 years ago

@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<Page, VM>(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.

rogihee commented 4 years ago

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.

PureWeen commented 4 years ago

@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

ramondeklein commented 4 years ago

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.

pictos commented 4 years ago

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.

Lakritzator commented 4 years ago

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.

jsandv commented 4 years ago

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.

AtLeastITry commented 4 years ago

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.

johnshardman commented 4 years ago

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?

davidortinau commented 3 years ago

Registering Resources

Now that we're able to start using this, I'm wondering how we might include other common app config things starting with resources. Typically at this app level you set resources (styles, fonts, icons, converters, data templates, etc.) in the App.xaml.

I would like the ability to do something like RegisterResources("path\to\styles.xaml") and include one or more RDs, plus stylesheets.

radderz commented 3 years ago

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.

This could easily be simplified using a simple source generator, if there was naming convention, attribute used or base class? So that you just do a single extension method to add the views/viewmodels to the container without having to individually add them, this would still improve performance versus using reflection.

marinasundstrom commented 3 years ago

This is so cool. Inversion of Control and Dependency injection is kind if the default to many of us developers. Being able to inject services everywhere is so useful.

It will simplify unit testing too, having mock implementations of certain services that you previously had to make your own abstractions for.

The Routingtype should really be an injectable service: a Routerclass (IRouter), that can be used with NavigationPage too, perhaps. Detecting whether is is a Shell or a NavigationPage. Keeping the static Routing class for backwards compatibility until people have migrated.

marinasundstrom commented 3 years ago

I have been trying to implement custom Shell navigation with dependency injection in Xamarin.Forms, but no luck. After digging in the XF source code.

To take into consideration:

Shell was clearly following the rest of Xamarin.Forms legacy in being a closed system where DI is limited to internal use when allowed in a non-universal fashion.

My questions are:

In my case, I found that the functionality of this nested class, in particular, really should use have been extendible. This is where you could get the Page from an IoC container.

XF: https://github.com/xamarin/Xamarin.Forms/blob/caab66bcf9614aca0c0805d560a34e176d196e17/Xamarin.Forms.Core/Routing.cs

Maui: https://github.com/dotnet/maui/blob/18e0f4ebbcca7c904ccdb67e6439cb72382e2a40/src/Controls/src/Core/Routing.cs#L8

public static class Routing 
{
       // Omitted

            class TypeRouteFactory : RouteFactory
        {
            readonly Type _type;

            public TypeRouteFactory(Type type)
            {
                _type = type;
            }

            public override Element GetOrCreate()
            {
                return (Element)Activator.CreateInstance(_type);
            }
            public override bool Equals(object obj)
            {
                if ((obj is TypeRouteFactory typeRouteFactory))
                    return typeRouteFactory._type == _type;

                return false;
            }

            public override int GetHashCode()
            {
                return _type.GetHashCode();
            }
        }
}
schovan commented 3 years ago

Hello. What about ViewModel first approach? Do we need to use third party MVVM frameworks like in Xamarin.Forms?

marinasundstrom commented 3 years ago

@schovan MVVM is a pretty simple pattern. Just implement INotifyPropertyChanged. Create a base class. And MAUI, just like XF, does come with a Command implementation out of box.

Do you specifically mean for navigation? That is pretty easy to implement yourself on top of MAUIs navigation stack where views is object of navigation. Just create a NavigationService class that wraps that functionality to map view model to views.

Otherwise, navigation is pretty opinionated and MAUI cannot support every style.

schovan commented 3 years ago

@robertsundstrom Yes, I mean navigation and a View creation (or View - ViewModel pairing). I was thinking of something built-in, to avoid editing every View's code-behind, you can see it for example here:

public partial class MainPage : ContentPage
{
   public MainPage(IMainViewModel viewModel)
   {
            InitializeComponent();
            BindingContext = viewModel;
   }
}
marinasundstrom commented 3 years ago

@schovan Ah. OK. think this will be possible now when they use the HostBuilder for setting upp Dependency Injection - unless the containers still are separate. I have not tested it though.

This is what I did in XF: https://github.com/robertsundstrom/xamarin-forms-app/blob/main/MobileApp/ShellApp/App.xaml.cs

marinasundstrom commented 3 years ago

You should be able to inject your own services into places like Views and custom Markup Extensions.

In XF, there are separate containers, closed ones, under the hood. There should just be one that is being used in all applicable cases.

MAUI services should be swappable, through this one container.

pjmagee commented 3 years ago

This only integrates the DI side. The Hosting is using custom Host not GenericHost. I think some pages around how this differs from WebHost and GenericHost will be really helpful for Developers because if they see the standard extensions they might believe certain features to just work....like expecting IHostEnvironment to be populated when at the moment it doesnt.

AraHaan commented 3 years ago

What about people using Microsoft.Extensions.Logging.Console? Could one somehow reroute where those are outputted to an MAUI textbox?

KalleOlaviNiemitalo commented 3 years ago

Microsoft.Extensions.Logging.Console uses Console.Out and Console.Error, so if you want to redirect Microsoft.Extensions.Logging.Console to something in MAUI, then you need to implement a custom TextWriter class that forwards the output to the right place, perhaps via something AsyncLocal. This is made more complicated by the output possibly including SGR control sequences for changing colors.

https://github.com/dotnet/runtime/blob/9035d94a5075a482307383132e46974699fe08b3/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProvider.cs#L51-L60

https://github.com/dotnet/runtime/blob/9035d94a5075a482307383132e46974699fe08b3/src/libraries/Microsoft.Extensions.Logging.Console/src/AnsiLogConsole.cs#L15-L18

The better solution would be to implement ILogger and ILoggerProvider yourself and register the latter in the dependency container, thus using Microsoft.Extensions.Logging.Abstractions but not Microsoft.Extensions.Logging.Console.