Importable 1.0.13

dotnet add package Importable --version 1.0.13
                    
NuGet\Install-Package Importable -Version 1.0.13
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Importable" Version="1.0.13" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Importable" Version="1.0.13" />
                    
Directory.Packages.props
<PackageReference Include="Importable" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Importable --version 1.0.13
                    
#r "nuget: Importable, 1.0.13"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Importable@1.0.13
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Importable&version=1.0.13
                    
Install as a Cake Addin
#tool nuget:?package=Importable&version=1.0.13
                    
Install as a Cake Tool

Importable

An opinionated strongly typed DI registration library for C# 8+ and .NET 6+. It generates a fluent registration method for your library, with compile-time checks that every port has an adapter assigned.

The motivation for this repository comes from common annoyances with using Clean/Hexagonal/Onion architecture in C#.

The library produces the following fluent registration method for your library:

builder.Services.AddTeaParty(party => party
    .AddPantry(sp => new PantryCupboard(sp.GetRequiredService<IShoppingList>()))
    .AddGuestList<GuestBook>()
    .AddTeacup<EarlGrey, ChinaTeacup>()
    .AddTeacup<Darjeeling, PorcelainTeacup>()
    .AddSeatingPlan<HeadTableSeating>("headTable")
    .AddSeatingPlan<GardenSeating>("garden"));

Everything behind AddTeaParty, AddPantry, AddTeacup and so on is generated at compile time from attributes. If you forget to assign an adapter to a port, you get a compiler warning at the call site and a clear exception at startup, instead of a No service for type ... error the first time something resolves it.


Contents

  1. Requirements
  2. Installation
  3. Quick start
  4. Concepts
  5. Declaring the entry point — [assembly: Connect]
  6. Declaring ports — [Port]
  7. Declaring internal services — [Service]
  8. Wiring adapters in the host
  9. Adding your own registrations
  10. Missing-adapter checks
  11. Diagnostics reference
  12. What gets generated
  13. Migrating a hand-written registration class
  14. Troubleshooting
  15. Limitations

Requirements

Minimum
Consuming project C# 8+ (any modern net6.0+ target; netstandard2.0 libraries work too)
Compiler / SDK Roslyn 4.12 — .NET SDK 10.0.1xx / Visual Studio 17.12 or newer
DI package Microsoft.Extensions.DependencyInjection.Abstractions 8.0+ (keyed services are used)

The generator has no runtime footprint: it ships no assembly your application loads. All attributes and code are emitted into your own project.

Installation

Reference the generator project as an analyzer from the project that declares ports and services (TeaParty.Core) and from the host project that calls the generated method (TeaParty.Host). The host needs it for the missing-adapter analyzer.

<ItemGroup>
  <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.*" />
</ItemGroup>

<ItemGroup>
  <ProjectReference Include="..\Importable.Generator\Importable.Generator.csproj"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>

OutputItemType="Analyzer" loads it into the compiler; ReferenceOutputAssembly="false" stops it being copied into your build output. Both are required.

Optional, but handy while learning: write the generated files to disk under obj/.../generated/.

<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

Quick start

1. Name the entry point (in any file of TeaParty.Core):

[assembly: Importable.Connect("TeaParty", Namespace = "TeaParty.IoC")]

2. Mark your ports (what the party needs, but lets the host choose):

using Importable;

[Port(Lifetime.Scoped)]
public interface ITeapot
{
    Task<Tea> Brew(Blend blend);
}

[Port(Lifetime.Transient)]
public interface IGuestList
{
    IReadOnlyList<Patron> Arrivals();
}

3. Mark internal helpers the party always needs:

[Service(Lifetime = Lifetime.Singleton)]
internal sealed class PatronService : IPatronService { /* greets, seats and looks after patrons */ }

4. Wire adapters in the host:

using TeaParty.IoC;

builder.Services.AddTeaParty(party => party
    .AddTeapot<CopperTeapot>()
    .AddGuestList(sp => new GuestBook(sp.GetRequiredService<IOptions<InvitationOptions>>().Value)));

Build. ITeapot, IGuestList and IPatronService can now be resolved.

Concepts

Term Meaning Tea party example You write
Port An interface your core logic depends on. The host chooses the implementation. ITeapot, ITeacup<TBrew> [Port] on the interface
Adapter A class implementing a port. It can live anywhere. CopperTeapot, ChinaTeacup Nothing; you pass it to AddXxx<TAdapter>()
Service An internal class the library always needs. The host has no choice. PatronService, KettleTimer [Service] on the class
Entry point The generated Add{Name}(this IServiceCollection, Action<{Name}Configuration>) method. AddTeaParty [assembly: Connect("TeaParty")]

Declaring the entry point — [assembly: Connect]

[assembly: Importable.Connect("TeaParty", Namespace = "TeaParty.IoC")]
Argument Required Effect
name yes Must be a valid C# identifier. Produces class {name}Service, nested class {name}Configuration, method Add{name} and hook OnAdding{name}. Here: TeaPartyService, TeaPartyConfiguration, AddTeaParty, OnAddingTeaParty.
Namespace no Namespace of the generated class. Defaults to the assembly name.
Scope no Namespace whose ports and services belong to this connection, including nested namespaces. Defaults to Namespace. Only used when the assembly has more than one connection.

Without any Connect, nothing is generated and every [Port] or [Service] raises IMP003.

The generated class is public when every port is public. If any port is internal, the whole class becomes internal, because otherwise the generated public methods would expose internal types.

Several connections in one assembly

An assembly can declare more than one connection. Each gets its own generated file, entry point, missing-adapter checks and OnAdding{Name} hook:

[assembly: Importable.Connect("TeaParty", Namespace = "TeaParty.IoC",        Scope = "TeaParty")]
[assembly: Importable.Connect("Garden",   Namespace = "TeaParty.Garden.IoC", Scope = "TeaParty.Garden")]

namespace TeaParty.Ports        { [Port] public interface ITeapot { } }   // -> AddTeaParty
namespace TeaParty.Garden.Ports { [Port] public interface IParasol { } }  // -> AddGarden

// host
builder.Services
    .AddTeaParty(party => party.AddTeapot<CopperTeapot>())
    .AddGarden(garden => garden.AddParasol<StripedParasol>());

Each port or service belongs to the connection whose Scope is the longest prefix of its namespace. So TeaParty.Garden.Ports.IParasol goes to Garden, because TeaParty.Garden is longer than TeaParty, while TeaParty.Ports.ITeapot goes to TeaParty.

Situation Result
Only one connection It owns every port and service; Scope is ignored.
Namespace inside no scope IMP008: the type isn't registered anywhere. Move it, or widen a Scope.
Two connections with the same longest scope IMP009: the type isn't registered anywhere. Give the connections distinct scopes.
Two connections with the same name IMP012: only the first is generated.

A port's generic forms belong to the port's own connection, wherever they're used. For example, if a garden class takes an ITeacup<Assam>, AddTeaParty must assign an Assam teacup.

When Scope is left out it defaults to Namespace, which is usually an .IoC-style namespace that contains no ports. With several connections, set Scope explicitly.

Declaring ports — [Port]

[Port]                                                  // Singleton, Single
[Port(Lifetime.Scoped)]                                 // Scoped, Single
[Port(Lifetime.Singleton, Multiplicity.Many)]           // positional form
[Port(Lifetime = Lifetime.Transient, Optional = true)]  // named form
Property Default Meaning
Lifetime Singleton Singleton, Scoped or Transient.
Multiplicity Single Single, Many or Keyed; see below.
Group none Ports with the same group share one adapter and one Add{Group} method.
MethodName derived Overrides the generated method name.
Optional false No warning or exception when no adapter is assigned.

Each port gets up to three overloads of its registration method:

Overload Example When
Type .AddTeapot<CopperTeapot>() Always. The container constructs the adapter.
Factory .AddTeapot(sp => new CopperTeapot(sp.GetRequiredService<IKettle>())) Always. You construct it.
Instance .AddTeapot(grandmasTeapot) Singleton ports only, because an instance is a singleton by definition.

Every overload constrains TAdapter : class, IPort, so passing a class that doesn't implement the port is a compile error.

Lifetime

Maps one-to-one onto AddSingleton / AddScoped / AddTransient, or their AddKeyed* equivalents. The instance overload always registers as a singleton.

Multiplicity: Single, Many, Keyed

Single (default): exactly one adapter. Calling the method again replaces the earlier choice (last call wins), which makes environment overrides easy:

party.AddTeapot<CopperTeapot>();
if (env.IsDevelopment()) party.AddTeapot<ToyTeapot>(); // replaces the copper one

Many: every call adds another registration. Use it for plug-in style ports, or for generic ports with one adapter per type argument:

[Port(Lifetime.Singleton, Multiplicity.Many)]
public interface IEntertainment
{
    Task Perform();
}

party.AddEntertainment<Harpist>()
     .AddEntertainment<StringQuartet>();

// resolve all of them
var acts = sp.GetServices<IEntertainment>();

Keyed: every call adds a keyed registration, and the method gains a leading object key parameter:

[Port(Lifetime.Singleton, Multiplicity.Keyed)]
public interface ISeatingPlan
{
    Seat SeatFor(Patron patron);
}

party.AddSeatingPlan<HeadTableSeating>("headTable")
     .AddSeatingPlan("garden", _ => new GardenSeating(chairs: 24));

// resolve
var plan = sp.GetRequiredKeyedService<ISeatingPlan>("headTable");
// or inject: public Usher([FromKeyedServices("garden")] ISeatingPlan plan)

Grouping several ports behind one adapter

When one adapter implements several ports and must be the same instance for all of them, give the ports the same Group. A pantry cupboard holds the tea, the sugar and the milk, and every guest should take from the same cupboard:

[Port(Lifetime.Singleton, Group = "Pantry")] public interface ITeaCaddy  { Blend Scoop(string name); }
[Port(Lifetime.Singleton, Group = "Pantry")] public interface ISugarBowl { int Lumps(int wanted); }
[Port(Lifetime.Singleton, Group = "Pantry")] public interface IMilkJug   { Milk Pour(int millilitres); }

public sealed class PantryCupboard : ITeaCaddy, ISugarBowl, IMilkJug { /* ... */ }

This generates a single method:

party.AddPantry<PantryCupboard>();
// where TAdapter : class, IMilkJug, ISugarBowl, ITeaCaddy

The adapter is registered once as itself, and each port forwards to it, so ReferenceEquals(sp.GetService<ITeaCaddy>(), sp.GetService<ISugarBowl>()) is true.

Rules: every port in a group must use the same Lifetime and Multiplicity (IMP001), and generic ports can't be grouped with other ports (IMP002).

Generic ports

The port's type parameters become the leading type parameters of the method, with their constraints copied. Each brew gets a cup suited to it:

[Port(Lifetime.Singleton, Multiplicity.Many)]
public interface ITeacup<TBrew> where TBrew : notnull
{
    void Fill(TBrew brew);
}

party.AddTeacup<EarlGrey, ChinaTeacup>()
     .AddTeacup<Darjeeling, PorcelainTeacup>();
// AddTeacup<TBrew, TAdapter>() where TBrew : notnull
//                              where TAdapter : class, ITeacup<TBrew>

You must write the type arguments explicitly, because C# can't infer TBrew from the adapter.

Generic ports are usually Many, with one adapter per brew. The missing-adapter checks know this: every closed form the assembly actually uses must have an adapter. For example, if PourService takes an ITeacup<Assam> in its constructor, an Assam cup is required. See Missing-adapter checks.

Optional ports

Not every tea party has a photographer:

[Port(Optional = true)]
public interface IPhotographer
{
    Task Snap(IEnumerable<Patron> patrons);
}

The method is still generated, but leaving it out raises neither the warning nor the startup exception. Consumers should resolve it with GetService<T>() (which can return null) or accept IPhotographer? photographer = null in constructors.

Method naming

Port Generated method
ITeapot AddTeapot (a leading I is stripped when a capital follows it)
Icing AddIcing
Group = "Pantry" AddPantry
MethodName = "UseCrockery" UseCrockery

If two ports would produce the same name, you get IMP004. Fix it with Group (if they really share an adapter) or with MethodName.

Declaring internal services — [Service]

Services are classes the tea party needs but the host never chooses. They're registered automatically when AddTeaParty runs; no fluent call is needed.

[Service]                                           // Singleton
[Service(Lifetime.Scoped)]                          // positional
[Service(Lifetime = Lifetime.Transient)]            // named
[Service(As = typeof(IKettleTimer))]                // register only as IKettleTimer
[Service(Key = "afternoon")]                        // keyed registration

What the class is registered as:

  1. If As = typeof(X) is given, it's registered as X only. The class must implement or derive from X (IMP005).
  2. Otherwise, it's registered as every interface the class directly declares, except port interfaces and framework interfaces (System.*, Microsoft.*, such as IDisposable).
  3. If that leaves nothing, it's registered as the class itself.

When a service is registered as several interfaces, the class is registered once and each interface forwards to it, so they all share one instance per lifetime.

[Service(Lifetime = Lifetime.Singleton)]
internal sealed class PatronService : IPatronService, IDisposable { }
// => services.AddSingleton<IPatronService, PatronService>();      (IDisposable skipped)

[Service]
internal sealed class KettleTimer : IKettleTimer, IStewAlarm { }
// => services.AddSingleton<KettleTimer>();
//    services.AddSingleton<IKettleTimer>(sp => sp.GetRequiredService<KettleTimer>());
//    services.AddSingleton<IStewAlarm>(sp => sp.GetRequiredService<KettleTimer>());

[Service(Lifetime.Scoped)]
internal sealed class SconeCalculator { }
// => services.AddScoped<SconeCalculator>();

Services can be internal, because they're registered from inside your assembly. They can't be abstract, static, open generic, or private/protected nested types (IMP005). A [Service] that implements a [Port] gets IMP006: ports are the host's choice, so that class should be passed to AddXxx as an adapter instead.

Wiring adapters in the host

using TeaParty.IoC;   // the Namespace you gave [assembly: Connect]

builder.Services.AddTeaParty(party => party
    .AddPantry(sp => new PantryCupboard(sp.GetRequiredService<IShoppingList>()))
    .AddTeapot<CopperTeapot>()
    .AddGuestList<GuestBook>()
    .AddTeacup<EarlGrey, ChinaTeacup>()
    .AddTeacup<Darjeeling, PorcelainTeacup>()
    .AddTeacup<Assam, MugTeacup>()
    .AddSeatingPlan<HeadTableSeating>("headTable")
    .AddSeatingPlan<GardenSeating>("garden")
    .AddEntertainment<Harpist>());

When AddTeaParty runs, it does the following, in order:

  1. Runs your configure lambda and records each adapter choice.
  2. Checks completeness, and throws InvalidOperationException listing every required port with no adapter.
  3. Registers all [Service] classes.
  4. Calls your OnAddingTeaParty hook, if you wrote one.
  5. Registers the adapters.

Keep the configuration inline as a lambda if you want the compile-time check. See Missing-adapter checks.

Adding your own registrations

The generated class and the configuration class are both partial, so you can extend them. Leave out accessibility modifiers on your half, or match the generated ones exactly.

Registrations that are neither ports nor services (options, HTTP clients, third-party libraries) go in the OnAddingTeaParty hook:

namespace TeaParty.IoC;

public static partial class TeaPartyService
{
    static partial void OnAddingTeaParty(IServiceCollection services, TeaPartyConfiguration configuration)
    {
        services.AddOptions<PartyOptions>().BindConfiguration("TeaParty");
        services.AddHttpClient<ICateringClient, CateringClient>();
    }
}

Extra fluent options go on the configuration class as instance methods:

public static partial class TeaPartyService
{
    public partial class TeaPartyConfiguration
    {
        internal List<Action<Menu>> MenuSteps { get; } = new();

        public TeaPartyConfiguration WithMenu(Action<Menu> menu)
        {
            this.MenuSteps.Add(menu);
            return this;
        }
    }
}

// host
.AddTeaParty(party => party
    /* ...adapters... */
    .WithMenu(menu => menu.Add("Cucumber sandwiches").Add("Victoria sponge")))

Read configuration.MenuSteps in OnAddingTeaParty to act on them.

Add them as instance methods on the partial class, not as extension methods. The analyzer treats an extension method that receives the configuration as code it can't see into, and stops checking that call site (IMP011).

Missing-adapter checks

Two independent layers catch a port with no adapter.

1. Compile time: IMP010 (warning, at the call site)

The analyzer inspects the lambda passed to AddTeaParty and reports each required port that no call in it assigns:

Program.cs(8,6): warning IMP010: Port 'TeaParty.Ports.ITeacup<TeaParty.Brews.Assam>' has no adapter assigned;
                 call 'AddTeacup' in the AddTeaParty configuration

It works across projects: ports declared in TeaParty.Core, AddTeaParty called in TeaParty.Host.

To make it a build-breaking error, add this to .editorconfig:

[*.cs]
dotnet_diagnostic.IMP010.severity = error

2. Runtime: inside AddTeaParty

Even if the warning was ignored or couldn't be computed, startup fails fast and lists every missing port:

System.InvalidOperationException: No adapter registered for port(s):
  TeaParty.Ports.IGuestList (AddGuestList).
  Assign them in TeaPartyConfiguration.

What counts as "required"

Port Requirement
Non-generic, Optional = false One call to its method (any overload).
Group One call to the group method.
Generic, where the declaring assembly uses closed forms (for example ITeacup<Assam> in a constructor, field, property or GetRequiredService<...> call) One adapter per closed type used.
Generic, where no closed form is used anywhere At least one adapter, for any type argument.
Optional = true Nothing.

Implementing a port (class MugTeacup : ITeacup<Assam>) or naming it in typeof(...) or in an attribute does not count as using it.

When the analyzer can't check: IMP011 (info)

If you pass a method group or a variable, or hand the configuration object to another method, the analyzer can't see which adapters are assigned. It reports IMP011 instead and leaves it to the runtime check:

services.AddTeaParty(SetTheTable);                  // IMP011: method group
services.AddTeaParty(party => SetTheTable(party));  // IMP011: configuration passed to SetTheTable

Diagnostics reference

Id Severity Reported on Meaning / fix
IMP001 Error Port Ports in the same Group disagree on Lifetime or Multiplicity. Make them match.
IMP002 Error Port A generic port is grouped with other ports. Give it its own group, or none.
IMP003 Warning First port or service [Port] or [Service] is used but there's no [assembly: Connect("Name")]. Nothing is generated.
IMP004 Error Port Two ports would produce the same method name. Use Group or MethodName.
IMP005 Error Port or service Invalid target: inaccessible type; abstract, static or open-generic service; As type not implemented; or invalid Group/MethodName identifier.
IMP006 Warning Service A [Service] implements a [Port]. Port interfaces aren't registered for services, so assign the class as an adapter.
IMP007 Error Assembly attribute The Connect name isn't a valid identifier.
IMP008 Error Port or service Several connections exist and the type's namespace is inside none of their scopes. Move it or widen a Scope.
IMP009 Error Port or service Several connections share the longest scope that matches the type. Give them distinct Scope values.
IMP010 Warning Add{Name} call A required port, or a closed generic port that is used, has no adapter in the lambda.
IMP011 Info Configuration argument The configuration can't be analysed at compile time; the runtime check still applies.
IMP012 Error Assembly attribute Two connections have the same name. Only the first is generated.

You can change any severity in .editorconfig with dotnet_diagnostic.IMPxxx.severity = none|suggestion|warning|error.

What gets generated

For each project with [assembly: Connect("TeaParty")]:

  • Importable.Attributes.g.cs contains the attributes and enums (Port, Service, Connect, Lifetime, Multiplicity), plus internal metadata attributes the analyzer uses. They're internal and marked [Microsoft.CodeAnalysis.Embedded], so copies in different projects never conflict, even with InternalsVisibleTo.
  • TeaPartyService.g.cs has this shape:
public static partial class TeaPartyService
{
    public partial class TeaPartyConfiguration
    {
        public TeaPartyConfiguration AddTeapot<TAdapter>() where TAdapter : class, ITeapot { ... }
        public TeaPartyConfiguration AddTeapot<TAdapter>(Func<IServiceProvider, TAdapter> factory) ... { ... }
        // ...one set of overloads per port or group...

        internal void EnsureComplete() { ... }   // the runtime check
        internal void Apply(IServiceCollection services) { ... }
    }

    public static IServiceCollection AddTeaParty(this IServiceCollection services, Action<TeaPartyConfiguration> configure) { ... }

    static partial void OnAddingTeaParty(IServiceCollection services, TeaPartyConfiguration configuration);

    private static void RegisterServices(IServiceCollection services) { ... }   // all [Service] classes
}

To read the actual output, use Go To Definition on AddTeaParty in Visual Studio or Rider, or turn on EmitCompilerGeneratedFiles and look under obj/.

Migrating a hand-written registration class

Suppose TeaParty.Core already has a hand-written configuration class:

public static class TeaPartyService
{
    public class TeaPartyConfiguration
    {
        internal Action<IServiceCollection>? Pantry { get; set; }
        internal event Action<IServiceCollection>? Teacups;

        public TeaPartyConfiguration AddPantry<T>(Func<IServiceProvider, T> factory)
            where T : class, ITeaCaddy, ISugarBowl, IMilkJug { /* ... */ }

        public TeaPartyConfiguration AddTeacup<TBrew, TCup>()
            where TCup : class, ITeacup<TBrew> where TBrew : notnull { /* ... */ }
    }

    public static IServiceCollection AddTeaParty(this IServiceCollection services, Action<TeaPartyConfiguration> config) { /* ... */ }
}

To replace it:

  1. Add the analyzer reference to TeaParty.Core and TeaParty.Host.

  2. In TeaParty.Core, add [assembly: Importable.Connect("TeaParty", Namespace = "TeaParty.IoC")], using the namespace the hand-written class lived in.

  3. Annotate the ports:

    [Port(Lifetime.Singleton, Group = "Pantry")]        public interface ITeaCaddy { ... }
    [Port(Lifetime.Singleton, Group = "Pantry")]        public interface ISugarBowl { ... }
    [Port(Lifetime.Singleton, Group = "Pantry")]        public interface IMilkJug { ... }
    [Port(Lifetime.Singleton, Multiplicity.Many)]       public interface ITeacup<TBrew> where TBrew : notnull { ... }
    [Port(Lifetime.Singleton, Multiplicity.Keyed)]      public interface ISeatingPlan { ... }
    
  4. Mark the helpers it registered by hand with [Service].

  5. Delete the hand-written class. Move any other registrations into OnAddingTeaParty, and any extra fluent options into the partial TeaPartyConfiguration (see Adding your own registrations).

  6. Build. The host code compiles unchanged as long as the method names match. Use Group or MethodName where they don't.

Behaviour changes to expect:

  • A grouped adapter is registered once as its concrete type, and every port forwards to it.
  • Keyed methods take object key, which accepts strings unchanged.
  • A port with no adapter now fails at startup instead of on first resolve.

Troubleshooting

The generated method or the warnings don't appear in the IDE. Visual Studio and Rider cache analyzer assemblies that come from project references. After pulling a new generator version, restart the IDE or reload the solution. dotnet build on the command line always uses the latest build.

AddTeaParty doesn't exist. Check that [assembly: Importable.Connect("TeaParty")] is present (look for IMP003), that the analyzer reference has OutputItemType="Analyzer", and that you have a using for the namespace you passed as Namespace.

AddKeyedSingleton / GetRequiredKeyedService not found. The project references a DI abstractions package older than 8.0. Upgrade Microsoft.Extensions.DependencyInjection.Abstractions.

Inconsistent accessibility on the generated class. One of the ports is less accessible than the others. The generated class drops to internal automatically, so if you declared your own half of the partial class as public, remove that modifier.

IMP010 for a generic port I did configure. You configured a different type argument. The message names the exact closed type that's missing, for example ITeacup<Assam>.

IMP011 on my configuration. Make the lambda inline, or rely on the runtime check. The analyzer can't follow the configuration object into helper methods or extension methods.

No service for type ... even though there were no warnings. Either that closed type is used only outside the assembly that declares the port (for example only in the host project), or the port is Optional. See Limitations.

Limitations

  • Closed generic forms are detected only in the assembly that declares [assembly: Connect]. If only the host project uses an ITeacup<Oolong>, it isn't required automatically. Register it anyway; otherwise the DI container fails on first resolve.
  • The compile-time check needs the configuration as an inline lambda (see IMP011).
  • With several connections in one assembly, which connection owns a type is decided by namespace only. A type can't belong to two connections, and can't be assigned to one explicitly.
  • Generic ports can't be grouped, and open-generic services aren't supported.
  • Adapters must be classes (TAdapter : class).

Repository layout

Path Contents
Importable.Generator/ Source generator (PortsAndAdaptersGenerator) and call-site analyzer (MissingAdapterAnalyzer)
Importable.ConsoleApp/ Runnable sample covering every port shape: dotnet run --project Importable.ConsoleApp
Importable.Generator.Tests/ Generator, analyzer, runtime and incremental-caching tests: dotnet test
There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.13 44 10/14/2026
1.0.12 93 10/2/2026
1.0.8 82 10/2/2026
1.0.7 85 10/2/2026