Click here to Skip to main content
15,886,873 members
Articles / Programming Languages / C#

Enhanced C# IHostBuilder/IHost and Dependency Injection Resolver Libraries

Rate me:
Please Sign up or sign in to vote.
5.00/5 (4 votes)
8 Jan 2023GPL35 min read 20.5K   8  
An Autofac-based chained/declarative approach to creating an IHostBuilder/IHost and a generalized dependency injection resolver
I like the IHostBuilder/IHost approach for structuring applications. But I find configuring the necessary IHostBuilder confusing. There are also certain functionalities, like logging and encryption/decryption of configuration values, which I use so often I wanted them to be easily includable in an IHost. The J4JHostConfiguration library is my attempt to accomplish all of this. It also supports creating a dependency injection resolver object you can use in any C# application which lacks a built-in DI resolver.

Constraints

Someone once described a code framework as a voluntarily worn straitjacket. The extended IHost and ViewModelLocator API I describe here requires the use of Autofac as its dependency injection component. It also uses several other libraries I've written, for example, J4JLogging, which extends Serilog in various ways. All of my libraries which I use here are open source.

If you're interested in adapting these systems to other libraries, I encourage you to fork the GitHub repositories and have at it. And let me know what you've done!

Introduction

The basic approach for creating an IHost instance, which you can use as the framework for most kinds of Windows apps (and maybe others, too), involves creating an instance of HostBuilder, configuring it, and then building it. If everything goes well, you get an IHost instance which, among other things, gives you a Services property you can use to retrieve services on the fly.

I find configuring the HostBuilder to be rather confusing. That's particularly true for certain functionalities I use all the time, like logging, data protection (i.e., encryption/decryption) and configuring how command line arguments are parsed. You can do everything you need...but it's not particularly intuitive. There are also properties I want to refer to when using an IHost instance which aren't easily accessible or not available because they relate to other support libraries I've written.

I extended the IHost interface and the IHostBuilder system to satisfy these additional needs. The derived interface is IJ4JHost:

C#
public interface IJ4JHost : IHost
{
    string Publisher { get; }
    string ApplicationName { get; }

    string UserConfigurationFolder { get; }
    List<string> UserConfigurationFiles { get; }
    string ApplicationConfigurationFolder { get; }
    List<string> ApplicationConfigurationFiles { get; }

    bool FileSystemIsCaseSensitive { get; }
    StringComparison CommandLineTextComparison { get; }
    ILexicalElements? CommandLineLexicalElements { get; }
    CommandLineSource? CommandLineSource { get; }
    OptionCollection? Options { get; }

    OperatingSystem OperatingSystem { get; }
    AppEnvironment AppEnvironment { get; }
}

You can create instances of IJ4JHost by calling the Build() method on an instance of J4JHostConfiguration after you've configured it.

You configure an instance of J4JHostConfiguration by calling various extension methods. There are two required methods you must call, Publisher(), to define the app publisher's name, and ApplicationName(), to define the application's name. Both values are important for resolving configuration file paths and the built-in support for encryption/decryption.

You can read about all the optional extension methods in the GitHub documentation.

The Case for a Centralized Dependency Injection Resolver

By itself, IJ4JHost is useful. But I find it even more useful to couple it to a centralized dependency injection resolver. Here's why.

While the IJ4JHost system allows you to create an IHost-based application controller with a variety of useful features (e.g., logging, command line processing), it's more useful, as is, in simple console apps than Windows desktop apps. The reason has to do with the consequences of not having built-in support for dependency injection.

In a simple console app, there's typically only one "thing" running at a time. Incorporating dependency injection is relatively straightforward, because generally the only special case of creating an object on the fly you need to deal with is obtaining that very first singleton application controller. Once you've done that, most console app architectures simply create the objects they need as they need them, based on information local to the code that's creating them. Or so it seems to be in my simple console apps; your mileage may differ.

Windows desktop apps are quite different. It's very hard, if possible at all, to create a single root application controller and have everything get created locally on demand. It has to do with the multiplicity of ways in which code, and the objects that code requires, may be activated.

A similar thing happens in AspNetCore applications as well. But AspNetCore has built-in support for dependency injection. You can define your objects using constructor parameters that have to be created on the fly and, provided you've registered the parameter types with the dependency injection framework, be assured things will just work.

I'm not aware of any Windows desktop architecture that contains the same kind of built-in support for dependency injection. Windows Forms doesn't have it. WPF doesn't have it. Windows App v2 doesn't have it. Windows App v3 doesn't have it, although I think it's on the roadmap to be added. It's possible UWP has it; I've never done any work in UWP.

The net result is that to use dependency injection in most or all Windows desktop architectures, you need to use some kind of ViewModelLocator pattern: a class with static methods which can be called to create objects registered with the dependency injection system on demand.

I wrote J4JDeusEx to have a generalized ViewModelLocator object that integrates with my IJ4JHost API so I can have a uniform way of interacting with dependency injection regardless of whether I'm writing a console app or a Windows desktop app. Its interface is all static, and quite simple:

C#
public class J4JDeusEx
{
    public static IServiceProvider ServiceProvider { get; protected set; }
    public static bool IsInitialized { get; protected set; }
    public static string? CrashFilePath { get; protected set; }
    public static IJ4JLogger? Logger { get; protected set; }
    public static void OutputFatalMessage( string msg, IJ4JLogger? logger );
}

You can read more about J4JDeusEx's architecture and capabilities in its GitHub documentation.

Using the Code

If you don't want to use J4JDeusEx, building an instance of IJ4JHost is quite simple:

C#
var hostConfig = new J4JHostConfiguraton();

// configuration steps omitted; check the GitHub documentation for details

var host = hostConfig.Build();

However, it's recommended to check to ensure the J4JHostConfiguration object is properly configured before calling Build():

C#
var hostConfig = new J4JHostConfiguraton();

// configuration steps omitted; check the GitHub documentation for details

IJ4JHost? host = null;

if( hostConfig.MissingRequirements == J4JHostRequirements.AllMet )
    host = hostConfig.Build();
else
{
    // take remedial action, abort startup, etc.
}

If you want to take advantage of J4JDeusEx' capabilities, the process is slightly more involved. You create an instance of either J4JDeusExHosted, for non-sandboxed environments, or J4JDeusExWinApp, for sandboxed environments and implement a single abstract protected method:

C#
protected override J4JHostConfiguration? GetHostConfiguration();

Here's what a typical derived class looks like:

C#
// This class is marked as partial, but that's simply to keep the codebase clean
// check the GitHub documentation for details
internal partial class DeusEx : J4JDeusExHosted
{
    protected override J4JHostConfiguration? GetHostConfiguration()
    {
        var hostConfig = new J4JHostConfiguration( AppEnvironment.Console )
                        .ApplicationName( "WpFormsSurveyProcessor" )
                        .Publisher( "Jump for Joy Software" )
                        .LoggerInitializer( ConfigureLogging )
                        .AddDependencyInjectionInitializers
                                      ( ConfigureDependencyInjection )
                        .FilePathTrimmer( FilePathTrimmer );

        var cmdLineConfig = hostConfig.AddCommandLineProcessing
                                       ( CommandLineOperatingSystems.Windows )
                                      .OptionsInitializer( SetCommandLineConfiguration )
                                      .ConfigurationFileKeys
                                       ( true, false, "c", "config" );

        return hostConfig;
    }

    // remaining details omitted for clarity
}

The final step is to call your derived class's Initialize() method. Where you do that depends on whether you're writing a console app or a Windows desktop app.

Console Apps

C#
internal class Program
{
    static void Main( string[] args )
    {
        var deusEx = new DeusEx();

        if( !deusEx.Initialize() )
        {
            J4JDeusEx.Logger?.Fatal("Could not initialize application");
            Environment.ExitCode = 1;
            return;
        }

        // launch application, usually via calling a service
    }

    // remaining details omitted for clarity
}

Windows Desktop Apps

C#
public partial class App : Application
{
    private readonly IJ4JLogger _logger;

    public App()
    {
        this.InitializeComponent();

        this.UnhandledException += App_UnhandledException;

        var deusEx = new GPSLocatorDeusEx();

        if ( !deusEx.Initialize() )
            throw new J4JDeusExException( "Couldn't configure J4JDeusEx object" );

        _logger = J4JDeusEx.ServiceProvider.GetRequiredService<IJ4JLogger>();
    }

    private void App_UnhandledException
    ( object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e )
    {
        J4JDeusEx.OutputFatalMessage
        ($"Unhandled exception: {e.GetType().Name}", null);
        J4JDeusEx.OutputFatalMessage( $"{e.Message}", null );
    }
}

The Windows desktop example also shows how the crash file component of J4JDeusEx can be used. We implement a custom handler for unhandled exceptions and write the exception information to the crash file using J4JDeusEx.OutputFatalMessage().

Once you've initialized J4JDeusEx, you can use it as a ViewModelLocator anywhere in your codebase:

C#
var service = J4JDeusEx.ServiceProvider.Services.GetRequiredService<IFooBar>();

Sandboxed vs Non-Sandboxed Environments

A sandboxed environment is where the app does not have unfettered access to the file system. A non-sandboxed environment is where the app can, potentially, access any part of the file system.

Windows Forms, WPF, and console apps are examples of non-sandboxed environments.

Windows Applications v3 and UWP (and WinRT) are examples of sandboxed environments.

Points of Interest

IJ4JHost and J4JDeusEx evolved out of individual libraries I'd previously written. Like much code writing, their development was a result of realizing I was using the same or similar patterns over and over and could abstract them into a common framework. Which is why they've each undergone some pretty fundamental restructurings from time to time :). 

History

  • v2.3.1: First release on CodeProject
  • v2.3.3: Expand how application configuration files are located

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)


Written By
Jump for Joy Software
United States United States
Some people like to do crossword puzzles to hone their problem-solving skills. Me, I like to write software for the same reason.

A few years back I passed my 50th anniversary of programming. I believe that means it's officially more than a hobby or pastime. In fact, it may qualify as an addiction Smile | :) .

I mostly work in C# and Windows. But I also play around with Linux (mostly Debian on Raspberry Pis) and Python.

Comments and Discussions

 
-- There are no messages in this forum --