-
Notifications
You must be signed in to change notification settings - Fork 0
dotnet controller migration
GitHub Actions edited this page Feb 3, 2026
·
1 revision
This guide helps you convert existing Controller-based code to Minimal APIs.
Before:
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IUserService _service;
public UsersController(IUserService service)
{
_service = service;
}
[HttpGet]
public async Task<IActionResult> GetAll()
{
var users = await _service.GetAllAsync();
return Ok(users);
}
}After:
public static class UsersEndpoints
{
public static void MapUsersEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/users").WithTags("Users");
group.MapGet("/", async (IUserService service) =>
{
var users = await service.GetAllAsync();
return Results.Ok(users);
});
}
}
// In Program.cs
app.MapUsersEndpoints();Before:
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var user = await _service.GetByIdAsync(id);
if (user == null) return NotFound();
return Ok(user);
}After:
group.MapGet("/{id:int}", async (int id, IUserService service) =>
{
var user = await service.GetByIdAsync(id);
return user is null ? Results.NotFound() : Results.Ok(user);
});Before:
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateUserDto dto)
{
var result = await _service.CreateAsync(dto);
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
}After:
group.MapPost("/", async (CreateUserDto dto, IUserService service) =>
{
var result = await service.CreateAsync(dto);
return Results.CreatedAtRoute("GetUserById", new { id = result.Id }, result);
})
.WithName("CreateUser");Before:
[HttpGet("search")]
public async Task<IActionResult> Search([FromQuery] string name, [FromQuery] int? age)
{
var results = await _service.SearchAsync(name, age);
return Ok(results);
}After:
group.MapGet("/search", async (string name, int? age, IUserService service) =>
{
var results = await service.SearchAsync(name, age);
return Results.Ok(results);
});Before:
[Authorize]
[HttpGet("protected")]
public async Task<IActionResult> GetProtected()
{
return Ok("Protected data");
}
[Authorize(Roles = "Admin")]
[HttpPost("admin-only")]
public async Task<IActionResult> AdminOnly()
{
return Ok("Admin data");
}After:
group.MapGet("/protected", async () => Results.Ok("Protected data"))
.RequireAuthorization();
group.MapPost("/admin-only", async () => Results.Ok("Admin data"))
.RequireAuthorization("AdminPolicy");Before:
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateUserDto dto)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var result = await _service.CreateAsync(dto);
return Ok(result);
}After:
// Add validation filter
group.MapPost("/", async (CreateUserDto dto, IUserService service) =>
{
var result = await service.CreateAsync(dto);
return Results.Ok(result);
})
.AddEndpointFilter<ValidationFilter<CreateUserDto>>();
// ValidationFilter implementation
public class ValidationFilter<T> : IEndpointFilter where T : class
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var argument = context.GetArgument<T>(0);
var validationResults = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(
argument,
new ValidationContext(argument),
validationResults,
true);
if (!isValid)
{
return Results.ValidationProblem(
validationResults.ToDictionary(
vr => vr.MemberNames.First(),
vr => new[] { vr.ErrorMessage! }));
}
return await next(context);
}
}Before:
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
private readonly ILogger<UsersController> _logger;
private readonly IMapper _mapper;
public UsersController(
IUserService userService,
ILogger<UsersController> logger,
IMapper mapper)
{
_userService = userService;
_logger = logger;
_mapper = mapper;
}
[HttpGet]
public async Task<IActionResult> GetAll()
{
_logger.LogInformation("Getting all users");
var users = await _userService.GetAllAsync();
return Ok(_mapper.Map<List<UserDto>>(users));
}
}After:
group.MapGet("/", async (
IUserService userService,
ILogger<Program> logger,
IMapper mapper) =>
{
logger.LogInformation("Getting all users");
var users = await userService.GetAllAsync();
return Results.Ok(mapper.Map<List<UserDto>>(users));
});Before:
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IMediator _mediator;
public AuthController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] RegisterCommand command)
{
var result = await _mediator.Send(command);
return Ok(result);
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginCommand command)
{
var result = await _mediator.Send(command);
return Ok(result);
}
}After:
public static class AuthEndpoints
{
public static void MapAuthEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/auth")
.WithTags("Auth")
.WithOpenApi();
group.MapPost("/register", async (RegisterCommand command, IMediator mediator) =>
{
var result = await mediator.Send(command);
return Results.Ok(result);
})
.WithName("Register")
.Produces<AuthResponseDto>(StatusCodes.Status200OK);
group.MapPost("/login", async (LoginCommand command, IMediator mediator) =>
{
var result = await mediator.Send(command);
return Results.Ok(result);
})
.WithName("Login")
.Produces<AuthResponseDto>(StatusCodes.Status200OK);
}
}
// In Program.cs
app.MapAuthEndpoints();| Controller Return | Minimal API Return |
|---|---|
Ok(data) |
Results.Ok(data) |
NotFound() |
Results.NotFound() |
BadRequest() |
Results.BadRequest() |
Created(uri, data) |
Results.Created(uri, data) |
CreatedAtAction() |
Results.CreatedAtRoute() |
NoContent() |
Results.NoContent() |
Unauthorized() |
Results.Unauthorized() |
Forbid() |
Results.Forbid() |
ValidationProblem() |
Results.ValidationProblem() |
Problem() |
Results.Problem() |
src/Api/
├── Controllers/
│ ├── UsersController.cs
│ ├── OrdersController.cs
│ └── AuthController.cs
└── Program.cs
src/Api/
├── Endpoints/
│ ├── UsersEndpoints.cs
│ ├── OrdersEndpoints.cs
│ └── AuthEndpoints.cs
└── Program.cs
Remove:
builder.Services.AddControllers();
// ...
app.MapControllers();Add:
// Register endpoints
app.MapUsersEndpoints();
app.MapOrdersEndpoints();
app.MapAuthEndpoints();- Remove all
using Microsoft.AspNetCore.Mvcstatements from endpoint files - Change
[ApiController]classes tostaticclasses - Remove all
[Route],[HttpGet],[HttpPost], etc. attributes - Convert method signatures to lambda expressions
- Change
IActionResulttoResults.*methods - Update dependency injection from constructor to method parameters
- Rename
*Controller.csto*Endpoints.cs - Move files from
Controllers/toEndpoints/folder - Add
Map{Entity}Endpointsextension method - Update
Program.csto call endpoint mapping methods - Remove
builder.Services.AddControllers() - Remove
app.MapControllers() - Update all tests to use new endpoint structure
- Update OpenAPI/Swagger configuration if needed
Before:
var controller = new UsersController(mockService.Object);
var result = await controller.GetAll();
var okResult = Assert.IsType<OkObjectResult>(result);After:
// Use WebApplicationFactory for integration tests
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/users");
response.EnsureSuccessStatusCode();
var users = await response.Content.ReadFromJsonAsync<List<UserDto>>();✅ Less Code: No controller classes, no attributes
✅ Better Performance: Reduced memory allocation
✅ Easier Testing: Pure functions with direct DI
✅ Modern Pattern: Aligned with .NET 6+ best practices
✅ Cleaner Code: Functional programming style
Run @developer "Convert {ControllerName} to Minimal API" and the agent will do the conversion for you!
Les contributions sont bienvenues ! Voir CONTRIBUTING.md
This documentation is automatically synced from the main repository.