Resolution Workflow
This page describes exactly what happens inside Resolver when Resolve<T>()/Resolve(Type) is called — the most complex single algorithm in the library.
Top-Level Entry
public object Resolve(Type interfaceType, Scope scope, object id = null)
{
using ResolutionContext context = new ResolutionContext(scope, id);
return ResolveInternal(interfaceType, context);
}
A fresh ResolutionContext is created for every top-level Resolve call (this is what makes the Cached lifetime scoped to a single top-level resolution — its CachedInstances dictionary lives only as long as this using block).
Resolution Algorithm
flowchart TD
Start["ResolveInternal(interfaceType, context)"] --> IsGeneric{Is interfaceType generic?}
IsGeneric -->|IEnumerable<T>| Enum[ResolveAllEnumerable: find all ExactBindings assignable to T, resolve each]
IsGeneric -->|Lazy<T>| Lazy[CreateLazy: build Lazy<T> wrapping a compiled Func]
IsGeneric -->|Func<T>| Func[CreateFunc: compile Expression.Lambda calling Resolve<T> on demand]
IsGeneric -->|Other/No| Registration
Registration["GetRegistration(interfaceType, context)"] --> HasId{context.Id set?}
HasId -->|Yes| CondId[FindConditionalRegistration by id - throws if missing]
HasId -->|No| HasReq{context.RequestType set?}
HasReq -->|Yes| CondType[FindConditionalRegistration by RequestType - optional]
CondType -->|found| UseReg[Use registration]
CondType -->|not found| Exact
HasReq -->|No| Exact[FindExactRegistration]
Exact -->|found| UseReg
Exact -->|not found| IsClosedGeneric{Closed generic type?}
IsClosedGeneric -->|Yes| OpenGen[GetClosedGenericRegistration: find OpenGenericBindings, MakeGenericType, check SatisfiesOpenedGenericConstraints]
OpenGen -->|found| UseReg
OpenGen -->|not found| IsConcrete{IsConcreteClass?}
IsClosedGeneric -->|No| IsConcrete
IsConcrete -->|Yes| SelfReg[Create ad-hoc Transient Registration for the concrete type itself]
IsConcrete -->|No| NullReg[null]
UseReg --> ResolveReg["ResolveRegistration(registration, interfaceType, context)"]
SelfReg --> ResolveReg
NullReg --> ThrowEx["throw InvalidOperationException: No binding found for {interfaceType}"]
CondId -.throws if null.-> ThrowEx2["throw InvalidOperationException with id in message"]
ResolveReg --> Existing{Existing instance for lifetime?}
Existing -->|Yes: Singleton/Scoped/Cached hit| ReturnExisting[Return cached instance]
Existing -->|No| ResolveCtor[Resolve constructor parameters recursively]
ResolveCtor --> Invoke[Invoke compiled factory delegate]
Invoke --> InjectMembers[Inject fields/properties/methods marked Inject]
InjectMembers --> Store["StoreInstance per lifetime (Singleton/Scoped/Cached/Transient tracking)"]
Store --> InitCall["Invoke IInitializable.Initialize()"]
InitCall --> OnActivationCall[Invoke registration.OnActivation]
OnActivationCall --> ReturnNew[Return new instance]
Enum --> Decorators
Lazy --> Decorators
Func --> Decorators
ReturnExisting --> Decorators
ReturnNew --> Decorators
Decorators["ResolveDecorators(instance, interfaceType, context)"] --> Final["Return final (possibly decorated) instance"]
Registration Lookup Priority
Resolver.GetRegistration checks candidates in this exact order:
- Explicit id (
context.Id != null) — looked up viaFindConditionalRegistration(interfaceType, context.Id), walking the scope chain (this scope → parent → parent's parent → ...). ThrowsInvalidOperationExceptionimmediately if not found (no fallback for explicit ids). - Request-type condition (
context.RequestType != null, i.e. we're resolving a dependency of some other type) —FindConditionalRegistration(interfaceType, context.RequestType). If not found, falls through to the next step (no throw) — conditional-by-type is a soft preference, not a hard requirement. - Exact registration —
FindExactRegistration(interfaceType), walking the scope chain. - Closed-generic-from-open-generic — only if
interfaceTypeis a non-open generic type; looks for a matchingOpenGenericBindingsentry forinterfaceType.GetGenericTypeDefinition(), closes the registered open implementation viaMakeGenericType, and validatesSatisfiesOpenedGenericConstraintsbefore accepting it. - Self-registration fallback — if
interfaceType.IsConcreteClass(isIgnoreGeneratedType: true), an ad-hocTransientRegistrationis created on the fly (no explicitBindcall required to resolve a concrete, injectable class — a common convenience in many DI containers). - Otherwise,
nullis returned, and the caller (ResolveInternal) throwsInvalidOperationException("No binding found for {interfaceType}").
Generic Wrapper Resolution
Before the standard registration lookup runs at all, ResolveInternal special-cases three generic shapes (checked via interfaceType.GetGenericTypeDefinition()):
| Generic Shape | Behavior |
|---|---|
IEnumerable<T> | Enumerates all ExactBindings in the current scope whose key is assignable to T, resolves each one, and returns them as a List<T> (constructed reflectively via Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType))). Note: only the current scope's own exact bindings are enumerated here (via context.CurrentScope.GetAllExactRegistration(), which actually does merge parent-scope bindings too — see Scope.GetAllExactRegistration). |
Lazy<T> | Builds a Lazy<T> wrapping a compiled Func<T> (see below), so the dependency is only actually resolved on first access to .Value. |
Func<T> | Compiles (once per resolution, not cached across calls) an Expression.Lambda that, when invoked, calls back into Resolver.Resolve(Type, Scope, object) for T against the original scope captured at expression-build time — enabling factory-style "resolve me a new one later" dependencies. |
Constructor and Member Injection
For a "normal" (non-wrapper) registration, ResolveRegistration:
- Checks for an existing instance appropriate to the registration's lifetime (
GetExistingInstance) — see Scopes, Lifetimes and Disposal for the exact per-lifetime lookup logic. If found, that instance is returned immediately (skipping construction, member injection,OnActivation, andIInitializableentirely — those only fire on new instantiation). - Otherwise, resolves the constructor's parameters:
- If the registration has explicit
Arguments(from.WithArguments(...)), usesResolveConstructorWithArguments, which greedily matches each supplied argument to the first constructor parameter it's type-assignable to (consumed once matched, so duplicates don't double-match), resolving any unmatched parameters normally (respecting[Id]attributes per-parameter). - Otherwise, uses
ResolveParameters, which resolves every parameter recursively viaResolveInternal, temporarily settingcontext.RequestTypeto the type being constructed (enablingWhenInjectedInto<T>()conditional matches for its dependencies) and swappingcontext.Idin/out per-parameter based on any[Id]attribute present.
- If the registration has explicit
- Invokes the registration's compiled
Factorydelegate with the resolved parameter array. - Injects members (
InjectMembers): iteratesGetInjectableMembersfor the implementation type, setting fields/properties via reflection and invoking[Inject]-marked methods with resolved parameters (same per-parameter[Id]handling as constructor parameters). - Stores the instance according to its lifetime (
StoreInstance). - Invokes
IInitializable.Initialize()if implemented. - Invokes
registration.OnActivation?.Invoke(instance)if a callback was registered via.OnActivation(...).
Decorator Wrapping
After the base instance (or a cache/singleton/scoped hit) is obtained, ResolveDecorators is always consulted (even for cache hits) — see Decorators for full detail on ordering, lifetime handling per-decorator, and open-generic decorator support.
Async Resolution
ResolveAsync (on Scope/DIContainer) is a thin async wrapper: it calls the synchronous Resolve first, then, if the result implements IAsyncInitializable, awaits its InitializeAsync() before returning the same instance. It does not change any part of the core synchronous resolution algorithm described above.
Continue to Scopes, Lifetimes and Disposal.