FullStackHero uses .NET Minimal APIs to map HTTP routes to commands and queries. Every endpoint follows a consistent pattern with built-in permission-based authorization.
using FSH.Framework.Shared.Identity;using FSH.Framework.Shared.Identity.Authorization;using FSH.Modules.Identity.Contracts.v1.Groups.CreateGroup;using Mediator;using Microsoft.AspNetCore.Builder;using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Mvc;using Microsoft.AspNetCore.Routing;namespace FSH.Modules.Identity.Features.v1.Groups.CreateGroup;public static class CreateGroupEndpoint{ public static RouteHandlerBuilder MapCreateGroupEndpoint(this IEndpointRouteBuilder endpoints) { return endpoints.MapPost("/groups", (IMediator mediator, [FromBody] CreateGroupCommand request, CancellationToken cancellationToken) => mediator.Send(request, cancellationToken)) .WithName("CreateGroup") .WithSummary("Create a new group") .RequirePermission(IdentityPermissionConstants.Groups.Create) .WithDescription("Create a new group with optional role assignments."); }}
using FSH.Framework.Shared.Identity;using FSH.Framework.Shared.Identity.Authorization;using FSH.Modules.Identity.Contracts.v1.Groups.GetGroupById;using Mediator;using Microsoft.AspNetCore.Builder;using Microsoft.AspNetCore.Http;using Microsoft.AspNetCore.Routing;namespace FSH.Modules.Identity.Features.v1.Groups.GetGroupById;public static class GetGroupByIdEndpoint{ public static RouteHandlerBuilder MapGetGroupByIdEndpoint(this IEndpointRouteBuilder endpoints) { return endpoints.MapGet("/groups/{id:guid}", (Guid id, IMediator mediator, CancellationToken cancellationToken) => mediator.Send(new GetGroupByIdQuery(id), cancellationToken)) .WithName("GetGroupById") .WithSummary("Get group by ID") .RequirePermission(IdentityPermissionConstants.Groups.View) .WithDescription("Retrieve a specific group by its ID including roles and member count."); }}
public static RouteHandlerBuilder MapGetGroupsEndpoint(this IEndpointRouteBuilder endpoints){ return endpoints.MapGet("/groups", (IMediator mediator, string? search, CancellationToken cancellationToken) => mediator.Send(new GetGroupsQuery(search), cancellationToken)) .WithName("GetGroups") .WithSummary("Get all groups") .RequirePermission(IdentityPermissionConstants.Groups.View) .WithDescription("Retrieve a list of all groups with optional search.");}
This extension method is defined in src/BuildingBlocks/Shared/Identity/Authorization/EndpointExtensions.cs:
EndpointExtensions.cs
using Microsoft.AspNetCore.Builder;namespace FSH.Framework.Shared.Identity.Authorization;public static class EndpointExtensions{ public static TBuilder RequirePermission<TBuilder>( this TBuilder endpointConventionBuilder, string requiredPermission, params string[] additionalRequiredPermissions) where TBuilder : IEndpointConventionBuilder { return endpointConventionBuilder.WithMetadata( new RequiredPermissionAttribute(requiredPermission, additionalRequiredPermissions)); }}
return endpoints.MapPost("/groups", handler) .WithName("CreateGroup") // Operation ID for OpenAPI .WithSummary("Create a new group") // Brief description .WithDescription("Long description...") // Detailed description .WithTags("Groups") // Group in Swagger UI .Produces<GroupDto>(StatusCodes.Status201Created) .ProducesProblem(StatusCodes.Status400BadRequest) .RequirePermission(IdentityPermissionConstants.Groups.Create);
Endpoints are registered in the module’s main class:
IdentityModule.cs
public static IEndpointRouteBuilder MapIdentityEndpoints(this IEndpointRouteBuilder app){ var versionedApi = app.NewVersionedApi("Identity") .HasApiVersion(1); var groups = versionedApi.MapGroup("/api/v{version:apiVersion}/identity") .WithTags("Identity"); // Register all group endpoints groups.MapCreateGroupEndpoint(); groups.MapGetGroupByIdEndpoint(); groups.MapGetGroupsEndpoint(); groups.MapUpdateGroupEndpoint(); groups.MapDeleteGroupEndpoint(); return app;}