External services
Not everything comes from Awaiten. When you run under ASP.NET Core or the generic host, some services live in the host's provider. The payment gateway, the framework logger, and the configuration are wired there. External services let an Awaiten graph pull those in.
Pull a single type with [ImportService]
Declare a service type external with [ImportService<T>] on the container, or with its typeof form [ImportService(typeof(T))]; the two behave the same. Every unregistered dependency of that type is satisfied from the external provider instead of the graph, whether it is keyed or not, and whether it arrives as a constructor parameter, a factory parameter, or an [Inject] property. No Awaiten registration is needed for it. Every other unresolved dependency still gets the missing-dependency check (AWT101), so this is safer than the blanket fall-through below.
- Generic
- typeof
[Container]
[ImportService<IPaymentGateway>]
[Singleton<Till>]
[Singleton<Register>]
public static partial class CoffeeShop;
public sealed class Register(Till till, IPaymentGateway gateway); // IPaymentGateway comes from the host
[Container]
[ImportService(typeof(IPaymentGateway))] // same as [ImportService<IPaymentGateway>]
[Singleton<Till>]
[Singleton<Register>]
public static partial class CoffeeShop;
For a keyed external service, select the key on a container factory method so the consumer stays a plain class; the [FromKey] is forwarded to the resolver.
public sealed class Report(ITimeSystem timeSystem); // a plain class, no attributes
[Container]
[ImportService<ITimeSystem>]
[Singleton<Report>(Factory = nameof(MakeReport))]
public static partial class CoffeeShop
{
private static Report MakeReport([FromKey("utc")] ITimeSystem timeSystem) => new(timeSystem);
}
[ImportService] works on an imported [Module] too, exactly as it does on the container.
Because external-ness is declared on the container, a service that consumes a host type just asks for it as an ordinary parameter, with no Awaiten reference in the domain class. [ImportService] is the clean-domain form. The only per-parameter attribute involved is [FromKey], and only when you need a keyed external service.
Only the direct dependency of type T is routed. A relationship or collection over it (Func<T>, Lazy<T>, Task<T>, IEnumerable<T>, and the like) is not, because the resolver hands back an instance, not a deferred or fanned-out shape. Such a dependency still resolves from the Awaiten graph, so with no registration it surfaces as AWT101 (or an empty collection). Declaring a type external that nothing in the graph consumes is a dead declaration and is reported as AWT176.
Pull every closing of a generic type
Some host services are generic, and every consumer asks for its own closing: under the generic host each class wants an ILogger<ThatClass>. Pass the unbound generic definition to the typeof form, and every closing that a dependency asks for comes from the external provider. The generic form cannot express this, because C# does not allow an unbound generic such as ILogger<> as a type argument.
- Generic
- typeof
using Microsoft.Extensions.Logging;
public sealed class OrderDesk(ILogger<OrderDesk> logger);
public sealed class Roaster(ILogger<Roaster> logger);
[Container]
[ImportService<ILogger<OrderDesk>>] // one declaration per consumer
[ImportService<ILogger<Roaster>>]
[Singleton<OrderDesk>]
[Singleton<Roaster>]
public static partial class CoffeeShop;
using Microsoft.Extensions.Logging;
public sealed class OrderDesk(ILogger<OrderDesk> logger);
public sealed class Roaster(ILogger<Roaster> logger);
[Container]
[ImportService(typeof(ILogger<>))] // ILogger<OrderDesk>, ILogger<Roaster>, and any later consumer
[Singleton<OrderDesk>]
[Singleton<Roaster>]
public static partial class CoffeeShop;
Each closing is routed exactly as if it were imported on its own: a [FromKey] key is forwarded, a relationship or collection over a closing is not routed, and the closing is listed as an external dependency of the container, so startup verification checks every closing the graph consumes. A closing that is routed to the host but also registered on the container is reported as AWT175, and an open import none of whose closings is consumed as AWT176. The argument must be a closed type or an unbound generic definition; anything in between, such as typeof(Outer<>.Inner), is reported as AWT215.
Fall through everything with [ImportServices]
Put [ImportServices] on the container and any unregistered direct dependency falls through to the external provider automatically. You trade some compile-time safety for less annotation.
[Container]
[ImportServices]
[Singleton<Register>]
public static partial class CoffeeShop;
// Register's IPaymentGateway is resolved from the host, unannotated
Providing the external resolver
Awaiten does not create the external provider. It asks an external resolver for these dependencies, and you supply that resolver once, up front.
Under the MS.DI bridge this is automatic. AddAwaitenContainer and the startup initialization wire the container to the host's IServiceProvider, so [ImportService] and [ImportServices] dependencies resolve from the host with nothing extra to do.
Standing alone, you set it yourself. Implement IExternalResolver and pass it to the generated Root constructor, so it is in place before anything resolves, eager singletons included.
using System.Diagnostics.CodeAnalysis;
public sealed class HostResolver : IExternalResolver
{
public bool TryResolve(Type serviceType, object? key, [NotNullWhen(true)] out object? instance)
{
if (serviceType == typeof(IPaymentGateway))
{
instance = new StripeGateway();
return true;
}
instance = null;
return false; // not found: Awaiten reports a missing external dependency
}
}
using CoffeeShop.Root shop = new(new HostResolver());
Register register = shop.Resolve<Register>(); // its IPaymentGateway comes from HostResolver
The key argument carries the [FromKey] value for a keyed external dependency. A child scope, and the throwaway scope behind an Owned<T>, inherit the resolver from the scope that created them, so passing it once to the root is enough.
A scope's resolver is set once. Hosts that wire a resolver later, as the MS.DI bridge does for each scope, call IAwaitenExternalResolverHost.SetExternalResolver. Setting the resolver a scope already holds again does nothing; setting a different one throws an InvalidOperationException, because the scope would then mix instances from two providers.
Verifying the wiring
Awaiten advertises its external dependencies so you can check them at startup. If one is missing from the provider, verification throws and names it. See MS.DI bridge for VerifyAwaitenContainers.
Ownership
An externally supplied instance belongs to the external provider. Awaiten never disposes it.
Where to go next
- MS.DI bridge for host integration and verification.
- Keyed services for
[FromKey].