ASP.NET Core
02 / 03

Web API: Controllers, Minimal APIs & Validation

ASP.NET Core: Web API Controllers & Minimal APIs

ApiController Pattern

[ApiController]
[Route("api/[controller]")]
[Authorize]   // require auth for all endpoints
public class ArticlesController : ControllerBase
{
    private readonly IArticleService _service;

    public ArticlesController(IArticleService service)
    {
        _service = service;
    }

    [HttpGet]
    [AllowAnonymous]
    [ProducesResponseType<IEnumerable<ArticleDto>>(StatusCodes.Status200OK)]
    public async Task<IActionResult> GetAll([FromQuery] ArticleQueryParams query)
    {
        var articles = await _service.GetAllAsync(query);
        return Ok(articles);
    }

    [HttpGet("{id:int}")]
    [ProducesResponseType<ArticleDto>(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetById(int id)
    {
        var article = await _service.GetByIdAsync(id);
        return article is null ? NotFound() : Ok(article);
    }

    [HttpPost]
    [ProducesResponseType<ArticleDto>(StatusCodes.Status201Created)]
    [ProducesResponseType<ValidationProblemDetails>(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> Create([FromBody] CreateArticleRequest request)
    {
        // [ApiController] auto-validates and returns 400 on failure
        var article = await _service.CreateAsync(request, User);
        return CreatedAtAction(nameof(GetById), new { id = article.Id }, article);
    }

    [HttpPut("{id}")]
    public async Task<IActionResult> Update(int id, [FromBody] UpdateArticleRequest request)
    {
        var success = await _service.UpdateAsync(id, request, User);
        return success ? NoContent() : NotFound();
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(int id)
    {
        await _service.DeleteAsync(id);
        return NoContent();
    }
}

Validation

// Data annotations
public class CreateArticleRequest
{
    [Required]
    [StringLength(200, MinimumLength = 5)]
    public string Title { get; set; } = string.Empty;

    [Required]
    [MinLength(50)]
    public string Content { get; set; } = string.Empty;

    [Url]
    public string? ImageUrl { get; set; }

    [Range(1, 10)]
    public int Priority { get; set; }
}

// FluentValidation (recommended for complex rules)
// dotnet add package FluentValidation.AspNetCore
public class CreateArticleValidator : AbstractValidator<CreateArticleRequest>
{
    public CreateArticleValidator()
    {
        RuleFor(x => x.Title)
            .NotEmpty()
            .Length(5, 200)
            .Must(title => !title.Contains("spam"))
                .WithMessage("Title cannot contain 'spam'");

        RuleFor(x => x.Content)
            .NotEmpty()
            .MinimumLength(50);

        RuleFor(x => x.ImageUrl)
            .Must(url => Uri.TryCreate(url, UriKind.Absolute, out _))
                .When(x => x.ImageUrl != null)
                .WithMessage("Invalid URL");
    }
}

// Register in Program.cs
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssemblyContaining<CreateArticleValidator>();

Minimal APIs (.NET 6+)

// Program.cs — no controllers needed
var app = builder.Build();

var articles = app.MapGroup("/api/articles").RequireAuthorization();

articles.MapGet("/", async (IArticleService svc, [AsParameters] ArticleQueryParams query)
    => await svc.GetAllAsync(query));

articles.MapGet("/{id:int}", async (int id, IArticleService svc) =>
{
    var article = await svc.GetByIdAsync(id);
    return article is null ? Results.NotFound() : Results.Ok(article);
});

articles.MapPost("/", async (CreateArticleRequest req, IArticleService svc, ClaimsPrincipal user) =>
{
    var article = await svc.CreateAsync(req, user);
    return Results.CreatedAtRoute("GetArticle", new { id = article.Id }, article);
}).WithName("GetArticle");

articles.MapPut("/{id}", async (int id, UpdateArticleRequest req, IArticleService svc) =>
    await svc.UpdateAsync(id, req) ? Results.NoContent() : Results.NotFound());

articles.MapDelete("/{id}", async (int id, IArticleService svc) =>
{
    await svc.DeleteAsync(id);
    return Results.NoContent();
});

// Typed Results (compile-time checked return types)
articles.MapGet("/{id}", async Task<Results<Ok<ArticleDto>, NotFound>> (int id, IArticleService svc) =>
{
    var article = await svc.GetByIdAsync(id);
    return article is null ? TypedResults.NotFound() : TypedResults.Ok(article);
});

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free