
Table of Contents
Introduction to RESTful APIs
REST Principles & Constraints
Setting Up a RESTful API Project
API Versioning Strategies
Advanced Serialization Techniques
Input Validation & Model Binding
Error Handling & Exception Management
Authentication & Authorization
Caching Strategies
Rate Limiting & Throttling
Documentation with Swagger/OpenAPI
Testing RESTful APIs
Performance Optimization
Real-World E-Commerce API
Microservices Communication
Monitoring & Logging
Security Best Practices
Deployment Strategies
API Gateway Integration
Future Trends & Best Practices
1. Introduction to RESTful APIs {#introduction}
REST (Representational State Transfer) has become the standard architectural style for designing networked applications. In the ASP.NET Core ecosystem, building RESTful APIs involves creating services that adhere to REST principles while leveraging the powerful features of the framework.
What Makes an API RESTful?
RESTful APIs are characterized by:
Statelessness: Each request contains all necessary information
Resource-Based: Focus on resources rather than actions
Uniform Interface: Consistent naming and structure
Cacheability: Responses can be cached
Layered System: Architecture can have multiple layers
Code on Demand: Optional execution of client-side code
2. REST Principles & Constraints {#rest-principles}
Core REST Constraints
// Example demonstrating REST principles in ASP.NET Core
public class RESTPrinciplesDemo
{
// Stateless - each request contains all necessary information
public class StatelessAuthentication
{
// Token must be included in every request
public string BearerToken { get; set; } = string.Empty;
}
// Resource-based URL structure
public class ResourceBasedUrls
{
// Good RESTful design
// GET /api/products/1
// POST /api/products
// PUT /api/products/1
// DELETE /api/products/1
// Not RESTful
// GET /api/getProduct?id=1
// POST /api/createProduct
// POST /api/updateProduct
// GET /api/deleteProduct?id=1
}
// Uniform interface with standard HTTP methods
public class UniformInterface
{
// GET - Retrieve resources
// POST - Create resources
// PUT - Update/replace resources
// PATCH - Partial update
// DELETE - Remove resources
}
}
3. Setting Up RESTful API Project {#setup-project}
Complete Project Structure
RESTfulApiDemo/
├── Controllers/
│ ├── ProductsController.cs
│ ├── OrdersController.cs
│ └── UsersController.cs
├── Models/
│ ├── DTOs/
│ ├── Entities/
│ └── Requests/
├── Services/
│ ├── Interfaces/
│ └── Implementations/
├── Data/
├── Middleware/
├── Filters/
├── Program.cs
└── appsettings.json
Program.cs - Complete Setup
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RESTfulApiDemo.Data;
using RESTfulApiDemo.Services;
using System.Text.Json.Serialization;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase;
});
// Database Context
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Custom Services
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IUserService, UserService>();
// API Versioning
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
});
builder.Services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
// Swagger/OpenAPI
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
{
Title = "E-Commerce API",
Version = "v1",
Description = "A comprehensive RESTful API for e-commerce operations"
});
});
// Caching
builder.Services.AddMemoryCache();
builder.Services.AddResponseCaching();
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "E-Commerce API v1");
});
}
app.UseHttpsRedirection();
app.UseResponseCaching();
app.UseAuthorization();
app.MapControllers();
app.Run();
4. API Versioning Strategies {#api-versioning}
Multiple Versioning Approaches
// Models for versioning demonstration
namespace RESTfulApiDemo.Models.V1
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public int StockQuantity { get; set; }
public DateTime CreatedDate { get; set; }
}
public class ProductResponse
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Stock { get; set; }
}
}
namespace RESTfulApiDemo.Models.V2
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Currency { get; set; } = "USD";
public int StockQuantity { get; set; }
public ProductCategory Category { get; set; } = new();
public List<string> Tags { get; set; } = new();
public DateTime CreatedDate { get; set; }
public DateTime? UpdatedDate { get; set; }
}
public class ProductCategory
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
}
public class ProductResponse
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Currency { get; set; } = "USD";
public int Stock { get; set; }
public string Category { get; set; } = string.Empty;
public List<string> Tags { get; set; } = new();
}
}
Version 1 Controller
using Microsoft.AspNetCore.Mvc;
using RESTfulApiDemo.Models.V1;
using RESTfulApiDemo.Services;
namespace RESTfulApiDemo.Controllers.V1
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
[HttpGet]
[ProducesResponseType(typeof(List<ProductResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> GetProducts()
{
try
{
var products = await _productService.GetAllProductsAsync();
var response = products.Select(p => new ProductResponse
{
Id = p.Id,
Name = p.Name,
Description = p.Description,
Price = p.Price,
Stock = p.StockQuantity
}).ToList();
return Ok(response);
}
catch (Exception ex)
{
return StatusCode(500, new { error = "An error occurred while retrieving products" });
}
}
[HttpGet("{id}")]
[ProducesResponseType(typeof(ProductResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetProduct(int id)
{
var product = await _productService.GetProductByIdAsync(id);
if (product == null)
{
return NotFound(new { error = $"Product with ID {id} not found" });
}
var response = new ProductResponse
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Price = product.Price,
Stock = product.StockQuantity
};
return Ok(response);
}
[HttpPost]
[ProducesResponseType(typeof(ProductResponse), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateProduct([FromBody] Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var createdProduct = await _productService.CreateProductAsync(product);
var response = new ProductResponse
{
Id = createdProduct.Id,
Name = createdProduct.Name,
Description = createdProduct.Description,
Price = createdProduct.Price,
Stock = createdProduct.StockQuantity
};
return CreatedAtAction(nameof(GetProduct), new { id = createdProduct.Id, version = "1" }, response);
}
}
}
Version 2 Controller with Enhanced Features
using Microsoft.AspNetCore.Mvc;
using RESTfulApiDemo.Models.V2;
using RESTfulApiDemo.Services;
namespace RESTfulApiDemo.Controllers.V2
{
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
[HttpGet]
[ProducesResponseType(typeof(PagedResponse<ProductResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> GetProducts(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? category = null,
[FromQuery] decimal? minPrice = null,
[FromQuery] decimal? maxPrice = null,
[FromQuery] string? search = null)
{
if (page < 1 || pageSize < 1 || pageSize > 100)
{
return BadRequest(new { error = "Invalid pagination parameters" });
}
var (products, totalCount) = await _productService.GetProductsPagedAsync(
page, pageSize, category, minPrice, maxPrice, search);
var response = new PagedResponse<ProductResponse>
{
Data = products.Select(p => new ProductResponse
{
Id = p.Id,
Name = p.Name,
Description = p.Description,
Price = p.Price,
Currency = p.Currency,
Stock = p.StockQuantity,
Category = p.Category.Name,
Tags = p.Tags
}).ToList(),
Page = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
};
return Ok(response);
}
[HttpGet("{id}")]
[ProducesResponseType(typeof(ProductResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ResponseCache(Duration = 30)] // Cache for 30 seconds
public async Task<IActionResult> GetProduct(int id)
{
var product = await _productService.GetProductByIdAsync(id);
if (product == null)
{
return NotFound(new { error = $"Product with ID {id} not found" });
}
var response = new ProductResponse
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Price = product.Price,
Currency = product.Currency,
Stock = product.StockQuantity,
Category = product.Category.Name,
Tags = product.Tags
};
return Ok(response);
}
[HttpPost]
[ProducesResponseType(typeof(ProductResponse), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateProduct([FromBody] Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var createdProduct = await _productService.CreateProductAsync(product);
var response = new ProductResponse
{
Id = createdProduct.Id,
Name = createdProduct.Name,
Description = createdProduct.Description,
Price = createdProduct.Price,
Currency = createdProduct.Currency,
Stock = createdProduct.StockQuantity,
Category = createdProduct.Category.Name,
Tags = createdProduct.Tags
};
return CreatedAtAction(nameof(GetProduct), new { id = createdProduct.Id, version = "2" }, response);
}
[HttpPatch("{id}")]
[ProducesResponseType(typeof(ProductResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> UpdateProduct(int id, [FromBody] ProductUpdateRequest request)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var updatedProduct = await _productService.UpdateProductAsync(id, request);
if (updatedProduct == null)
{
return NotFound(new { error = $"Product with ID {id} not found" });
}
var response = new ProductResponse
{
Id = updatedProduct.Id,
Name = updatedProduct.Name,
Description = updatedProduct.Description,
Price = updatedProduct.Price,
Currency = updatedProduct.Currency,
Stock = updatedProduct.StockQuantity,
Category = updatedProduct.Category.Name,
Tags = updatedProduct.Tags
};
return Ok(response);
}
}
}
5. Advanced Serialization Techniques {#serialization}
Custom JSON Serialization Configuration
using System.Text.Json;
using System.Text.Json.Serialization;
namespace RESTfulApiDemo.Configuration
{
public static class JsonSerializationConfiguration
{
public static JsonSerializerOptions ConfigureJsonOptions()
{
return new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
ReferenceHandler = ReferenceHandler.IgnoreCycles,
WriteIndented = true,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase),
new CustomDateTimeConverter()
}
};
}
}
public class CustomDateTimeConverter : JsonConverter<DateTime>
{
private readonly string _format = "yyyy-MM-ddTHH:mm:ss.fffZ";
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return DateTime.Parse(reader.GetString()!);
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToUniversalTime().ToString(_format));
}
}
}
Custom Response Formatting
using System.Text.Json;
namespace RESTfulApiDemo.Models
{
public class ApiResponse<T>
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public T? Data { get; set; }
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public string? RequestId { get; set; }
public static ApiResponse<T> SuccessResponse(T data, string message = "Success")
{
return new ApiResponse<T>
{
Success = true,
Message = message,
Data = data
};
}
public static ApiResponse<T> ErrorResponse(string message, string? requestId = null)
{
return new ApiResponse<T>
{
Success = false,
Message = message,
RequestId = requestId
};
}
}
public class PagedResponse<T>
{
public List<T> Data { get; set; } = new();
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalCount { get; set; }
public int TotalPages { get; set; }
public bool HasPrevious => Page > 1;
public bool HasNext => Page < TotalPages;
}
}
6. Input Validation & Model Binding {#validation-binding}
Advanced Validation with FluentValidation
using FluentValidation;
namespace RESTfulApiDemo.Models.V2
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Currency { get; set; } = "USD";
public int StockQuantity { get; set; }
public ProductCategory Category { get; set; } = new();
public List<string> Tags { get; set; } = new();
public DateTime CreatedDate { get; set; }
public DateTime? UpdatedDate { get; set; }
}
public class ProductCreateRequest
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Currency { get; set; } = "USD";
public int StockQuantity { get; set; }
public int CategoryId { get; set; }
public List<string> Tags { get; set; } = new();
}
public class ProductUpdateRequest
{
public string? Name { get; set; }
public string? Description { get; set; }
public decimal? Price { get; set; }
public int? StockQuantity { get; set; }
public int? CategoryId { get; set; }
public List<string>? Tags { get; set; }
}
}
namespace RESTfulApiDemo.Validators
{
public class ProductCreateRequestValidator : AbstractValidator<ProductCreateRequest>
{
public ProductCreateRequestValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Product name is required")
.Length(3, 100).WithMessage("Product name must be between 3 and 100 characters")
.Matches("^[a-zA-Z0-9 ]+$").WithMessage("Product name can only contain letters, numbers, and spaces");
RuleFor(x => x.Description)
.MaximumLength(500).WithMessage("Description cannot exceed 500 characters");
RuleFor(x => x.Price)
.GreaterThan(0).WithMessage("Price must be greater than 0")
.LessThan(1000000).WithMessage("Price must be less than 1,000,000");
RuleFor(x => x.Currency)
.Length(3).WithMessage("Currency must be a 3-letter code")
.Matches("^[A-Z]{3}$").WithMessage("Currency must be in ISO 4217 format");
RuleFor(x => x.StockQuantity)
.GreaterThanOrEqualTo(0).WithMessage("Stock quantity cannot be negative");
RuleFor(x => x.CategoryId)
.GreaterThan(0).WithMessage("Category ID must be valid");
RuleForEach(x => x.Tags)
.MaximumLength(50).WithMessage("Tag cannot exceed 50 characters")
.Matches("^[a-zA-Z0-9-#]+$").WithMessage("Tags can only contain letters, numbers, hyphens, and hashes");
}
}
public class ProductUpdateRequestValidator : AbstractValidator<ProductUpdateRequest>
{
public ProductUpdateRequestValidator()
{
RuleFor(x => x.Name)
.Length(3, 100).When(x => !string.IsNullOrEmpty(x.Name))
.WithMessage("Product name must be between 3 and 100 characters");
RuleFor(x => x.Description)
.MaximumLength(500).When(x => !string.IsNullOrEmpty(x.Description))
.WithMessage("Description cannot exceed 500 characters");
RuleFor(x => x.Price)
.GreaterThan(0).When(x => x.Price.HasValue)
.WithMessage("Price must be greater than 0");
RuleFor(x => x.StockQuantity)
.GreaterThanOrEqualTo(0).When(x => x.StockQuantity.HasValue)
.WithMessage("Stock quantity cannot be negative");
}
}
}
Custom Model Binders
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Globalization;
namespace RESTfulApiDemo.ModelBinders
{
public class DecimalModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var modelName = bindingContext.ModelName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);
var value = valueProviderResult.FirstValue;
if (string.IsNullOrEmpty(value))
{
return Task.CompletedTask;
}
// Remove any commas or spaces
value = value.Replace(",", "").Replace(" ", "");
if (!decimal.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var decimalValue))
{
bindingContext.ModelState.TryAddModelError(modelName, "Decimal value is invalid.");
return Task.CompletedTask;
}
bindingContext.Result = ModelBindingResult.Success(decimalValue);
return Task.CompletedTask;
}
}
public class DecimalModelBinderProvider : IModelBinderProvider
{
public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(decimal) || context.Metadata.ModelType == typeof(decimal?))
{
return new DecimalModelBinder();
}
return null;
}
}
}
7. Error Handling & Exception Management {#error-handling}
Global Exception Handling Middleware
using System.Net;
using System.Text.Json;
namespace RESTfulApiDemo.Middleware
{
public class GlobalExceptionHandlerMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;
private readonly IWebHostEnvironment _environment;
public GlobalExceptionHandlerMiddleware(
RequestDelegate next,
ILogger<GlobalExceptionHandlerMiddleware> logger,
IWebHostEnvironment environment)
{
_next = next;
_logger = logger;
_environment = environment;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var code = HttpStatusCode.InternalServerError;
var message = "An unexpected error occurred.";
string? stackTrace = null;
switch (exception)
{
case KeyNotFoundException _:
code = HttpStatusCode.NotFound;
message = "The requested resource was not found.";
break;
case ArgumentException _:
case InvalidOperationException _:
code = HttpStatusCode.BadRequest;
message = exception.Message;
break;
case UnauthorizedAccessException _:
code = HttpStatusCode.Unauthorized;
message = "Access denied.";
break;
case NotImplementedException _:
code = HttpStatusCode.NotImplemented;
message = "The requested functionality is not implemented.";
break;
}
// Log the exception
_logger.LogError(exception, "An exception occurred: {Message}", exception.Message);
// Include stack trace in development
if (_environment.IsDevelopment())
{
stackTrace = exception.StackTrace;
}
var response = new
{
Success = false,
Message = message,
StackTrace = stackTrace,
Timestamp = DateTime.UtcNow,
RequestId = context.TraceIdentifier
};
var result = JsonSerializer.Serialize(response, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)code;
await context.Response.WriteAsync(result);
}
}
// Custom exceptions
public class ProductNotFoundException : Exception
{
public ProductNotFoundException(int productId)
: base($"Product with ID {productId} was not found.")
{
ProductId = productId;
}
public int ProductId { get; }
}
public class InsufficientStockException : Exception
{
public InsufficientStockException(string productName, int requested, int available)
: base($"Insufficient stock for {productName}. Requested: {requested}, Available: {available}")
{
ProductName = productName;
RequestedQuantity = requested;
AvailableQuantity = available;
}
public string ProductName { get; }
public int RequestedQuantity { get; }
public int AvailableQuantity { get; }
}
public class DuplicateProductException : Exception
{
public DuplicateProductException(string productName)
: base($"A product with the name '{productName}' already exists.")
{
ProductName = productName;
}
public string ProductName { get; }
}
}
Exception Handling Filters
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace RESTfulApiDemo.Filters
{
public class ApiExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly IDictionary<Type, Action<ExceptionContext>> _exceptionHandlers;
public ApiExceptionFilterAttribute()
{
_exceptionHandlers = new Dictionary<Type, Action<ExceptionContext>>
{
{ typeof(ProductNotFoundException), HandleProductNotFoundException },
{ typeof(InsufficientStockException), HandleInsufficientStockException },
{ typeof(DuplicateProductException), HandleDuplicateProductException },
{ typeof(ValidationException), HandleValidationException }
};
}
public override void OnException(ExceptionContext context)
{
HandleException(context);
base.OnException(context);
}
private void HandleException(ExceptionContext context)
{
var type = context.Exception.GetType();
if (_exceptionHandlers.ContainsKey(type))
{
_exceptionHandlers[type].Invoke(context);
return;
}
HandleUnknownException(context);
}
private void HandleProductNotFoundException(ExceptionContext context)
{
var exception = (ProductNotFoundException)context.Exception;
var details = new ProblemDetails
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Title = "Product not found",
Detail = exception.Message,
Instance = context.HttpContext.Request.Path
};
context.Result = new NotFoundObjectResult(details);
context.ExceptionHandled = true;
}
private void HandleInsufficientStockException(ExceptionContext context)
{
var exception = (InsufficientStockException)context.Exception;
var details = new ProblemDetails
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
Title = "Insufficient stock",
Detail = exception.Message,
Instance = context.HttpContext.Request.Path,
Extensions =
{
["productName"] = exception.ProductName,
["requestedQuantity"] = exception.RequestedQuantity,
["availableQuantity"] = exception.AvailableQuantity
}
};
context.Result = new BadRequestObjectResult(details);
context.ExceptionHandled = true;
}
private void HandleDuplicateProductException(ExceptionContext context)
{
var exception = (DuplicateProductException)context.Exception;
var details = new ProblemDetails
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.8",
Title = "Duplicate product",
Detail = exception.Message,
Instance = context.HttpContext.Request.Path
};
context.Result = new ConflictObjectResult(details);
context.ExceptionHandled = true;
}
private void HandleValidationException(ExceptionContext context)
{
var exception = (ValidationException)context.Exception;
var details = new ValidationProblemDetails(exception.Errors)
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
Title = "Validation failed",
Detail = "One or more validation errors occurred.",
Instance = context.HttpContext.Request.Path
};
context.Result = new BadRequestObjectResult(details);
context.ExceptionHandled = true;
}
private void HandleUnknownException(ExceptionContext context)
{
var details = new ProblemDetails
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
Title = "An error occurred while processing your request.",
Status = StatusCodes.Status500InternalServerError,
Instance = context.HttpContext.Request.Path
};
if (context.HttpContext.RequestServices.GetService<IWebHostEnvironment>()?.IsDevelopment() == true)
{
details.Detail = context.Exception.ToString();
details.Extensions["stackTrace"] = context.Exception.StackTrace;
}
context.Result = new ObjectResult(details)
{
StatusCode = StatusCodes.Status500InternalServerError
};
context.ExceptionHandled = true;
}
}
public class ValidationException : Exception
{
public ValidationException() : base("One or more validation failures have occurred.")
{
Errors = new Dictionary<string, string[]>();
}
public ValidationException(IDictionary<string, string[]> errors)
: this()
{
Errors = errors;
}
public IDictionary<string, string[]> Errors { get; }
}
}
8. Authentication & Authorization {#authentication}
JWT Authentication Setup
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
namespace RESTfulApiDemo.Configuration
{
public static class AuthenticationConfiguration
{
public static IServiceCollection AddJwtAuthentication(this IServiceCollection services, IConfiguration configuration)
{
var jwtSettings = configuration.GetSection("JwtSettings");
var secretKey = jwtSettings["SecretKey"]
?? throw new InvalidOperationException("JWT Secret Key is not configured");
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings["Issuer"],
ValidAudience = jwtSettings["Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)),
ClockSkew = TimeSpan.Zero
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
{
context.Response.Headers.Add("Token-Expired", "true");
}
return Task.CompletedTask;
}
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdminRole", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("RequireCustomerRole", policy =>
policy.RequireRole("Customer", "Admin"));
options.AddPolicy("Over18", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim(c =>
(c.Type == "DateOfBirth" &&
DateTime.TryParse(c.Value, out var dateOfBirth) &&
dateOfBirth <= DateTime.Now.AddYears(-18)) ||
context.User.IsInRole("Admin")
)));
});
return services;
}
}
}
User Management Models and Services
using System.ComponentModel.DataAnnotations;
using System.Security.Claims;
namespace RESTfulApiDemo.Models
{
public class User
{
public int Id { get; set; }
[Required]
[StringLength(50)]
public string Username { get; set; } = string.Empty;
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
public string PasswordHash { get; set; } = string.Empty;
[StringLength(100)]
public string FirstName { get; set; } = string.Empty;
[StringLength(100)]
public string LastName { get; set; } = string.Empty;
public DateTime DateOfBirth { get; set; }
public string Role { get; set; } = "Customer";
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
public DateTime? LastLogin { get; set; }
}
public class UserRegisterRequest
{
[Required]
[StringLength(50, MinimumLength = 3)]
public string Username { get; set; } = string.Empty;
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Required]
[StringLength(100, MinimumLength = 6)]
public string Password { get; set; } = string.Empty;
[Required]
[Compare("Password")]
public string ConfirmPassword { get; set; } = string.Empty;
[StringLength(100)]
public string FirstName { get; set; } = string.Empty;
[StringLength(100)]
public string LastName { get; set; } = string.Empty;
public DateTime DateOfBirth { get; set; }
}
public class UserLoginRequest
{
[Required]
public string Username { get; set; } = string.Empty;
[Required]
public string Password { get; set; } = string.Empty;
}
public class AuthResponse
{
public string Token { get; set; } = string.Empty;
public DateTime Expires { get; set; }
public UserResponse User { get; set; } = new();
}
public class UserResponse
{
public int Id { get; set; }
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string Role { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}
}
namespace RESTfulApiDemo.Services
{
public interface IUserService
{
Task<User?> AuthenticateAsync(string username, string password);
Task<User> RegisterAsync(UserRegisterRequest request);
Task<User?> GetUserByIdAsync(int id);
Task<bool> UserExistsAsync(string username, string email);
string GenerateJwtToken(User user);
ClaimsPrincipal? ValidateToken(string token);
}
public class UserService : IUserService
{
private readonly List<User> _users = new();
private readonly IConfiguration _configuration;
public UserService(IConfiguration configuration)
{
_configuration = configuration;
// Add default admin user
_users.Add(new User
{
Id = 1,
Username = "admin",
Email = "[email protected]",
PasswordHash = BCrypt.Net.BCrypt.HashPassword("admin123"),
FirstName = "System",
LastName = "Administrator",
Role = "Admin",
DateOfBirth = new DateTime(1980, 1, 1)
});
}
public async Task<User?> AuthenticateAsync(string username, string password)
{
var user = _users.FirstOrDefault(u =>
u.Username == username || u.Email == username);
if (user == null || !BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
return null;
user.LastLogin = DateTime.UtcNow;
return user;
}
public async Task<User> RegisterAsync(UserRegisterRequest request)
{
if (await UserExistsAsync(request.Username, request.Email))
throw new InvalidOperationException("Username or email already exists");
var user = new User
{
Id = _users.Count + 1,
Username = request.Username,
Email = request.Email,
PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.Password),
FirstName = request.FirstName,
LastName = request.LastName,
DateOfBirth = request.DateOfBirth,
Role = "Customer"
};
_users.Add(user);
return user;
}
public async Task<User?> GetUserByIdAsync(int id)
{
return _users.FirstOrDefault(u => u.Id == id);
}
public async Task<bool> UserExistsAsync(string username, string email)
{
return _users.Any(u => u.Username == username || u.Email == email);
}
public string GenerateJwtToken(User user)
{
var tokenHandler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_configuration["JwtSettings:SecretKey"]!);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.Role, user.Role),
new Claim("DateOfBirth", user.DateOfBirth.ToString("yyyy-MM-dd"))
}),
Expires = DateTime.UtcNow.AddHours(24),
Issuer = _configuration["JwtSettings:Issuer"],
Audience = _configuration["JwtSettings:Audience"],
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
public ClaimsPrincipal? ValidateToken(string token)
{
try
{
var tokenHandler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_configuration["JwtSettings:SecretKey"]!);
var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = true,
ValidateAudience = true,
ValidIssuer = _configuration["JwtSettings:Issuer"],
ValidAudience = _configuration["JwtSettings:Audience"],
ClockSkew = TimeSpan.Zero
}, out _);
return principal;
}
catch
{
return null;
}
}
}
}

Join the conversation! Your thoughts help the community grow.