Auto-generated controllers & CRUD scaffolding for ASP.NET Core – generic CRUD controllers, dynamic service controllers from interfaces (via IApplicationFeatureProvider), optional repository layer, Gridify paging/filter/sort, and mapping profiles via Klausmd5.AutoMapperLegacy — all with file-scoped namespaces and a modern C# style.
Namespaces:
AutoController.AbstractionsAutoController.AspNetCoreAutoController.EfCoreAutoController.Mapping
- Installation
- Quickstart
- Registrations (Entities & Services)
- Routes & API Versioning
- Swagger / OperationIds
- Service controllers via interface (
[RouteAs]) - CRUD service: optional repository
- AutoMapper profiles (generic)
- Gridify paging/filter/sort
- AOT / NativeAOT note
- Configuration / Extensibility
- Troubleshooting
- License
dotnet add package Klausmd5.AutoController
# optional/recommended
dotnet add package Swashbuckle.AspNetCore.Annotations
dotnet add package Gridify
dotnet add package Gridify.EntityFrameworkKlausmd5.AutoController and Klausmd5.AutoMapperLegacy are publicly installable via nuget.org.
Note:
Klausmd5.AutoControllerbringsKlausmd5.AutoMapperLegacy,Asp.Versioning.Abstractions,EF Coreetc. as dependencies. You do not needAutoMapper.Extensions.Microsoft.DependencyInjection.If you want to use the mapper package directly, install:
dotnet add package Klausmd5.AutoMapperLegacy
Klausmd5.AutoMapperLegacyintentionally stays on the AutoMapper v14 API surface due to the licensing changes in newer upstream releases.
// file-scoped
namespace AutoController_Demo;
using Asp.Versioning;
using Asp.Versioning.Conventions;
using AutoController.Abstractions;
using AutoController.AspNetCore;
using AutoController.Bootstrap;
using AutoController.EfCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
// DbContext
builder.Services.AddDbContext<TestDbContext>(opt =>
{
opt.UseSqlServer("Data source=(localdb)\\MSSQLLocalDB;initial catalog=AutoControllerDemo;persist security info=True;");
});
// Bridge: GenericCrudService<,,,> expects DbContext base type
builder.Services.AddScoped<DbContext>(sp => sp.GetRequiredService<TestDbContext>());
// Registrations (CRUD for TestEntity)
var regs = new[]
{
new GenericRegistration(typeof(TestEntity), typeof(TestDto), typeof(TestUpdateDto), typeof(TestUpdateDto))
};
// AutoController bootstrap (controllers/feature provider/conventions + mapping + EF defaults)
builder.Services.AddControllers()
.AddAutoControllerSetup(regs, opts =>
{
opts.RouteFormat = ApiRouteFormat.Versioned; // v{version}/[controller]/[action]
opts.OperationIdGenerator = CustomOperationIdGenerator.Generate;
},
soft =>
{
soft.FlagPropertyName = "IsDeleted"; // soft-delete flag
soft.IdPropertyName = "Id";
});
// API versioning (URL segment) + ApiExplorer
builder.Services.AddApiVersioning(opt =>
{
opt.DefaultApiVersion = new ApiVersion(1);
opt.AssumeDefaultVersionWhenUnspecified = true;
opt.ReportApiVersions = true;
opt.ApiVersionReader = new UrlSegmentApiVersionReader();
})
.AddMvc(opt =>
{
// Optional: namespace-based versioning; can be omitted for dynamic controllers
opt.Conventions.Add(new VersionByNamespaceConvention());
})
.AddApiExplorer(opt =>
{
opt.GroupNameFormat = "'v'VVV"; // v1.0
opt.SubstituteApiVersionInUrl = true;
});
// Generic CRUD service (open generic)
builder.Services.AddScoped(typeof(IGenericCrudService<,,,>), typeof(GenericCrudService<,,,>));
// Swagger
builder.Services.AddSwaggerGen(opt =>
{
opt.SwaggerDoc("v1", new OpenApiInfo
{
Title = "AutoController Demo API",
Version = "v1",
Description = "Demo for AutoController with EF Core"
});
opt.EnableAnnotations();
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
c.RoutePrefix = string.Empty;
});
}
app.UseHttpsRedirection();
app.MapControllers();
// (Optional) dump endpoints – handy for debugging
var eds = app.Services.GetRequiredService<EndpointDataSource>();
foreach (var e in eds.Endpoints)
Console.WriteLine(e.DisplayName);
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<TestDbContext>();
await db.Database.EnsureDeletedAsync();
await db.Database.EnsureCreatedAsync();
}
app.Run();namespace AutoController_Demo;
public sealed class TestEntity
{
public Guid Id { get; set; }
public string Name { get; set; } = "";
public bool IsDeleted { get; set; }
}
public sealed class TestDto
{
public Guid Id { get; set; }
public string Name { get; set; } = "";
}
public sealed class TestUpdateDto
{
public string Name { get; set; } = "";
}Done. You immediately get (with Versioned):
GET v1/TestEntity/GetById/{id}GET v1/TestEntity/GetAllGET v1/TestEntity/GetAllNotDeletedGET v1/TestEntity/GetAllDeletedPOST v1/TestEntity/CreatePUT v1/TestEntity/Update/{id}DELETE v1/TestEntity/Delete/{id}
Pluralization depends on your generator configuration (e.g.,
TestEntities). Defaults use the type name.
GenericRegistration controls what is generated:
new GenericRegistration(
Entity: typeof(TestEntity),
Dto: typeof(TestDto),
CreateDto: typeof(TestUpdateDto), // Create=Update for demo
UpdateDto: typeof(TestUpdateDto),
ServiceInterface: null, // custom interface? -> service controller
ServiceImplementation: null, // custom implementation
ControllerNameOverride: "TestEntity", // optional
CreateController: true,
CreateMappingProfile: true,
CreateCrudService: true
);- CRUD controller is generated when
ServiceInterface == null. - Service controller is generated when
ServiceInterface != null(see next section).
ApiRouteFormat.Versioned→v{version:apiVersion}/[controller]/[action]ApiRouteFormat.Plain→[controller]/[action]
API Versioning:
- URL segment (
/v1/...) viaUrlSegmentApiVersionReader() - Alternatively query (
?api-version=1.0) or header (x-api-version), if desired (can be combined)
Enabling v2: Add a second API version in your versioning config (and ensure your dynamic controllers are marked for it if you use a custom convention). Example:
builder.Services.AddApiVersioning(opt =>
{
opt.DefaultApiVersion = new ApiVersion(1, 0);
opt.AssumeDefaultVersionWhenUnspecified = true;
opt.ApiVersionReader = new UrlSegmentApiVersionReader();
opt.ReportApiVersions = true;
});If your AutoController options expose supported versions, add both 1.0 and 2.0 there; otherwise annotate per controller via attribute/convention.
- For dynamic service methods (with annotations enabled) meaningful OperationIds are set automatically.
- Global generator (optional):
opts.OperationIdGenerator = CustomOperationIdGenerator.Generate;Annotate interface methods with [RouteAs("template", HttpMethodKind.POST|GET|...)].
Parameter-binding attributes ([FromQuery], [FromBody], [FromRoute]) on the interface are mirrored.
namespace AutoController_Demo;
using AutoController.Abstractions;
public interface ITimeMachineService
{
[RouteAs("travel-to", HttpMethodKind.POST)]
Task<DateTime> TravelTo(DateTime date);
[RouteAs("reset", HttpMethodKind.POST)]
Task<DateTime> Reset();
[RouteAs("get-current-time", HttpMethodKind.GET)]
Task<DateTime> GetCurrentTime();
}With Versioned:
POST v1/TimeMachine/travel-toPOST v1/TimeMachine/resetGET v1/TimeMachine/get-current-time
Without a custom repository, GenericCrudService<,,,> falls back to EF Core (DbContext).
Plug in your repo:
namespace AutoController_Demo;
using AutoController.Abstractions;
public sealed class TestRepository(TestDbContext db) : ICrudRepository<TestEntity>
{
// Implement QueryAll/Insert/Update/SoftDelete/HardDelete
}builder.Services.AddScoped<ICrudRepository<TestEntity>, TestRepository>();The bootstrap extension (AddAutoControllerSetup) automatically creates generic profiles (GenericMappingProfile<,,,>) for your regs.
You therefore don’t need a separate Klausmd5.AutoMapperLegacy / AutoMapper registration.
The generic profiles map entity ↔ DTOs and ignore non-collection members marked with
[IncludeObject]during updates.
Example:
GET v1/TestEntity/GetAll?filter=Name==~"ann"&orderBy=Name desc&page=1&pageSize=20
Utility extensions include:
PaginateAsync<TEntity, TDto>(query, gridifyQuery, mapper[, gridifyMapper])GridifyEntityAsync<T>(query, gridifyQuery)
- CRUD controllers: regular usage.
- Dynamic service controllers rely on Reflection/Emit → not NativeAOT-friendly. For NativeAOT, use static controllers or Minimal APIs.
- Route format:
opts.RouteFormat = ApiRouteFormat.Versioned | Plain - OperationIds:
opts.OperationIdGenerator = … - Soft-delete / Id properties: via
AddAutoControllerSetup(..., soft => { ... }) - Custom repos:
services.AddScoped<ICrudRepository<TEntity>, MyRepo>(); - Custom services:
services.AddScoped(ServiceInterface, ServiceImplementation);
404 (Not Found) for generated endpoints
→ Feature provider wasn’t active. Ensure you use AddControllers().AddAutoControllerSetup(...) (it wires the ApplicationPartManager correctly).
Optionally dump EndpointDataSource (as in Quickstart) to see registered routes.
400 (Bad Request) at /v1/...
→ Usually API versioning/model state (missing/incompatible version). Try:
/v1.0/......?api-version=1.0
Unable to resolve service for type 'Microsoft.EntityFrameworkCore.DbContext' …
→ Add the bridge:
builder.Services.AddScoped<DbContext>(sp => sp.GetRequiredService<TestDbContext>());Ambiguous AddAutoMapper(...)
→ Not needed — the bootstrap extension registers Klausmd5.AutoMapperLegacy manually. Remove your own AddAutoMapper calls.
MIT