Importable 1.0.13
dotnet add package Importable --version 1.0.13
NuGet\Install-Package Importable -Version 1.0.13
<PackageReference Include="Importable" Version="1.0.13" />
<PackageVersion Include="Importable" Version="1.0.13" />
<PackageReference Include="Importable" />
paket add Importable --version 1.0.13
#r "nuget: Importable, 1.0.13"
#:package Importable@1.0.13
#addin nuget:?package=Importable&version=1.0.13
#tool nuget:?package=Importable&version=1.0.13
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
- Requirements
- Installation
- Quick start
- Concepts
- Declaring the entry point —
[assembly: Connect] - Declaring ports —
[Port] - Declaring internal services —
[Service] - Wiring adapters in the host
- Adding your own registrations
- Missing-adapter checks
- Diagnostics reference
- What gets generated
- Migrating a hand-written registration class
- Troubleshooting
- 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
Scopeis left out it defaults toNamespace, which is usually an.IoC-style namespace that contains no ports. With several connections, setScopeexplicitly.
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:
- If
As = typeof(X)is given, it's registered asXonly. The class must implement or derive fromX(IMP005). - Otherwise, it's registered as every interface the class directly declares, except port interfaces and framework
interfaces (
System.*,Microsoft.*, such asIDisposable). - 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:
- Runs your
configurelambda and records each adapter choice. - Checks completeness, and throws
InvalidOperationExceptionlisting every required port with no adapter. - Registers all
[Service]classes. - Calls your
OnAddingTeaPartyhook, if you wrote one. - 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.cscontains the attributes and enums (Port,Service,Connect,Lifetime,Multiplicity), plus internal metadata attributes the analyzer uses. They'reinternaland marked[Microsoft.CodeAnalysis.Embedded], so copies in different projects never conflict, even withInternalsVisibleTo.TeaPartyService.g.cshas 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:
Add the analyzer reference to
TeaParty.CoreandTeaParty.Host.In
TeaParty.Core, add[assembly: Importable.Connect("TeaParty", Namespace = "TeaParty.IoC")], using the namespace the hand-written class lived in.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 { ... }Mark the helpers it registered by hand with
[Service].Delete the hand-written class. Move any other registrations into
OnAddingTeaParty, and any extra fluent options into the partialTeaPartyConfiguration(see Adding your own registrations).Build. The host code compiles unchanged as long as the method names match. Use
GrouporMethodNamewhere 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 anITeacup<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 |
Learn more about Target Frameworks and .NET Standard.
-
.NETStandard 2.0
- Microsoft.CodeAnalysis.CSharp (>= 4.12.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.