In ASP.NET Core dependency injection, we usually register injectable dependencies at the start of our program. We can then resolve these dependencies (services) much later once when we need them. Sometimes, however, we may need to resolve those dependencies even before building our app. This happens especially when we need to initialize something at startup. This could range from a simple database for storage to a set of SCADA-controlled devices.

If you are new to dependency injection, you should definitely read our article on Dependency Injection, and come back before we start with this one.

To download the source code for this article, you can visit our GitHub repository.

Let’s get started.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Injecting Dependencies

In this article, we’ll be dealing with a simple yet important common application of dependency injection; Globalization and Localization.

With globalization, we add support for input, display, and output of a defined set of language scripts that relate to various geographic areas and cultures. We adapt a globalized app’s content to a specific language or culture in localization. This could be based on the users’ settings or requests.

To implement localization in .NET, we inject the localization service into our application builder’s services in the Program.cs file, and point it to a Resources directory where we’ll put all our language resources:

builder.Services.AddLocalization();

Now, we want to be able to provide localized error messages from our model binders whenever our app receives an incorrect model. To accomplish this, we need to set the accessor messages on the ModelBindingMessageProvider for our controller. 

Let’s create a Resources folder where we will store all our language resources. In this folder, let’s create a SharedResource class and two resource files with the same name for English (en) and Japanese (ja).

Different Initialization Phases in Program Class

In order to set up localized accessor messages for model binding, we need to invoke our localization service within our Controller service’s configuration options. An ASP.NET Core Program has two distinct initialization phases:

The Add phase, where we add registrations to the container builder’s IServiceCollection.

The Use phase, where we express how we want to use the previously registered services. During the Use phase, .NET turns the IServiceCollection into an IServiceProvider and we can then inject those services into the various methods.

Let’s take a look at this in an illustration:

Program class two phases for registering services and pipeline

As we can see from our code snippet, we have only just declared our services and have not yet built the app. Therefore, we do not yet have a tangible instance of any service. This is where we encounter our challenge.

How do we invoke a service that we have only declared but not yet built an instance of?

Manually Resolving Dependencies

A common but technically wrong approach would be to build the service provider at that early stage:

builder.Services.AddLocalization();
builder.Services.AddControllers(options =>
{
    var localizerFactory = builder.Services.BuildServiceProvider().GetService<IStringLocalizerFactory>();
    var localizer = localizerFactory.Create(typeof(SharedResource));
    options.ModelBindingMessageProvider.SetValueIsInvalidAccessor((x) => localizer["The value '{0}' is invalid.", x]);
});

In our snippet, we’re declaring the localization service and making our app build a service provider so that we can retrieve an instance of IStringLocalizerFactory. Using the localizer factory, we then set custom binding messages. The trouble with this approach is that although we built the service provider here, the app must still build another one at the app = builder.Build() stage. Let’s take a look at this illustrated:

Wrong way to resolve instances in ASP.NET Core

When this happens, each service provider will have its own cache of resolved instances. In the case of singleton instances, since we have two separate service providers, we will therefore end up with more than one instance. This violates the guarantee that there must be at most, one instance for a given singleton registration. For scoped dependencies, building in one provider and caching in the other, will result in the dependency outliving its scope, and lasting for the lifetime of the application.

Now, let’s see a better way to resolve this.

Resolving Dependencies With Static Classes

A neat way to resolve this issue is by creating a static class or a class with static properties which we can then reference from any point in our program.

Let’s create the class file LocalizerManager.cs for our project. It contains a static property of type IStringLocalizer called localizer and a static action SetLocalizationOptions which accepts an MvcOptions input:

public static class LocalizerManager
{
    private static IStringLocalizer? _localizer;
    
    public static void SetLocalizationOptions(MvcOptions options)
    {
        options.ModelBindingMessageProvider
             .SetMissingRequestBodyRequiredValueAccessor(() => localizer.GetString("MissingRequestBodyRequiredValue"));
    }

    public static void SetLocalizer(IStringLocalizer localizer)
    {
        _localizer = localizer;
    }
}

In our static method, we can now set the accessors using the static localizer property available to us within the class. Note that we made our IStringLocalizer nullable because we are not initializing it in the constructor. Instead, we will assign it a value back in our program file.

Now, let’s update our Program file to assign the localizer and call our static method:

builder.Services.AddControllers(options =>
{
    LocalizerManager.SetLocalizationOptions(options);
});

/*
   Other services
*/

var app = builder.Build();


using (var serviceScope = app.Services.CreateScope())
{
    var services = serviceScope.ServiceProvider;

    var localizer = services.GetRequiredService<IStringLocalizerFactory>()
                        .Create(typeof(SharedResource));

    LocalizerManager.SetLocalizer(localizer);
}

In the AddControllers method, we are calling the static method we created and passing in the options object to it.

Since the input to the AddControllers method is a delegate, we know that it does not execute immediately after it is declared. Instead, execution continues through the build and run phases before hitting the delegate. Therefore, we have leeway to assign the localizer after safely building the app, which is exactly what we’ve done here. By creating a disposable service scope, we ensure that we aren’t leaving any orphaned services in our cache.

Conclusion

In this article, we learned about the order in which .NET initializes our application’s services, and how to maintain it. We also learned about the challenges and pitfalls we face when we need to resolve a service before runtime. Dependency injection is an important part of good software architecture, and when we do it right, we can avoid many pitfalls.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!