Skip to content
Open
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
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,40 @@ It adds MediatR requests handlers, although you might need to add other types li
public static partial IServiceCollection AddRepositories(this IServiceCollection services);
```

### Register ASP.NET Core controllers as services
By default, ASP.NET Core controllers are not registered in the DI container — they are instantiated using an activator. However, if you want controllers to be resolved from DI (equivalent to calling `AddControllersAsServices()`), you can register them with `ServiceScan`:
```csharp
[GenerateServiceRegistrations(
AssignableTo = typeof(ControllerBase),
AsSelf = true,
Lifetime = ServiceLifetime.Transient)]
public static partial IServiceCollection AddControllersAsServices(this IServiceCollection services);
```
This discovers all classes inheriting from `ControllerBase` and registers each one as a transient service with its concrete type. You can then combine this with `services.AddControllers()`:
```csharp
services.AddControllers();
services.AddControllersAsServices();
```
This gives you full DI control over controllers — allowing lifetime customization, decoration, or replacement of individual controllers.
Comment on lines +77 to +91
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copilot is right, it won't work without

builder.Services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());

see https://source.dot.net/#Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreMvcBuilderExtensions.cs,d4d338b14589af6c,references


You can also use `TypeNameFilter` to discover controllers by naming convention instead:
```csharp
[GenerateServiceRegistrations(
TypeNameFilter = "*Controller",
AsSelf = true,
Lifetime = ServiceLifetime.Transient)]
public static partial IServiceCollection AddControllersAsServices(this IServiceCollection services);
```

And you can exclude specific controllers using `ExcludeByTypeName`:
```csharp
[GenerateServiceRegistrations(
AssignableTo = typeof(ControllerBase),
AsSelf = true,
ExcludeByTypeName = "*InternalController")]
public static partial IServiceCollection AddControllersAsServices(this IServiceCollection services);
```

### Add AspNetCore Minimal API endpoints
You can add custom type handler, if you need to do something non-trivial with that type. For example, you can automatically discover
and map Minimal API endpoints:
Expand Down
117 changes: 117 additions & 0 deletions ServiceScan.SourceGenerator.Tests/AddServicesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,123 @@ public class MyService2 : IService
Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[2].ToString());
}

[Fact]
public void AddAspNetCoreControllersAsServices()
{
var attribute = """[GenerateServiceRegistrations(AssignableTo = typeof(ControllerBase), AsSelf = true, Lifetime = ServiceLifetime.Transient)]""";

var compilation = CreateCompilation(
Sources.MethodWithAttribute(attribute),
"""
namespace GeneratorTests;

public abstract class ControllerBase { }
public abstract class Controller : ControllerBase { }
public class HomeController : Controller { }
public class AccountController : Controller { }
""");

var results = CSharpGeneratorDriver
.Create(_generator)
.RunGenerators(compilation)
.GetRunResult();

var registrations = $"""
return services
.AddTransient<global::GeneratorTests.HomeController, global::GeneratorTests.HomeController>()
.AddTransient<global::GeneratorTests.AccountController, global::GeneratorTests.AccountController>();
""";
Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[2].ToString());
}

[Fact]
public void AddAspNetCoreControllersAsServicesWithTypeNameFilter()
{
var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Controller", AsSelf = true, Lifetime = ServiceLifetime.Transient)]""";

var compilation = CreateCompilation(
Sources.MethodWithAttribute(attribute),
"""
namespace GeneratorTests;

public abstract class ControllerBase { }
public abstract class Controller : ControllerBase { }
public class HomeController : Controller { }
public class AccountController : Controller { }
public class NotAControllerService { }
""");

var results = CSharpGeneratorDriver
.Create(_generator)
.RunGenerators(compilation)
.GetRunResult();

var registrations = $"""
return services
.AddTransient<global::GeneratorTests.HomeController, global::GeneratorTests.HomeController>()
.AddTransient<global::GeneratorTests.AccountController, global::GeneratorTests.AccountController>();
""";
Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[2].ToString());
}

[Fact]
public void AddAspNetCoreControllersAsScopedServices()
{
var attribute = """[GenerateServiceRegistrations(AssignableTo = typeof(ControllerBase), AsSelf = true, Lifetime = ServiceLifetime.Scoped)]""";

var compilation = CreateCompilation(
Sources.MethodWithAttribute(attribute),
"""
namespace GeneratorTests;

public abstract class ControllerBase { }
public abstract class Controller : ControllerBase { }
public class HomeController : Controller { }
public class AccountController : Controller { }
""");

var results = CSharpGeneratorDriver
.Create(_generator)
.RunGenerators(compilation)
.GetRunResult();

var registrations = $"""
return services
.AddScoped<global::GeneratorTests.HomeController, global::GeneratorTests.HomeController>()
.AddScoped<global::GeneratorTests.AccountController, global::GeneratorTests.AccountController>();
""";
Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[2].ToString());
}

[Fact]
public void AddAspNetCoreControllersWithExclusion()
{
var attribute = """[GenerateServiceRegistrations(AssignableTo = typeof(ControllerBase), AsSelf = true, ExcludeByTypeName = "*Account*")]""";

var compilation = CreateCompilation(
Sources.MethodWithAttribute(attribute),
"""
namespace GeneratorTests;

public abstract class ControllerBase { }
public abstract class Controller : ControllerBase { }
public class HomeController : Controller { }
public class AccountController : Controller { }
public class AccountSettingsController : Controller { }
""");

var results = CSharpGeneratorDriver
.Create(_generator)
.RunGenerators(compilation)
.GetRunResult();

var registrations = $"""
return services
.AddTransient<global::GeneratorTests.HomeController, global::GeneratorTests.HomeController>();
""";
Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[2].ToString());
}

[Fact]
public void DontGenerateAnythingIfTypeIsInvalid()
{
Expand Down
Loading