From the initial project setup using the .NET CLI to configuring middleware, controllers, and services, learn every step to build a robust API. Discover best practices for dependency injection, asynchronous actions, and handling exceptions to create scalable, efficient web applications.
Use the .NET CLI to create a new Web API project. This sets up a basic project structure including Program.cs for startup and a WeatherForecast controller as an example.
dotnet new webapi -n MyWebApi
.NET 8 continues the trend towards minimal APIs, allowing you to configure services and endpoints in a simplified and concise manner directly in the Program.cs file.
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/", () => "Hello, World!"); app.Run();
Controllers handle incoming HTTP requests and respond to the client. They are defined by inheriting from ControllerBase and annotating with [ApiController].
[ApiController] [Route("[controller]")] public class MyController : ControllerBase { [HttpGet] public IActionResult Get() => Ok("Hello from MyController"); }
.NET Core’s built-in dependency injection (DI) makes it easy to manage dependencies. You can inject services into your controllers through their constructors.
public class MyService { public string GetMessage() => "Injected message"; } public class MyController : ControllerBase { private readonly MyService _myService; public MyController(MyService myService) { _myService = myService; } [HttpGet] public IActionResult Get() => Ok(_myService.GetMessage()); }
Services (like database contexts, custom services, etc.) are configured in the Program.cs file, making them available for dependency injection throughout your application.
builder.Services.AddScoped<MyService>();
.NET supports environment-specific configuration files (appsettings.json, appsettings.Development.json, etc.), allowing for different settings based on the application’s environment.
// appsettings.Development.json { "Logging": { "LogLevel": { "Default": "Debug" } } }
Middleware components form a pipeline that handles requests and responses. Custom middleware can be created for cross-cutting concerns like logging or error handling.
app.Use(async (context, next) => { // Custom logic before passing to the next middleware await next(); // Custom logic after executing the next middleware });
Routing in .NET Web API is achieved through attribute routing on controllers and action methods. This allows URLs to be mapped directly to controller actions.
[HttpGet("myaction/{id}")] public IActionResult GetAction(int id) => Ok($"Action with ID = {id}");
Model binding automatically maps data from HTTP requests to action method parameters. It supports complex types, including JSON bodies and query string parameters.
public class MyModel { public int Id { get; set; } public string Name { get; set; } } [HttpPost] public IActionResult PostAction([FromBody] MyModel model) => Ok(model);
Data annotations can be used to validate model data. The [ApiController] attribute automatically enforces validation, responding with 400 if the model is invalid.
public class MyModel { [Required] public int Id { get; set; } [StringLength(100)] public string Name { get; set; } }
Asynchronous actions improve scalability by freeing up threads while waiting for I/O operations to complete. Use the async keyword, and return Task or Task<IActionResult>.
[HttpGet("{id}")] public async Task<IActionResult> GetAsync(int id) { var result = await _service.GetByIdAsync(id); return Ok(result); }
Global exception handling allows for centralized error processing, logging, and standardized API responses on unhandled exceptions.
app.UseExceptionHandler(a => a.Run(async context => { var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>(); var exception = exceptionHandlerPathFeature.Error; // Log the exception, generate a custom response, etc. context.Response.StatusCode = 500; await context.Response.WriteAsJsonAsync(new { Error = "An unexpected error occurred" }); }));
API versioning helps manage changes to the API over time. The .NET platform supports versioning through query string, URL path, or request header.
builder.Services.AddApiVersioning(options => { options.AssumeDefaultVersionWhenUnspecified = true; options.DefaultApiVersion = new ApiVersion(1, 0); options.ReportApiVersions = true; });
Content negotiation allows an API to serve different formats of the response based on the Accept header in the request, enabling support for formats like JSON, XML, etc.
builder.Services.AddControllers() .AddXmlDataContractSerializerFormatters();
Customize JSON response formatting, such as camelCase naming or ignoring null values, by configuring JSON serializer settings.
builder.Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; options.JsonSerializerOptions.IgnoreNullValues = true; });
Cross-Origin Resource Sharing (CORS) allows your API to be called from web applications hosted on different domains. Configure the CORS policy as per your requirements.
builder.Services.AddCors(options => { options.AddPolicy("AllowSpecificOrigin", builder => builder.WithOrigins("http://example.com")); }); app.UseCors("AllowSpecificOrigin");
Secure your API by enabling authentication, which verifies the identity of users or services making requests.
builder.Services.AddAuthentication("Bearer") .AddJwtBearer(options => { options.Authority = "https://your-auth-server"; options.Audience = "your-api"; });
After authentication, authorization determines if an authenticated user has permission to perform an action or access a resource.
[Authorize] public class SecureController : ControllerBase { // Action methods here }
Swagger (OpenAPI) provides interactive documentation for your API, allowing developers to understand and consume it easily.
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); app.UseSwagger(); app.UseSwaggerUI();
.NET Core provides a built-in logging framework that can log messages to various outputs (console, debug window, external services, etc.).
logger.LogInformation("This is an informational message"); app.Use(async (context, next) => { logger.LogError("This is an error message before the next middleware"); await next.Invoke(); // Log after calling the next middleware });
Entity Framework Core is an ORM used for data access in .NET applications. It allows you to query and manipulate data using strongly typed objects.
public class MyDbContext : DbContext { public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) {} public DbSet<MyModel> MyModels { get; set; } }
Migrations allow you to apply version control to your database schema by tracking changes in your data models.
dotnet ef migrations add InitialCreate dotnet ef database update
The Repository pattern abstracts the data layer, making your application more modular and easier to maintain.
public interface IRepository<T> { Task<IEnumerable<T>> GetAllAsync(); Task<T> GetByIdAsync(int id); // Other methods... } public class MyRepository<T> : IRepository<T> where T : class { private readonly MyDbContext _context; public MyRepository(MyDbContext context) { _context = context; } // Implement methods... }
Unit testing ensures your Web API functions correctly by testing individual units of code in isolation.
public class MyControllerTests { [Fact] public async Task Get_ReturnsExpectedValue() { // Arrange var serviceMock = new Mock<IMyService>(); serviceMock.Setup(service => service.GetAsync()).ReturnsAsync("test"); var controller = new MyController(serviceMock.Object); // Act var result = await controller.Get(); // Assert Assert.Equal("test", result.Value); } }
.NET Web API can serve as the backend for a front-end application, providing RESTful services.
fetch('https://localhost:5001/mycontroller') .then(response => response.json()) .then(data => console.log(data));
Health checks provide a way to monitor the status of your application and its dependencies, useful for microservices architectures.
builder.Services.AddHealthChecks(); app.MapHealthChecks("/health");
SignalR enables real-time web functionality, allowing server-side code to send asynchronous notifications to client-side web applications.
public class MyHub : Hub { public async Task SendMessage(string user, string message) { await Clients.All.SendAsync("ReceiveMessage", user, message); } }
Response caching reduces the number of requests a server must handle by storing a copy of previously requested resources.
[HttpGet("{id}")] [ResponseCache(Duration = 60)] public IActionResult GetById(int id) { // Retrieve and return your resource }
Serving static files (HTML, CSS, JavaScript, etc.) is essential for backing front-end applications with a .NET Web API.
app.UseStaticFiles(); // Enable static file serving
The options pattern uses classes to represent groups of related settings. Using IOptions<T>, you can access these settings anywhere in your application.
public class MySettings { public string Setting1 { get; set; } // Other settings } builder.Services.Configure<MySettings>(builder.Configuration.GetSection("MySettings")); public class MyService { private readonly MySettings _settings; public MyService(IOptions<MySettings> settings) { _settings = settings.Value; } // Use _settings.Setting1 }
Middleware is software that’s assembled into an application pipeline to handle requests and responses. Custom middleware can be created to perform specific tasks.
public class MyCustomMiddleware { private readonly RequestDelegate _next; public MyCustomMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext httpContext) { // Pre-processing logic here await _next(httpContext); // Call the next middleware in the pipeline // Post-processing logic here } } // Extension method for easy middleware registration public static class MyCustomMiddlewareExtensions { public static IApplicationBuilder UseMyCustomMiddleware(this IApplicationBuilder builder) { return builder.UseMiddleware<MyCustomMiddleware>(); } }
Rate limiting protects your API from overuse by limiting how often a user can make requests within a certain time frame.
// Assume using a third-party library like AspNetCoreRateLimit builder.Services.AddInMemoryRateLimiting(); builder.Services.Configure<IpRateLimitOptions>(options => { options.GeneralRules = new List<RateLimitRule> { new RateLimitRule { Endpoint = "*", Limit = 100, Period = "1h" } }; });
API keys are a simple way to authenticate and authorize API calls. They’re passed from client to server either in the query string or header.
public class ApiKeyMiddleware { private readonly RequestDelegate _next; private const string APIKEYNAME = "x-api-key"; public ApiKeyMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext context) { if (!context.Request.Headers.TryGetValue(APIKEYNAME, out var extractedApiKey)) { context.Response.StatusCode = 401; await context.Response.WriteAsync("API Key was not provided."); return; } // Validate the extracted API Key here... await _next(context); } }
Output caching allows you to store the response to a request. Subsequent requests can be served from the cache, significantly improving performance.
[HttpGet] [ResponseCache(Duration = 120, Location = ResponseCacheLocation.Client, NoStore = false)] public IActionResult Get() { // Your logic here }
Background tasks enable operations to run in the background, independent of user requests, like sending emails or processing long-running jobs.
public class MyBackgroundService : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { // Your background task logic here await Task.Delay(TimeSpan.FromHours(1), stoppingToken); } } }
WebSockets provide a full-duplex communication channel over a single, long-lived connection, ideal for real-time applications.
app.UseWebSockets(); app.Use(async (context, next) => { if (context.WebSockets.IsWebSocketRequest) { WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync(); // Handle the WebSocket request here } else { await next(); } });
Request localization provides a way to localize content for different cultures and languages, based on the request’s information.
var supportedCultures = new[] { "en-US", "fr-FR" }; var localizationOptions = new RequestLocalizationOptions() .SetDefaultCulture(supportedCultures[0]) .AddSupportedCultures(supportedCultures) .AddSupportedUICultures(supportedCultures); app.UseRequestLocalization(localizationOptions);
GraphQL is a query language for APIs. Integrating a .NET Web API with GraphQL allows for more efficient data retrieval.
// Assume using a library like HotChocolate builder.Services .AddGraphQLServer() .AddQueryType<Query>(); app.MapGraphQL();
Monitoring and telemetry involve collecting, analyzing, and acting on data about your application’s performance and usage.
// Assume using Application Insights builder.Services.AddApplicationInsightsTelemetry("YOUR_INSTRUMENTATION_KEY");
SignalR is a library that simplifies adding real-time web functionality to apps. Real-time web functionality is the ability to have server code push content to connected clients instantly as it happens, not requiring the server to wait for a client to request new data. SignalR is perfect for developing chat applications, real-time dashboards, and more interactive web applications.
public class ChatHub : Hub { public async Task SendMessage(string user, string message) { // Call the broadcastMessage method to update clients. await Clients.All.SendAsync("broadcastMessage", user, message); } } // Startup or Program.cs app.MapHub<ChatHub>("/chathub");
Integration in Program.cs:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // Other configurations... app.UseEndpoints(endpoints => { endpoints.MapHub<ChatHub>("/chathub"); }); }
Entity Framework Core allows for the mapping of complex relationships between entities, such as one-to-one, one-to-many, and many-to-many.
public class Author { public int AuthorId { get; set; } public string Name { get; set; } public ICollection<Book> Books { get; set; } } public class Book { public int BookId { get; set; } public string Title { get; set; } public int AuthorId { get; set; } public Author Author { get; set; } }
Custom validation attributes allow you to define your validation logic for data models, extending the built-in validation attributes.
public class MyCustomValidationAttribute : ValidationAttribute { protected override ValidationResult IsValid(object value, ValidationContext validationContext) { // Your custom validation logic here if (value is int intValue && intValue > 0) { return ValidationResult.Success; } return new ValidationResult("Value must be positive"); } } public class MyModel { [MyCustomValidationAttribute] public int MyProperty { get; set; } }
.NET’s options pattern supports complex configuration scenarios, including nested objects, lists, and validation.
public class MyOptions { public MyNestedOptions Nested { get; set; } public List<string> Items { get; set; } } public class MyNestedOptions { public string Key { get; set; } } // In Program.cs or Startup.cs builder.Services.Configure<MyOptions>(builder.Configuration.GetSection("MyOptions"));
Monitoring and profiling an application can identify bottlenecks and inefficiencies, essential for optimizing performance.
app.UseMiniProfiler();
Enhance your API documentation by integrating XML comments into your Swagger UI, providing a richer experience for developers consuming your API.
builder.Services.AddSwaggerGen(c => { var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); c.IncludeXmlComments(xmlPath); });
Globalization and localization allow your application to support multiple languages and cultures, making it accessible to a global audience.
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); app.UseRequestLocalization(app.Services.GetRequiredService<IOptions<RequestLocalizationOptions>>().Value);
Improving the security of your web application by adding various HTTP headers can protect against common attacks and vulnerabilities.
app.UseHsts(); app.UseXContentTypeOptions(); app.UseReferrerPolicy(opts => opts.NoReferrer()); app.UseXXssProtection(options => options.EnabledWithBlockMode()); app.UseXfo(options => options.Deny());
Feature flags allow you to toggle features of your application on and off without deploying new code, facilitating easier testing and rollouts.
// Using a library like Microsoft.FeatureManagement builder.Services.AddFeatureManagement();
Blazor allows you to build interactive web UIs using C# instead of JavaScript. Integrating Blazor with Web API provides a seamless full-stack development experience.
// In a Blazor Server app @code { private IEnumerable<WeatherForecast> forecasts; protected override async Task OnInitializedAsync() { forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast"); } }
Response compression can reduce the size of your API responses, improving load times for clients over slow networks.
builder.Services.AddResponseCompression(options => { options.Providers.Add<GzipCompressionProvider>(); options.EnableForHttps = true; }); app.UseResponseCompression();
Thank you for being a part of the C# community! Before you leave:
Follow us: X | LinkedIn | Dev.to | Hashnode | Newsletter | Tumblr
Visit our other platforms: GitHub | Instagram | Tiktok | Quora | Daily.dev
Also published here