Skip to content

Source Generation

We have built a Source Generator to use with Dovetail that allows us to export joints and import joints with easy, in a way that is statically compiled.

Any joint marked with [assembly: Dovetail(typeof(MyDovetail))] will automatically be added to a public partial static class named Exports that will exist in the root namespace of the assembly being built.

Each exported joint carries the host type it declares with [LiveJoint], [UnitTestJoint] or [DovetailHostType], the same as when a joint is added through reflection at runtime. See which attributes are honoured where.

A joint with constructor parameters is exported as a factory, because the exported method has no context to read from: the context builder that holds the metadata creates the joint from its properties, and an optional parameter falls back to its default. See how constructor parameters are resolved.

The generator creates an internal static partial Imports class in the assembly’s root namespace. Imports.Joints is a DovetailContextBuilderFactory that creates a context builder loaded with the assembly’s own joints and those of every referenced assembly that exports joints. The DovetailImportNamespace, DovetailImportClassName and DovetailImportMethodName MSBuild properties change those names.

You can also import the GetDovetails method onto a given class by using the [ImportDovetails] attribute on the class.

Two MSBuild properties decide what the generator emits for a project:

PropertyWhen true
ImportDovetailAssemblyThe Imports class is generated.
ExportDovetailAssemblyReferencing assemblies import this assembly’s joints. When false, the joints still reach this assembly’s own Imports class.

Joinery.Dovetail.Sdk sets both as part of its host detection. Projects that don’t use the SDK get these defaults:

  • test projects (IsTestProject) import but don’t export;
  • applications (OutputType of Exe or WinExe) import and export;
  • libraries export but don’t import.

Set either property in a project file to override its default.

The generator runs in every project that references a Dovetail package, including projects that only reference one indirectly, for example through Joinery.Dovetail.Hosting. Its diagnostics (DT0001 to DT0005, such as the error for a joint with more than one constructor, and DT0017, the error for a joint with no constructor it can be created with) apply there too. The code it generates needs C# 12 or later: in a project whose LangVersion is older, it generates nothing and reports warning DT0006 instead.

An application that references Specular doesn’t set its provider by hand. Specular generates an internal SpecularProvider into each project that emits one, and the generated Imports seeds that provider into the context builder it creates, so a joint reads it with context.Get<ISpecularProvider>(). Scanning through it covers the assemblies the application references, including the joints of libraries that don’t generate a provider of their own (SpecularEmitProvider set to false) and so keep receiving the application’s through the context.

public sealed class PluginJoint : ISetupJoint
{
public void Register(IDovetailContext context)
{
var provider = context.Get<ISpecularProvider>()!;
var plugins = provider.GetTypes(z => z.FromAssemblyOf<IPlugin>().GetTypes(f => f.AssignableTo<IPlugin>()));
}
}

Dovetail has no dependency on Specular, so the seeding is generated code that exists only where Specular does:

  • The generator adds it when the project’s compilation references the assembly that defines Specular.ISpecularProvider and the SpecularEmitProvider MSBuild property isn’t false, the two things Specular’s own generator looks at before it generates a SpecularProvider. It can’t look for SpecularProvider itself: a generator doesn’t see what another one adds.
  • In every other project nothing is generated and no diagnostic is raised.
  • A provider you set yourself wins. The generated code only adds the provider when the context doesn’t hold one, so builder.ConfigureDovetail(b => b.Set<ISpecularProvider>(provider)), or a provider passed in with the properties, is never overwritten.
  • Set SpecularEmitProvider to false for a project that references Specular without generating a provider (say it excludes Specular’s analyzers) and Dovetail stops naming one. Otherwise the build fails with CS0103 for SpecularProvider.

The seeding adds no reflection: it is a generic call to AddIfMissing<ISpecularProvider> with the value of a static property, both of which the trimmer and the AOT compiler can follow. Specular generates SpecularProvider without reflection as well. The Joinery.Dovetail.Sdk.Tests publish an application that uses it trimmed and with Native AOT, without trim or AOT warnings, and the published application starts with the provider in its context and scans across assemblies.