PageSourceSearch

https://docs.testably.org/assets/js/5a4c41a7.df6a85c4.js

js testably.org collected 2026-10-07 11:31:26 UTC 17,293 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunkpages=self.webpackChunkpages||[]).push([["6271"],{4359(e,n,r){r.r(n),r.d(n,{metadata:()=>i,default:()=>l,frontMatter:()=>o,contentTitle:()=>a,toc:()=>d,assets:()=>c});var i=JSON.parse('{"id":"Awaiten/msdi-bridge","title":"MS.DI bridge","description":"Most .NET apps run on Microsoft.Extensions.DependencyInjection. ASP.NET Core, the generic host, and worker services all speak it. The Awaiten.Extensions.DependencyInjection package bridges your generated container into that world, so you keep Awaiten\'s compile-time safety and still fit the host.","source":"@site/docs/Awaiten/08-msdi-bridge.md","sourceDirName":"Awaiten","slug":"/Awaiten/msdi-bridge","permalink":"/Awaiten/msdi-bridge","draft":false,"unlisted":false,"editUrl":"https://github.com/Testably/Testably.Site/tree/main/Docs/pages/docs/Awaiten/08-msdi-bridge.md","tags":[],"version":"current","sidebarPosition":8,"frontMatter":{},"sidebar":"awaitenSidebar","previous":{"title":"Generic variance","permalink":"/Awaiten/advanced/generic-variance"},"next":{"title":"Diagnostics","permalink":"/Awaiten/diagnostics"}}'),t=r(4848),s=r(8453);let o={},a="MS.DI bridge",c={},d=[{value:"Project into a service collection",id:"project-into-a-service-collection",level:2},{value:"Use it as the host&#39;s provider factory",id:"use-it-as-the-hosts-provider-factory",level:2},{value:"Or let Awaiten own everything",id:"or-let-awaiten-own-everything",level:2},{value:"Feature detection",id:"feature-detection",level:3},{value:"Warm async services on startup",id:"warm-async-services-on-startup",level:2},{value:"Verify external dependencies at startup",id:"verify-external-dependencies-at-startup",level:2},{value:"Native AOT",id:"native-aot",level:2},{value:"What the bridge does not project",id:"what-the-bridge-does-not-project",level:2},{value:"Where to go next",id:"where-to-go-next",level:2}];function h(e){let n={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",header:"header",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.header,{children:(0,t.jsx)(n.h1,{id:"msdi-bridge",children:"MS.DI bridge"})}),"\n",(0,t.jsxs)(n.p,{children:["Most .NET apps run on Microsoft.Extensions.DependencyInjection. ASP.NET Core, the generic host, and worker services all speak it. The ",(0,t.jsx)(n.code,{children:"Awaiten.Extensions.DependencyInjection"})," package bridges your generated container into that world, so you keep Awaiten's compile-time safety and still fit the host."]}),"\n",(0,t.jsx)(n.p,{children:"Install it alongside the core package:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-sh",children:"dotnet add package Awaiten.Extensions.DependencyInjection\n"})}),"\n",(0,t.jsx)(n.h2,{id:"project-into-a-service-collection",children:"Project into a service collection"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"AddGeneratedContainer<TRoot>"})," registers the container and projects its registrations into the ",(0,t.jsx)(n.code,{children:"IServiceCollection"}),". Awaiten lifetimes align to ",(0,t.jsx)(n.code,{children:"ServiceLifetime"}),", and one Awaiten scope aligns to each MS.DI scope."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"var services = new ServiceCollection();\nservices.AddGeneratedContainer<CoffeeShop.Root>();\n\nusing var provider = services.BuildServiceProvider();\nIBrewer brewer = provider.GetRequiredService<IBrewer>();\n"})}),"\n",(0,t.jsxs)(n.p,{children:["An async-only service has no synchronous path, so this projection does not register its bare type. It is available as ",(0,t.jsx)(n.code,{children:"Task<T>"}),", which routes through ",(0,t.jsx)(n.code,{children:"ResolveAsync"}),". (Under the ",(0,t.jsx)(n.a,{href:"#or-let-awaiten-own-everything",children:"replacement path"})," the bare type does resolve, into the container's guidance naming this projection, because there the container answers every request rather than contributing descriptors.)"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"Task<EspressoMachine> machine = provider.GetRequiredService<Task<EspressoMachine>>();\n"})}),"\n",(0,t.jsxs)(n.p,{children:["A keyed registration is projected under its key, so ",(0,t.jsx)(n.code,{children:"[FromKeyedServices]"})," and ",(0,t.jsx)(n.code,{children:"GetKeyedService"})," reach it. An async-only keyed service is projected as a keyed ",(0,t.jsx)(n.code,{children:"Task<T>"}),", the same way its unkeyed counterpart is."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'public sealed class Router([FromKeyedServices("fast")] IChannel channel);\n'})}),"\n",(0,t.jsx)(n.h2,{id:"use-it-as-the-hosts-provider-factory",children:"Use it as the host's provider factory"}),"\n",(0,t.jsxs)(n.p,{children:["For the generic host or ",(0,t.jsx)(n.code,{children:"WebApplicationBuilder"}),", plug in ",(0,t.jsx)(n.code,{children:"AwaitenServiceProviderFactory<TRoot>"}),". Host registrations and Awaiten services coexist, so a host component can inject an Awaiten service and the other way round."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"builder.Host.UseServiceProviderFactory(\n    new AwaitenServiceProviderFactory<CoffeeShop.Root>());\n"})}),"\n",(0,t.jsx)(n.h2,{id:"or-let-awaiten-own-everything",children:"Or let Awaiten own everything"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"AwaitenServiceProvider"})," adapts an Awaiten scope directly to ",(0,t.jsx)(n.code,{children:"IServiceProvider"}),", ",(0,t.jsx)(n.code,{children:"IKeyedServiceProvider"}),", and ",(0,t.jsx)(n.code,{children:"IServiceScopeFactory"}),". Awaiten is then the single owner of construction and disposal. ",(0,t.jsx)(n.code,{children:"GetService"})," maps to ",(0,t.jsx)(n.code,{children:"TryResolve"}),", so a missing service returns ",(0,t.jsx)(n.code,{children:"null"}),", and ",(0,t.jsx)(n.code,{children:"GetKeyedService"}),"/",(0,t.jsx)(n.code,{children:"GetRequiredKeyedService"})," reach a keyed registration by its key."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"using var shop = new CoffeeShop.Root();\nusing var provider = new AwaitenServiceProvider(shop);\n\nusing IServiceScope scope = provider.CreateScope();\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Pass ",(0,t.jsx)(n.code,{children:"ownsContainer: false"})," if you want to keep ownership of the container yourself."]}
1),"\n",(0,t.jsx)(n.h3,{id:"feature-detection",children:"Feature detection"}),"\n",(0,t.jsxs)(n.p,{children:["The provider also answers ",(0,t.jsx)(n.code,{children:"IServiceProviderIsService"})," and ",(0,t.jsx)(n.code,{children:"IServiceProviderIsKeyedService"}),", from the container's registration metadata and without constructing anything. ASP.NET Core depends on this: minimal APIs ask it whether a handler parameter comes from dependency injection or from the request, and MVC's controller activation asks the same."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"var probe = provider.GetRequiredService<IServiceProviderIsService>();\nprobe.IsService(typeof(IBrewer));                 // true\nprobe.IsService(typeof(EspressoMachine));         // true: it exists, but resolving it names Task<EspressoMachine>\n"})}),"\n",(0,t.jsxs)(n.p,{children:["The answer comes from the container itself, through ",(0,t.jsx)(n.code,{children:"IAwaitenContainerMetadata.IsResolvable"}),", so it covers every shape the container dispatches and not merely what it advertises as a registration: a registration, the ",(0,t.jsx)(n.a,{href:"./resolution/relationships",children:"relationship shapes"})," over it, the synthesized ",(0,t.jsx)(n.a,{href:"./resolution/collections",children:"collections"})," and ",(0,t.jsx)(n.a,{href:"./resolution/keyed-dictionaries",children:"keyed dictionaries"}),", the awaited views over those, and a ",(0,t.jsx)(n.a,{href:"./advanced/generic-variance",children:"variance"}),"-compatible closing of a registered variant generic interface. The ",(0,t.jsx)(n.code,{children:"Task<T>"})," projection of an async-only registration is reported that way. Its bare service type is reported too, but from ",(0,t.jsx)(n.code,{children:"WithheldReason"})," below rather than from ",(0,t.jsx)(n.code,{children:"IsResolvable"}),", which answers only what the container dispatches; asking for it then throws the guidance naming the projection."]}),"\n",(0,t.jsxs)(n.p,{children:["Wrapping a bare scope rather than a ",(0,t.jsx)(n.code,{children:"Root"})," leaves the provider without metadata, and it then does not offer the probe at all, so the framework keeps its own fallback."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"IsService"})," answers whether the service ",(0,t.jsx)(n.em,{children:"exists"}),", not whether this scope will build it, which is MS.DI's own semantics. A disposable transient asked on the root is reported, and so is an async-initialized service asked synchronously; resolving either throws the container's guidance naming the fix rather than returning ",(0,t.jsx)(n.code,{children:"null"}),", so the failure lands at the cause instead of letting a host bind the parameter from somewhere else."]}),"\n",(0,t.jsxs)(n.p,{children:["That distinction is available to your own code through ",(0,t.jsx)(n.code,{children:"IAwaitenContainerMetadata.WithheldReason"}),". It returns the message ",(0,t.jsx)(n.code,{children:"Resolve"})," would throw, or ",(0,t.jsx)(n.code,{children:"null"})," when the container has no such reason, so a failed ",(0,t.jsx)(n.code,{children:"TryResolve"})," can be told apart from a withheld one: one is genuinely not the container's service, the other is a mistake with a named fix. Any adapter that must answer an unknown type with ",(0,t.jsx)(n.code,{children:"null"}),', because that is how it says "not mine" to a framework, wants this before it decides between returning ',(0,t.jsx)(n.code,{children:"null"})," and throwing. It covers the keyed surface under a non-null key, exactly as the keyed ",(0,t.jsx)(n.code,{children:"Resolve"})," does. Ask it on the ",(0,t.jsx)(n.code,{children:"Root"}),", which is where the answer belongs: a child scope builds a root-withheld transient normally, so only the root can say it is withholding one."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"IEnumerable<T>"})," resolves for every ",(0,t.jsx)(n.code,{children:"T"}),", empty when the container has no ",(0,t.jsx)(n.em,{children:"unkeyed"}),' registration of the element type, matching the guarantee MS.DI consumers rely on: a collection is a framework\'s usual extension point, and "every registered handler, of which there may be none" has to answer rather than hand back ',(0,t.jsx)(n.code,{children:"null"}),". A service type registered only under keys counts as empty here, exactly as in MS.DI, where keyed registrations do not join an unkeyed ",(0,t.jsx)(n.code,{children:"IEnumerable<T>"}),". The guarantee covers ",(0,t.jsx)(n.code,{children:"IEnumerable<T>"})," alone, again as in MS.DI, which answers ",(0,t.jsx)(n.code,{children:"null"})," for ",(0,t.jsx)(n.code,{children:"T[]"}),", ",(0,t.jsx)(n.code,{children:"IList<T>"})," and ",(0,t.jsx)(n.code,{children:"IReadOnlyList<T>"})," of an unregistered element type. Where the container does have unkeyed members and simply cannot produce the shape for them, such as a collection with an async-initialized member, resolution throws the container's guidance rather than reporting an empty sequence that would silently drop them."]}),"\n",(0,t.jsxs)(n.p,{children:["Two answers still under-report, and both want a ",(0,t.jsx)(n.code,{children:"[FromServices]"})," on the parameter. An ",(0,t.jsx)(n.a,{href:"./registration/open-generics",children:"open generic"})," registration is expanded per closing that something in the container's own graph asks for, so a closing only a framework asks for is never synthesized: with ",(0,t.jsx)(n.code,{children:"[Singleton(typeof(Repo<>), typeof(IRepo<>))]"}
1)," and nothing in the graph consuming ",(0,t.jsx)(n.code,{children:"IRepo<Order>"}),", ",(0,t.jsx)(n.code,{children:"IsService(typeof(IRepo<Order>))"})," is ",(0,t.jsx)(n.code,{children:"false"})," where MS.DI answers ",(0,t.jsx)(n.code,{children:"true"}),". And an ",(0,t.jsx)(n.code,{children:"IEnumerable<T>"})," over a ",(0,t.jsx)(n.em,{children:"value"})," element type the graph never mentioned stays unreported, which is a native-AOT constraint rather than a semantic one: the empty sequence needs the ",(0,t.jsx)(n.code,{children:"T[]"})," type, which AOT generates on demand for a reference element type but not for a value one. In both the probe is faithful to what the bridge will do. Each is pinned by a test in ",(0,t.jsx)(n.code,{children:"FeatureDetectionTests"}),"."]}),"\n",(0,t.jsxs)(n.p,{children:["Writing an adapter for a framework whose dependency-injection surface is not MS.DI means honouring the same convention yourself, because it is the framework's expectation rather than MS.DI's implementation detail. A resolver that answers ",(0,t.jsx)(n.code,{children:"null"})," for an unresolvable ",(0,t.jsx)(n.code,{children:"IEnumerable<T>"})," breaks any framework that uses a collection as an extension point, and the failure names the collection rather than the cause. Answer an empty array of the element type instead, and only for a reference element type, for the AOT reason above."]}),"\n",(0,t.jsx)(n.h2,{id:"warm-async-services-on-startup",children:"Warm async services on startup"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"AddAwaitenInitialization<TContainer>"})," registers an ",(0,t.jsx)(n.code,{children:"IHostedService"})," that calls ",(0,t.jsx)(n.code,{children:"InitializeAsync"})," on startup. It wires the external resolver first, so ",(0,t.jsx)(n.code,{children:"[ImportService<T>]"})," dependencies resolve during warm-up. It is idempotent."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"services.AddGeneratedContainer<CoffeeShop.Root>();\nservices.AddAwaitenInitialization<CoffeeShop.Root>();\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Pair this with ",(0,t.jsx)(n.code,{children:"[Container(SyncResolveAfterInit = true)]"})," so warmed singletons resolve synchronously afterwards."]}),"\n",(0,t.jsx)(n.h2,{id:"verify-external-dependencies-at-startup",children:"Verify external dependencies at startup"}),"\n",(0,t.jsxs)(n.p,{children:["An Awaiten graph can depend on host services through ",(0,t.jsx)(n.code,{children:"[ImportService<T>]"})," and ",(0,t.jsx)(n.code,{children:"[ImportServices]"}),". See ",(0,t.jsx)(n.a,{href:"./advanced/external-services",children:"External services"}),". Verify they are actually registered before you serve traffic."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"provider.VerifyAwaitenContainers();\n"})}),"\n",(0,t.jsx)(n.p,{children:"If a dependency is missing, verification throws and names it, including keyed ones. This is the runtime version of the compile-time missing-dependency check, across the host boundary."}),"\n",(0,t.jsx)(n.h2,{id:"native-aot",children:"Native AOT"}),"\n",(0,t.jsxs)(n.p,{children:["The bridge is reflection-free. The generator emits the closed ",(0,t.jsx)(n.code,{children:"Task<T>"})," type and a typed converter into the registration metadata, so the bridge never calls ",(0,t.jsx)(n.code,{children:"MakeGenericType"})," or ",(0,t.jsx)(n.code,{children:"MakeGenericMethod"}),". The container and its bridge are native-AOT clean, which the AOT sample in the repository proves."]}),"\n",(0,t.jsx)(n.h2,{id:"what-the-bridge-does-not-project",children:"What the bridge does not project"}),"\n",(0,t.jsxs)(n.p,{children:["The bridge surfaces each individual registration and the external dependencies. It does not project Awaiten collections into MS.DI-side resolution; those stay internal to the container. If you need the container to be the single disposal owner for multi-service singletons and nested scoped dependencies, use the ",(0,t.jsx)(n.code,{children:"AwaitenServiceProvider"})," replacement path."]}),"\n",(0,t.jsx)(n.h2,{id:"where-to-go-next",children:"Where to go next"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.a,{href:"./async-initialization",children:"Async initialization"})," for the initialization the hosted service drives."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.a,{href:"./advanced/external-services",children:"Exter
1nal services"})," for ",(0,t.jsx)(n.code,{children:"[ImportService<T>]"})," and ",(0,t.jsx)(n.code,{children:"[ImportServices]"}),"."]}),"\n"]})]})}function l(e={}){let{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},8453(e,n,r){r.d(n,{R:()=>o,x:()=>a});var i=r(6540);let t={},s=i.createContext(t);function o(e){let n=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:o(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.