An open API service indexing awesome lists of open source software.

https://github.com/x39/x39.util.dependencyinjection


https://github.com/x39/x39.util.dependencyinjection

attributes csharp dependency-injection dotnet foss library nuget reflection

Last synced: 28 days ago
JSON representation

Awesome Lists containing this project

README

          

# X39.Util.DependencyInjection

Attribute-based service registration for `Microsoft.Extensions.DependencyInjection` with
compile-time source generation and NativeAOT support.

[![NuGet](https://img.shields.io/nuget/v/X39.Util.DependencyInjection)](https://www.nuget.org/packages/X39.Util.DependencyInjection)

* [X39.Util.DependencyInjection](#x39utildependencyinjection)
* [Installation](#installation)
* [Quick Start](#quick-start)
* [Source Generator](#source-generator)
* [How It Works](#how-it-works)
* [Generated Code Example](#generated-code-example)
* [NativeAOT Support](#nativeaot-support)
* [Customizing the Generated Class](#customizing-the-generated-class)
* [Compile-Time Diagnostics](#compile-time-diagnostics)
* [Lifetime Attributes](#lifetime-attributes)
* [Conditional Registration](#conditional-registration)
* [Reflection-Based Registration (Legacy)](#reflection-based-registration-legacy)
* [`AddAttributedServicesOf(IConfiguration, Assembly)`](#addattributedservicesoficonfiguration-assembly)
* [`AddAttributedServicesFromAssemblyOf(IConfiguration)`](#addattributedservicesfromassemblyofticonfiguration)
* [`AddAttributedServicesOf(IConfiguration, AppDomain)`](#addattributedservicesoficonfiguration-appdomain)
* [Behavior Notes](#behavior-notes)
* [Exceptions](#exceptions)
* [Semantic Versioning](#semantic-versioning)
* [Contributing](#contributing)
* [Code of Conduct](#code-of-conduct)
* [Contributor License Agreement](#contributor-license-agreement)
* [License](#license)

## Installation

```shell
dotnet add package X39.Util.DependencyInjection
```

Or add directly to your `.csproj`:

```xml

```

The package includes a Roslyn source generator that runs automatically at compile time.
No additional packages are required.

## Quick Start

Decorate your service class with a lifetime attribute and call the generated `AddDependencies`
method during startup:

```csharp
using X39.Util.DependencyInjection;

IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
services.AddDependencies(context.Configuration);
})
.Build();

host.Run();
```

```csharp
using X39.Util.DependencyInjection.Attributes;

public interface IMyService
{
void DoWork();
}

[Singleton]
public class MyService : IMyService
{
public void DoWork() { /* ... */ }
}
```

The source generator discovers all attributed classes at compile time and generates the
`AddDependencies` extension method with explicit registration calls — no runtime reflection needed.

## Source Generator

### How It Works

At compile time, the included Roslyn incremental source generator:

1. Scans your project for classes decorated with `[Singleton]`, `[Transient]`, or `[Scoped]` attributes.
2. Validates attribute usage and condition methods, reporting errors as compiler diagnostics.
3. Emits a static extension method (default: `Dependencies.AddDependencies()`) containing all
service registrations as direct `IServiceCollection` calls.

The generated code is fully AOT-safe — all types are statically known and no reflection is used at
runtime.

### Generated Code Example

For a class like:

```csharp
[Singleton]
public class MyService : IMyService
{
[DependencyInjectionCondition]
internal static bool IsEnabled(IConfiguration configuration)
=> configuration.GetValue("Features:MyService");
}
```

The generator produces:

```csharp
//
public static class Dependencies
{
public static IServiceCollection AddDependencies(
this IServiceCollection services,
IConfiguration configuration)
{
RuntimeHelpers.RunClassConstructor(typeof(MyApp.MyService).TypeHandle);
if (MyApp.MyService.IsEnabled(configuration))
{
services.AddSingleton();
}

return services;
}
}
```

### NativeAOT Support

Because the source generator resolves all registrations at compile time, the generated code is
fully compatible with NativeAOT publishing. There is no runtime reflection, no dynamic assembly
scanning, and all types are explicitly referenced in the generated output.

### Customizing the Generated Class

By default, the generator creates a class named `Dependencies` in the
`X39.Util.DependencyInjection` namespace with an `AddDependencies` extension method. You can
customize both via MSBuild properties in your `.csproj`:

```xml

MyServices
MyApp.DI

```

This produces `MyApp.DI.MyServices.AddMyServices(...)` instead. The method name is always
`Add` + the class name.

### Compile-Time Diagnostics

The source generator reports errors at build time rather than at runtime:

| ID | Description |
|------------|------------------------------------------------------------------------------------------------------------------------------------------|
| `X39DI001` | Condition method is private. Change to `internal` or `public` for source-generated registration. |
| `X39DI002` | Multiple dependency injection attributes on the same class. |
| `X39DI003` | Invalid condition method signature — must be `static`, return `bool`, and accept zero parameters or a single `IConfiguration` parameter. |

## Lifetime Attributes

Each attribute corresponds to a standard DI lifetime. The generic forms require .NET 7+.

| Attribute | Lifetime | Equivalent call |
|---------------------------------------|-----------|------------------------------------------|
| `[Singleton]` | Singleton | `AddSingleton()` |
| `[Singleton]` | Singleton | `AddSingleton()` |
| `[Transient]` | Transient | `AddTransient()` |
| `[Transient]` | Transient | `AddTransient()` |
| `[Scoped]` | Scoped | `AddScoped()` |
| `[Scoped]` | Scoped | `AddScoped()` |

In the two-type-parameter form, `TService` is the implementation class and `TAbstraction` is the
interface or base class (`TService : TAbstraction`).

**Pre-.NET 7:** Non-generic versions are available using `typeof(...)`. These are marked
`[Obsolete]` on .NET 7+ in favor of the generic forms.

```csharp
// Without abstraction (registers as itself)
[Singleton(typeof(MyService))]

// With abstraction (note: parameter order is serviceType, actualType)
[Singleton(typeof(IMyService), typeof(MyService))]
```

## Conditional Registration

Use `[DependencyInjectionCondition]` on a static method to control whether a service is registered.
The method must be `static`, return `bool`, and accept either no parameters or a single
`IConfiguration` parameter.

**Important:** Condition methods must be `internal` or `public` when using the source generator.
Private condition methods produce a compile-time error (`X39DI001`).

```csharp
public interface IMyService
{
bool SomeFunc();
}

[Singleton]
public class DebugService : IMyService
{
[DependencyInjectionCondition]
internal static bool Condition()
{
#if DEBUG
return true;
#else
return false;
#endif
}

public bool SomeFunc() => true;
}

[Singleton]
public class ReleaseService : IMyService
{
[DependencyInjectionCondition]
internal static bool Condition()
{
#if DEBUG
return false;
#else
return true;
#endif
}

public bool SomeFunc() => true;
}
```

A condition method can also accept `IConfiguration` to make decisions based on app configuration:

```csharp
[DependencyInjectionCondition]
internal static bool IsEnabled(IConfiguration configuration)
{
return configuration.GetValue("Features:MyService");
}
```

If a class has multiple condition methods, **all** must return `true` for the service to be
registered (AND logic).

## Reflection-Based Registration (Legacy)

The library also includes reflection-based registration methods that scan assemblies at runtime.
These are useful when source generation is not available or when scanning assemblies outside your
project.

All methods are extension methods on `IServiceCollection`.

#### `AddAttributedServicesOf(IConfiguration, Assembly)`

Scans the given assembly for classes decorated with lifetime attributes and registers them.

```csharp
services.AddAttributedServicesOf(configuration, typeof(Program).Assembly);
```

#### `AddAttributedServicesFromAssemblyOf(IConfiguration)`

Convenience overload that scans the assembly containing type `T`.

```csharp
services.AddAttributedServicesFromAssemblyOf(configuration);
```

#### `AddAttributedServicesOf(IConfiguration, AppDomain)`

Scans all assemblies loaded in the given `AppDomain`.

```csharp
services.AddAttributedServicesOf(configuration, AppDomain.CurrentDomain);
```

> **Note:** The reflection-based methods use runtime type scanning and are not compatible with
> NativeAOT. Prefer the source-generated `AddDependencies` method for new projects.

## Behavior Notes

- **Static constructors** are executed during registration (before condition methods are evaluated).
- Only **one** lifetime attribute is allowed per class. The source generator reports `X39DI002` at
compile time; the reflection-based path throws `MultipleDependencyInjectionAttributesPresentException`.
- Lifetime attributes are **not inherited** (`Inherited = false`).

## Exceptions

Exceptions are thrown by the reflection-based registration path. The source generator reports
equivalent issues as compile-time diagnostics instead.

All exceptions derive from `DependencyInjectionException`.

| Exception | Thrown when |
|---------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
| `ActualTypeIsNotMatchingDecoratedTypeException` | The `TService` type parameter does not match the class the attribute is applied to. |
| `ConditionMethodHasInvalidSignatureException` | A `[DependencyInjectionCondition]` method is not static, does not return `bool`, or has unsupported parameters. |
| `MultipleDependencyInjectionAttributesPresentException` | A class has more than one lifetime attribute (`[Singleton]`, `[Transient]`, `[Scoped]`). |
| `ServiceTypeIsNotImplementingDecoratedTypeException` | The decorated class does not implement the `TAbstraction` type. |

## Semantic Versioning

This library follows the principles of [Semantic Versioning](https://semver.org/).

## Contributing

Contributions are welcome!
Please submit a pull request or create a discussion to discuss any changes you wish to make.

### Code of Conduct

Be excellent to each other.

### Contributor License Agreement

By submitting a contribution (pull request, patch, or any other form) to this project, you agree
to the following terms:

1. **License Grant.** You grant the project maintainer ("Maintainer") and all recipients of the
software a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license to use,
reproduce, modify, distribute, sublicense, and otherwise exploit your contribution under the
terms of the GNU Lesser General Public License v3.0 (LGPL-3.0-only). You additionally grant
the Maintainer the right to relicense your contribution under any other open-source or
proprietary license at the Maintainer's sole discretion.

2. **Originality.** You represent that your contribution is your original work, or that you have
sufficient rights to grant the licenses above. If your contribution includes third-party
material, you represent that its license is compatible with the LGPL-3.0-only and permits the
grants made herein.

3. **No Conflicting Obligations.** You represent that your contribution is not subject to any
agreement, obligation, or encumbrance (including but not limited to employment agreements or
prior license grants) that would conflict with or restrict the rights granted under this
agreement.

4. **No Compensation.** Your contribution is made voluntarily and without expectation of
compensation, unless separately agreed in writing.

5. **Right to Remove.** The Maintainer may remove, modify, or replace your contribution at any
time, for any reason, without notice or obligation to you.

6. **Liability.** To the maximum extent permitted by applicable law, your contribution is provided
"as is", without warranty of any kind. You shall be solely liable for any damage arising from
the inclusion of your contribution to the extent such damage is caused by a defect, rights
violation, or other issue originating in your contribution.

7. **Governing Law.** This agreement is governed by the laws of the Federal Republic of Germany
(Bundesrepublik Deutschland), in particular the German Civil Code (BGB), without regard to
its conflict-of-laws provisions. For contributors outside Germany, this choice of law applies
to the extent permitted by the contributor's local jurisdiction.

Please add yourself to the [CONTRIBUTORS](CONTRIBUTORS.md) file when submitting your first pull
request, and include the following statement in your pull request description:

> I have read and agree to the Contributor License Agreement in this project's README.

## License

This project is licensed under the GNU Lesser General Public License v3.0.
See the [LICENSE](LICENSE) file for details.