DovetailHost
DovetailHost
Section titled “DovetailHost”A DovetailHost item declares the entry points of one host: the ConfigureDovetail extension methods on its builder type.
Calling one creates the Dovetail context, runs every joint the application imports, and builds the host.
var host = await Host.CreateApplicationBuilder(args).ConfigureDovetail();The item is expanded by MSBuild targets, not by the Roslyn source generator: the C# is written to obj/ before the compiler
runs, so you can open it in your IDE like any other source file.
Who declares one, and when
Section titled “Who declares one, and when”In most projects, nobody: Joinery.Dovetail.Sdk looks at the project and declares the host for you. You declare your own when
the SDK does not, which is one of:
- the project does not use
Joinery.Dovetail.Sdkbut referencesJoinery.Dovetail.sample/Sample, the benchmarks and most test projects declare their host by hand; - the host is one the SDK does not detect. Aspire’s
IDistributedApplicationBuilderis declared by hand intest/Joinery.Dovetail.Aspire.Tests; the SDK has that case commented out.
Do not declare a host the SDK already did. A second host on the same builder type makes ConfigureDovetail ambiguous (see
common mistakes) and gives you two copies of the entry points.
The hosts the SDK declares
Section titled “The hosts the SDK declares”Joinery.Dovetail.Sdk declares at most one host, the first of these that matches the project (Joinery.Dovetail.HostDetection.targets).
A library matches none, and gets no entry points. Each also adds the Joinery.Dovetail.Hosting package, which emits the helpers
the entry points call.
| Host name | The project is | HostBuilderType | HostType | Helper |
|---|---|---|---|---|
Maui | UseMaui and an Exe or WinExe | Microsoft.Maui.Hosting.MauiAppBuilder | Microsoft.Maui.Hosting.MauiApp | DovetailMauiHelpers |
Uno | an Uno.Sdk application whose UnoFeatures include Hosting | Uno.Extensions.Hosting.IApplicationBuilder | Microsoft.Extensions.Hosting.IHost | DovetailUnoHelpers |
WebAssembly | a Blazor WebAssembly or Microsoft.NET.Sdk.WebAssembly project | Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilder | Microsoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHost | DovetailWebAssemblyHelpers (the default) |
WebApplication | a Microsoft.NET.Sdk.Web or Microsoft.NET.Sdk.Razor project | Microsoft.AspNetCore.Builder.WebApplicationBuilder | Microsoft.AspNetCore.Builder.WebApplication | DovetailHostApplicationHelpers |
HostApplication | a Microsoft.NET.Sdk.Worker project, or any other Exe | Microsoft.Extensions.Hosting.HostApplicationBuilder | Microsoft.Extensions.Hosting.IHost | DovetailHostApplicationHelpers |
An Uno application without Hosting has no host and gets a build warning. MAUI is matched first, because a MAUI application is also
an Exe.
Declaring a host
Section titled “Declaring a host”The item’s Include is the host’s name. It becomes part of the generated class and file names, and, unless you set HelperName, of
the name of the helper the code calls, so it has to be a valid C# identifier and it must be unique in the project.
<!-- test/Joinery.Dovetail.Hosting.Tests/Joinery.Dovetail.Hosting.Tests.csproj --><DovetailHost Include="HostApplication" HostBuilderType="Microsoft.Extensions.Hosting.HostApplicationBuilder" HostBuilderName="builder" HostType="Microsoft.Extensions.Hosting.IHost"/>HostBuilderType and HostType are all a host needs, and the project needs a helper of the default name (HostApplication is named for
the one Joinery.Dovetail.Hosting emits for the generic host).
Metadata
Section titled “Metadata”Metadata is optional unless stated. A name the item does not read is ignored without a warning, so a misspelling falls back to the
default. Types are written into the generated code exactly as you give them, so write them with global::, as the SDK does for builder types. Boolean
values are compared with true ignoring case; any other non-empty value counts as false.
| Metadata | Default | What it changes |
|---|---|---|
Namespace | $(RootNamespace) | The namespace of the generated class, and so the namespace a caller imports for ConfigureDovetail. |
HostBuilderType | none; required | The type the ConfigureDovetail extension methods extend (and Task of it). Written into every signature. |
HostType | none; required | The type the methods return, inside a ValueTask<>. |
HostBuilderName | empty | Read and handed to the template, which has no use for it. It changes nothing in the generated code today; the SDK sets it to builder. |
HelperName | Dovetail{Name}Helpers | The static class whose Configure method the generated code calls, written as given. Joinery.Dovetail.Hosting emits DovetailHostApplicationHelpers, DovetailMauiHelpers, DovetailUnoHelpers and DovetailWebAssemblyHelpers; any other host brings its own. |
BuildExpression | , static b => b.Build() | The arguments spliced into that call right after builder, so it begins with a comma. The default is the delegate that builds the host. MAUI and Uno add their application assembly. |
HasBuildExpression | true | false calls Configure without the BuildExpression, even one you set, for helpers that build the host themselves. |
SyncEntryPoint | empty (false) | Internal, for the MAUI host model. true also emits blocking UseDovetail overloads. |
NavigateEntryPoint | empty (false) | Internal, for the Uno host model. true also emits ConfigureDovetail<TShell> overloads that navigate. |
Internal metadata
Section titled “Internal metadata”SyncEntryPoint and NavigateEntryPoint exist for the MAUI and Uno host models. The SDK’s MAUI and Uno detection sets them, and the
repository’s MAUI and Uno samples and tests set them by hand. They are documented so the generated files make sense, not as a supported
way to extend a host: leave them alone on a host of your own.
SyncEntryPoint="true"addsUseDovetail(fromDovetailHostSync.cs.template) next toConfigureDovetail, for hosts whose bootstrap method cannot be async (MauiProgram.CreateMauiApp). It blocks onConfigureDovetail, which is safe only because Dovetail never resumes on the calling thread’s context: joints must not post back to the UI thread during startup.NavigateEntryPoint="true"addsConfigureDovetail<TShell>(fromDovetailHostNavigate.cs.template), for Uno.Extensions.Navigation applications that start withNavigateAsync<TShell>()instead ofBuild().Joinery.Dovetail.Hostingsets it back tofalsewhen the project does not referenceUno.Extensions.Navigation.
What is generated
Section titled “What is generated”Each host is written to $(IntermediateOutputPath), which is obj/<Configuration>/<TargetFramework>/, as ordinary Compile items:
| File | Contents |
|---|---|
DovetailHost.{Name}.g.cs | The ConfigureDovetail entry points. |
DovetailHost.{Name}.Sync.g.cs | The UseDovetail entry points. Only with SyncEntryPoint. |
DovetailHost.{Name}.Navigate.g.cs | The ConfigureDovetail<TShell> entry points. Only with NavigateEntryPoint. |
DovetailContextBuilder.g.cs | DovetailContextBuilder.Create(), once for the project as soon as it declares any host: a builder populated with the joints the assembly imports. |
All of them are in Namespace, in internal static partial class GeneratedDovetail{Name}Extensions. They are internal, so each assembly
that hosts an application declares its own, and a test project that hosts one needs its own DovetailHost too.
ConfigureDovetail has the same five forms on the builder and on a Task of it (so a builder you are still awaiting can be chained),
each returning ValueTask<{HostType}>:
ConfigureDovetail(this {HostBuilderType} builder, CancellationToken cancellationToken = default)ConfigureDovetail(this {HostBuilderType} builder, Action<DovetailContextBuilder> action, CancellationToken cancellationToken = default)ConfigureDovetail(this {HostBuilderType} builder, Func<DovetailContextBuilder, ValueTask> action, CancellationToken cancellationToken = default)ConfigureDovetail(this {HostBuilderType} builder, Func<DovetailContextBuilder, CancellationToken, ValueTask> action, CancellationToken cancellationToken = default)ConfigureDovetail(this {HostBuilderType} builder, DovetailContextBuilder contextBuilder, CancellationToken cancellationToken = default)Every one ends in the same call, which is where HelperName, BuildExpression and HasBuildExpression apply:
return await {HelperName}.Configure(builder{BuildExpression}, contextBuilder, cancellationToken);UseDovetail has the same forms on the builder only and returns the {HostType} itself. ConfigureDovetail<TShell> has them on the builder
only, with where TShell : Microsoft.UI.Xaml.UIElement, new().
Examples
Section titled “Examples”A host the SDK declares
Section titled “A host the SDK declares”For a MAUI application the SDK declares this, and UseDovetail is the blocking entry point it adds for MauiProgram.CreateMauiApp
(src/Joinery.Dovetail.Sdk/Sdk/Joinery.Dovetail.HostDetection.targets):
<DovetailHost Include="Maui" HostBuilderType="global::Microsoft.Maui.Hosting.MauiAppBuilder" HostBuilderName="builder" HostType="global::Microsoft.Maui.Hosting.MauiApp" HelperName="global::Joinery.Dovetail.DovetailMauiHelpers" BuildExpression=", static b => b.Build(), typeof(GeneratedDovetailMauiExtensions).Assembly" SyncEntryPoint="true" />The generated call is await global::Joinery.Dovetail.DovetailMauiHelpers.Configure(builder, static b => b.Build(), typeof(GeneratedDovetailMauiExtensions).Assembly, contextBuilder, cancellationToken).
The Uno declaration has the same shape with DovetailUnoHelpers and NavigateEntryPoint="true" in place of SyncEntryPoint. The other three
set only the three types, and a HelperName for WebApplication and HostApplication.
A project without the SDK
Section titled “A project without the SDK”sample/Sample references Joinery.Dovetail.Hosting directly and declares the generic host itself:
<!-- sample/Sample/Sample.csproj --><DovetailHost Include="HostApplication" HostBuilderType="global::Microsoft.Extensions.Hosting.HostApplicationBuilder" HostBuilderName="builder" HostType="global::Microsoft.Extensions.Hosting.IHost" HelperName="global::Joinery.Dovetail.DovetailHostApplicationHelpers"/>await builder .ConfigureDovetail(z => z.ConfigureCommandLine(configurator => configurator.AddCommand<Dump>("dump"))) .RunConsoleAppAsync();A host of your own: Aspire
Section titled “A host of your own: Aspire”The Aspire helper builds the host itself, so its Configure takes no build delegate, and its tests declare the host with
HasBuildExpression="false". The helper is DovetailDistributedApplicationHelpers, which is the default for a host named
DistributedApplication, so there is no HelperName:
<!-- test/Joinery.Dovetail.Aspire.Tests/Joinery.Dovetail.Aspire.Tests.csproj --><DovetailHost Include="DistributedApplication" Namespace="Joinery.Dovetail.Joints" HostBuilderType="global::Aspire.Hosting.IDistributedApplicationBuilder" HostBuilderName="builder" HostType="global::Aspire.Hosting.DistributedApplication" HasBuildExpression="false"/>public static async ValueTask<DistributedApplication> Configure( IDistributedApplicationBuilder builder, DovetailContextBuilder contextBuilder, CancellationToken cancellationToken)Your own helper
Section titled “Your own helper”When nothing ships a helper for your builder, write one with the shape the call expects. The three declarations below are compiled by
test/Joinery.Dovetail.Sdk.Tests/DovetailItemReferenceTests.cs, together with these helpers:
<DovetailHost Include="Mine" HostBuilderType="global::Microsoft.Extensions.Hosting.HostApplicationBuilder" HostType="global::Microsoft.Extensions.Hosting.IHost" /><DovetailHost Include="Custom" Namespace="Hosts.Custom" HostBuilderType="global::Microsoft.Extensions.Hosting.HostApplicationBuilder" HostType="global::Microsoft.Extensions.Hosting.IHost" HelperName="global::worker.CustomHelper" HostBuilderName="ignored" BuildExpression=", static b => b.Build(), "extra"" /><DovetailHost Include="NoBuild" Namespace="Hosts.NoBuild" HostBuilderType="global::Microsoft.Extensions.Hosting.HostApplicationBuilder" HostType="global::Microsoft.Extensions.Hosting.IHost" HelperName="global::worker.NoBuildHelper" HasBuildExpression="false" BuildExpression=", static b => b.Build()" />internal static class DovetailMineHelpers // the default HelperName of a host named Mine{ public static ValueTask<IHost> Configure(HostApplicationBuilder builder, Func<HostApplicationBuilder, IHost> build, DovetailContextBuilder contextBuilder, CancellationToken cancellationToken) => ValueTask.FromResult(build(builder));}
internal static class CustomHelper // BuildExpression adds the string between the delegate and the context builder{ public static ValueTask<IHost> Configure(HostApplicationBuilder builder, Func<HostApplicationBuilder, IHost> build, string extra, DovetailContextBuilder contextBuilder, CancellationToken cancellationToken) => ValueTask.FromResult(build(builder));}
internal static class NoBuildHelper // HasBuildExpression="false" drops the delegate, although BuildExpression is set{ public static ValueTask<IHost> Configure(HostApplicationBuilder builder, DovetailContextBuilder contextBuilder, CancellationToken cancellationToken) => ValueTask.FromResult(builder.Build());}Mine generates GeneratedDovetailMineExtensions in $(RootNamespace), and the other two use a namespace each, so their
ConfigureDovetail methods do not collide.
Common mistakes
Section titled “Common mistakes”- A host with no
HelperNameneeds a class namedDovetail{Name}Helpers. A host namedOrphancallsDovetailOrphanHelpers.Configure, and the build fails with CS0103 in the generated file unless you wrote that class or named another withHelperName. The default fits the hostsJoinery.Dovetail.Hostingemits helpers for (HostApplication,Maui,Uno,WebAssembly) and Aspire’sDistributedApplication; for any other name, setHelperName. - Two hosts on the same builder type in the same namespace make
ConfigureDovetailambiguous (CS0121). An application built with the SDK already hasHostApplicationin$(RootNamespace), so a second host onHostApplicationBuilderneeds aNamespaceof its own, and the caller imports only the one it wants. HostBuilderTypeandHostTypehave no default. A host that sets neither generates a file that is not valid C# (CS1001).BuildExpressionis spliced in as text. It goes straight afterbuilder, so it starts with a comma, and theConfiguremethod it calls must take exactly those arguments.HasBuildExpression="false"removes it, including aBuildExpressionyou set.HostBuilderNamechanges nothing. The template does not use it today.- Types are written as given.
HostBuilderType,HostTypeandHelperNamego into the generated code unchanged, so a bare name is looked up fromNamespaceoutwards, as in any C# file. Useglobal::as the SDK does. SyncEntryPointandNavigateEntryPointbelong to the MAUI and Uno host models.NavigateEntryPointemits calls to{HelperName}.Navigate<TShell>, which onlyDovetailUnoHelpershas; leave both alone on a host of your own.- A misspelled metadata name is ignored. The default applies, with no warning.
- When
Joinery.Dovetail.Hostingis referenced, a host whose framework is missing is dropped. If the helper a host names is one of the package’s but the project does not reference the framework it compiles against (aMicrosoft.NET.Sdk.WebAssemblyapp without Blazor, say), the host is removed from the build, with a message at normal verbosity, andConfigureDovetaildoes not exist for it.
See also
Section titled “See also”- DovetailJoint: the joint kinds that
ConfigureDovetailruns. - DovetailTemplate: how
Joinery.Dovetail.Hostingships the helpers a host calls, and gates them on the host’s framework. - Host Lifecycle: code that runs as the host starts and stops.
- Defining Dovetails: the host type a joint can be filtered by.