Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,16 @@ Both generators follow the **Incremental Generator** pattern (IIncrementalGenera
- Generates `AddDependencyRegistrationsFrom{SmartSuffix}()` extension methods with 4 overloads
- **Smart naming** - uses short suffix if unique, full name if conflicts exist
- **Transitive dependency registration** - automatically registers services from referenced assemblies
- **Hosted service detection** - automatically uses `AddHostedService<T>()` for `BackgroundService` or `IHostedService` implementations
- Default lifetime: Singleton (can specify Scoped or Transient)

**Generated Code Pattern:**
```csharp
// Input: [Registration] public class UserService : IUserService { }
// Output: services.AddSingleton<IUserService, UserService>();

// Hosted Service Input: [Registration] public class MaintenanceService : BackgroundService { }
// Hosted Service Output: services.AddHostedService<MaintenanceService>();
```

**Smart Naming:**
Expand Down Expand Up @@ -149,6 +153,7 @@ services.AddDependencyRegistrationsFromDomain("DataAccess", "Infrastructure");
- `ATCDIR001` - Service 'As' type must be an interface (Error)
- `ATCDIR002` - Class does not implement specified interface (Error)
- `ATCDIR003` - Duplicate registration with different lifetimes (Warning)
- `ATCDIR004` - Hosted services must use Singleton lifetime (Error)

### OptionsBindingGenerator

Expand Down Expand Up @@ -374,7 +379,8 @@ The `PetStore.Api` sample demonstrates all four generators working together in a
┌─────────────────────────────────────────────────────────────┐
│ PetStore.Domain │
│ - [Registration] PetService, ValidationService │
│ - [OptionsBinding] PetStoreOptions │
│ - [Registration] PetMaintenanceService (BackgroundService) │
│ - [OptionsBinding] PetStoreOptions, PetMaintenanceOptions │
│ - [MapTo] Pet → PetDto, Pet → PetEntity │
│ - GenerateDocumentationFile=false │
└─────────────────────────────────────────────────────────────┘
Expand Down Expand Up @@ -437,7 +443,8 @@ Return PetDto to client

### Key Features Demonstrated

- **Zero boilerplate DI registration**: All services auto-registered
- **Zero boilerplate DI registration**: All services auto-registered, including hosted services
- **Background service support**: `PetMaintenanceService` automatically registered with `AddHostedService<T>()`
- **Type-safe configuration**: Options validated and bound automatically
- **Automatic mapping chains**: Entity ↔ Domain ↔ DTO conversions
- **OpenAPI integration**: Full API documentation with Scalar UI
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ builder.Services.AddDependencyRegistrationsFromDataAccess();
- **🎯 Auto-Detection**: Automatically registers against all implemented interfaces - no more `As = typeof(IService)`
- **🧹 Smart Filtering**: System interfaces (IDisposable, etc.) are excluded automatically
- **🔍 Multi-Interface**: Implementing multiple interfaces? Registers against all of them
- **🏃 Hosted Service Support**: Automatically detects BackgroundService and IHostedService implementations and uses AddHostedService<T>()
- **✨ Smart Naming**: Generates clean method names using suffixes when unique, full names when conflicts exist
- **⚡ Zero Runtime Cost**: All code generated at compile time
- **🚀 Native AOT Compatible**: No reflection or runtime code generation - fully trimming-safe
Expand Down Expand Up @@ -139,6 +140,7 @@ Get errors at compile time, not runtime:
| ATCDIR001 | `As` parameter must be an interface type |
| ATCDIR002 | Class must implement the specified interface |
| ATCDIR003 | Duplicate registration with different lifetimes |
| ATCDIR004 | Hosted services must use Singleton lifetime |

---

Expand Down
39 changes: 38 additions & 1 deletion docs/generators/DependencyRegistration.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Automatically register services in the dependency injection container using attr
- [❌ ATCDIR001: As Type Must Be Interface](#-ATCDIR001-as-type-must-be-interface)
- [❌ ATCDIR002: Class Does Not Implement Interface](#-ATCDIR002-class-does-not-implement-interface)
- [⚠️ ATCDIR003: Duplicate Registration with Different Lifetime](#️-ATCDIR003-duplicate-registration-with-different-lifetime)
- [❌ ATCDIR004: Hosted Services Must Use Singleton Lifetime](#-ATCDIR004-hosted-services-must-use-singleton-lifetime)
- [📚 Additional Examples](#-additional-examples)

---
Expand Down Expand Up @@ -605,13 +606,14 @@ builder.Services.AddDependencyRegistrationsFromApi();
## ✨ Features

- **Automatic Service Registration**: Decorate classes with `[Registration]` attribute for automatic DI registration
- **Hosted Service Support**: Automatically detects `BackgroundService` and `IHostedService` implementations and uses `AddHostedService<T>()`
- **Interface Auto-Detection**: Automatically registers against all implemented interfaces (no `As` parameter needed!)
- **Smart Filtering**: System interfaces (IDisposable, etc.) are automatically excluded
- **Multiple Interface Support**: Services implementing multiple interfaces are registered against all of them
- **Flexible Lifetimes**: Support for Singleton, Scoped, and Transient service lifetimes
- **Explicit Override**: Optional `As` parameter to override auto-detection when needed
- **Dual Registration**: Register services as both interface and concrete type with `AsSelf`
- **Compile-time Validation**: Diagnostics for common errors (invalid interface types, missing implementations)
- **Compile-time Validation**: Diagnostics for common errors (invalid interface types, missing implementations, incorrect hosted service lifetimes)
- **Zero Runtime Overhead**: All code is generated at compile time
- **Native AOT Compatible**: No reflection or runtime code generation - fully trimming-safe and AOT-ready
- **Multi-Project Support**: Each project generates its own registration method
Expand Down Expand Up @@ -1102,6 +1104,41 @@ public class UserServiceScoped : IUserService { }

**Fix:** Ensure consistent lifetimes or use different interfaces.

### ❌ ATCDIR004: Hosted Services Must Use Singleton Lifetime

**Severity:** Error

**Description:** Hosted services (BackgroundService or IHostedService implementations) must use Singleton lifetime.

```csharp
// ❌ Error: Hosted services cannot use Scoped or Transient lifetime
[Registration(Lifetime.Scoped)]
public class MyBackgroundService : BackgroundService
{
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
return Task.CompletedTask;
}
}
```

**Fix:** Use Singleton lifetime (or default [Registration]) for hosted services:

```csharp
// ✅ Correct: Singleton lifetime (explicit)
[Registration(Lifetime.Singleton)]
public class MyBackgroundService : BackgroundService { }

// ✅ Correct: Default lifetime is Singleton
[Registration]
public class MyBackgroundService : BackgroundService { }
```

**Generated Registration:**
```csharp
services.AddHostedService<MyBackgroundService>();
```

---

## 📚 Additional Examples
Expand Down
13 changes: 12 additions & 1 deletion docs/samples/PetStoreApi.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ graph TB
subgraph "PetStore.Domain"
PS["PetService - @Registration"]
VS["ValidationService - @Registration"]
BG["PetMaintenanceService - @Registration (BackgroundService)"]
OPT["PetStoreOptions - @OptionsBinding"]
OPT2["PetMaintenanceServiceOptions - @OptionsBinding"]
PET["Pet - @MapTo(PetResponse)"]
PET2["Pet - @MapTo(PetEntity, Bidirectional=true)"]
DOMENUM["PetStatus (Domain)"]
Expand Down Expand Up @@ -86,8 +88,10 @@ graph TB

style PS fill:#0969da
style VS fill:#0969da
style BG fill:#0969da
style PR fill:#0969da
style OPT fill:#0969da
style OPT2 fill:#0969da
style DI1 fill:#2ea44f
style DI2 fill:#2ea44f
style DI3 fill:#2ea44f
Expand Down Expand Up @@ -566,11 +570,12 @@ app.Run();

### DependencyRegistration Generator

Creates 2 extension methods with transitive registration:
Creates extension methods with transitive registration:

```csharp
// From PetStore.Domain (with includeReferencedAssemblies: true)
services.AddSingleton<IPetService, PetService>();
services.AddHostedService<PetMaintenanceService>(); // ✨ Automatic BackgroundService registration
services.AddSingleton<IPetRepository, PetRepository>(); // From referenced PetStore.DataAccess
```

Expand All @@ -584,6 +589,11 @@ services.AddOptions<PetStoreOptions>()
.Bind(configuration.GetSection("PetStore"))
.ValidateDataAnnotations()
.ValidateOnStart();

services.AddOptions<PetMaintenanceServiceOptions>()
.Bind(configuration.GetSection("PetMaintenanceService"))
.ValidateDataAnnotations()
.ValidateOnStart();
```

### Mapping Generator
Expand Down Expand Up @@ -644,6 +654,7 @@ public static Pet MapToPet(this PetEntity source)
### 1. **Complete Integration**
All three generators work seamlessly together:
- Services auto-registered via `[Registration]`
- Background services auto-registered with `AddHostedService<T>()` via `[Registration]`
- Configuration auto-bound via `[OptionsBinding]`
- Objects auto-mapped via `[MapTo]` with bidirectional support

Expand Down
5 changes: 4 additions & 1 deletion sample/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,11 @@ dotnet_diagnostic.CA1062.severity = none # In externally visible meth
dotnet_diagnostic.CA1056.severity = none #
dotnet_diagnostic.CA1303.severity = none #
dotnet_diagnostic.SA1615.severity = none # Element return value should be documented
dotnet_diagnostic.CA1848.severity = none # For improved performance
dotnet_diagnostic.CA1819.severity = none # Properties should not return arrays

dotnet_diagnostic.SA1611.severity = none # The documentation for parameter 'pet' is missing

dotnet_diagnostic.S6580.severity = none # Use a format provider when parsing date and time.
dotnet_diagnostic.S6580.severity = none # Use a format provider when parsing date and time.
dotnet_diagnostic.S6667.severity = none # Logging in a catch clause should pass the caught exception as a parameter.
dotnet_diagnostic.S6672.severity = none # Update this logger to use its enclosing type
10 changes: 10 additions & 0 deletions sample/PetStore.Api.Contract/PetResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,14 @@ public class PetResponse
/// Gets or sets when the pet was added to the system.
/// </summary>
public DateTimeOffset CreatedAt { get; set; }

/// <summary>
/// Gets or sets when the pet was last modified.
/// </summary>
public DateTimeOffset? ModifiedAt { get; set; }

/// <summary>
/// Gets or sets who last modified the pet.
/// </summary>
public string? ModifiedBy { get; set; }
}
1 change: 1 addition & 0 deletions sample/PetStore.Api/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@

global using PetStore.Api.Contract;
global using PetStore.Domain;
global using PetStore.Domain.BackgroundServices;
global using PetStore.Domain.Services;
global using Scalar.AspNetCore;
2 changes: 1 addition & 1 deletion sample/PetStore.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
includeReferencedAssemblies: true);

// ✨ Register configuration options automatically via [OptionsBinding] attribute
// This single call registers options from PetStore.Domain (PetStoreOptions)
// This single call registers options from PetStore.Domain (PetStoreOptions + PetMaintenanceServiceOptions)
builder.Services.AddOptionsFromDomain(
builder.Configuration,
includeReferencedAssemblies: true);
Expand Down
5 changes: 4 additions & 1 deletion sample/PetStore.Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@
"MaxPetsPerPage": 20,
"StoreName": "Furry Friends Pet Store",
"EnableAutoStatusUpdates": true
},
"PetMaintenanceService": {
"RepeatIntervalInSeconds": 10
}
}
}
10 changes: 10 additions & 0 deletions sample/PetStore.DataAccess/Entities/PetEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,14 @@ public class PetEntity
/// Gets or sets when the pet was added to the system.
/// </summary>
public DateTimeOffset CreatedAt { get; set; }

/// <summary>
/// Gets or sets when the pet was last modified.
/// </summary>
public DateTimeOffset? ModifiedAt { get; set; }

/// <summary>
/// Gets or sets who last modified the pet.
/// </summary>
public string? ModifiedBy { get; set; }
}
89 changes: 89 additions & 0 deletions sample/PetStore.Domain/BackgroundServices/PetMaintenanceService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// ReSharper disable RedundantArgumentDefaultValue
// ReSharper disable UnusedParameter.Local
namespace PetStore.Domain.BackgroundServices;

/// <summary>
/// Background service that performs periodic pet maintenance tasks every 30 seconds.
/// </summary>
[Registration(Lifetime.Singleton)]
public class PetMaintenanceService : BackgroundService
{
private readonly IPetRepository petRepository;
private readonly ILogger<PetMaintenanceService> logger;
private readonly TimeSpan interval;

/// <summary>
/// Initializes a new instance of the <see cref="PetMaintenanceService"/> class.
/// </summary>
/// <param name="logger">The logger instance.</param>
/// <param name="options">The service options.</param>
/// <param name="petRepository">The pet repository.</param>
public PetMaintenanceService(
ILogger<PetMaintenanceService> logger,
IOptions<PetMaintenanceServiceOptions> options,
IPetRepository petRepository)
{
this.logger = logger;
this.petRepository = petRepository;
this.interval = TimeSpan.FromSeconds(options.Value.RepeatIntervalInSeconds);
}

/// <summary>
/// Executes the background service.
/// </summary>
/// <param name="stoppingToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation(
"PetMaintenanceService started. Will execute every {Interval} seconds",
interval.TotalSeconds);

using var timer = new PeriodicTimer(interval);

try
{
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await DoWorkAsync(stoppingToken);
}
}
catch (OperationCanceledException)
{
logger.LogInformation("PetMaintenanceService is stopping");
}
}

/// <summary>
/// Performs the maintenance work on all pets.
/// </summary>
/// <param name="stoppingToken">The cancellation token.</param>
/// <returns>A task representing the asynchronous operation.</returns>
[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "OK - Testing")]
private Task DoWorkAsync(CancellationToken stoppingToken)
{
logger.LogInformation("PetMaintenanceService: Starting pet maintenance");

var now = DateTimeOffset.UtcNow;
const string modifiedBy = nameof(PetMaintenanceService);

// Get all pets and update their modification tracking
var pets = petRepository
.GetAll()
.ToList();

foreach (var pet in pets)
{
pet.ModifiedAt = now;
pet.ModifiedBy = modifiedBy;
}

logger.LogInformation(
"PetMaintenanceService: Updated {Count} pets with ModifiedAt={ModifiedAt}, ModifiedBy={ModifiedBy}",
pets.Count,
now,
modifiedBy);

return Task.CompletedTask;
}
}
5 changes: 4 additions & 1 deletion sample/PetStore.Domain/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
global using System.ComponentModel.DataAnnotations;

global using System.Diagnostics.CodeAnalysis;
global using Atc.DependencyInjection;
global using Atc.Mapping;
global using Atc.SourceGenerators.Annotations;

global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Logging;
global using Microsoft.Extensions.Options;

global using PetStore.Api.Contract;
global using PetStore.DataAccess.Entities;
global using PetStore.DataAccess.Repositories;
global using PetStore.Domain.BackgroundServices;
global using PetStore.Domain.Models;
global using PetStore.Domain.Options;
10 changes: 10 additions & 0 deletions sample/PetStore.Domain/Models/Pet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,14 @@ public partial class Pet
/// Gets or sets when the pet was added to the system.
/// </summary>
public DateTimeOffset CreatedAt { get; set; }

/// <summary>
/// Gets or sets when the pet was last modified.
/// </summary>
public DateTimeOffset? ModifiedAt { get; set; }

/// <summary>
/// Gets or sets who last modified the pet.
/// </summary>
public string? ModifiedBy { get; set; }
}
14 changes: 14 additions & 0 deletions sample/PetStore.Domain/Options/PetMaintenanceServiceOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace PetStore.Domain.Options;

/// <summary>
/// Configuration options for the pet maintenance background service.
/// </summary>
[OptionsBinding("PetMaintenanceService")]
public partial class PetMaintenanceServiceOptions
{
/// <summary>
/// Gets or sets the repeat interval in seconds.
/// Default: 60 seconds.
/// </summary>
public int RepeatIntervalInSeconds { get; set; } = 60;
}
Loading