Add ASP.NET Core's Dependency Injection into Xamarin Apps with HostBuilder
Dependency injection(DI) and inversion of control (IoC), have been a recurring theme in questions that I have received over the last six years. Sometimes it is around using constructor injection(), simple service containers, and often around full frameworks such as Prism or MVVM Light. I will be honest with you, I have never been a fan of DI/IoC when building mobile apps. My main reason is that there has never been any official pattern or recommendations from Google or Apple (or Microsoft/Xamarin) when building Android and iOS apps. Also, I have always been worried around startup performance in my apps and didn't want to introduce another dependency. However, it is 2019 and I think that my concerns have finally been resolved after talking with my friend Glenn Condron, and here is why:
- There is a new official library from Microsoft that handles everything for you.
- Using this model gives you other powerful features like logging, HTTPClientFactory, and more.
- These devices are crazy powerful and the frameworks we use such as Xamarin.Forms, have really matured.
So, what is this magical library? It is the Microsoft.Extensions framework, but specifically the Generic Host framework that lives inside of Microsoft.Extensions.Hosting. It is a platform independent implementation of ASP.NET Core's extensibility framework.
The cool part about this library is that if you already have been building ASP.NET Core applications, then you already know exactly how to use the library. Additionally, it means that just about every other library that extends it works with your Xamarin apps like app configuration, logging, HTTPClientFactory, Polly, and more! Let's start with the basics and first setup a HostBuild, which is the base for the Extensions library and then setup some dependency injection with constructor injection.
Installation and Setup
The first thing to do is to install the v3 pre-release Micosoft.Extensions.Hosting NuGet into all of our projects. This will give our projects access to the base foundation and also includes the ability to use the built-in logging framework and application configuration via a json file.
With the library installed we will create two files in our shared code (also if you are building a single iOS/Android app this will also work):
Startup.cs
Will host all of our HostBuilder configuration and setup all of the dependencies in our application. Here is the base foundation for the file with all of the using statements we will need:
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Xamarin.Essentials;
namespace MyApp
{
public static class Startup
{
public static IServiceProvider ServiceProvider { get; set; }
public static void Init()
{
}
static void ConfigureServices(HostBuilderContext ctx, IServiceCollection services)
{
}
}
}
appsettings.json
This file will need to be set as an EmbeddedResource and will host configuration properties and settings. Here is a generic default appsettings file:
{
"Hello": "World",
"Environment": "Development",
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
Host Builder setup
The extensions library has a few main concepts to understand:
- HostBuilder: Acts as a builder for adding services (logging, app config, etc.) onto the app.
- IServiceProvider: Handles all dependency injection & acts as a service locator and comes from the HostBuilder.
Our Init method must be called from either the main Xamarin.Forms or platform specific app startup. We will fill in the Init method by getting a stream to the app settings file so the HostBuilder can read it, configure services, and setup the built in logging service:
var a = Assembly.GetExecutingAssembly();
using var stream = a.GetManifestResourceStream("MyAssemblyName.appsettings.json");
var host = new HostBuilder()
.ConfigureHostConfiguration(c =>
{
// Tell the host configuration where to file the file (this is required for Xamarin apps)
c.AddCommandLine(new string[] { $"ContentRoot={FileSystem.AppDataDirectory}" });
//read in the configuration file!
c.AddJsonStream(stream);
})
.ConfigureServices((c, x) =>
{
// Configure our local services and access the host configuration
ConfigureServices(c, x);
})
.ConfigureLogging(l => l.AddConsole(o =>
{
//setup a console logger and disable colors since they don't have any colors in VS
o.DisableColors = true;
}))
.Build();
//Save our service provider so we can use it later.
ServiceProvider = host.Services;
Configure Dependencies
We have setup our HostBuilder and called ConfigureServices, but we haven't done anything yet. So let's do it!
Let's say we have the following code for a ViewModel and a service that it needs to use:
public interface IDataService
{
void DoStuff();
}
public class MyDataService : IDataService
{
// We need access to the ILogger from Microsoft.Extensions so pass it into the constructor
ILogger<MyDataService> logger;
public MyDataService(ILogger<MyDataService> logger)
{
this.logger = logger;
}
public void DoStuff()
{
logger.LogCritical("You just called DoStuff from MyDataService");
}
}
public class MockDataService : IDataService
{
// We need access to the ILogger from Microsoft.Extensions so pass it into the constructor
ILogger<MyDataService> logger;
public MockDataService(ILogger<MyDataService> logger)
{
this.logger = logger;
}
public void DoStuff()
{
logger.LogCritical("You just called DoStuff from MockDataService");
}
}
public class MyViewModel
{
IDataService dataService;
public MyViewModel(IDataService dataService)
{
this.dataService = dataService;
}
public void DoIt()
{
dataService.DoStuff();
}
}
Here we have a ViewModel that needs access to the IDataService and one of the implementations needs access to a ILogger. This is where the ServiceCollection comes in allowing us to configure all of our dependencies. Let's setup the ConfigureServices method in the Startup.cs again and configure these new classes.
static void ConfigureServices(HostBuilderContext ctx, IServiceCollection services)
{
// The HostingEnvironment comes from the appsetting.json and could be optionally used to configure the mock service
if (ctx.HostingEnvironment.IsDevelopment())
{
// add as a singleton so only one ever will be created.
services.AddSingleton<IDataService, MockDataService>();
}
else
{
services.AddSingleton<IDataService, MyDataService>();
}
// add the ViewModel, but as a Transient, which means it will create a new one each time.
services.AddTransient<MyViewModel>();
//Another thing we can do is access variables from that json file
var world = ctx.Configuration["Hello"];
}
Access Dependencies
With our HostBuilder and dependencies configured, we can now access them from anywhere. Since we only went up to the ViewModel level we will need to grab it from the ServiceProvider in the code behind from our page:
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
BindingContext = Startup.ServiceProvider.GetService<MyViewModel>();
}
protected override void OnAppearing()
{
base.OnAppearing();
((MyViewModel)BindingContect).DoIt();
}
}
When we call GetService the Microsoft.Extensions framework will handle creating and injecting any dependencies including our IDataService and the ILogger that came for free just by using the library. When we call DoIt in the OnAppering our app ill print out a message to the console using the logger that will have info of what data service it was using.
HTTPClientFactory In Your Xamarin App!
That's right, if you are using Microsoft.Extensions you can use the awesome HTTPClientFactory class that is essentially HTTPClient done right! It handles caching, retries, logging, and all other sorts of awesome! It is really easy to put into your app with just a few lines of code.
First, we will need to install the v3 prerelease Microsoft.Extensions.Http NuGet to get access to HTTPClientFactory. Then in the ConfigureServices method add the following line of code:
services.AddHttpClient();
With this single line of code in place it is now time to get access to the HTTPClientFactory from one of our classes. This is handled like our interface that was passed in as a parameter to the constructor. Microsoft.Extensions takes care of everything for you:
public class MyDataService : IDataService
{
ILogger<MyDataService> logger;
HttpClient client;
public MyDataService(ILogger<MyDataService> logger, IHttpClientFactory httpClientFactory)
{
this.logger = logger;
client = httpClientFactory.CreateClient();
}
public void DoStuff()
{
logger.LogCritical("You just called DoStuff from MyDataService");
}
}
If you don't need access to the IHttpClientFactory at all this code can be simplified down to just passing in the HttpClient itself:
ILogger<MyDataService> logger;
HttpClient client;
public MyDataService(ILogger<MyDataService> logger, HttpClient client)
{
this.logger = logger;
this.client = client;
}
What is cool now is that when you make any calls with this HttpClient you will automatically get console logging, which is super helpful:
To top it off you can even extend the HttpClient beyond just logging and introduce Polly into the mix with the Microsoft.Extenions.Http.Polly library that will enable you to automatically setup retries and other really cool features. You can find an example of this in my "First App Example" with ASP.NET Core + Xamarin
Platform Specific Dependencies
At this point you are probably like WOW, this is amazing, but what about platform specific implementations that I may need to get access to. Do not fear because it is totally possible and pretty easy to setup. Instead of calling the Init from your shared code, you can call it from your iOS/Android project and pass in a configuration Action like this:
First, let's modify the Init:
public static App Init(Action<HostBuilderContext, IServiceCollection> nativeConfigureServices)
{
// Resource code
var host = new HostBuilder()
//ConfigureHostConfiguration
.ConfigureServices((c, x) =>
{
//Add this line to call back into your native code
nativeConfigureServices(c, x);
ConfigureServices(c, x);
})
// Logging
.Build();
}
Then from inside of the Android project's MainActivity and iOS project's AppDelegate you can call this Init method:
Android Example
public class MainActivity : FormsAppCompatActivity
{
protected override void OnCreate(Bundle savedInstanceState)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(savedInstanceState);
//setup code
Startup.Init(ConfigureServices);
LoadApplication(new App());
}
void ConfigureServices(HostBuilderContext ctx, IServiceCollection services)
{
services.AddSingleton<INativeCalls, NativeCalls>();
}
}
public class NativeCalls : INativeCalls
{
public void OpenToast(string text)
{
Toast.MakeText(Application.Context, text, ToastLength.Long).Show();
}
}
iOS Example
[Register("AppDelegate")]
public partial class AppDelegate : FormsApplicationDelegate
{
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
//Init Code:
Startup.Init(ConfigureServices)
LoadApplication(new App();
return base.FinishedLaunching(app, options);
}
void ConfigureServices(HostBuilderContext ctx, IServiceCollection services)
{
services.AddSingleton<INativeCalls, NativeCalls>();
}
}
public class NativeCalls : INativeCalls
{
public void OpenToast(string text)
{
var vc = UIApplication.SharedApplication.KeyWindow.RootViewController;
var okAlert = UIAlertController.Create(string.Empty, text, UIAlertControllerStyle.Alert);
okAlert.AddAction(UIAlertAction.Create("OK", UIAlertActionStyle.Default, null));
vc.PresentViewController(okAlert, true, null);
}
}
Inject The Entire App
As you can see this framework is extremely flexible and allows for optimal extensibility, but you can go even further. As I was trying out this API Glenn challenged me to see how far I could take this dependency/constructor injection madness... so I took it all the way up to the app level. That is right, I injected the entire app, pages, and view models.... because why not! Well maybe you shouldn't do this, but it is pretty cool.
Keeping the same dependencies and view models from above all we need to do is update our MainPage, App, and Init calls:
MainPage
public partial class MainPage : ContentPage
{
//simply pass in the view model you desire!
public MainPage(MyViewModel viewModel = null)
{
InitializeComponent();
BindingContext = viewModel;
}
}
App
There are a few ways to do this. You could pass in the page that you desire into the App constructor or you could have some logic and grab it from the ServiceProvider:
public App()
{
InitializeComponent();
MainPage = ServiceProvider.GetService<MainPage>();
}
Update Init
This is where it gets pretty cool, we will create Transient and Singletons of all of the services, view models, pages, and the app itself and have the method return the App intead of being a void method.
public static App Init(Action<HostBuilderContext, IServiceCollection> nativeConfigureServices)
{
//all of the same code from before
return ServiceProvider.GetService<App>();
}
static void ConfigureServices(HostBuilderContext ctx, IServiceCollection services)
{
services.AddHttpClient();
services.AddSingleton<IDataService, MyDataService>();
services.AddTransient<MyViewModel>();
services.AddTransient<MainPage>();
services.AddSingleton<App>();
}
Then, call Init from the LoadApplication method instead of calling Init randomly in the iOS/Android project:
LoadApplication(Startup.Init(ConfigureServices));
Just like that we dependency injected our entire application and that is crazy! If you want to try it yourself, you can grab my sample project from GitHub that shows off just about everything in this blog post.
Required Versions of Xamarin/VS
As of today, you should be able to develop and debug iOS/Android apps with all these goodies using any version of Visual Studio 2019 or Visual Studio for Mac 2019. However, if you want to use ship your app and turn on the Linker you will need to have a newer version of mono, which ships with Visual Studio 2019 16.2 (preview 2) and Visual Studio for Mac 2019 8.2 (preview). If you are on Windows you can easily install VS 2019 Preview side by side the normal release.
Watch The Build Session
I had the honor of presenting how ASP.NET and Xamarin can come closer in app model with Glenn in a 20 minute theater session. It is available on the Xamarin Developers YouTube channel:
Learn More
This stuff is super fun and I really hope that it takes off and we can encorporate it into user testing, and perhaps one day into the official templates! Until then it is easy to setup and can lead to some really clean and powerful code. Be sure to read through all of the great docs for Generic Host and through any of the extensions documentation. Enjoy!