IoCTools.Abstractions
1.3.0
See the version list below for details.
dotnet add package IoCTools.Abstractions --version 1.3.0
NuGet\Install-Package IoCTools.Abstractions -Version 1.3.0
<PackageReference Include="IoCTools.Abstractions" Version="1.3.0" />
<PackageVersion Include="IoCTools.Abstractions" Version="1.3.0" />
<PackageReference Include="IoCTools.Abstractions" />
paket add IoCTools.Abstractions --version 1.3.0
#r "nuget: IoCTools.Abstractions, 1.3.0"
#:package IoCTools.Abstractions@1.3.0
#addin nuget:?package=IoCTools.Abstractions&version=1.3.0
#tool nuget:?package=IoCTools.Abstractions&version=1.3.0
IoCTools
A Roslyn source generator that lets each service declare its own lifetime, dependencies, and registration intent with small attributes. IoCTools emits constructors, service registrations, and analyzers at build time—no reflection, no runtime scanning.
Highlights
- Self-describing services –
[Scoped],[DependsOn<T>],[DependsOnConfiguration<…>],[RegisterAs<…>], and[ConditionalService]live on the class, so intent never leaves the type. Use[DependsOnConfiguration<…>]for configuration whenever possible; fall back to[InjectConfiguration]only when you truly need a hand-authored field. - Dependency sets without drilling – Implement
IDependencySeton an interface/class and list[DependsOn]/[DependsOnConfiguration]there. Anywhere else, a plain[DependsOn<MySet>]flattens every dependency from the set ( and its ancestor sets) directly into the consuming service’s generated constructor/fields—noset.dereferences, no runtime object. Cycles are rejected, lifetimes are validated across the expanded set, and origin info is preserved for diagnostics/CLI output. - Accurate registrations – The generator produces
Add<YourAssembly>RegisteredServices()extensions that register concrete types, interfaces, options bindings, conditional services, and background workers. - Inheritance-aware lifetimes – Derived services inherit the base class lifetime automatically for registrations and
diagnostics. Redundant lifetime warnings respect inheritance (no false “missing” when a base is
[Scoped]), and an explicit[Scoped]is flagged as redundant (IOC033) whenever it matches the implicit default lifetime. conflicting lifetimes across the chain trigger IOC015. - Analyzer coverage – 84 diagnostics (IOC001–IOC086) keep registrations honest: missing lifetimes, redundant
RegisterAs, conflicting[SkipRegistration], invalid config keys, singleton/scoped mismatches, manual-constructor mixing with IoCTools dependencies, options misuse, primitive/collection dependency bans, redundant/unused dependencies (including configuration), overlapping options/config sections, dependency set validation, inheritance-based redundancy detection, hosted service lifetime validation, and framework dependency recognition. - Zero reflection – Everything happens at compile time. Startup cost stays flat, and generated code is plain C# you can inspect.
IoCTools treats each service class as the single source of truth for its registration story. Lifetimes, interface exposure, configuration needs, and conditional flags live beside the implementation, so setup code isn’t forced to guess or duplicate those concerns. This separation keeps startup/bootstrap files lean while ensuring services are designed with the lifetime/registration model they actually require.
Installation
dotnet add package IoCTools.Abstractions
dotnet add package IoCTools.Generator --prerelease
Or directly in your project file:
<ItemGroup>
<PackageReference Include="IoCTools.Abstractions" Version="*" />
<PackageReference Include="IoCTools.Generator" Version="*" PrivateAssets="all" />
</ItemGroup>
Getting Started in Three Steps
Annotate a partial service
[DependsOn<ILogger<EmailService>>] // Scoped implied when DependsOn/Inject/etc. are present (IOC033 otherwise) public partial class EmailService : IEmailService { public Task SendAsync(string to, string subject, string body) => Task.CompletedTask; }Tip: Any partial class that implements at least one interface is treated as a scoped service even without
[Scoped]. Add[Singleton]/[Transient](or[Scoped]when you truly need to override diagnostics) only when you want to change that default. You can change the implicit lifetime globally viabuild_property.IoCToolsDefaultServiceLifetime, and both the generated registrations and IOC012/IOC013 diagnostics honor whatever value you pick.Build – IoCTools emits
Add<YourAssembly>RegisteredServices()into<AssemblyName>.Extensions.Generated.Call the extension during startup
using YourAssembly.Extensions.Generated; var builder = WebApplication.CreateBuilder(args); builder.Services.AddYourAssemblyRegisteredServices(builder.Configuration);
IoCTools CLI
IoCTools.Tools.Cli ships as a dotnet global/local tool (dotnet ioc-tools …). It interrogates your project with the
real IoCTools generator, so you can see exactly what the build produced without spelunking through obj/.
Installation
# From the repo root
dotnet pack IoCTools.Tools.Cli/IoCTools.Tools.Cli.csproj -c Release -o ./artifacts
# Install globally or add to a local manifest
dotnet tool install --global --add-source ./artifacts IoCTools.Tools.Cli
# or
dotnet new tool-manifest
dotnet tool install --add-source ./artifacts IoCTools.Tools.Cli
Commands
| Command | What it surfaces |
|---|---|
fields --project <csproj> --file <class.cs> [--type Namespace.Service] [--source] |
Lists IoCTools-aware services in a file (or filtered types), showing generated [DependsOn] and [DependsOnConfiguration] fields, inferred names, and external flags. With --source, outputs the generated constructor source code. |
fields-path --project … --file … --type … [--output <dir>] |
Emits the absolute path to the generated constructor .g.cs (defaults to %TEMP%/IoCTools.Tools.Cli/<project>/<timestamp> unless --output overrides). |
services --project <csproj> [--output <dir>] [--source] [--type …] |
Summaries of generated registration extension: lifetimes, interface/implementation pairings, factories, conditionals, and configuration bindings. With --source, outputs the raw generated source code. --type filters to specific service registrations. |
services-path --project <csproj> [--output <dir>] |
Prints the path to the generated registration extension so you can open or diff the raw file. |
explain --project <csproj> --type Namespace.Service |
Explains a single service: generated dependency fields, config bindings (keys/required/reload), and external flags. |
graph --project <csproj> [--type …] [--format json | puml |mermaid] [--output <dir>] |
Emits a lightweight graph of service registrations (service → implementation edges) in JSON/PlantUML/Mermaid. Optional --type filters the graph. |
why --project <csproj> --type … --dependency Fully.Qualified.Type |
Shows which generated field/config binding matches the requested dependency for a service. |
doctor --project <csproj> [--fixable-only] |
Runs the generator and prints diagnostics with locations; --fixable-only filters to warnings/infos (no auto-fix scripts yet). |
compare --project <csproj> --output <dir> [--baseline <dir>] |
Captures current generated artifacts into --output; if --baseline is provided, lists changed .g.cs files relative to that snapshot. |
profile --project <csproj> [--type …] |
Prints generator warm/analysis timing for the project (type filter currently informational only). |
config-audit --project <csproj> [--settings appsettings.json] |
Lists required config bindings from IoCTools services and reports which keys are missing from an optional appsettings.json. |
By default the CLI copies generator artifacts into your system temp directory under
IoCTools.Tools.Cli/<project>/<timestamp>, so running it against other repositories will never dirty their working
trees. Specify --output when you need the artifacts copied into a deterministic location.
Key switches: --configuration (default Debug), --framework (for multi-targeting), --type (can repeat),
--output (deterministic artifact directory), and --source (output raw generated code instead of summaries). Because
the CLI drives Roslyn directly, it works immediately even when IoCTools is referenced as a project dependency and
EmitCompilerGeneratedFiles is disabled.
Before & After: Replacing DI Smells
Legacy BillingService (manual, brittle)
public class LegacyBillingService : IBillingService, ILegacyDiagnostics
{
private readonly ILogger<LegacyBillingService> _logger;
private readonly IHttpClientFactory _httpClients;
private readonly BillingOptions _options;
private readonly IConfiguration _config;
public LegacyBillingService(
ILogger<LegacyBillingService> logger,
IHttpClientFactory httpClients,
IOptionsMonitor<BillingOptions> options,
IConfiguration config)
{
_logger = logger;
_httpClients = httpClients;
_options = options.CurrentValue;
_config = config;
}
public async Task ChargeAsync(BillingRequest request)
{
var baseUrl = _config["Billing:BaseUrl"] ?? throw new InvalidOperationException("Billing options missing");
// ... manual retry logic based on _options.RetryCount ...
}
}
services.AddHttpClient();
services.AddSingleton<IOptionsMonitor<BillingOptions>, BillingOptionsMonitor>();
services.Configure<BillingOptions>(configuration.GetSection("Billing"));
services.AddScoped<IBillingService, LegacyBillingService>();
services.AddScoped<ILegacyDiagnostics, LegacyBillingService>();
Problems: duplicated registrations, runtime config lookups every call, manual interface wiring, no analyzer guardrails.
IoCTools BillingService (attributes, analyzers, generated DI)
using IoCTools.Abstractions.Annotations;
using IoCTools.Abstractions.Enumerations;
[Scoped]
[RegisterAs<IBillingService, IBillingDiagnostics>(InstanceSharing.Shared)]
[ConditionalService(Environment = "Production,Staging")]
[DependsOn<ILogger<BillingService>, IHttpClientFactory, IClock>]
[SkipRegistration<ILegacyDiagnostics>] // Keep internal interface private to DI
public partial class BillingService : IBillingService, IBillingDiagnostics, ILegacyDiagnostics
{
[InjectConfiguration("Billing:BaseUrl", Required = true)] private readonly string _baseUrl;
[InjectConfiguration("Billing:RetryCount", DefaultValue = "3")] private readonly int _retryCount;
[Inject] private readonly IMeter<BillingService> _meter; // field genuinely reused across methods
public async Task ChargeAsync(BillingRequest request)
{
using var client = _httpClientFactory.CreateClient("billing");
// Generated constructor supplies _logger, _httpClientFactory, _clock, and configuration fields.
}
}
Generated code now:
- Creates a constructor that accepts
ILogger<BillingService> logger,IHttpClientFactory httpClientFactory, andIClock clock(from[DependsOn<…>]). - Injects
_meter,_baseUrl, and_retryCountper attribute metadata. - Registers the service once via
builder.Services.AddYourAssemblyRegisteredServices(configuration);– IoCTools emitsservices.AddScoped<IBillingService, BillingService>()plus shared-instance factory wiring forIBillingDiagnostics. - Configuration-backed types are auto-bound: any
[InjectConfiguration]/[DependsOnConfiguration](orIOptions<T>dependency) getsAddOptions<T>().Bind(configuration.GetSection("…"))and a singletonTviaTryAddSingleton(sp => sp.GetRequiredService<IOptions<T>>().Value), so manual options boilerplate isn’t needed. - Emits diagnostics if
[Scoped]becomes redundant (IOC033), if[SkipRegistration<ILegacyDiagnostics>]can never trigger (IOC009/IOC038), or if configuration keys are invalid (IOC016–IOC017).
Class-level configuration with [DependsOnConfiguration]
using IoCTools.Abstractions.Annotations;
[DependsOnConfiguration<string>("Billing:BaseUrl")]
[DependsOnConfiguration<int>("Billing:RetryCount", SupportsReloading = true)]
public partial class BillingService : IBillingService
{
// IoCTools generates the `_baseUrl` and `_retryCount` fields plus binding logic.
}
Use this attribute when you want the analyzer/generator to manage configuration fields for you. You still get
[InjectConfiguration] semantics—required/default values, reload support, options binding—plus [DependsOn]-style
naming controls and multi-slot declarations.
Best practice: Always reach for
[DependsOnConfiguration<…>]first so the generator owns the field, naming, and binding logic. Resort to[InjectConfiguration]only when you need a mutable/manual field (for example, lazy caching with??=or instrumentation).
Reusable dependency sets (flattened, natural syntax)
// Define a set – metadata only, never registered
[DependsOn<ILogger<PaymentInfra>>]
[DependsOn<IHttpClientFactory>]
[DependsOnConfiguration<string>("Billing:BaseUrl")]
public interface PaymentInfra : IDependencySet {}
// Consume it – dependencies are flattened into ctor/fields, no drilling
[Scoped]
[DependsOn<PaymentInfra>] // expands logger, httpClientFactory, baseUrl
public partial class BillingService : IBillingService
{
public Task ChargeAsync() { /* use _logger, _httpClientFactory, _baseUrl directly */ return Task.CompletedTask; }
}
// Inheritance works; ancestors are included and deduped
public interface CoreInfra : IDependencySet
{
[DependsOn<IMeter<CoreInfra>>]
}
public interface ReportingInfra : CoreInfra
{
[DependsOn<IClock>]
}
[Scoped]
[DependsOn<ReportingInfra>] // brings IMeter + IClock + any ReportingInfra deps
public partial class ReportingService {}
Rules and behavior:
- Implement
IDependencySeton any interface/class to mark it as a bundle. Sets can inherit other sets; dependencies ( services + configuration) from the entire chain are flattened and deduped. - Consumers keep the natural form
[DependsOn<MySet>]; when the target implementsIDependencySet, the generator expands the set instead of expecting a DI registration. - Lifetime safety: the set’s effective lifetime is the most restrictive of its members. Singleton consumers with scoped/transient members still trigger IOC012/IOC013.
- Cycles are rejected (e.g.,
SetA -> SetB -> SetA), and name collisions are errors unless type and member name match exactly. - External/config metadata is preserved; CLI
fieldsoutput labels each dependency with its originating set for readability. - Flattening is fully recursive—sets can reference other sets. Member-name overrides flow through recursion, and conflicting names on the same dependency type surface IOC051.
- DRY nudges include configuration slots: IOC053–IOC055 consider both services and config bindings when proposing new sets, near-misses, or shared-base extraction.
- Optional future knobs (carrier injection, naming overrides) can hang off
[DependsOn<Set>(…)]when the target is a set, without changing the default flat experience.
Refactor suggestions (info-level analyzers):
- DRY new set (IOC053): when ≥3 dependencies recur on ≥2 services (unordered match), suggest extracting an
IDependencySet, generating it, and replacing the matching[DependsOn]blocks with[DependsOn<NewSet>]in all those services. - Near-miss reuse (IOC054): if a service already has most of an existing set (e.g., 4/5 deps), suggest adopting the set plus adding the missing dep locally, or splitting a core set for broader reuse. Shown as a quick-fix preview.
- Shared base refactor (IOC055): when services that share a base type also share a dependency cluster, suggest moving the deps into a new set (or the base when appropriate) to reduce duplication while respecting lifetime rules.
Naming & Generated Surface
Dependency name derivation
IoCTools strips a leading I from interface names, applies the configured naming style, and prefixes fields with _ by
default. When SnakeCase is enabled it uses:
Regex.Replace(fieldBaseName, "(?<!^)([A-Z])", "_$1").ToLowerInvariant();
So IEmailService → _emailService, IDetailedInvoiceAuditor → _detailedInvoiceAuditor, and with snake_case
_detailed_invoice_auditor. Diagnostics IOC035 fire when an [Inject] field matches this auto-generated pattern,
nudging you back to [DependsOn<…>] when a field isn’t required.
Need a hand-authored name? Every [DependsOn<…>] overload now offers params-style memberNames, so you can write
[DependsOn<ILogger<BillingService>, IHttpClientFactory>(memberNames: "logger", "client")] instead of setting
MemberNames = new[] { … } on the attribute. Likewise, [DependsOnConfiguration<…>] keeps params-style
configurationKeys on its constructors, now alongside the naming options (namingConvention, stripI, prefix,
stripSettingsSuffix).
If you still use the legacy MemberNames/ConfigurationKeys named arguments, the analyzer now emits an informational
hint (IOC047) pointing to the params-friendly form.
Nullable dependencies are treated as misconfigurations: IOC048 warns when constructor-surface dependencies (including
[DependsOn], [Inject], and [DependsOnConfiguration]) are declared nullable—register a no-op implementation or
refactor the feature instead of using nullable dependency types.
Generated registration extension
For assembly IoCTools.Sample the generator emits:
namespace IoCTools.Sample.Extensions.Generated;
public static class GeneratedServiceCollectionExtensions
{
public static IServiceCollection AddIoCToolsSampleRegisteredServices(
this IServiceCollection services,
IConfiguration configuration)
{
// registrations, configuration bindings, AddHostedService calls, etc.
return services;
}
}
- Namespace:
<AssemblyNameWithoutInvalidChars>.Extensions.Generated. - Method name:
Add<SafeAssemblyName>RegisteredServices(periods removed, hyphens/spaces replaced with_). - Bring the namespace into scope (
using YourAssembly.Extensions.Generated;) and call the method fromProgram.cs. Background services, conditional registrations, and configuration bindings flow through that single call. - The generator adds the
IConfigurationparameter only when a project actually uses[InjectConfiguration]or[ConditionalService]—otherwise the extension isAddYourAssemblyRegisteredServices(this IServiceCollection services).
Attribute Reference
| Attribute | Category | When to use | Notes |
|---|---|---|---|
[Scoped], [Singleton], [Transient] |
Lifetime | Declare how the service is registered. | Services own their lifetimes so startup code doesn’t. Use one per class; IOC036 warns otherwise. Scoped is implicit for partial classes that implement interfaces or when other service indicators exist. |
[DependsOn<T1, T2, …>] |
Dependencies | Request constructor parameters without fields. | Preferred approach—constructor is generated with parameters for each type. Apply the attribute multiple times (e.g., three [DependsOn<…>] blocks of five types each for 15 dependencies) when you need more than the generic arity allows. |
IDependencySet marker + [DependsOn<Set>] |
Dependency sets | Reuse dependency bundles; consumer syntax stays natural. | Any interface/class implementing IDependencySet is a bundle (not registered). [DependsOn<Set>] flattens the set (and inherited sets) into the consumer’s generated ctor/fields, preserving diagnostics and metadata. Cycles and name collisions are errors. |
[DependsOnConfiguration<T1, …>] |
Configuration | Declare configuration dependencies without writing backing fields. | Generator emits the fields, constructor parameters, and binding logic you’d normally get from [InjectConfiguration], plus [DependsOn]-style naming controls and multi-slot support. |
[Inject] |
Dependencies | Last resort when a field must exist (custom naming, mutability). | IOC035 tells you when the default naming regex already covers the dependency. |
[InjectConfiguration("Key", DefaultValue = "…", Required = bool, SupportsReloading = bool)] |
Configuration | Bind simple values or options straight into fields. | Last resort. Use this only when a handwritten field is unavoidable (mutable state, custom lazy assignment). Otherwise prefer [DependsOnConfiguration<…>]. |
[RegisterAs<T1, …>(InstanceSharing.Shared\|Separate)] |
Interface control | Register only selected interfaces, optionally sharing instances. | Shared mode emits factory registrations so all listed interfaces reuse one instance. |
[RegisterAsAll(RegistrationMode.All\|Exclusionary\|DirectOnly, InstanceSharing)] |
Interface control | Register every implemented interface (or concrete only). | Combine with [SkipRegistration<T>] to prune specific interfaces. |
[SkipRegistration] / [SkipRegistration<T1, …>] |
Interface control | Disable registration completely or exclude individual interfaces. | IOC005/IOC037/IOC038 guard invalid combinations. |
[ConditionalService(Environment = "Prod", ConfigValue = "Feature:X", Equals = "true")] |
Conditional | Register only when env/config matches. | Requires a lifetime attribute (IOC021). |
[ManualService] / [ExternalService] |
Advanced | Mark services or dependencies that are registered manually. | Keeps analyzers quiet when DI is handled elsewhere. |
[InjectConfigurationOptions] / [DependsOnOptions] |
Options | Let IoCTools bind strongly typed options classes once and reuse them. | Automatically wires IOptionsMonitor<T> behind the scenes. |
Analyzer (Diagnostic) Reference
84 diagnostics (IOC001–IOC086) keep registrations honest. Each includes a remediation tip in Visual Studio / Rider / CLI build output.
| Rule | Severity | Summary |
|---|---|---|
| IOC001 | Error | Service depends on an interface with no implementation in the project. |
| IOC002 | Error | Implementation exists but is missing a lifetime attribute, so it never registers. |
| IOC003 | Error | Circular dependency detected (message lists the cycle). |
| IOC004 | Error | [RegisterAsAll] requires a lifetime attribute because it defines a service. |
| IOC005 | Warning | [SkipRegistration] without [RegisterAsAll] has no effect. |
| IOC006 | Warning | Duplicate dependency types across multiple [DependsOn] attributes. |
| IOC007 | Warning | Deprecated – replaced by IOC040 redundant dependency warnings. |
| IOC008 | Warning | Duplicate type listed inside a single [DependsOn] attribute. |
| IOC009 | Warning | [SkipRegistration<T>] targets an interface that would never be registered. |
| IOC010 | Warning | Deprecated (background-service lifetime warnings are handled by IOC014). |
| IOC011 | Error | Background services must be declared partial. |
| IOC012 | Error | Singleton service depends on a scoped service. |
| IOC013 | Warning | Singleton service depends on a transient service. |
| IOC014 | Error | Background service uses a non-singleton lifetime. |
| IOC015 | Error | Lifetime mismatch across an inheritance chain. |
| IOC016 | Error | [InjectConfiguration] uses an invalid configuration key. |
| IOC017 | Warning | [InjectConfiguration] targets an unsupported type. |
| IOC018 | Error | [InjectConfiguration] applied to a non-partial class. |
| IOC019 | Warning | [InjectConfiguration] cannot target static fields. |
| IOC020 | Warning | [ConditionalService] contains conflicting conditions. |
| IOC021 | Error | [ConditionalService] requires a lifetime attribute. |
| IOC022 | Warning | [ConditionalService] declared with no conditions. |
| IOC023 | Warning | ConfigValue set without Equals / NotEquals. |
| IOC024 | Warning | Equals / NotEquals provided without a ConfigValue. |
| IOC025 | Warning | ConfigValue is empty or whitespace. |
| IOC026 | Warning | Multiple [ConditionalService] attributes on the same class. |
| IOC027 | Info | Potential duplicate service registrations detected. |
| IOC028 | Error | [RegisterAs] used without any service indicators/lifetime metadata. |
| IOC029 | Error | [RegisterAs] lists an interface the class does not implement. |
| IOC030 | Warning | Duplicate interface listed inside [RegisterAs]. |
| IOC031 | Error | [RegisterAs] references a non-interface type. |
| IOC032 | Warning | [RegisterAs] duplicates what the generator already infers. |
| IOC033 | Warning | [Scoped] attribute is redundant because the service is implicitly scoped. |
| IOC034 | Warning | [RegisterAsAll] combined with [RegisterAs] is redundant. |
| IOC035 | Warning | [Inject] field matches the default [DependsOn] naming pattern. |
| IOC036 | Warning | Multiple lifetime attributes are applied to the same class. |
| IOC037 | Warning | [SkipRegistration] overrides other registration attributes on the same class. |
| IOC038 | Warning | [SkipRegistration<T>] does nothing when [RegisterAsAll(RegistrationMode.DirectOnly)] is used. |
| IOC039 | Warning | Dependency declared via [Inject]/[DependsOn] is never referenced. |
| IOC040 | Warning | A dependency type is declared multiple times via [Inject], [DependsOn], or configuration bindings (including inheritance). |
| IOC041 | Error | A class mixes IoCTools dependency annotations with a manual or primary constructor. |
| IOC042 | Warning | [DependsOn(..., external: true)] used even though an implementation exists. Remove external: true. |
| IOC043 | Warning | IOptions-based dependencies detected. Use [DependsOnConfiguration<…>] instead. |
| IOC044 | Warning | Dependency type is not a service (primitive/struct/string). Prefer [DependsOnConfiguration<…>]. |
| IOC045 | Warning | Collection dependency shape is unsupported. Only IReadOnlyCollection<T> is allowed. |
| IOC046 | Warning | Overlapping configuration bindings (options + per-field bindings for the same section). |
| IOC047 | Info | Use params-style attribute arguments for cleaner syntax. |
| IOC048 | Warning | Dependencies must be non-nullable. Prefer non-nullable types. |
| IOC049 | Error | Dependency set types (IDependencySet) must be metadata-only. |
| IOC050 | Error | Dependency set recursion detected (SetA → SetB → SetA). Remove the cycle. |
| IOC051 | Error | Dependency set expansion collides with an existing dependency of the same type but different member name. |
| IOC052 | Warning | Type implementing IDependencySet is marked for registration. Dependency sets must not be registered. |
| IOC053 | Info | Repeated dependency cluster found across services; extract an IDependencySet. |
| IOC054 | Info | Service is a near-match to an existing dependency set; consider using the set. |
| IOC055 | Info | Shared dependency cluster on services with a common base suggests moving dependencies into a set. |
| IOC056 | Info | Configuration section is bound both as options and primitive values in the same hierarchy. |
| IOC057 | Warning | Configuration binding not found for options type. |
| IOC058 | Info | Apply lifetime attribute to shared base class instead of each derived class. |
| IOC059 | Warning | [Singleton] attribute is redundant because base class already declares it. |
| IOC060 | Warning | [Transient] attribute is redundant because base class already declares it. |
| IOC061 | Warning | Dependency set already applied in base class. Remove redundant [DependsOn<Set>]. |
| IOC062 | Info | Move shared dependency set to base class to reduce duplication. |
| IOC063 | Warning | [RegisterAs] attribute is redundant on derived class (inherited from base). |
| IOC064 | Info | Move shared [RegisterAs] to base class to reduce duplication. |
| IOC065 | Warning | [RegisterAsAll] attribute is redundant on derived class. |
| IOC067 | Warning | [ConditionalService] attribute is redundant on derived class (same condition as base). |
| IOC068 | Info | Class has a manual constructor with injectable parameters but no IoCTools attributes. Consider opting in. |
| IOC069 | Warning | [RegisterAs] requires a lifetime attribute. |
| IOC070 | Warning | [DependsOn]/[Inject] used without lifetime. Add [Scoped], [Singleton], or [Transient]. |
| IOC071 | Warning | [ConditionalService] missing lifetime. Add a lifetime attribute. |
| IOC072 | Warning | Hosted service declares explicit lifetime. Remove it (IHostedService is registered implicitly). |
| IOC074 | Info | Multi-interface class could use [RegisterAsAll] to register all interfaces. |
| IOC075 | Warning | Inconsistent lifetimes across inherited services. Move lifetime to base class. |
| IOC076 | Warning | Property redundantly wraps IoCTools dependency field. Access the field directly. |
| IOC077 | Error | Manual field shadows IoCTools-generated dependency. Remove the manual field. |
| IOC078 | Warning | MemberNames entry is suppressed by existing field. Remove the field or drop MemberNames. |
| IOC079 | Warning | Prefer [DependsOnConfiguration<…>] over raw IConfiguration dependency. |
| IOC080 | Error | Class uses IoCTools code-generating attributes but is not marked as partial. |
| IOC081 | Error | Manual registration duplicates IoCTools registration with same lifetime. |
| IOC082 | Error | Manual registration lifetime differs from IoCTools. Align lifetimes or remove manual registration. |
| IOC083 | Error | Manual options registration duplicates IoCTools binding. Remove manual AddOptions/Configure. |
| IOC084 | Warning | Lifetime attribute duplicates inherited lifetime. Remove redundant attribute. |
| IOC085 | Warning | Member name matches default naming. Remove explicit memberName parameter. |
| IOC086 | Warning | Manual registration could use IoCTools attributes instead. |
Key Workflows
- Dependency hygiene – IOC039 warns when
[Inject]or[DependsOn]declarations never get referenced, and IOC040 catches redundant combinations of[Inject]fields and[DependsOn]attributes before they reach generated constructors. - Configuration injection:
[InjectConfiguration]supports complex objects, primitives, and arrays, and[DependsOnConfiguration<…>]gives you the same binding behavior without writing backing fields—IoCTools generates them from the class-level attribute and still enforces IOC016–IOC019 diagnostics. - Dependency sets: IOC049 forbids non-metadata members on
IDependencySettypes; IOC050 flags cycles (including recursive nesting); IOC051 reports name/type collisions when sets are flattened into consumers; IOC052 warns if a dependency set is ever considered for registration. - DRY set suggestions: info-level analyzers (IOC053–IOC055) spot repeated dependency clusters, near-misses with
existing sets, and base-class sharing opportunities, offering quick-fixes to extract or reuse
IDependencySetbundles. IOC056 surfaces mixed options/primitive bindings even when they arrive through dependency sets. - Conditional services: Use
Environment/NotEnvironmentfor environment-specific registrations andConfigValue+Equals/NotEqualsfor feature toggles. - Background workers: Any partial
BackgroundServiceis registered throughAddHostedService<T>(); analyzers enforce singleton lifetimes. - Lifetime validation: IOC012/IOC013 warn when a singleton captures scoped or transient services; IOC015 watches inheritance chains so longer-lived services never depend on shorter-lived implementations.
- Inheritance chains: Partial base/derived services share the same constructor graph. The generator walks the hierarchy so lifetimes stay consistent (IOC015 protects you) and dependencies from base classes are included automatically.
- Manual/External services: Mark services you register yourself with
[ManualService]or[ExternalService]to satisfy the analyzers without disabling them globally. - Collections: The generator produces
IEnumerable<T>/IReadOnlyList<T>wrappers when multiple implementations exist—no manualservices.AddSingleton<IEnumerable<T>>needed.
Future Ideas
The current roadmap builds on IOC039/IOC040 by surfacing more of the generator’s work directly in source, so developers
rarely need to open .g.cs files:
- IDE quick fixes – ship Roslyn light-bulb actions for IOC039/IOC040 so you can convert
[Inject]→[DependsOn], drop redundant declarations, or remove dead dependencies with a click. - CLI parity for fixes – pair the IDE actions with a
dotnet ioc fix/report/describetool so console and CI workflows can apply the same quick fixes, view redundancy reports, and inspect generated members without opening.g.cs. - Structured warning aggregates – add a low-severity diagnostic summarizing the dependency hygiene status per class (e.g., “2 unused dependencies, 1 redundant”) to make large refactors easier to triage.
- Fine-grained analyzer knobs – expose MSBuild properties (e.g.,
IoCToolsUnusedDependencySeverity,IoCToolsRedundantDependencyScope) so teams can tune these warnings per project or per configuration. - Rich XML documentation & metadata – have generated fields/constructors emit
<summary>/<param>details noting which attribute produced them, and optionally tag them with[GeneratedDependency(Attribute = …, DeclaredAt = …)]so Go-To-Definition jumps back to the originating partial. - Analyzer-assisted navigation – provide info diagnostics/code actions that list generated constructor signatures inline or open a preview window, eliminating the need to browse generated files.
- Partial-class alignment hints – warn (info) when another partial already defines a conflicting field or when
[DependsOn]output is unused, including the generated signature in the message for quick fixes. - Debugger-friendly instrumentation – optionally register a lightweight inspector in DEBUG builds so you can inspect
the generated dependency graph at runtime without touching the
.g.csoutput. - Service graph dumps – behind an MSBuild flag (e.g.,
IoCToolsDumpServiceGraph=true), emit a compact JSON summary of each service, its generated fields, and registrations to simplify reviews and CI audits. - Partial-type mapping guidance – extend analyzers to suggest relocating
[DependsOn]declarations to the partial that actually consumes the dependency, preventing future IOC039 hits. - DependsOnConfiguration diagnostics – add IOC04x warnings that: (1) surface duplicate slots across
[DependsOnConfiguration]+[InjectConfiguration], (2) highlight unused configuration slots, (3) flag redundant attributes when an identical key/type combo is declared twice, (4) detect conflictingMemberNames/ConfigurationKeyslengths, and (5) offer a fixer to convert eligible[InjectConfiguration]fields into class-level attributes.
Configuration
IoCTools reads configuration from MSBuild properties/.editorconfig and from an optional
IoCTools.Generator.Configuration.GeneratorOptions class. Common knobs:
| Property / API | Purpose | Example |
|---|---|---|
build_property.IoCToolsNoImplementationSeverity, IoCToolsManualSeverity, IoCToolsLifetimeValidationSeverity |
Override analyzer severity per category. | .editorconfig: build_property.IoCToolsNoImplementationSeverity = error |
build_property.IoCToolsDisableDiagnostics |
Disable all IoCTools diagnostics (not recommended except in migration). | true |
build_property.IoCToolsDisableLifetimeValidation |
Turn off lifetime-specific analyzers (IOC012–IOC015). | true |
build_property.IoCToolsSkipAssignableTypesUseDefaults / IoCToolsSkipAssignableTypes / …Add / …Remove |
Control “skip-by-assignable” generator style filters (exclude categories of services from registration). Default skips ASP.NET ControllerBase only; add more (e.g., Mediator/MediatR handlers) via …Add. |
IoCToolsSkipAssignableTypesAdd = Mediator.*;MediatR.* |
build_property.IoCToolsSkipAssignableExceptions |
Carve exceptions back in when using skip lists. | IoCToolsSkipAssignableExceptions = Namespace.ImportantService |
build_property.IoCToolsDefaultServiceLifetime |
Sets the implicit lifetime applied when a service has intent but no explicit [Scoped]/[Singleton]/[Transient]; generator output and IOC012/IOC013 both use the configured value. |
IoCToolsDefaultServiceLifetime = Singleton (values: Scoped, Singleton, Transient). |
IoCTools.Generator.Configuration.GeneratorOptions class |
Configure the same skip/exceptions options via code when MSBuild isn’t convenient. | Define a static class with public static GeneratorStyleOptions Current => new(...); |
All properties can live in Directory.Build.props, .editorconfig, or project files. The generator merges “base list +
add/remove + exceptions” so you can set organization-wide defaults then fine-tune per project.
Samples & License
IoCTools.Sampledemonstrates every attribute, diagnostic, and configuration scenario (background services, RegisterAs vs RegisterAsAll, shared instances, options binding, etc.).- Licensed under MIT. See
LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
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.9.1 | 90 | 5/23/2026 |
| 1.8.0 | 1,297 | 5/12/2026 |
| 1.7.3 | 1,569 | 5/6/2026 |
| 1.7.2 | 88 | 5/6/2026 |
| 1.7.1 | 94 | 5/6/2026 |
| 1.6.1 | 98 | 4/29/2026 |
| 1.6.0 | 91 | 4/29/2026 |
| 1.5.1 | 143 | 4/12/2026 |
| 1.4.0 | 211 | 3/21/2026 |
| 1.3.0 | 118 | 1/24/2026 |
| 1.2.0 | 403 | 11/18/2025 |
| 1.1.0 | 294 | 11/12/2025 |
| 1.0.0 | 198 | 9/10/2025 |
| 1.0.0-alpha | 243 | 8/28/2025 |
| 0.4.1 | 177 | 11/30/2024 |
| 0.4.0 | 150 | 11/30/2024 |
| 0.3.0 | 214 | 3/18/2024 |
| 0.2.6 | 187 | 2/6/2024 |
| 0.2.5 | 309 | 2/6/2024 |
| 0.2.4 | 184 | 2/6/2024 |
v1.3.0: Dependency set support, expanded configuration diagnostics (IOC057), richer CLI tooling (explain/graph/why/doctor/compare/profile/config-audit), and broader test coverage.