Skip to main content

Wrappers & resolvers

Stashbox uses so-called Wrapper and Resolver implementations to handle special resolution requests that none of the service registrations can fulfill. Functionalities like wrapper and unknown type resolution, cross-container requests, optional and default value injection are all built with resolvers.

Pre-defined wrappers & resolvers

  • EnumerableWrapper: Used to resolve a collection of services wrapped in one of the collection interfaces that a .NET Array implements. (IEnumerable<>, IList<>, ICollection<>, IReadOnlyList<>, IReadOnlyCollection<>)
  • LazyWrapper: Used to resolve services wrapped in Lazy<>.
  • FuncWrapper: Used to resolve services wrapped in a Delegate that has a non-void return type like Func<>.
  • MetadataWrapper: Used to resolve services wrapped in ValueTuple<,>, Tuple<,>, or Metadata<,>.
  • KeyValueWrapper: Used to resolve services wrapped in KeyValuePair<,> or ReadOnlyKeyValue<,>.
  • ServiceProviderResolver: Used to resolve the actual scope as IServiceProvider when no other implementation is registered.
  • OptionalValueResolver: Used to resolve optional parameters.
  • DefaultValueResolver: Used to resolve default values.
  • ParentContainerResolver: Used to resolve services that are only registered in one of the parent containers.
  • UnknownTypeResolver: Used to resolve services that are not registered into the container.

Wrappers

Stashbox can implicitly wrap your services into different data structures. All functionalities covered in the service resolution are applied to the wrappers. Every wrapper request starts as a standard resolution; only the result is wrapped in the requested structure.

Enumerable

Stashbox can compose a collection from each implementation registered to a service type. The requested type can be wrapped by any of the collection interfaces that a .NET Array implements.

IJob[] jobs = container.Resolve<IJob[]>();
IEnumerable<IJob> jobs = container.Resolve<IEnumerable<IJob>>();
IList<IJob> jobs = container.Resolve<IList<IJob>>();
ICollection<IJob> jobs = container.Resolve<ICollection<IJob>>();
IReadOnlyList<IJob> jobs = container.Resolve<IReadOnlyList<IJob>>();
IReadOnlyCollection<IJob> jobs = container.Resolve<IReadOnlyCollection<IJob>>();

Lazy

When requesting Lazy<>, the container implicitly constructs a new Lazy<> instance with a factory delegate as its constructor argument used to instantiate the underlying service.

container.Register<IJob, DbBackup>();

// new Lazy(() => new DbBackup())
Lazy<IJob> lazyJob = container.Resolve<Lazy<IJob>>();
IJob job = lazyJob.Value;

Delegate

When requesting a Delegate, the container implicitly creates a factory used to instantiate the underlying service.

It's possible to request a delegate that expects some or all of the dependencies as delegate parameters. Parameters are used for sub-dependencies as well, like: (arg) => new A(new B(arg))

When a dependency is not available as a parameter, it will be resolved from the container directly.

container.Register<IJob, DbBackup>();

// (conn, logger) => new DbBackup(conn, logger)
Func<string, ILogger, IJob> funcOfJob = container
.Resolve<Func<string, ILogger, IJob>>();

IJob job = funcOfJob(config["connectionString"], new ConsoleLogger());

Metadata & Tuple

With the .WithMetadata() registration option, you can attach additional information to a service. To gather this information, you can request the service wrapped in either Metadata<,>, ValueTuple<,>, or Tuple<,>.

Metadata<,> is a type from the Stashbox package, so you might prefer using ValueTuple<,> or Tuple<,> if you want to avoid referencing Stashbox in certain parts of your project.

You can also filter a collection of services by their metadata. Requesting IEnumerable<ValueTuple<,>> will yield only those services that have the given type of metadata.

container.Register<IJob, DbBackup>(options => options
.WithMetadata("connection-string-to-db"));

var jobWithConnectionString = container.Resolve<Metadata<IJob, string>>();
// prints: "connection-string-to-db"
Console.WriteLine(jobWithConnectionString.Data);

var alsoJobWithConnectionString = container.Resolve<ValueTuple<IJob, string>>();
// prints: "connection-string-to-db"
Console.WriteLine(alsoJobWithConnectionString.Item2);

var stillJobWithConnectionString = container.Resolve<Tuple<IJob, string>>();
// prints: "connection-string-to-db"
Console.WriteLine(stillJobWithConnectionString.Item2);
note

Metadata can also be a complex type e.g., an IDictionary<,>.

info

When no service found for a particular metadata type, the container throws a ResolutionFailedException. In case of an IEnumerable<> request, an empty collection will be returned for a non-existing metadata.

KeyValuePair & ReadOnlyKeyValue

With named registration, you can give your service unique identifiers. Requesting a service wrapped in a KeyValuePair<object, TYourService> or ReadOnlyKeyValue<object, TYourService> returns the requested service with its identifier as key.

ReadOnlyKeyValue<,> is a type from the Stashbox package, so you might prefer using KeyValuePair<,> if you want to avoid referencing Stashbox in certain parts of your project.

Requesting an IEnumerable<KeyValuePair<,>> will return all services of the requested type along their identifiers. When a service don't have an identifier the Key will be set to null.

container.Register<IService, Service1>("FirstServiceId");
container.Register<IService, Service2>("SecondServiceId");
container.Register<IService, Service3>();

var serviceKeyValue1 = container
.Resolve<KeyValuePair<object, IService>>("FirstServiceId");
// prints: "FirstServiceId"
Console.WriteLine(serviceKeyValue1.Key);

var serviceKeyValue2 = container
.Resolve<ReadOnlyKeyValue<object, IService>>("SecondServiceId");
// prints: "SecondServiceId"
Console.WriteLine(serviceKeyValue2.Key);

// ["FirstServiceId": Service1, "SecondServiceId": Service2, null: Service3 ]
var servicesWithKeys = container.Resolve<KeyValuePair<object, IService>[]>();
note

Wrappers can be composed e.g., IEnumerable<Func<ILogger, Tuple<Lazy<IJob>, string>>>.

User-defined wrappers & resolvers

You can add support for more wrapper types by implementing the IServiceWrapper interface.

class CustomWrapper : IServiceWrapper
{
// this method is supposed to generate the expression for the given wrapper's
// instantiation when it's selected by the container to resolve the actual service.
public Expression WrapExpression(
TypeInformation originalTypeInformation,
TypeInformation wrappedTypeInformation,
ServiceContext serviceContext)
{
// produce the expression for the wrapper.
}

// this method is called by the container to determine whether a
// given requested type is wrapped by a supported wrapper type.
public bool TryUnWrap(Type type, out Type unWrappedType)
{
// this is just a reference implementation of
// un-wrapping a service from a given wrapper.
if (!CanUnWrapServiceType(type))
{
unWrappedType = typeof(object);
return false;
}

unWrappedType = UnWrapServiceType(type);
return true;
}
}

You can extend the functionality of the container by implementing the IServiceResolver interface.

class CustomResolver : IServiceResolver
{
// called to generate the expression for the given service
// when this resolver is selected (through CanUseForResolution())
// to fulfill the request.
public ServiceContext GetExpression(
IResolutionStrategy resolutionStrategy,
TypeInformation typeInfo,
ResolutionContext resolutionContext)
{
var expression = GenerateExpression(); // resolution expression generation.
return expression.AsServiceContext();
}

public bool CanUseForResolution(
TypeInformation typeInfo,
ResolutionContext resolutionContext)
{
// the predicate that determines whether the resolver
// is able to resolve the requested service or not.
return IsUsableFor(typeInfo);
}
}

Then you can register your custom wrapper or resolver like this:

container.RegisterResolver(new CustomWrapper());
container.RegisterResolver(new CustomResolver());

Visiting order

Stashbox visits the wrappers and resolvers in the following order to satisfy the actual resolution request:

  1. EnumerableWrapper
  2. LazyWrapper
  3. FuncWrapper
  4. MetadataWrapper
  5. KeyValueWrapper
  6. Custom, user-defined wrappers & resolvers
  7. ServiceProviderResolver
  8. OptionalValueResolver
  9. DefaultValueResolver
  10. ParentContainerResolver
  11. UnknownTypeResolver