.NET
02 / 08

ASP.NET Core Web APIs

.NET: ASP.NET Core Web APIs

Minimal API (modern approach)

// Program.cs — entry point and composition root
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();
builder.Services.AddDbContext<AppDbContext>(opts =>
    opts.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<IUserService, UserService>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
    app.MapOpenApi();

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

// Map routes
app.MapGet("/users", async (IUserService svc) =>
    Results.Ok(await svc.GetAllAsync()))
    .WithName("GetUsers")
    .WithTags("Users");

app.MapGet("/users/{id:int}", async (int id, IUserService svc) => {
    var user = await svc.GetByIdAsync(id);
    return user is null ? Results.NotFound() : Results.Ok(user);
});

app.MapPost("/users", async (CreateUserDto dto, IUserService svc) => {
    var user = await svc.CreateAsync(dto);
    return Results.CreatedAtRoute("GetUser", new { id = user.Id }, user);
});

app.MapPut("/users/{id:int}", async (int id, UpdateUserDto dto, IUserService svc) => {
    var updated = await svc.UpdateAsync(id, dto);
    return updated ? Results.NoContent() : Results.NotFound();
});

app.MapDelete("/users/{id:int}", async (int id, IUserService svc) => {
    await svc.DeleteAsync(id);
    return Results.NoContent();
});

app.Run();

Controller-Based API

// Controllers/UsersController.cs
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class UsersController : ControllerBase
{
    private readonly IUserService _service;

    public UsersController(IUserService service)
    {
        _service = service;
    }

    [HttpGet]
    [ProducesResponseType<List<UserDto>>(StatusCodes.Status200OK)]
    public async Task<IActionResult> GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 25)
    {
        var users = await _service.GetPagedAsync(page, pageSize);
        return Ok(users);
    }

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

    [HttpPost]
    [ProducesResponseType<UserDto>(StatusCodes.Status201Created)]
    [ProducesResponseType<ValidationProblemDetails>(StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> Create([FromBody] CreateUserDto dto)
    {
        var user = await _service.CreateAsync(dto);
        return CreatedAtAction(nameof(GetById), new { id = user.Id }, user);
    }

    [HttpPatch("{id:int}")]
    public async Task<IActionResult> Update(int id, [FromBody] UpdateUserDto dto)
    {
        return await _service.UpdateAsync(id, dto) ? NoContent() : NotFound();
    }
}

DTOs & Validation

// Data Transfer Objects — separate from domain models
public record UserDto(int Id, string Name, string Email, DateTime CreatedAt);

public record CreateUserDto(
    [Required, MaxLength(100)] string Name,
    [Required, EmailAddress] string Email,
    [Required, MinLength(8)] string Password
);

public record UpdateUserDto(
    [MaxLength(100)] string? Name,
    [EmailAddress] string? Email
);

// Fluent validation (alternative to data annotations)
// Install: dotnet add package FluentValidation.AspNetCore
public class CreateUserValidator : AbstractValidator<CreateUserDto>
{
    public CreateUserValidator()
    {
        RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
        RuleFor(x => x.Email).NotEmpty().EmailAddress();
        RuleFor(x => x.Password).MinimumLength(8)
            .Matches("[A-Z]").WithMessage("Password must contain an uppercase letter");
    }
}

Authentication (JWT)

// Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(opts => {
        opts.TokenValidationParameters = new TokenValidationParameters {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"]!)),
            ValidateIssuer = true,
            ValidIssuer = builder.Configuration["Jwt:Issuer"],
            ValidateAudience = true,
            ValidAudience = builder.Configuration["Jwt:Audience"],
            ValidateLifetime = true,
        };
    });

builder.Services.AddAuthorization(opts => {
    opts.AddPolicy("AdminOnly", p => p.RequireRole("admin"));
});

// Protect endpoints
[Authorize]                            // any authenticated user
[Authorize(Policy = "AdminOnly")]     // admin only
[Authorize(Roles = "admin,moderator")] // role-based
[AllowAnonymous]                       // override — no auth

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

Start free