Skip to main content

Service resolution

When you have all your components registered and configured adequately, you can resolve them from the container or a scope by requesting their service type.

During a service's resolution, the container walks through the entire resolution tree and instantiates all dependencies required for the service construction. When the container encounters any violations of these rules (circular dependencies, missing required services, lifetime misconfigurations) during the walkthrough, it lets you know that something is wrong by throwing a specific exception.

Injection patterns

Constructor injection is the primary dependency injection pattern. It encourages the organization of dependencies to a single place - the constructor.

Stashbox, by default, uses the constructor that has the most parameters it knows how to resolve. This behavior is configurable through constructor selection.

Property/field injection is also supported in cases where constructor injection is not applicable.

Members defined with C# 11's required keyword are automatically injected by the container. This behavior can be controlled with registration or container configuration options

info

Constructor selection and property/field injection is also configurable container-wide.

class DbBackup : IJob
{
private readonly ILogger logger;
private readonly IEventBroadcaster eventBroadcaster;

public DbBackup(ILogger logger, IEventBroadcaster eventBroadcaster)
{
this.logger = logger;
this.eventBroadcaster = eventBroadcaster;
}
}

container.Register<ILogger, ConsoleLogger>();
container.Register<IEventBroadcaster, MessageBus>();

container.Register<IJob, DbBackup>();

// resolution using the available constructor.
IJob job = container.Resolve<IJob>();
caution

It's a common mistake to use the property/field injection only to disencumber the constructor from having too many parameters. That's a code smell and also violates the Single-responsibility principle. If you recognize these conditions, you should consider splitting your class into multiple smaller units rather than adding an extra property-injected dependency.

Attributes

Attributes can give you control over how Stashbox selects dependencies for a service's resolution.

Dependency attribute:

  • On a constructor/method parameter: used with the name property, it works as a marker for named resolution.

  • On a property/field: first, it enables auto-injection on the marked property/field (even if it wasn't configured at registration explicitly), and just as with the method parameter, it allows named resolution.

DependencyName attribute: a parameter marked with this attribute will get the related service's dependency name.

InjectionMethod attribute: marks a method to be called when the requested service is instantiated.

class DbBackup : IJob
{
private readonly ILogger logger;

public DbBackup([Dependency("Console")]ILogger logger)
{
this.logger = logger;
}
}

container.Register<ILogger, ConsoleLogger>("Console");
container.Register<ILogger, FileLogger>("File");

container.Register<IJob, DbBackup>();

// the container will resolve DbBackup with ConsoleLogger.
IJob job = container.Resolve<IJob>();
caution

Attributes provide a more straightforward configuration, but using them also tightens the bond between your application and Stashbox. If you consider this an issue, you can use the dependency binding API or your own attributes.

Using your own attributes

There's an option to extend the container's dependency finding mechanism with your own attributes.

class DbBackup : IJob
{
[CustomDependency("Console")]
public ILogger Logger { get; set; }

public DbBackup()
{ }
}

var container = new StashboxContainer(options => options
.WithAdditionalDependencyAttribute<CustomDependencyAttribute>());

container.Register<ILogger, ConsoleLogger>("Console");
container.Register<ILogger, FileLogger>("File");

container.Register<IJob, DbBackup>();

// the container will resolve DbBackup with ConsoleLogger.
IJob job = container.Resolve<IJob>();

Dependency binding

The same dependency configuration functionality as attributes, but without attributes.

  • Binding to a parameter: the same functionality as the Dependency attribute on a constructor or method parameter, enabling named resolution.

  • Binding to a property/field: the same functionality as the Dependency attribute, enabling the injection of the given property/field.

info

There are further dependency binding options available on the registration configuration API.

class DbBackup : IJob
{
public DbBackup(ILogger logger)
{ }
}

container.Register<ILogger, ConsoleLogger>("Console");
container.Register<ILogger, FileLogger>("File");

// registration of service with the dependency binding.
container.Register<IJob, DbBackup>(options => options
.WithDependencyBinding("logger", "Console"));

// the container will resolve DbBackup with ConsoleLogger.
IJob job = container.Resolve<IJob>();

Conventional resolution

When you enable conventional resolution, the container treats member and method parameter names as their dependency identifier.

It's like an implicit dependency binding on every class member.

First, you have to enable conventional resolution through the configuration of the container:

new StashboxContainer(options => options
.TreatParameterAndMemberNameAsDependencyName());
note

The container will attempt a named resolution on each dependency based on their parameter or property/field name.

class DbBackup : IJob
{
public DbBackup(
// the parameter name identifies the dependency.
ILogger consoleLogger)
{ }
}

container.Register<ILogger, ConsoleLogger>("consoleLogger");
container.Register<ILogger, FileLogger>("fileLogger");

container.Register<IJob, DbBackup>();

// the container will resolve DbBackup with ConsoleLogger.
IJob job = container.Resolve<IJob>();

Conditional resolution

Stashbox can resolve a particular dependency based on its context. This context is typically the reflected type of dependency, its usage, and the type it gets injected into.

  • Attribute: you can filter on constructor, method, property, or field attributes to select the desired dependency for your service. In contrast to the Dependency attribute, this configuration doesn't tie your application to Stashbox because you use your own attributes.

  • Parent type: you can filter on what type the given service is injected into.

  • Resolution path: similar to the parent type and attribute condition but extended with inheritance. You can set that the given service is only usable in a type's resolution path. This means that each direct and sub-dependency of the selected type must use the provided service as a dependency.

  • Custom: with this, you can build your own selection logic based on the given contextual type information.

class ConsoleAttribute : Attribute { }

class DbBackup : IJob
{
public DbBackup([Console]ILogger logger)
{ }
}

container.Register<ILogger, ConsoleLogger>(options => options
// resolve only when the injected parameter,
// property or field has the 'Console' attribute
.WhenHas<ConsoleAttribute>());

container.Register<IJob, DbBackup>();

// the container will resolve DbBackup with ConsoleLogger.
IJob job = container.Resolve<IJob>();

The specified conditions are behaving like filters when a collection is requested.

When you use the same conditional option multiple times, the container will evaluate them with OR logical operator.

tip

Here you can find each condition related registration option.

Optional resolution

In cases where it's not guaranteed that a service is resolvable, either because it's not registered or any of its dependencies are missing, you can attempt an optional resolution using the ResolveOrDefault() method.

When the resolution attempt fails, it will return null (or default in case of value types).

// returns null when the resolution fails.
IJob job = container.ResolveOrDefault<IJob>();

// throws ResolutionFailedException when the resolution fails.
IJob job = container.Resolve<IJob>();

Dependency overrides

At resolution time, you can override a service's dependencies by passing an object[] to the Resolve() method.

class DbBackup : IJob
{
public DbBackup(ILogger logger)
{ }
}
DbBackup backup = container.Resolve<DbBackup>( 
dependencyOverrides: new object[]
{
new ConsoleLogger()
});

Activation

You can use the container's .Activate() method when you only want to build up an instance from a type on the fly without registration.

It allows dependency overriding with object arguments and performs property/field/method injection (when configured).

It works like Activator.CreateInstance() except that Stashbox supplies the dependencies.

// use dependency injected by container.
DbBackup backup = container.Activate<DbBackup>();

// override the injected dependency.
DbBackup backup = container.Activate<DbBackup>(new ConsoleLogger());

Build-up

With the .BuildUp() method, you can do the same on the fly post-processing (property/field/method injection) on already constructed instances.

caution

.BuildUp() won't register the given instance into the container.

class DbBackup : IJob
{
public ILogger Logger { get; set; }
}

DbBackup backup = new DbBackup();
// the container fills the Logger property.
container.BuildUp(backup);