Table of Contents

Source generator reference

Arc4u.Dependency.Tool contains two incremental source generators. This page describes exactly what each one reads and emits. For the everyday setup, see Dependency injection; for the architecture behind it, see the design principles.

Generator Reads Emits Method
DependencyToolGenerator The classes of the project marked [Export] Dependencies.g.cs Register<Name>Types
GenerateRegisteredTypes Configs/appsettings.json (additional file) GeneratedTypes.g.cs RegisterTypes
GenerateRegisteredTypes wwwroot/appsettings.json (additional file) GeneratedWwwRootTypes.g.cs RegisterWwwTypes

Every method is a public static void extension method on IServiceCollection, declared in public static partial class RegisterExtensions in the Arc4u.Dependency namespace. Add using Arc4u.Dependency; to call it. The methods call Add..., never TryAdd...: calling a method twice registers its services twice.

Note

On develop/9.0.0, each generated file starts with // <auto-generated/>, so the analyzers of your project skip it. The published 9.0.0-preview37 package and earlier previews omit this header: the analyzer rules of your project then also run on the generated files.

Configuration

The generators have no MSBuild options. DependencyToolGenerator needs no configuration. GenerateRegisteredTypes needs the settings file declared as an additional file:

<ItemGroup>
  <AdditionalFiles Include="Configs/appsettings.json" />
  <AdditionalFiles Include="wwwroot/appsettings.json" />
</ItemGroup>

To keep the generated files on disk, set EmitCompilerGeneratedFiles to true. They are then written to:

obj/<Configuration>/<TargetFramework>/generated/Arc4u.Dependency.Tool/
  Arc4u.Dependency.Tool.DependencyToolGenerator/Dependencies.g.cs
  Arc4u.Dependency.Tool.GenerateRegisteredTypes/GeneratedTypes.g.cs
  Arc4u.Dependency.Tool.GenerateRegisteredTypes/GeneratedWwwRootTypes.g.cs

DependencyToolGenerator

Method name

The method is named Register<Name>Types, where <Name> is the last dot-separated segment of the assembly name. Characters that are not letters, digits or _ are removed, and _ is added in front of a name that starts with a digit.

Assembly name Method
Contoso.Api RegisterApiTypes
Contoso.Billing.Business RegisterBusinessTypes
Contoso.Worker RegisterWorkerTypes

The name is the layer of the project, not its full name, because a host references the layers of a single microservice, where each layer exists once. Every generated method is an extension of IServiceCollection in the Arc4u.Dependency namespace, so two referenced projects with the same last segment make the call ambiguous (CS0121) (see Register the services of several projects).

The method is generated even when the project has no [Export] class; it is then empty.

What is registered

The generator visits every class and record class declared in the project, public or not, including nested classes. The attribute is resolved by its type, so [Export], [ExportAttribute], [Arc4u.Dependency.Attribute.Export] and a using alias are all recognised. It writes one registration for each [Export] attribute of a class, so a class exported for two contracts is registered twice; [Scoped] and [Shared] apply to all of them. The lifetime is Scoped when the class has [Scoped], otherwise Singleton when it has [Shared], otherwise Transient. A class with both [Scoped] and [Shared] is registered as scoped, with warning ARC4UDEP005. The registrations are independent: a [Shared] class exported for two contracts has one instance per contract, not one shared instance.

[Export] arguments Generated call
none services.Add{Lifetime}<TClass>()
typeof(IService) services.Add{Lifetime}<IService, TClass>()
"key" services.AddKeyed{Lifetime}<TClass>("key")
"key", typeof(IService) services.AddKeyed{Lifetime}<IService, TClass>("key")

An open generic class TClass<T> is registered with the Type overloads instead: services.Add{Lifetime}(typeof(TClass<>)), services.Add{Lifetime}(typeof(IService<>), typeof(TClass<>)) or services.AddKeyed{Lifetime}(typeof(IService<>), "key", typeof(TClass<>)). The contract must be open (typeof(IService<>)) and implemented by the class with its type parameters in the same order. Any other generic combination, or a class nested in a generic class, is skipped with error ARC4UDEP006.

For example, this project Contoso.Billing.Business:

using Arc4u.Dependency.Attribute;

namespace Contoso.Billing.Business;

public interface IPaymentGateway
{
    string Name { get; }
}

public interface IRepository<T>
{
}

[Export("Stripe", typeof(IPaymentGateway)), Shared]
public class StripeGateway : IPaymentGateway
{
    public string Name => "Stripe";
}

[Export(typeof(IRepository<string>))]
public class StringRepository : IRepository<string>
{
}

[Export, Scoped]
public class InvoiceNumberGenerator
{
}

produces this Dependencies.g.cs (9.0.0-preview37 and earlier write the same file without the first line):

// <auto-generated/>
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Arc4u.Dependency;

public static partial class RegisterExtensions
{
    public static void RegisterBusinessTypes(this IServiceCollection services)
    {
        services.AddKeyedSingleton<Contoso.Billing.Business.IPaymentGateway, global::Contoso.Billing.Business.StripeGateway>("Stripe");
        services.AddTransient<Contoso.Billing.Business.IRepository<string>, global::Contoso.Billing.Business.StringRepository>();
        services.AddScoped<global::Contoso.Billing.Business.InvoiceNumberGenerator>();
    }
}

GenerateRegisteredTypes

Which file is read

The generator reads an additional file whose path, relative to the project, is exactly Configs/appsettings.json or wwwroot/appsettings.json (case-insensitive). The project folder is the ProjectDir MSBuild property, which every project using the .NET SDK sets. When the property is missing, the generator falls back to the shortest folder path that contains a .cs file of the compilation, and reports warning ARC4UDEP007 if there is none. Other files named appsettings.json are ignored, even when declared as additional files.

The file is parsed with System.Text.Json, accepting comments and trailing commas. Property names are case-sensitive. If the file cannot be read, the build reports error ARC4UDEP001 at the offending line and the method is generated empty, so that its callers still compile. If the file has no Application.Dependency section, the method is generated empty.

Entry format

{
  "Application.Dependency": {
    "RegisterTypes": [
      "Arc4u.Caching.Memory.MemoryCache, Arc4u.Caching.Memory"
    ]
  }
}

Each entry of RegisterTypes is a string Namespace.Type, AssemblyName:

  • AssemblyName is matched against the names of the assemblies the project references. Package references and project references both work.
  • Version= is optional. When present, it is compared with the assembly version of the reference (Version=9.0.0.0 matches the assembly of package 9.0.0-preview37); only the parts given are compared, so Version=9.0 matches 9.0.1.0. A reference whose path contains the version string is also accepted.
  • The assembly is read as metadata, without being loaded. The type must be public, since the generated code references it, and must not be nested in another type: nested types are skipped.
  • The registration follows the [Export], [Shared] and [Scoped] attributes of the type, with the same table as DependencyToolGenerator: a type with both [Shared] and [Scoped] is registered as scoped, with warning ARC4UDEP005. A generic type cannot be listed: it is skipped with error ARC4UDEP006.
  • An entry that cannot be registered is skipped with a warning that points at it in appsettings.json: ARC4UDEP002 for a malformed entry, ARC4UDEP003 when the assembly is not referenced or has another version, ARC4UDEP004 when the type is not found or has no [Export].

The generated method groups the registrations by assembly:

public static void RegisterTypes(this IServiceCollection services)
{
    // Types registered by nuget package: Arc4u.Caching.Memory
    services.AddKeyedTransient<Arc4u.Caching.ICache, Arc4u.Caching.Memory.MemoryCache>("Memory");
}

The 8.x keys Assemblies and RejectedTypes are ignored.

Troubleshooting

See Troubleshooting in the dependency injection guide.

See also