Skip to content

DovetailTemplate

A DovetailTemplate item names a *.cs.template file that a package ships and says which projects should get a copy. When the project’s references have resolved, Dovetail checks every template’s gates, writes the ones that pass to obj/ and adds them to the compilation. Nothing needs to be compiled against, so the shipping package depends on none of the assemblies its templates use: referencing them is what turns a template on.

This page lists what the item accepts. Shipping templates is the guide to using it: how to write a template, how to pack the targets that declare it, and how to test the gates.

The item is expanded by MSBuild targets (Joinery.Dovetail.Templates.targets), not by the Roslyn source generator.

A package that wants to put C# into the projects that use it declares its templates from its build/ or buildTransitive/ targets. Joinery.Dovetail.Hosting is the main user: its ASP.NET Core, MAUI, Uno and Blazor WebAssembly helpers and its OpenTelemetry joints are all templates, which is why that package has no dependency on any of those frameworks. A solution can declare templates in a Directory.Build.targets to the same effect.

Declare one when the code you ship has to be compiled into the consumer, because it uses types the consumer references and you do not. Do not declare one when ordinary compiled code will do: a library the consumer references is simpler, and a template is only worth it for what needs to be gated on the project.

A project that does not run the Dovetail source generator (Joinery.Dovetail.Analyzers) gets no templates, however they are gated, since a template is meant to hold joints and [Embedded] helpers that the generator discovers.

The item’s Include is the template’s name. It names the generated file, so it has to be unique across every package a project references.

<!-- a package's declaration, built by test/Joinery.Dovetail.Sdk.Tests/TemplateExtensionPointTests.cs -->
<DovetailTemplate Include="Plain" File="$(MSBuildThisFileDirectory)Plain.cs.template" Package="DovetailTemplateShipper" />

File is the only metadata that is required. Leave out Package, References and Hosts and the template goes into every project that runs the generator.

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. Every gate you set has to pass; a gate you leave out does not restrict anything. Boolean values are compared with true ignoring case; any other non-empty value counts as false.

MetadataDefaultWhat it changes
Filenone; requiredThe full path of the template, usually $(MSBuildThisFileDirectory)Name.cs.template. A template without one, or whose file does not exist, fails the build. It must not contain backslashes: MSBuild rewrites them to forward slashes on Linux and macOS when the file is written.
PackageemptyThe id of the package that ships the template. The template is only written into a project that references it: the package’s assembly, which is named after the package, is among the project’s resolved references (or, with DirectReferenceOnly, the project declares the reference itself).
DirectReferenceOnlyempty (false)true limits the template to projects with a direct reference to Package: a PackageReference to it (the ones the Dovetail SDK adds count), or a ProjectReference to a project named after it. A project that only gets the package transitively gets nothing. Requires Package; without it the build fails.
ReferencesemptySemicolon-separated assembly names (file names without extension). Every one has to be among the project’s resolved references.
HostsemptySemicolon-separated DovetailHost names, at least one of which the project has to declare: the host model, as Joinery.Dovetail.Sdk detects it (HostApplication, WebApplication, WebAssembly, Maui, Uno). These are the hosts declared when the templates resolve, before a package narrows them.
HostNameemptyReplaces {HostName} in the template, but only when HostType is set too.
HostTypeemptyReplaces {HostType} in the template, and switches the {HostName} replacement on. Together they let one template be emitted once per host model under different names.
OutputPrefixDovetailTemplateThe prefix of the generated file name, <prefix>.<name>.g.cs.

The gates are applied together, and a template that fails any of them is not written. They are read once the project’s references have resolved, which is right after ResolveAssemblyReferences and before the compiler runs.

Each template that passes is written to $(IntermediateOutputPath), which is obj/<Configuration>/<TargetFramework>/, as an ordinary Compile item:

FileContents
<prefix>.<name>.g.csThe template’s text, with {HostName} and {HostType} replaced when HostType is set. The prefix is OutputPrefix, DovetailTemplate by default.

The item adds no namespace, type or method of its own: the template is the C#. For Joinery.Dovetail.Hosting, which sets OutputPrefix="DovetailHosting", a generic-host application gets obj/Debug/net10.0/DovetailHosting.DovetailHostApplicationHelpers.g.cs.

The templates that passed are the DovetailEmittedTemplate items, with the metadata above carried over. A target can add to them or remove from them between DovetailResolveTemplates and DovetailGenerateTemplates; the guide shows how.

The declaration above is the one the extension-point tests build. They pack a package that ships it, reference it from projects that declare the reference themselves and from one that only receives it transitively, and read which DovetailTemplate.*.g.cs files land in each obj/.

Joinery.Dovetail.Hosting lists its helpers with the assemblies each needs, then declares every one as a template for the package:

<!-- src/Joinery.Dovetail.Hosting/build/Joinery.Dovetail.Hosting.targets (verbatim) -->
<ItemGroup>
<_DovetailHostingHelper Include="DovetailHostApplicationHelpers" References="Microsoft.Extensions.Hosting" />
<_DovetailHostingHelper Include="DovetailHostLifecycleHelpers" References="Microsoft.Extensions.Hosting" />
<_DovetailHostingHelper Include="DovetailMauiHelpers" References="Microsoft.Maui" />
<_DovetailHostingHelper Include="DovetailUnoHelpers" References="Uno.Extensions.Hosting.WinUI" />
<_DovetailHostingHelper Include="DovetailUnoNavigationHelpers" References="Uno.Extensions.Hosting.WinUI;Uno.Extensions.Navigation.UI" />
<_DovetailHostingHelper Include="DovetailWebAssemblyHelpers" References="Microsoft.AspNetCore.Components.WebAssembly" />
<_DovetailHostingHelper Include="DovetailConfigurationSchemaEndpointExtensions" References="Microsoft.AspNetCore" />
</ItemGroup>
<ItemGroup>
<DovetailTemplate
Include="@(_DovetailHostingHelper)"
File="$(MSBuildThisFileDirectory)%(Identity).cs.template"
Package="Joinery.Dovetail.Hosting"
OutputPrefix="DovetailHosting"
/>
</ItemGroup>

References carries over from _DovetailHostingHelper to each template, so a project gets DovetailMauiHelpers only when it references Microsoft.Maui.

The OpenTelemetry start joint is one template emitted once per host model, which is what HostName and HostType are for. The package adds the instances to DovetailEmittedTemplate itself, because which of them a host needs depends on helpers that have already been resolved:

<!-- src/Joinery.Dovetail.Hosting/build/Joinery.Dovetail.Hosting.targets: one of the items of an ItemGroup, verbatim apart from the indentation -->
<DovetailEmittedTemplate
Include="DovetailOpenTelemetryMauiStartJoint"
Condition="'$(_DovetailHostingMaui)' == 'true'"
File="$(MSBuildThisFileDirectory)DovetailOpenTelemetryStartJoint.cs.template"
OutputPrefix="DovetailHosting"
HostName="Maui"
HostType="global::Microsoft.Maui.Hosting.MauiApp" />

test/Joinery.Dovetail.Sdk.Tests/DovetailItemReferenceTests.cs builds a project that declares the following, and checks which files land in obj/ (the project is a generic-host application that references Microsoft.Extensions.Hosting; its templates are Plain.cs.template, and so on):

<DovetailTemplate Include="Plain" File="$(MSBuildThisFileDirectory)Plain.cs.template" />
<DovetailTemplate Include="Prefixed" File="$(MSBuildThisFileDirectory)Prefixed.cs.template" OutputPrefix="Contoso" />
<DovetailTemplate Include="Referenced" File="$(MSBuildThisFileDirectory)Referenced.cs.template" References="Microsoft.Extensions.Hosting" />
<DovetailTemplate Include="MissingReference" File="$(MSBuildThisFileDirectory)MissingReference.cs.template" References="Microsoft.Extensions.Hosting;Not.Referenced.Anywhere" />
<DovetailTemplate Include="ForThisHost" File="$(MSBuildThisFileDirectory)ForThisHost.cs.template" Hosts="Maui;HostApplication" />
<DovetailTemplate Include="ForOtherHosts" File="$(MSBuildThisFileDirectory)ForOtherHosts.cs.template" Hosts="Maui;Uno" />
<DovetailTemplate Include="PerHost" File="$(MSBuildThisFileDirectory)PerHost.cs.template" HostName="Mine" HostType="global::System.Object" />
<DovetailTemplate Include="NameOnly" File="$(MSBuildThisFileDirectory)NameOnly.cs.template" HostName="Mine" />

Plain, Referenced, ForThisHost, PerHost and NameOnly are written, Prefixed as Contoso.Prefixed.g.cs; MissingReference and ForOtherHosts are not. In PerHost {HostName} and {HostType} are replaced; in NameOnly the text {HostName} is left as it is.

  • Include is the file name, so it has to be unique across packages. Two templates with one name write one file.
  • File has to be a full path. Use $(MSBuildThisFileDirectory), which is the directory of the targets file that declares the item. A missing File fails the build with the template '<name>' has no File, and a path that does not exist with names a file that doesn't exist.
  • DirectReferenceOnly without Package fails the build (is DirectReferenceOnly, so it needs the Package it is direct to).
  • Package is matched against an assembly name. It is the package’s id, and it has to be the name of the package’s assembly as well, since that is what the project’s resolved references are searched for.
  • HostName alone does nothing. {HostName} is replaced only when HostType is set; set both.
  • Hosts names are DovetailHost names. Each is matched, by name, against the DovetailHost items declared when the templates resolve, which are the ones Joinery.Dovetail.Sdk detected (HostApplication, WebApplication, WebAssembly, Maui, Uno) plus any you declared. See DovetailHost.
  • A transitive reference declares your items but gets no file under DirectReferenceOnly. Its buildTransitive/ targets still run, so the DovetailTemplate items are there; the gate is what leaves the file out.
  • A misspelled metadata name is ignored. The default applies, with no warning.
  • Templates do not reach projects that skip the generator. A project without Joinery.Dovetail.Analyzers gets none.
  • Shipping templates: the guide to writing, packing and testing templates.
  • DovetailHost: the hosts that Hosts refers to, and the helpers Joinery.Dovetail.Hosting ships as templates.
  • DovetailJoint: the joint kinds a template can declare joints of.