Skip to content

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.

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.Sdk but references Joinery.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 IDistributedApplicationBuilder is declared by hand in test/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.

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 nameThe project isHostBuilderTypeHostTypeHelper
MauiUseMaui and an Exe or WinExeMicrosoft.Maui.Hosting.MauiAppBuilderMicrosoft.Maui.Hosting.MauiAppDovetailMauiHelpers
Unoan Uno.Sdk application whose UnoFeatures include HostingUno.Extensions.Hosting.IApplicationBuilderMicrosoft.Extensions.Hosting.IHostDovetailUnoHelpers
WebAssemblya Blazor WebAssembly or Microsoft.NET.Sdk.WebAssembly projectMicrosoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostBuilderMicrosoft.AspNetCore.Components.WebAssembly.Hosting.WebAssemblyHostDovetailWebAssemblyHelpers (the default)
WebApplicationa Microsoft.NET.Sdk.Web or Microsoft.NET.Sdk.Razor projectMicrosoft.AspNetCore.Builder.WebApplicationBuilderMicrosoft.AspNetCore.Builder.WebApplicationDovetailHostApplicationHelpers
HostApplicationa Microsoft.NET.Sdk.Worker project, or any other ExeMicrosoft.Extensions.Hosting.HostApplicationBuilderMicrosoft.Extensions.Hosting.IHostDovetailHostApplicationHelpers

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.

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 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.

MetadataDefaultWhat it changes
Namespace$(RootNamespace)The namespace of the generated class, and so the namespace a caller imports for ConfigureDovetail.
HostBuilderTypenone; requiredThe type the ConfigureDovetail extension methods extend (and Task of it). Written into every signature.
HostTypenone; requiredThe type the methods return, inside a ValueTask<>.
HostBuilderNameemptyRead 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.
HelperNameDovetail{Name}HelpersThe 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.
HasBuildExpressiontruefalse calls Configure without the BuildExpression, even one you set, for helpers that build the host themselves.
SyncEntryPointempty (false)Internal, for the MAUI host model. true also emits blocking UseDovetail overloads.
NavigateEntryPointempty (false)Internal, for the Uno host model. true also emits ConfigureDovetail<TShell> overloads that navigate.

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" adds UseDovetail (from DovetailHostSync.cs.template) next to ConfigureDovetail, for hosts whose bootstrap method cannot be async (MauiProgram.CreateMauiApp). It blocks on ConfigureDovetail, 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" adds ConfigureDovetail<TShell> (from DovetailHostNavigate.cs.template), for Uno.Extensions.Navigation applications that start with NavigateAsync<TShell>() instead of Build(). Joinery.Dovetail.Hosting sets it back to false when the project does not reference Uno.Extensions.Navigation.

Each host is written to $(IntermediateOutputPath), which is obj/<Configuration>/<TargetFramework>/, as ordinary Compile items:

FileContents
DovetailHost.{Name}.g.csThe ConfigureDovetail entry points.
DovetailHost.{Name}.Sync.g.csThe UseDovetail entry points. Only with SyncEntryPoint.
DovetailHost.{Name}.Navigate.g.csThe ConfigureDovetail<TShell> entry points. Only with NavigateEntryPoint.
DovetailContextBuilder.g.csDovetailContextBuilder.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().

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.

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"
/>
sample/Sample/Program.cs
await builder
.ConfigureDovetail(z => z.ConfigureCommandLine(configurator => configurator.AddCommand<Dump>("dump")))
.RunConsoleAppAsync();

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"
/>
src/Joinery.Dovetail.Aspire/RocketDistributedApplicationExtensions.cs
public static async ValueTask<DistributedApplication> Configure(
IDistributedApplicationBuilder builder,
DovetailContextBuilder contextBuilder,
CancellationToken cancellationToken
)

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(), &quot;extra&quot;" />
<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.

  • A host with no HelperName needs a class named Dovetail{Name}Helpers. A host named Orphan calls DovetailOrphanHelpers.Configure, and the build fails with CS0103 in the generated file unless you wrote that class or named another with HelperName. The default fits the hosts Joinery.Dovetail.Hosting emits helpers for (HostApplication, Maui, Uno, WebAssembly) and Aspire’s DistributedApplication; for any other name, set HelperName.
  • Two hosts on the same builder type in the same namespace make ConfigureDovetail ambiguous (CS0121). An application built with the SDK already has HostApplication in $(RootNamespace), so a second host on HostApplicationBuilder needs a Namespace of its own, and the caller imports only the one it wants.
  • HostBuilderType and HostType have no default. A host that sets neither generates a file that is not valid C# (CS1001).
  • BuildExpression is spliced in as text. It goes straight after builder, so it starts with a comma, and the Configure method it calls must take exactly those arguments. HasBuildExpression="false" removes it, including a BuildExpression you set.
  • HostBuilderName changes nothing. The template does not use it today.
  • Types are written as given. HostBuilderType, HostType and HelperName go into the generated code unchanged, so a bare name is looked up from Namespace outwards, as in any C# file. Use global:: as the SDK does.
  • SyncEntryPoint and NavigateEntryPoint belong to the MAUI and Uno host models. NavigateEntryPoint emits calls to {HelperName}.Navigate<TShell>, which only DovetailUnoHelpers has; leave both alone on a host of your own.
  • A misspelled metadata name is ignored. The default applies, with no warning.
  • When Joinery.Dovetail.Hosting is 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 (a Microsoft.NET.Sdk.WebAssembly app without Blazor, say), the host is removed from the build, with a message at normal verbosity, and ConfigureDovetail does not exist for it.
  • DovetailJoint: the joint kinds that ConfigureDovetail runs.
  • DovetailTemplate: how Joinery.Dovetail.Hosting ships 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.