ASP.NET Core

Comprehensive guide to building REST APIs with ASP.NET Core framework

Introduction to ASP.NET Core for REST APIs

ASP.NET Core is Microsoft’s modern, high-performance, cross-platform framework for building cloud-based, internet-connected applications. It excels at creating RESTful services with its rich HTTP programming model and extensive middleware ecosystem.

Framework Overview

ASP.NET Core represents a complete redesign of the original ASP.NET framework, built with modern software design principles including:

  • Modularity: Use only the components you need
  • Dependency injection: Built-in IoC container
  • Middleware architecture: Composable request processing pipeline
  • Platform independence: Run on Windows, Linux, macOS, or in containers
  • Open source development: Community-driven with Microsoft stewardship

Key Features for REST API Development

  • Performance: Consistently ranks among the fastest web frameworks in industry benchmarks, with throughput often 10-20× faster than traditional ASP.NET
  • Cross-Platform: Runs natively on Windows, Linux, macOS, and in Docker containers
  • Built-in Dependency Injection: Native IoC container eliminates the need for third-party DI libraries
  • Middleware Architecture: Flexible HTTP pipeline configuration with over 80 official and community middleware components
  • Native OpenAPI/Swagger Support: Automatic API documentation generation and interactive testing UI
  • Content Negotiation: Format-independent responses that adapt based on client requests
  • Robust Security: Comprehensive authentication and authorization capabilities
  • Minimal APIs: Lightweight, lambda-based approach introduced in .NET 6 for lower ceremony
  • Scalability: Designed for cloud-scale with efficient resource utilization
  • API-First Design: Built with modern API development patterns in mind

ASP.NET Core offers two primary approaches to building REST APIs:

  1. Controller-based APIs - The traditional MVC pattern with explicit controllers
  2. Minimal APIs - A streamlined, lambda-based approach with reduced ceremony

Getting Started with ASP.NET Core REST APIs

Prerequisites

Creating a Basic REST API Project

Using the Command Line

Create an API project using the .NET CLI:

# Create a new WebAPI project
dotnet new webapi -n TaskManager.Api

# Navigate to the project directory
cd TaskManager.Api

# Run the application
dotnet run

By default, this creates a Weather Forecast API example. The application will start, and you can access the Swagger UI at https://localhost:<port>/swagger.

Project Structure

A typical ASP.NET Core REST API project structure:

TaskManager.Api/
├── Controllers/
│   └── TasksController.cs
├── Models/
│   ├── Task.cs
│   └── TaskDTO.cs
├── Services/
│   └── ITaskService.cs
│   └── TaskService.cs
├── Data/
│   └── TaskDbContext.cs
├── Properties/
│   └── launchSettings.json
├── appsettings.json
├── appsettings.Development.json
├── Program.cs
└── TaskManager.Api.csproj

Controllers-Based REST APIs

Controllers are the traditional way to build REST APIs in ASP.NET Core. They provide a structured approach for organizing related API endpoints.

Basic Controller Example

using Microsoft.AspNetCore.Mvc;
using TaskManager.Api.Models;
using TaskManager.Api.Services;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace TaskManager.Api.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class TasksController : ControllerBase
    {
        private readonly ITaskService _taskService;

        public TasksController(ITaskService taskService)
        {
            _taskService = taskService;
        }

        // GET api/tasks
        [HttpGet]
        [ProducesResponseType(typeof(IEnumerable<TaskDTO>), StatusCodes.Status200OK)]
        public async Task<ActionResult<IEnumerable<TaskDTO>>> GetAll()
        {
            IEnumerable<TaskDTO> tasks = await _taskService.GetAllTasksAsync();
            return Ok(tasks);
        }

        // GET api/tasks/{id}
        [HttpGet("{id}")]
        [ProducesResponseType(typeof(TaskDTO), StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public async Task<ActionResult<TaskDTO>> GetById(int id)
        {
            TaskDTO task = await _taskService.GetTaskByIdAsync(id);
            
            if (task == null)
            {
                return NotFound();
            }
            
            return Ok(task);
        }

        // POST api/tasks
        [HttpPost]
        [ProducesResponseType(typeof(TaskDTO), StatusCodes.Status201Created)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        public async Task<ActionResult<TaskDTO>> Create(CreateTaskDTO taskDTO)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            TaskDTO createdTask = await _taskService.CreateTaskAsync(taskDTO);
            
            return CreatedAtAction(
                nameof(GetById), 
                new { id = createdTask.Id }, 
                createdTask);
        }

        // PUT api/tasks/{id}
        [HttpPut("{id}")]
        [ProducesResponseType(StatusCodes.Status204NoContent)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public async Task<IActionResult> Update(int id, UpdateTaskDTO taskDTO)
        {
            if (id != taskDTO.Id)
            {
                return BadRequest();
            }

            bool exists = await _taskService.TaskExistsAsync(id);
            if (!exists)
            {
                return NotFound();
            }

            await _taskService.UpdateTaskAsync(taskDTO);
            
            return NoContent();
        }

        // DELETE api/tasks/{id}
        [HttpDelete("{id}")]
        [ProducesResponseType(StatusCodes.Status204NoContent)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        public async Task<IActionResult> Delete(int id)
        {
            bool exists = await _taskService.TaskExistsAsync(id);
            if (!exists)
            {
                return NotFound();
            }

            await _taskService.DeleteTaskAsync(id);
            
            return NoContent();
        }
    }
}

Model Classes

namespace TaskManager.Api.Models
{
    public class TaskDTO
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public bool IsCompleted { get; set; }
        public DateTime DueDate { get; set; }
        public TaskPriority Priority { get; set; }
    }

    public class CreateTaskDTO
    {
        [Required]
        [StringLength(100, MinimumLength = 3)]
        public string Title { get; set; }
        
        [MaxLength(500)]
        public string Description { get; set; }
        
        public bool IsCompleted { get; set; } = false;
        
        public DateTime? DueDate { get; set; }
        
        public TaskPriority Priority { get; set; } = TaskPriority.Medium;
    }

    public class UpdateTaskDTO
    {
        public int Id { get; set; }
        
        [Required]
        [StringLength(100, MinimumLength = 3)]
        public string Title { get; set; }
        
        [MaxLength(500)]
        public string Description { get; set; }
        
        public bool IsCompleted { get; set; }
        
        public DateTime? DueDate { get; set; }
        
        public TaskPriority Priority { get; set; }
    }

    public enum TaskPriority
    {
        Low = 0,
        Medium = 1,
        High = 2,
        Critical = 3
    }
}

Minimal APIs

ASP.NET Core 6.0 introduced Minimal APIs, a simpler alternative to controllers for building small REST services with less ceremony.

Basic Minimal API Example

// Program.cs
using Microsoft.EntityFrameworkCore;
using TaskManager.Api.Models;
using TaskManager.Api.Data;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// Add services
builder.Services.AddDbContext<TaskDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

WebApplication app = builder.Build();

// Configure middleware
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// Define API endpoints
app.MapGet("/api/tasks", async (TaskDbContext db) =>
{
    return await db.Tasks.ToListAsync();
})
.WithName("GetAllTasks")
.WithOpenApi()
.Produces<List<TaskDTO>>(StatusCodes.Status200OK);

app.MapGet("/api/tasks/{id}", async (int id, TaskDbContext db) =>
{
    TaskDTO task = await db.Tasks.FindAsync(id);
    
    return task is null
        ? Results.NotFound()
        : Results.Ok(task);
})
.WithName("GetTaskById")
.WithOpenApi()
.Produces<TaskDTO>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);

app.MapPost("/api/tasks", async (CreateTaskDTO taskDTO, TaskDbContext db) =>
{
    TaskDTO task = new TaskDTO
    {
        Title = taskDTO.Title,
        Description = taskDTO.Description,
        IsCompleted = taskDTO.IsCompleted,
        DueDate = taskDTO.DueDate ?? DateTime.UtcNow.AddDays(1),
        Priority = taskDTO.Priority
    };
    
    db.Tasks.Add(task);
    await db.SaveChangesAsync();
    
    return Results.CreatedAtRoute("GetTaskById", new { id = task.Id }, task);
})
.WithName("CreateTask")
.WithOpenApi()
.Produces<TaskDTO>(StatusCodes.Status201Created)
.Produces(StatusCodes.Status400BadRequest);

app.MapPut("/api/tasks/{id}", async (int id, UpdateTaskDTO taskDTO, TaskDbContext db) =>
{
    if (id != taskDTO.Id)
        return Results.BadRequest();
        
    TaskDTO existingTask = await db.Tasks.FindAsync(id);
    if (existingTask is null)
        return Results.NotFound();
        
    existingTask.Title = taskDTO.Title;
    existingTask.Description = taskDTO.Description;
    existingTask.IsCompleted = taskDTO.IsCompleted;
    existingTask.DueDate = taskDTO.DueDate ?? existingTask.DueDate;
    existingTask.Priority = taskDTO.Priority;
    
    await db.SaveChangesAsync();
    
    return Results.NoContent();
})
.WithName("UpdateTask")
.WithOpenApi()
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status404NotFound);

app.MapDelete("/api/tasks/{id}", async (int id, TaskDbContext db) =>
{
    TaskDTO task = await db.Tasks.FindAsync(id);
    
    if (task is null)
        return Results.NotFound();
        
    db.Tasks.Remove(task);
    await db.SaveChangesAsync();
    
    return Results.NoContent();
})
.WithName("DeleteTask")
.WithOpenApi()
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status404NotFound);

app.Run();

REST API Best Practices in ASP.NET Core

1. Use HTTP Methods Appropriately

HTTP MethodUsage
GETRetrieve resources
POSTCreate new resources
PUTUpdate existing resources (full replacement)
PATCHPartially update resources
DELETERemove resources

2. Return Proper Status Codes

Status CodeDescriptionCommon Usage
200 OKRequest succeededSuccessful GET, PUT, PATCH, or DELETE
201 CreatedResource createdSuccessful POST
204 No ContentRequest succeeded with no response bodySuccessful DELETE or PUT
400 Bad RequestInvalid request formatValidation errors
401 UnauthorizedAuthentication requiredMissing or invalid credentials
403 ForbiddenInsufficient permissionsAuthenticated but not authorized
404 Not FoundResource not foundInvalid ID or endpoint
409 ConflictRequest conflicts with current stateVersion conflicts
500 Server ErrorUnexpected server errorExceptions, database errors

3. Use Resource-Based URLs

# Good
GET /api/tasks                 # Get all tasks
GET /api/tasks/42              # Get task with ID 42
GET /api/users/5/tasks         # Get all tasks for user 5
POST /api/tasks                # Create a new task

# Avoid
GET /api/getAllTasks           # Action-based instead of resource-based
GET /api/taskById?id=42        # Using query parameters for resource identifier

Best Practices for REST API Development

Design Principles

  1. Use Nouns, Not Verbs for Resources

    // Good
    GET /api/tasks         // Get all tasks
    GET /api/tasks/123     // Get task with ID 123
    
    // Avoid
    GET /api/getTasks
    GET /api/getTask/123
    
  2. Use Plural Resource Names

    // Good
    GET /api/customers
    GET /api/products
    
    // Avoid
    GET /api/customer
    GET /api/product
    
  3. Use Resource Nesting for Relationships

    // Good
    GET /api/customers/42/orders       // Get all orders for customer 42
    GET /api/customers/42/orders/7     // Get order 7 for customer 42
    
    // Avoid
    GET /api/customerOrders?customerId=42
    GET /api/customerOrder?customerId=42&orderId=7
    
  4. Use HTTP Methods Appropriately

    GET     /api/tasks      // Get tasks (idempotent, safe)
    POST    /api/tasks      // Create a task (non-idempotent)
    PUT     /api/tasks/123  // Update/replace task 123 (idempotent)
    PATCH   /api/tasks/123  // Partial update of task 123 (idempotent)
    DELETE  /api/tasks/123  // Delete task 123 (idempotent)
    
  5. Provide Meaningful HTTP Status Codes

    // Successful operations
    return Ok(resource);                  // 200 OK
    return Created("uri", resource);      // 201 Created
    return NoContent();                   // 204 No Content
    
    // Client errors
    return BadRequest(error);             // 400 Bad Request
    return Unauthorized();                // 401 Unauthorized
    return Forbid();                      // 403 Forbidden
    return NotFound();                    // 404 Not Found
    return Conflict(error);               // 409 Conflict
    return UnprocessableEntity(error);    // 422 Unprocessable Entity
    
    // Server errors
    return StatusCode(
        StatusCodes.Status500InternalServerError, 
        error);                           // 500 Internal Server Error
    

Implementation Best Practices

  1. Versioning Your API

    Implement API versioning to ensure backward compatibility while evolving your API:

    // First, install the versioning package:
    // dotnet add package Microsoft.AspNetCore.Mvc.Versioning
    
    // Program.cs
    builder.Services.AddApiVersioning(options =>
    {
        options.DefaultApiVersion = new ApiVersion(1, 0);
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.ReportApiVersions = true;  // Add version info to response headers
    });
    
    builder.Services.AddVersionedApiExplorer(options =>
    {
        options.GroupNameFormat = "'v'VVV";  // Format: 'v'major[.minor][-status]
        options.SubstituteApiVersionInUrl = true;
    });
    
    // Versioning through URL path
    [ApiController]
    [ApiVersion("1.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class TasksV1Controller : ControllerBase
    {
        // V1 implementation
    }
    
    [ApiController]
    [ApiVersion("2.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class TasksV2Controller : ControllerBase
    {
        // V2 implementation
    }
    
    // Versioning through query string
    [ApiController]
    [ApiVersion("1.0")]
    [ApiVersion("2.0")]
    [Route("api/[controller]")]
    public class ProductsController : ControllerBase
    {
        [HttpGet]
        [MapToApiVersion("1.0")]
        public IActionResult GetV1() 
        {
            // V1 implementation
        }
    
        [HttpGet]
        [MapToApiVersion("2.0")]
        public IActionResult GetV2() 
        {
            // V2 implementation
        }
    }
    
  2. Error Handling Standardization

    Create standardized error responses:

    // Create a problem details factory
    public interface IProblemDetailsFactory
    {
        ProblemDetails CreateProblemDetails(
            HttpContext context,
            string title = null,
            string detail = null,
            string instance = null,
            int? statusCode = null,
            string type = null);
    
        ValidationProblemDetails CreateValidationProblemDetails(
            HttpContext context,
            ModelStateDictionary modelStateDictionary,
            string detail = null,
            string instance = null,
            int? statusCode = null,
            string title = null,
            string type = null);
    }
    
    // Controller with standardized error responses
    [ApiController]
    [Route("api/[controller]")]
    public class OrdersController : ControllerBase
    {
        private readonly IOrderService _orderService;
        private readonly IProblemDetailsFactory _problemDetailsFactory;
    
        public OrdersController(
            IOrderService orderService,
            IProblemDetailsFactory problemDetailsFactory)
        {
            _orderService = orderService;
            _problemDetailsFactory = problemDetailsFactory;
        }
    
        [HttpPost]
        public async Task<IActionResult> CreateOrder(CreateOrderRequest request)
        {
            try
            {
                var result = await _orderService.CreateOrderAsync(request);
    
                if (result.ValidationErrors.Any())
                {
                    var validationProblem = _problemDetailsFactory
                        .CreateValidationProblemDetails(
                            HttpContext,
                            new ModelStateDictionary(),
                            "The order data is invalid",
                            statusCode: StatusCodes.Status400BadRequest);
    
                    foreach (var error in result.ValidationErrors)
                    {
                        validationProblem.Errors.Add(
                            error.PropertyName, 
                            new[] { error.ErrorMessage });
                    }
    
                    return new BadRequestObjectResult(validationProblem);
                }
    
                if (result.Status == OrderCreationStatus.ProductNotAvailable)
                {
                    var problem = _problemDetailsFactory.CreateProblemDetails(
                        HttpContext,
                        title: "Product unavailable",
                        detail: "One or more products in the order are not available",
                        statusCode: StatusCodes.Status409Conflict,
                        type: "https://example.com/errors/product-unavailable");
    
                    return new ObjectResult(problem)
                    {
                        StatusCode = StatusCodes.Status409Conflict
                    };
                }
    
                return CreatedAtAction(
                    nameof(GetOrder), 
                    new { id = result.Order.Id }, 
                    result.Order);
            }
            catch (Exception ex)
            {
                return StatusCode(
                    StatusCodes.Status500InternalServerError,
                    _problemDetailsFactory.CreateProblemDetails(
                        HttpContext,
                        title: "An unexpected error occurred",
                        statusCode: StatusCodes.Status500InternalServerError));
            }
        }
    
        // Other action methods...
    }
    
  3. Asynchronous Processing for Long-Running Operations

    Use background processing for long-running operations:

    // Use an interface for the queue service
    public interface IBackgroundTaskQueue
    {
        ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);
        ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(CancellationToken cancellationToken);
    }
    
    // Implementation using Channel
    public class BackgroundTaskQueue : IBackgroundTaskQueue
    {
        private readonly Channel<Func<CancellationToken, ValueTask>> _queue;
    
        public BackgroundTaskQueue(int capacity)
        {
            var options = new BoundedChannelOptions(capacity)
            {
                FullMode = BoundedChannelFullMode.Wait
            };
            _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
        }
    
        public async ValueTask QueueBackgroundWorkItemAsync(
            Func<CancellationToken, ValueTask> workItem)
        {
            if (workItem == null)
            {
                throw new ArgumentNullException(nameof(workItem));
            }
    
            await _queue.Writer.WriteAsync(workItem);
        }
    
        public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
            CancellationToken cancellationToken)
        {
            return await _queue.Reader.ReadAsync(cancellationToken);
        }
    }
    
    // Background service to process the queue
    public class QueuedHostedService : BackgroundService
    {
        private readonly IBackgroundTaskQueue _taskQueue;
        private readonly ILogger<QueuedHostedService> _logger;
    
        public QueuedHostedService(
            IBackgroundTaskQueue taskQueue, 
            ILogger<QueuedHostedService> logger)
        {
            _taskQueue = taskQueue;
            _logger = logger;
        }
    
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _logger.LogInformation("Queued Hosted Service is starting.");
    
            while (!stoppingToken.IsCancellationRequested)
            {
                var workItem = await _taskQueue.DequeueAsync(stoppingToken);
    
                try
                {
                    await workItem(stoppingToken);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Error occurred executing {WorkItem}", nameof(workItem));
                }
            }
    
            _logger.LogInformation("Queued Hosted Service is stopping.");
        }
    }
    
    // Controller using the background queue
    [ApiController]
    [Route("api/[controller]")]
    public class ReportsController : ControllerBase
    {
        private readonly IBackgroundTaskQueue _taskQueue;
        private readonly IReportService _reportService;
    
        public ReportsController(
            IBackgroundTaskQueue taskQueue,
            IReportService reportService)
        {
            _taskQueue = taskQueue;
            _reportService = reportService;
        }
    
        [HttpPost]
        public async Task<IActionResult> GenerateReport(GenerateReportRequest request)
        {
            // Create a report tracking record
            var reportId = Guid.NewGuid();
    
            await _reportService.CreateReportTrackingRecordAsync(
                new ReportTrackingRecord
                {
                    Id = reportId,
                    Status = ReportStatus.Pending,
                    CreatedAt = DateTime.UtcNow,
                    Parameters = request
                });
    
            // Queue the actual report generation
            await _taskQueue.QueueBackgroundWorkItemAsync(async token =>
            {
                try
                {
                    await _reportService.UpdateReportStatusAsync(
                        reportId, 
                        ReportStatus.Processing);
    
                    await _reportService.GenerateReportAsync(
                        reportId, 
                        request);
    
                    await _reportService.UpdateReportStatusAsync(
                        reportId, 
                        ReportStatus.Completed);
                }
                catch (Exception ex)
                {
                    await _reportService.UpdateReportStatusAsync(
                        reportId, 
                        ReportStatus.Failed,
                        ex.Message);
    
                    throw;
                }
            });
    
            // Return 202 Accepted with a location header to check status
            return AcceptedAtAction(
                nameof(GetReportStatus),
                new { id = reportId },
                new { id = reportId, status = "Pending" });
        }
    
        [HttpGet("{id}/status")]
        public async Task<IActionResult> GetReportStatus(Guid id)
        {
            var report = await _reportService.GetReportStatusAsync(id);
    
            if (report == null)
            {
                return NotFound();
            }
    
            return Ok(report);
        }
    
        [HttpGet("{id}/download")]
        public async Task<IActionResult> DownloadReport(Guid id)
        {
            var report = await _reportService.GetReportAsync(id);
    
            if (report == null)
            {
                return NotFound();
            }
    
            if (report.Status != ReportStatus.Completed)
            {
                return BadRequest(new ProblemDetails
                {
                    Title = "Report not ready",
                    Detail = $"The report status is {report.Status}",
                    Status = StatusCodes.Status400BadRequest
                });
            }
    
            return File(
                report.FileContents,
                report.ContentType,
                report.FileName);
        }
    }
    
  4. Consistent Data Filtering, Sorting, and Pagination

    // Query parameters class
    public class ResourceParameters
    {
        private const int MaxPageSize = 50;
        private int _pageSize = 10;
    
        [FromQuery(Name = "page")]
        public int PageNumber { get; set; } = 1;
    
        [FromQuery(Name = "size")]
        public int PageSize
        {
            get => _pageSize;
            set => _pageSize = Math.Min(value, MaxPageSize);
        }
    
        [FromQuery(Name = "sort")]
        public string SortBy { get; set; }
    
        [FromQuery(Name = "dir")]
        public string SortDirection { get; set; } = "asc";
    
        [FromQuery]
        public string Filter { get; set; }
    
        [FromQuery]
        public string Fields { get; set; }
    
        // Create a paged query using Dynamic LINQ
        public IQueryable<T> ApplySorting<T>(IQueryable<T> query)
        {
            if (string.IsNullOrWhiteSpace(SortBy))
            {
                return query;
            }
    
            // Validate sort fields and directions to prevent SQL injection
            var validSortFields = GetValidSortFieldsForType<T>();
            if (!validSortFields.Contains(SortBy))
            {
                return query;
            }
    
            string sortExpression = SortDirection?.ToLower() == "desc"
                ? $"{SortBy} desc"
                : SortBy;
    
            return query.OrderBy(sortExpression);
        }
    
        public IQueryable<T> ApplyFiltering<T>(IQueryable<T> query)
        {
            if (string.IsNullOrWhiteSpace(Filter))
            {
                return query;
            }
    
            // Parse the filter expression and apply safely
            // Implementation will depend on your filtering approach
            return FilterHelper.ApplySafeFilter<T>(query, Filter);
        }
    
        // Helper method to validate sort fields based on the type
        private static HashSet<string> GetValidSortFieldsForType<T>()
        {
            // Return the valid sort fields for the given type
            // This could be hardcoded or derived from the type's properties
            return typeof(T).GetProperties()
                .Select(p => p.Name.ToLowerInvariant())
                .ToHashSet();
        }
    }
    
    // Controller with filtering, sorting, and pagination
    [ApiController]
    [Route("api/[controller]")]
    public class ProductsController : ControllerBase
    {
        private readonly IProductRepository _repository;
        private readonly IUriService _uriService;
    
        public ProductsController(
            IProductRepository repository,
            IUriService uriService)
        {
            _repository = repository;
            _uriService = uriService;
        }
    
        [HttpGet]
        public async Task<IActionResult> GetProducts([FromQuery] ResourceParameters parameters)
        {
            var pagedResult = await _repository.GetProductsAsync(parameters);
    
            var previousPageLink = pagedResult.HasPrevious
                ? _uriService.CreateResourceUri(
                    Request.Path.Value,
                    parameters,
                    ResourceUriType.PreviousPage)
                : null;
    
            var nextPageLink = pagedResult.HasNext
                ? _uriService.CreateResourceUri(
                    Request.Path.Value,
                    parameters,
                    ResourceUriType.NextPage)
                : null;
    
            var paginationMetadata = new
            {
                totalCount = pagedResult.TotalCount,
                pageSize = pagedResult.PageSize,
                currentPage = pagedResult.CurrentPage,
                totalPages = pagedResult.TotalPages,
                previousPageLink,
                nextPageLink
            };
    
            Response.Headers.Add(
                "X-Pagination",
                JsonSerializer.Serialize(paginationMetadata));
    
            return Ok(pagedResult.Items);
        }
    }
    
  5. Input Validation with FluentValidation

    // First, install the package:
    // dotnet add package FluentValidation.AspNetCore
    
    // Program.cs
    builder.Services.AddControllers()
        .AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Program>());
    
    // Create model validator
    public class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>
    {
        public CreateUserRequestValidator(IUserRepository userRepository)
        {
            RuleFor(x => x.Username)
                .NotEmpty().WithMessage("Username is required")
                .Length(3, 50).WithMessage("Username must be between 3 and 50 characters")
                .Matches("^[a-zA-Z0-9_]+$").WithMessage("Username can only contain letters, numbers, and underscores")
                .MustAsync(async (username, cancellation) => 
                    !await userRepository.UsernameExistsAsync(username))
                    .WithMessage("Username is already taken");
    
            RuleFor(x => x.Email)
                .NotEmpty().WithMessage("Email is required")
                .EmailAddress().WithMessage("Invalid email format")
                .MustAsync(async (email, cancellation) => 
                    !await userRepository.EmailExistsAsync(email))
                    .WithMessage("Email is already registered");
    
            RuleFor(x => x.Password)
                .NotEmpty().WithMessage("Password is required")
                .MinimumLength(8).WithMessage("Password must be at least 8 characters")
                .Matches("[A-Z]").WithMessage("Password must contain at least one uppercase letter")
                .Matches("[a-z]").WithMessage("Password must contain at least one lowercase letter")
                .Matches("[0-9]").WithMessage("Password must contain at least one digit")
                .Matches("[^a-zA-Z0-9]").WithMessage("Password must contain at least one special character");
    
            RuleFor(x => x.ConfirmPassword)
                .NotEmpty().WithMessage("Confirm Password is required")
                .Equal(x => x.Password).WithMessage("Passwords do not match");
        }
    }
    
  6. Security Headers and Content Security Policy

    // Program.cs
    // Add security headers
    builder.Services.AddAntiforgery();
    
    // Configure security headers
    app.Use(async (context, next) =>
    {
        // Security Headers
        context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
        context.Response.Headers.Add("X-Frame-Options", "DENY");
        context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
        context.Response.Headers.Add("Referrer-Policy", "strict-origin-when-cross-origin");
        context.Response.Headers.Add("Permissions-Policy", 
            "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()");
    
        // Content Security Policy
        context.Response.Headers.Add("Content-Security-Policy", 
            "default-src 'self'; " +
            "img-src 'self' data: https://trusted-cdn.com; " +
            "style-src 'self' https://trusted-cdn.com; " +
            "script-src 'self' https://trusted-cdn.com; " +
            "connect-src 'self' https://api.trusted-service.com; " +
            "frame-ancestors 'none'; " +
            "form-action 'self';");
    
        await next();
    });
    

API Design Patterns

  1. Repository Pattern

    Abstracting data access logic:

    public interface IRepository<T> where T : class
    {
        Task<T> GetByIdAsync(int id);
        Task<IEnumerable<T>> GetAllAsync();
        Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);
        Task AddAsync(T entity);
        Task UpdateAsync(T entity);
        Task DeleteAsync(int id);
        Task SaveChangesAsync();
    }
    
    public class Repository<T> : IRepository<T> where T : class
    {
        protected readonly DbContext _context;
    
        public Repository(DbContext context)
        {
            _context = context;
        }
    
        public async Task<T> GetByIdAsync(int id)
        {
            return await _context.Set<T>().FindAsync(id);
        }
    
        public async Task<IEnumerable<T>> GetAllAsync()
        {
            return await _context.Set<T>().ToListAsync();
        }
    
        public async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate)
        {
            return await _context.Set<T>().Where(predicate).ToListAsync();
        }
    
        public async Task AddAsync(T entity)
        {
            await _context.Set<T>().AddAsync(entity);
        }
    
        public Task UpdateAsync(T entity)
        {
            _context.Entry(entity).State = EntityState.Modified;
            return Task.CompletedTask;
        }
    
        public async Task DeleteAsync(int id)
        {
            T entity = await GetByIdAsync(id);
            _context.Set<T>().Remove(entity);
        }
    
        public async Task SaveChangesAsync()
        {
            await _context.SaveChangesAsync();
        }
    }
    
  2. Unit of Work Pattern

    Managing transactions and coordinating multiple repositories:

    public interface IUnitOfWork : IDisposable
    {
        ITaskRepository Tasks { get; }
        IUserRepository Users { get; }
        IProjectRepository Projects { get; }
    
        Task<int> SaveChangesAsync();
    }
    
    public class UnitOfWork : IUnitOfWork
    {
        private readonly DbContext _context;
    
        public ITaskRepository Tasks { get; }
        public IUserRepository Users { get; }
        public IProjectRepository Projects { get; }
    
        public UnitOfWork(
            DbContext context,
            ITaskRepository taskRepository,
            IUserRepository userRepository,
            IProjectRepository projectRepository)
        {
            _context = context;
            Tasks = taskRepository;
            Users = userRepository;
            Projects = projectRepository;
        }
    
        public async Task<int> SaveChangesAsync()
        {
            return await _context.SaveChangesAsync();
        }
    
        public void Dispose()
        {
            _context.Dispose();
        }
    }
    
  3. Mediator Pattern (CQRS)

    Separating commands and queries:

    // First, install the package:
    // dotnet add package MediatR
    
    // Command
    public class CreateTaskCommand : IRequest<int>
    {
        public string Title { get; set; }
        public string Description { get; set; }
        public DateTime? DueDate { get; set; }
        public int Priority { get; set; }
    }
    
    // Command Handler
    public class CreateTaskCommandHandler : IRequestHandler<CreateTaskCommand, int>
    {
        private readonly ITaskRepository _repository;
        private readonly ILogger<CreateTaskCommandHandler> _logger;
    
        public CreateTaskCommandHandler(
            ITaskRepository repository,
            ILogger<CreateTaskCommandHandler> logger)
        {
            _repository = repository;
            _logger = logger;
        }
    
        public async Task<int> Handle(
            CreateTaskCommand request, 
            CancellationToken cancellationToken)
        {
            try
            {
                var task = new TaskEntity
                {
                    Title = request.Title,
                    Description = request.Description,
                    DueDate = request.DueDate,
                    Priority = (TaskPriority)request.Priority,
                    CreatedAt = DateTime.UtcNow
                };
    
                await _repository.AddAsync(task);
                await _repository.SaveChangesAsync();
    
                _logger.LogInformation(
                    "Task created with ID {TaskId}", task.Id);
    
                return task.Id;
            }
            catch (Exception ex)
            {
                _logger.LogError(
                    ex, "Error creating task: {ErrorMessage}", ex.Message);
                throw;
            }
        }
    }
    
    // Query
    public class GetTaskQuery : IRequest<TaskDTO>
    {
        public int Id { get; set; }
    }
    
    // Query Handler
    public class GetTaskQueryHandler : IRequestHandler<GetTaskQuery, TaskDTO>
    {
        private readonly ITaskRepository _repository;
        private readonly IMapper _mapper;
    
        public GetTaskQueryHandler(
            ITaskRepository repository,
            IMapper mapper)
        {
            _repository = repository;
            _mapper = mapper;
        }
    
        public async Task<TaskDTO> Handle(
            GetTaskQuery request, 
            CancellationToken cancellationToken)
        {
            var task = await _repository.GetByIdAsync(request.Id);
            return _mapper.Map<TaskDTO>(task);
        }
    }
    
    // Controller using MediatR
    [ApiController]
    [Route("api/[controller]")]
    public class TasksController : ControllerBase
    {
        private readonly IMediator _mediator;
    
        public TasksController(IMediator mediator)
        {
            _mediator = mediator;
        }
    
        [HttpGet("{id}")]
        public async Task<IActionResult> GetTask(int id)
        {
            var task = await _mediator.Send(new GetTaskQuery { Id = id });
    
            if (task == null)
            {
                return NotFound();
            }
    
            return Ok(task);
        }
    
        [HttpPost]
        public async Task<IActionResult> CreateTask(CreateTaskCommand command)
        {
            int taskId = await _mediator.Send(command);
    
            return CreatedAtAction(
                nameof(GetTask), 
                new { id = taskId }, 
                new { id = taskId });
        }
    }
    

Real-World Examples

E-Commerce API: Product Catalog Endpoint

[ApiController]
[Route("api/v{version:apiVersion}/products")]
[ApiVersion("1.0")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;
    private readonly ILogger<ProductsController> _logger;
    private readonly LinkGenerator _linkGenerator;
    
    public ProductsController(
        IProductService productService,
        ILogger<ProductsController> logger,
        LinkGenerator linkGenerator)
    {
        _productService = productService;
        _logger = logger;
        _linkGenerator = linkGenerator;
    }
    
    /// <summary>
    /// Retrieves products with support for filtering, sorting and pagination
    /// </summary>
    /// <param name="parameters">Query parameters for filtering and pagination</param>
    /// <returns>A collection of products</returns>
    [HttpGet(Name = "GetProducts")]
    [ProducesResponseType(typeof(PagedResponse<ProductDTO>), StatusCodes.Status200OK)]
    [ResponseCache(Duration = 60)] // Cache for 1 minute
    public async Task<IActionResult> GetProducts([FromQuery] ProductQueryParameters parameters)
    {
        _logger.LogInformation(
            "Getting products with parameters: {@Parameters}", parameters);
            
        try
        {
            var products = await _productService.GetProductsAsync(parameters);
            
            var previousPageLink = products.HasPrevious
                ? CreateProductsResourceUri(parameters, ResourceUriType.PreviousPage)
                : null;
                
            var nextPageLink = products.HasNext
                ? CreateProductsResourceUri(parameters, ResourceUriType.NextPage)
                : null;
                
            var paginationMetadata = new
            {
                totalCount = products.TotalCount,
                pageSize = products.PageSize,
                currentPage = products.CurrentPage,
                totalPages = products.TotalPages,
                previousPageLink,
                nextPageLink
            };
            
            Response.Headers.Add(
                "X-Pagination", 
                JsonSerializer.Serialize(paginationMetadata));
                
            return Ok(new PagedResponse<ProductDTO>(
                products.Items, 
                products.CurrentPage, 
                products.PageSize, 
                products.TotalCount,
                previousPageLink,
                nextPageLink));
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error retrieving products");
            return StatusCode(
                StatusCodes.Status500InternalServerError,
                new ProblemDetails
                {
                    Status = StatusCodes.Status500InternalServerError,
                    Title = "An error occurred while retrieving products",
                    Detail = "Please try again later or contact support if the problem persists"
                });
        }
    }
    
    /// <summary>
    /// Retrieves a specific product by ID
    /// </summary>
    /// <param name="id">Product ID</param>
    /// <returns>The requested product</returns>
    [HttpGet("{id}", Name = "GetProductById")]
    [ProducesResponseType(typeof(ProductDetailDTO), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ResponseCache(Duration = 300)] // Cache for 5 minutes
    public async Task<IActionResult> GetProduct(int id)
    {
        try
        {
            var product = await _productService.GetProductByIdAsync(id);
            
            if (product == null)
            {
                _logger.LogWarning("Product with ID {ProductId} not found", id);
                return NotFound(new ProblemDetails
                {
                    Status = StatusCodes.Status404NotFound,
                    Title = "Product not found",
                    Detail = $"The product with ID {id} does not exist"
                });
            }
            
            // Add HATEOAS links
            var links = new List<LinkDTO>
            {
                new LinkDTO(
                    _linkGenerator.GetUriByAction(
                        HttpContext, 
                        action: nameof(GetProduct), 
                        controller: "Products", 
                        values: new { id, version = "1.0" })!,
                    "self",
                    "GET"),
                    
                new LinkDTO(
                    _linkGenerator.GetUriByAction(
                        HttpContext,
                        action: nameof(UpdateProduct),
                        controller: "Products",
                        values: new { id, version = "1.0" })!,
                    "update_product",
                    "PUT"),
                    
                new LinkDTO(
                    _linkGenerator.GetUriByAction(
                        HttpContext,
                        action: nameof(DeleteProduct),
                        controller: "Products",
                        values: new { id, version = "1.0" })!,
                    "delete_product",
                    "DELETE")
            };
            
            return Ok(new
            {
                Data = product,
                Links = links
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error retrieving product with ID {ProductId}", id);
            return StatusCode(
                StatusCodes.Status500InternalServerError,
                new ProblemDetails
                {
                    Status = StatusCodes.Status500InternalServerError,
                    Title = "An error occurred while retrieving the product",
                    Detail = "Please try again later or contact support if the problem persists"
                });
        }
    }
    
    /// <summary>
    /// Creates a new product
    /// </summary>
    /// <param name="productDto">The product information</param>
    /// <returns>The created product</returns>
    [HttpPost(Name = "CreateProduct")]
    [Authorize(Roles = "Admin,ProductManager")]
    [ProducesResponseType(typeof(ProductDTO), StatusCodes.Status201Created)]
    [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    public async Task<IActionResult> CreateProduct(CreateProductDTO productDto)
    {
        try
        {
            _logger.LogInformation(
                "Creating product with name {ProductName}", productDto.Name);
                
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            
            var createdProduct = await _productService.CreateProductAsync(productDto);
            
            // Return 201 Created with proper location header
            return CreatedAtAction(
                nameof(GetProduct),
                new { id = createdProduct.Id, version = "1.0" },
                createdProduct);
        }
        catch (ValidationException ex)
        {
            _logger.LogWarning(
                "Validation error creating product: {ErrorMessage}", ex.Message);
                
            return BadRequest(new ValidationProblemDetails(
                ex.Errors.ToDictionary(
                    e => e.PropertyName,
                    e => new[] { e.ErrorMessage })));
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error creating product");
            return StatusCode(
                StatusCodes.Status500InternalServerError,
                new ProblemDetails
                {
                    Status = StatusCodes.Status500InternalServerError,
                    Title = "An error occurred while creating the product",
                    Detail = "Please try again later or contact support if the problem persists"
                });
        }
    }
    
    // Additional CRUD methods...
    
    private string CreateProductsResourceUri(
        ProductQueryParameters parameters,
        ResourceUriType uriType)
    {
        switch (uriType)
        {
            case ResourceUriType.PreviousPage:
                return _linkGenerator.GetUriByAction(
                    HttpContext,
                    action: nameof(GetProducts),
                    controller: "Products",
                    values: new
                    {
                        version = "1.0",
                        pageNumber = parameters.PageNumber - 1,
                        pageSize = parameters.PageSize,
                        category = parameters.Category,
                        searchTerm = parameters.SearchTerm,
                        minPrice = parameters.MinPrice,
                        maxPrice = parameters.MaxPrice,
                        orderBy = parameters.OrderBy
                    })!;
            
            case ResourceUriType.NextPage:
                return _linkGenerator.GetUriByAction(
                    HttpContext,
                    action: nameof(GetProducts),
                    controller: "Products",
                    values: new
                    {
                        version = "1.0",
                        pageNumber = parameters.PageNumber + 1,
                        pageSize = parameters.PageSize,
                        category = parameters.Category,
                        searchTerm = parameters.SearchTerm,
                        minPrice = parameters.MinPrice,
                        maxPrice = parameters.MaxPrice,
                        orderBy = parameters.OrderBy
                    })!;
            
            default:
                return _linkGenerator.GetUriByAction(
                    HttpContext,
                    action: nameof(GetProducts),
                    controller: "Products",
                    values: new
                    {
                        version = "1.0",
                        pageNumber = parameters.PageNumber,
                        pageSize = parameters.PageSize,
                        category = parameters.Category,
                        searchTerm = parameters.SearchTerm,
                        minPrice = parameters.MinPrice,
                        maxPrice = parameters.MaxPrice,
                        orderBy = parameters.OrderBy
                    })!;
        }
    }
}

Streaming API for Large Data Sets

[ApiController]
[Route("api/[controller]")]
public class ReportsController : ControllerBase
{
    private readonly IReportService _reportService;
    
    public ReportsController(IReportService reportService)
    {
        _reportService = reportService;
    }
    
    // Streaming a large CSV file
    [HttpGet("sales/export")]
    [Authorize(Roles = "Admin,Analyst")]
    public async Task<IActionResult> ExportSalesData(
        [FromQuery] DateTime startDate,
        [FromQuery] DateTime endDate)
    {
        if (endDate < startDate)
        {
            return BadRequest("End date must be after start date");
        }
        
        // Set up the CSV streaming response
        Response.ContentType = "text/csv";
        Response.Headers.Add(
            "Content-Disposition", 
            $"attachment; filename=sales-{startDate:yyyy-MM-dd}-to-{endDate:yyyy-MM-dd}.csv");
            
        // Stream directly to the response, avoiding large memory usage
        await _reportService.StreamSalesReportAsync(
            HttpContext.Response.Body, 
            startDate, 
            endDate);
            
        return new EmptyResult();
    }
    
    // Server-sent events (SSE) for real-time data
    [HttpGet("sales/realtime")]
    [Authorize(Roles = "Admin,Analyst")]
    public async Task SalesDataRealtime(
        [FromQuery] string region = "all",
        [FromQuery] int intervalSeconds = 5)
    {
        Response.Headers.Add("Content-Type", "text/event-stream");
        Response.Headers.Add("Cache-Control", "no-cache");
        Response.Headers.Add("Connection", "keep-alive");
        
        // Keep connection alive until canceled
        var cancellationToken = HttpContext.RequestAborted;
        
        while (!cancellationToken.IsCancellationRequested)
        {
            var data = await _reportService.GetRealtimeSalesDataAsync(region);
            
            // Format for SSE
            await Response.WriteAsync($"data: {JsonSerializer.Serialize(data)}\n\n");
            await Response.Body.FlushAsync();
            
            await Task.Delay(intervalSeconds * 1000, cancellationToken);
        }
    }
}

// Service implementation for streaming
public class ReportService : IReportService
{
    private readonly ISalesRepository _salesRepository;
    
    public ReportService(ISalesRepository salesRepository)
    {
        _salesRepository = salesRepository;
    }
    
    public async Task StreamSalesReportAsync(
        Stream outputStream, 
        DateTime startDate, 
        DateTime endDate)
    {
        using var writer = new StreamWriter(outputStream, leaveOpen: true);
        using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
        
        // Write header
        csv.WriteHeader<SalesReportRow>();
        await csv.NextRecordAsync();
        
        // Stream data in batches to avoid memory issues
        int page = 1;
        const int pageSize = 1000;
        int totalRecords = 0;
        
        while (true)
        {
            var sales = await _salesRepository.GetSalesForPeriodAsync(
                startDate, 
                endDate, 
                page, 
                pageSize);
                
            if (!sales.Any())
            {
                break;
            }
            
            foreach (var sale in sales)
            {
                var reportRow = new SalesReportRow
                {
                    OrderId = sale.OrderId,
                    OrderDate = sale.OrderDate,
                    CustomerName = sale.Customer.Name,
                    ProductId = sale.ProductId,
                    ProductName = sale.Product.Name,
                    Quantity = sale.Quantity,
                    UnitPrice = sale.UnitPrice,
                    TotalPrice = sale.TotalPrice,
                    Region = sale.Customer.Region
                };
                
                csv.WriteRecord(reportRow);
                await csv.NextRecordAsync();
                totalRecords++;
            }
            
            page++;
            await writer.FlushAsync();
        }
    }
    
    public async Task<RealtimeSalesData> GetRealtimeSalesDataAsync(string region)
    {
        var startTime = DateTime.UtcNow.AddMinutes(-5);
        var endTime = DateTime.UtcNow;
        
        var salesData = await _salesRepository.GetRealtimeSalesAsync(startTime, endTime, region);
        
        return new RealtimeSalesData
        {
            Timestamp = DateTime.UtcNow,
            Region = region,
            TotalSales = salesData.Sum(s => s.TotalPrice),
            OrderCount = salesData.Count(),
            TopProducts = salesData
                .GroupBy(s => s.ProductId)
                .Select(g => new ProductSalesData
                {
                    ProductId = g.Key,
                    ProductName = g.First().Product.Name,
                    Quantity = g.Sum(s => s.Quantity),
                    Revenue = g.Sum(s => s.TotalPrice)
                })
                .OrderByDescending(p => p.Revenue)
                .Take(5)
                .ToList()
        };
    }
}

GraphQL Integration

// First, install required packages:
// dotnet add package HotChocolate.AspNetCore
// dotnet add package HotChocolate.Data.EntityFramework

// Program.cs
builder.Services
    .AddGraphQLServer()
    .AddQueryType<Query>()
    .AddMutationType<Mutation>()
    .AddSubscriptionType<Subscription>()
    .AddType<TaskType>()
    .AddType<UserType>()
    .AddFiltering()
    .AddSorting()
    .AddProjections()
    .AddAuthorization();

// In configure section
app.UseWebSockets();
app.MapGraphQL();

// Query class
public class Query
{
    [UseDbContext(typeof(TaskDbContext))]
    [UseProjection]
    [UseFiltering]
    [UseSorting]
    public IQueryable<TaskEntity> GetTasks([ScopedService] TaskDbContext context)
    {
        return context.Tasks;
    }
    
    [UseDbContext(typeof(TaskDbContext))]
    [UseProjection]
    public async Task<TaskEntity> GetTaskById(
        [ScopedService] TaskDbContext context, 
        int id)
    {
        return await context.Tasks.FindAsync(id);
    }
    
    [UseDbContext(typeof(TaskDbContext))]
    [UseProjection]
    [UseFiltering]
    [UseSorting]
    public IQueryable<UserEntity> GetUsers([ScopedService] TaskDbContext context)
    {
        return context.Users;
    }
}

// Mutation class
public class Mutation
{
    [UseDbContext(typeof(TaskDbContext))]
    [Authorize(Roles = new[] { "Admin", "TaskManager" })]
    public async Task<TaskPayload> CreateTask(
        [ScopedService] TaskDbContext context,
        CreateTaskInput input)
    {
        var task = new TaskEntity
        {
            Title = input.Title,
            Description = input.Description,
            DueDate = input.DueDate,
            Priority = input.Priority,
            CreatedAt = DateTime.UtcNow
        };
        
        context.Tasks.Add(task);
        await context.SaveChangesAsync();
        
        return new TaskPayload(task);
    }
    
    [UseDbContext(typeof(TaskDbContext))]
    [Authorize(Roles = new[] { "Admin", "TaskManager" })]
    public async Task<TaskPayload> UpdateTask(
        [ScopedService] TaskDbContext context,
        UpdateTaskInput input)
    {
        TaskEntity task = await context.Tasks.FindAsync(input.Id);
        
        if (task == null)
        {
            return new TaskPayload(
                new UserError("Task not found", "NOT_FOUND"));
        }
        
        task.Title = input.Title ?? task.Title;
        task.Description = input.Description ?? task.Description;
        task.IsCompleted = input.IsCompleted ?? task.IsCompleted;
        task.DueDate = input.DueDate ?? task.DueDate;
        task.Priority = input.Priority ?? task.Priority;
        
        await context.SaveChangesAsync();
        
        return new TaskPayload(task);
    }
}

// Subscription class
public class Subscription
{
    [Subscribe]
    [Topic]
    public TaskEntity OnTaskCreated([EventMessage] TaskEntity task)
    {
        return task;
    }
    
    [Subscribe]
    [Topic]
    public TaskEntity OnTaskUpdated([EventMessage] TaskEntity task)
    {
        return task;
    }
}

// REST controller with GraphQL integration
[ApiController]
[Route("api/[controller]")]
public class HybridController : ControllerBase
{
    private readonly IHttpClientFactory _clientFactory;
    
    public HybridController(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }
    
    // REST endpoint that uses GraphQL internally for complex queries
    [HttpGet("dashboard")]
    public async Task<IActionResult> GetDashboardData()
    {
        var client = _clientFactory.CreateClient("GraphQLClient");
        
        var query = @"
            query GetDashboardData {
                tasks(
                    where: { isCompleted: { eq: false } }
                    order: { dueDate: ASC }
                    first: 5
                ) {
                    nodes {
                        id
                        title
                        dueDate
                        priority
                        assignedUser {
                            name
                            avatarUrl
                        }
                    }
                }
                recentlyCompletedTasks: tasks(
                    where: { isCompleted: { eq: true } }
                    order: { completedAt: DESC }
                    first: 3
                ) {
                    nodes {
                        id
                        title
                        completedAt
                        assignedUser {
                            name
                        }
                    }
                }
                taskStats {
                    totalCount
                    completedCount
                    overdueCount
                    upcomingCount
                }
                userPerformance {
                    userName
                    tasksCompleted
                    averageCompletionTime
                }
            }";
            
        var request = new StringContent(
            JsonSerializer.Serialize(new { query }),
            Encoding.UTF8,
            "application/json");
            
        var response = await client.PostAsync("/graphql", request);
        var content = await response.Content.ReadAsStringAsync();
        
        // Process GraphQL response and return as REST response
        return Ok(JsonDocument.Parse(content));
    }
}

Conclusion

ASP.NET Core provides a comprehensive, high-performance platform for building modern REST APIs. By leveraging its built-in features such as content negotiation, security, middleware, and OpenAPI integration, developers can create robust, scalable API solutions that follow REST principles.

The framework’s flexibility allows for implementation approaches that suit various project sizes and requirements, from minimal APIs for simpler microservices to controller-based APIs for more complex enterprise applications.

When building REST APIs with ASP.NET Core, consider these key takeaways:

  1. Use the right approach for your needs - Choose between Controller-based or Minimal APIs based on your project’s complexity and team familiarity
  2. Embrace HTTP semantics - Leverage HTTP methods, status codes, and headers to create truly RESTful services
  3. Document your API - Use built-in OpenAPI/Swagger tools to provide interactive documentation
  4. Implement proper validation - Validate all input to ensure data integrity and security
  5. Design with performance in mind - Use asynchronous patterns, caching, and efficient resource management
  6. Monitor and measure - Implement logging, health checks, and metrics to maintain visibility into your API’s behavior

By following these principles and leveraging the patterns and practices outlined in this guide, you can build ASP.NET Core REST APIs that are maintainable, secure, and performant.

Additional Resources