Constructors with default values

Constructors with default values and dependency injection behavior

Home DailyDrop

Daily Knowledge Drop

Constructor and minimal api parameters can have default values, allowing for dependency injection to work - while not actually working. Specifying a default value allows for the application to run with a default implementation even if the services has not been registered with the dependency injection container.

This is not something one would usually do as a standard practice, but in specific use cases it does have a place.


Example

Setup

In the below examples, we have an interface, and a couple of implementations:

The interface:

public interface IDependencyInterface 
{
    string GetName();
}

And the implementations:

// a default implementation
public class DefaultImplementation : IDependencyInterface
{
    public string GetName()
    {
        return nameof(DefaultImplementation);
    }
}

// and another implementation
public class OtherImplementation : IDependencyInterface
{
    public string GetName()
    {
        return nameof(OtherImplementation);
    }
}

Injection

Usually to make use of the functionality provided by the interface and the associated implementation, the service is registered with the dependency injection container:

// Depending on the implementation the application requires:
builder.Services.AddTransient<IDependencyInterface, DefaultImplementation>();
// OR
builder.Services.AddTransient<IDependencyInterface, OtherImplementation>();

and then is injected into the relevent constructor, or minimal api delegate in the below sample:

// explicitly tell it to get the IDependencyInterface implementation
// from the DI service collection
app.MapGet("/name", ([FromServices]IDependencyInterface injected) =>
{
    return injected.GetName();
});

DI Assumption

In the above setup, the runtime assumes that the necessary registrations with the dependency injection container have take place. If neither of the following registrations are done:

builder.Services.AddTransient<IDependencyInterface, DefaultImplementation>();
builder.Services.AddTransient<IDependencyInterface, OtherImplementation>();

when trying to inject IDependencyInterface, the following error will be experienced:

InvalidOperationException: No service for type 'IDependencyInterface' has been registered.

We've told the runtime and dependency injection container to inject the IDependencyInterface implementation, but have not made it aware of any implementations!


Default value

Generally if in control of the entire dependency injection container registration, one should ensure that the required registrations are performed, and the error is resolved.

However, in some cases this might not be possible - for example if developing a library package, there is no direct control over what the developer configures with the dependency injection container.
As a developer of the library package, one could just allow the exception to occur, which directs the developer to configure the dependency injection container correctly. Another, arguably more developer friendly technique, is to automatically set a default implementation if one is not explicitly set. This does require a few updates to the code:

// make the parameter nullable, the default value will be null.
// In the case of a constructor (vs this minimal api delegate)
// the value can explicitly be set to null if desired:
// IDependencyInterface? injected = null
app.MapGet("/name", ([FromServices]IDependencyInterface? injected) =>
{
    // if it is null, set it to the default implementation
    injected ??= new DefaultImplementation();

    return injected.GetName();
});

Two changes are made to code:

  • Set the parameter as nullable using the ? operator
  • Instantiate the parameter to the default implementation if the value of the parameter is null

Now if no implementation is registered for IDependencyInterface, the application will still function and use the default implementation specified.


Notes

As mentioned, this is not a practice which is generally recommended - however in the case of developing an external library (which requires a dependency), there is no guarantee the host application has injected the required dependencies. By making use of default values the library will still function with default configuration, but allows for specific implementations to be overwritten by the developer if required.


References

Christian Findlay Tweet

Daily Drop 158: 12-09-2022

At the start of 2022 I set myself the goal of learning one new coding related piece of knowledge a day.
It could be anything - some.NET / C# functionality I wasn't aware of, a design practice, a cool new coding technique, or just something I find interesting. It could be something I knew at one point but had forgotten, or something completely new, which I may or may never actually use.

The Daily Drop is a record of these pieces of knowledge - writing about and summarizing them helps re-enforce the information for myself, as well as potentially helps others learn something new as well.
c# .net default dependencyinjection constructor