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
:
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:
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:
var hostConfig = new J4JHostConfiguraton();
var host = hostConfig.Build();
However, it's recommended to check to ensure the J4JHostConfiguration
object is properly configured before calling Build()
:
var hostConfig = new J4JHostConfiguraton();
IJ4JHost? host = null;
if( hostConfig.MissingRequirements == J4JHostRequirements.AllMet )
host = hostConfig.Build();
else
{
}
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:
protected override J4JHostConfiguration? GetHostConfiguration();
Here's what a typical derived class looks like:
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;
}
}
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
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;
}
}
}
Windows Desktop Apps
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:
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