Welcome to Module 7 of our ASP.NET Core Complete Course: Beginner to Advanced Guide for Modern Web Development. In this module, we dive deep into Working with Web APIs in ASP.NET Core, a critical skill for building modern, scalable, and interoperable web applications. This comprehensive, SEO-friendly guide covers Creating RESTful APIs, Attribute Routing for APIs, Model Binding & Validation, Versioning APIs, Consuming APIs with HttpClient, and Swagger/OpenAPI Documentation.
This blog post is packed with practical examples, detailed explanations, real-world scenarios, pros and cons, alternatives, and best practices for security, performance, and error handling. Whether you're a beginner or an advanced developer, this module will equip you with the skills to build robust, well-documented APIs and integrate them into your applications. Let’s get started!
Table of Contents
Introduction to Web APIs in ASP.NET Core
Creating RESTful APIs with ASP.NET Core
What is a RESTful API?
Pros, Cons, and Alternatives
Setting Up an API Project
Example: Building a RESTful API
Attribute Routing for APIs
What is Attribute Routing?
Pros, Cons, and Alternatives
Example: Implementing Attribute Routing
Model Binding & Validation
Understanding Model Binding
Implementing Validation
Pros, Cons, and Alternatives
Example: Model Binding and Validation in an API
Versioning APIs
Why Version APIs?
Pros, Cons, and Alternatives
Example: Implementing API Versioning
Consuming APIs with HttpClient
What is HttpClient?
Pros, Cons, and Alternatives
Example: Consuming an API with HttpClient
Swagger/OpenAPI Documentation
What is Swagger/OpenAPI?
Pros, Cons, and Alternatives
Example: Adding Swagger Documentation
Best Practices for Web APIs in ASP.NET Core
Security Best Practices
Performance Best Practices
Error Handling Best Practices
Real-Life Example: Building an E-Commerce Product API
Conclusion
FAQs
Introduction to Web APIs in ASP.NET Core
Web APIs are the backbone of modern web applications, enabling communication between clients (e.g., browsers, mobile apps) and servers. In ASP.NET Core, building Web APIs is streamlined with powerful features for creating RESTful services, handling routing, validating input, versioning, and documenting APIs.
In Module 7, we’ll explore:
Creating RESTful APIs: Building APIs that follow REST principles.
Attribute Routing: Defining precise URL patterns for APIs.
Model Binding & Validation: Handling input data and ensuring data integrity.
Versioning APIs: Supporting backward compatibility as APIs evolve.
Consuming APIs with HttpClient: Integrating APIs into client applications.
Swagger/OpenAPI Documentation: Generating interactive API documentation.
This blog post includes detailed explanations, practical examples, pros and cons, alternatives, and best practices for security, performance, and error handling. We’ll also walk through a real-life e-commerce product API to demonstrate these concepts in action.
Why is this important?
RESTful APIs: Enable scalable, interoperable communication.
Attribute Routing: Provides precise control over API endpoints.
Model Binding & Validation: Ensures robust data handling.
Versioning: Maintains compatibility as APIs evolve.
HttpClient: Simplifies API consumption in .NET applications.
Swagger/OpenAPI: Enhances API usability with interactive documentation.
Let’s dive into each topic with detailed explanations and real-world examples.
Creating RESTful APIs with ASP.NET Core
What is a RESTful API?
A RESTful API (Representational State Transfer) is an architectural style for designing networked applications. It uses HTTP methods (GET, POST, PUT, DELETE) to perform CRUD operations (Create, Read, Update, Delete) on resources identified by URLs.
REST Principles
Stateless: Each request is independent, with no server-side session.
Client-Server: Separates client (UI) from server (data).
Uniform Interface: Uses standard HTTP methods and status codes.
Resource-Based: Resources (e.g., /products) are accessed via URLs.
Hypermedia: Responses include links to related resources (HATEOAS).
Pros, Cons, and Alternatives
Pros
Scalability: Stateless design suits distributed systems.
Interoperability: Works with any client (web, mobile, etc.).
Standardized: Uses HTTP standards for consistency.
Flexibility: Supports JSON, XML, and other formats.
Cons
Complexity: Implementing HATEOAS or complex resources can be challenging.
Performance: Multiple requests may be needed for related data.
Security: Requires careful implementation (e.g., authentication, rate limiting).
Alternatives
GraphQL: For flexible, client-driven queries.
gRPC: For high-performance, binary communication.
SOAP: For legacy systems requiring strict contracts.
Setting Up an API Project
To create a RESTful API in ASP.NET Core:
Create an API project using the webapi template.
Define models and controllers.
Configure services and middleware.
Example: Building a RESTful API
Let’s create a RESTful API for managing products.
Step 1: Create a New API Project
dotnet new webapi -o MyApiApp
cd MyApiApp
Step 2: Install EF Core Packages
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
Step 3: Define the ModelCreate Models/Product.cs:
namespace MyApiApp.Models
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}
}
Step 4: Create the Database ContextCreate Data/AppDbContext.cs:
using Microsoft.EntityFrameworkCore;
using MyApiApp.Models;
namespace MyApiApp.Data
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<Product> Products { get; set; }
}
}
Step 5: Configure ServicesUpdate appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=MyApiAppDb;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
Update Program.cs:
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MyApiApp.Data;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
app.Run();
Step 6: Create a ControllerCreate Controllers/ProductsController.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MyApiApp.Data;
using MyApiApp.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MyApiApp.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
private readonly AppDbContext _context;
public ProductsController(AppDbContext context)
{
_context = context;
}
// GET: api/products
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
{
try
{
return await _context.Products.ToListAsync();
}
catch (Exception ex)
{
return StatusCode(500, new { error = "Failed to retrieve products", details = ex.Message });
}
}
// GET: api/products/5
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetProduct(int id)
{
try
{
var product = await _context.Products.FindAsync(id);
if (product == null)
{
return NotFound(new { error = "Product not found" });
}
return product;
}
catch (Exception ex)
{
return StatusCode(500, new { error = "Failed to retrieve product", details = ex.Message });
}
}
// POST: api/products
[HttpPost]
public async Task<ActionResult<Product>> PostProduct(Product product)
{
try
{
_context.Products.Add(product);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
catch (Exception ex)
{
return StatusCode(500, new { error = "Failed to create product", details = ex.Message });
}
}
// PUT: api/products/5
[HttpPut("{id}")]
public async Task<IActionResult> PutProduct(int id, Product product)
{
if (id != product.Id)
{
return BadRequest(new { error = "ID mismatch" });
}
try
{
_context.Entry(product).State = EntityState.Modified;
await _context.SaveChangesAsync();
return NoContent();
}
catch (DbUpdateConcurrencyException)
{
if (!_context.Products.Any(e => e.Id == id))
{
return NotFound(new { error = "Product not found" });
}
throw;
}
catch (Exception ex)
{
return StatusCode(500, new { error = "Failed to update product", details = ex.Message });
}
}
// DELETE: api/products/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProduct(int id)
{
try
{
var product = await _context.Products.FindAsync(id);
if (product == null)
{
return NotFound(new { error = "Product not found" });
}
_context.Products.Remove(product);
await _context.SaveChangesAsync();
return NoContent();
}
catch (Exception ex)
{
return StatusCode(500, new { error = "Failed to delete product", details = ex.Message });
}
}
}
}
Step 7: Create Migrations
dotnet ef migrations add InitialCreate
dotnet ef database update
Step 8: Test the APIRun the application with dotnet run. Use Postman or curl to test:
GET /api/products: Retrieve all products.
GET /api/products/1: Retrieve a specific product.
POST /api/products: Create a product (e.g., { "name": "Laptop", "price": 999.99, "category": "Electronics" }).
PUT /api/products/1: Update a product.
DELETE /api/products/1: Delete a product.
Output for GET /api/products:
[
{
"id": 1,
"name": "Laptop",
"price": 999.99,
"category": "Electronics"
}
]
This example demonstrates a RESTful API for managing products with basic CRUD operations and error handling.
Attribute Routing for APIs
What is Attribute Routing?
Attribute Routing allows you to define routes directly on controller actions using attributes like [Route], [HttpGet], [HttpPost], etc. It’s ideal for APIs due to its flexibility and clarity.
Key Features
Routes are defined at the controller or action level.
Supports complex URL patterns (e.g., /api/products/{id}/details).
Integrates with HTTP methods (GET, POST, etc.).
Pros, Cons, and Alternatives for Attribute Routing
Pros
Flexibility: Define custom, precise routes.
Clarity: Routes are tied to specific actions, improving readability.
API-Friendly: Ideal for RESTful APIs with specific endpoints.
Cons
Maintenance: Routes scattered across controllers can be harder to manage.
Duplication: Risk of repeating route patterns in large projects.
Alternatives
Conventional Routing: For simpler, centralized route definitions.
Minimal APIs: For lightweight APIs with fewer controllers (ASP.NET Core 6+).
Example: Implementing Attribute Routing
Let’s enhance the ProductsController with custom attribute routes.
Step 1: Update ProductsController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MyApiApp.Data;
using MyApiApp.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MyApiApp.Controllers
{
[Route("api/v1/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
private readonly AppDbContext _context;
public ProductsController(AppDbContext context)
{
_context = context;
}
[HttpGet("all")]
public async Task<ActionResult<IEnumerable<Product>>> GetAllProducts()
{
return await _context.Products.ToListAsync();
}
[HttpGet("{id}/details")]
public async Task<ActionResult<Product>> GetProductDetails(int id)
{
var product = await _context.Products.FindAsync(id);
if (product == null)
{
return NotFound(new { error = "Product not found" });
}
return product;
}
[HttpPost("create")]
public async Task<ActionResult<Product>> CreateProduct(Product product)
{
_context.Products.Add(product);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetProductDetails), new { id = product.Id }, product);
}
}
}
Step 2: Test the RoutesTest the following endpoints:
GET /api/v1/products/all
GET /api/v1/products/1/details
POST /api/v1/products/create
Output for GET /api/v1/products/all:
[
{
"id": 1,
"name": "Laptop",
"price": 999.99,
"category": "Electronics"
}
]
This example shows how to use attribute routing to create custom, descriptive API endpoints.
Model Binding & Validation
Understanding Model Binding
Model Binding maps HTTP request data (e.g., query strings, form data, JSON) to action method parameters or model objects. ASP.NET Core supports binding from:
Route parameters (e.g., {id}).
Query strings (e.g., ?name=value).
Request body (e.g., JSON in POST requests).
Form data.
Implementing Validation
Validation ensures input data meets requirements before processing. ASP.NET Core uses data annotations (e.g., [Required], [Range]) and the ModelState to validate models.
Pros
Simplicity: Built-in binding and validation reduce boilerplate code.
Flexibility: Supports multiple data sources.
Security: Validation prevents invalid or malicious input.
Cons
Complex Models: Binding large models can be error-prone.
Validation Overhead: Custom validation logic can add complexity.
Error Messages: Default error messages may need customization.
Alternatives
Manual Parsing: Parse JSON manually for full control.
FluentValidation: For advanced validation scenarios.
DTOs: Use Data Transfer Objects to simplify binding.
Example: Model Binding and Validation in an API
Let’s add validation to the product API.
Step 1: Create a DTOCreate Models/ProductDto.cs:
using System.ComponentModel.DataAnnotations;
namespace MyApiApp.Models
{
public class ProductDto
{
[Required(ErrorMessage = "Name is required")]
[StringLength(100, ErrorMessage = "Name cannot exceed 100 characters")]
public string Name { get; set; }
[Required(ErrorMessage = "Price is required")]
[Range(0.01, 10000, ErrorMessage = "Price must be between 0.01 and 10000")]
public decimal Price { get; set; }
[Required(ErrorMessage = "Category is required")]
public string Category { get; set; }
}
}
Step 2: Update ProductsController.cs
[HttpPost("create")]
public async Task<ActionResult<Product>> CreateProduct([FromBody] ProductDto productDto)
{
if (!ModelState.IsValid)
{
return BadRequest(new { error = "Invalid input", details = ModelState });
}
try
{
var product = new Product
{
Name = productDto.Name,
Price = productDto.Price,
Category = productDto.Category
};
_context.Products.Add(product);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetProductDetails), new { id = product.Id }, product);
}
catch (Exception ex)
{
return StatusCode(500, new { error = "Failed to create product", details = ex.Message });
}
}
Step 3: Test ValidationSend a POST request to /api/v1/products/create with invalid data (e.g., { "name": "", "price": 0, "category": "" }).
Output:
{
"error": "Invalid input",
"details": {
"Name": ["Name is required"],
"Price": ["Price must be between 0.01 and 10000"],
"Category": ["Category is required"]
}
}
This example demonstrates model binding with a DTO and validation using data annotations.
Versioning APIs
Why Version APIs?
API Versioning ensures backward compatibility as APIs evolve, allowing clients to use older versions while new features are added.
Versioning Strategies
URL Path: Include version in the URL (e.g., /api/v1/products).
Query String: Use a query parameter (e.g., /api/products?version=1).
Header: Specify version in a custom header.
Media Type: Use Accept header (e.g., application/vnd.myapi.v1+json).
Pros, Cons, and Alternatives for API Versioning
Pros
Compatibility: Supports multiple API versions simultaneously.
Clarity: Makes API evolution explicit to clients.
Flexibility: Allows gradual migration to new versions.
Cons
Maintenance: Multiple versions increase complexity.
Documentation: Requires clear versioning documentation.
Client Adoption: Clients may delay upgrading to new versions.
Alternatives
No Versioning: Use a single, evolving API (risky for breaking changes).
Feature Flags: Enable/disable features without versioning.
GraphQL: Avoids versioning by allowing clients to request specific fields.
Example: Implementing API Versioning
Let’s add versioning to the product API using the URL path strategy.
Step 1: Install Versioning Package
dotnet add package Microsoft.AspNetCore.Mvc.Versioning
Step 2: Configure VersioningUpdate Program.cs:
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MyApiApp.Data;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddApiVersioning(options =>
{
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new Microsoft.AspNetCore.Mvc.ApiVersion(1, 0);
options.ReportApiVersions = true;
});
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
app.Run();
Step 3: Update ProductsController.csCreate two versions of the API:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MyApiApp.Data;
using MyApiApp.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MyApiApp.Controllers
{
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
private readonly AppDbContext _context;
public ProductsController(AppDbContext context)
{
_context = context;
}
[HttpGet]
[MapToApiVersion("1.0")]
public async Task<ActionResult<IEnumerable<Product>>> GetProductsV1()
{
return await _context.Products.ToListAsync();
}
[HttpGet]
[MapToApiVersion("2.0")]
public async Task<ActionResult<IEnumerable<object>>> GetProductsV2()
{
var products = await _context.Products
.Select(p => new { p.Id, p.Name, FullPrice = p.Price * 1.1m }) // 10% tax
.ToListAsync();
return products;
}
}
}
Step 4: Test Versioned APIsTest:
GET /api/v1/products: Returns products in V1 format.
GET /api/v2/products: Returns products with a calculated FullPrice.
Output for GET /api/v2/products:
[
{
"id": 1,
"name": "Laptop",
"fullPrice": 1099.989
}
]
This example demonstrates URL-based API versioning with different response formats.
Consuming APIs with HttpClient
What is HttpClient?
HttpClient is a .NET class for sending HTTP requests and receiving responses, ideal for consuming APIs from ASP.NET Core applications or other clients.
Key Features
Supports GET, POST, PUT, DELETE, etc.
Handles JSON serialization/deserialization.
Configurable with headers, timeouts, and retries.
Pros, Cons, and Alternatives for HttpClient
Pros
Built-in: Included in .NET, no external dependencies.
Flexible: Supports all HTTP methods and custom headers.
Asynchronous: Uses async/await for non-blocking calls.
Cons
Resource Management: Improper use can lead to socket exhaustion.
Error Handling: Requires careful exception handling.
Configuration: Can be complex for advanced scenarios.
Alternatives
RestSharp: A third-party library for simpler API calls.
Flurl: Fluent API for HTTP requests.
Refit: Generates strongly-typed API clients.
Example: Consuming an API with HttpClient
Let’s create a client application to consume the product API.
Step 1: Create a Client Project
dotnet new console -o ApiClient
cd ApiClient
dotnet add package Microsoft.Extensions.Http
Step 2: Create a ClientCreate Program.cs:
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
namespace ApiClient
{
public class Program
{
public static async Task Main(string[] args)
{
var services = new ServiceCollection();
services.AddHttpClient("MyApi", client =>
{
client.BaseAddress = new Uri("https://localhost:5001");
});
var provider = services.BuildServiceProvider();
var httpClientFactory = provider.GetRequiredService<IHttpClientFactory>();
var client = httpClientFactory.CreateClient("MyApi");
try
{
// GET all products
var products = await client.GetFromJsonAsync<Product[]>("api/v1/products");
foreach (var product in products)
{
Console.WriteLine($"Name: {product.Name}, Price: {product.Price}, Category: {product.Category}");
}
// POST a new product
var newProduct = new Product { Name = "Tablet", Price = 299.99m, Category = "Electronics" };
var response = await client.PostAsJsonAsync("api/v1/products/create", newProduct);
response.EnsureSuccessStatusCode();
Console.WriteLine("Product created successfully");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"HTTP Error: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}
}
Step 3: Run the ClientEnsure the API is running, then run the client with dotnet run.
Output:
Name: Laptop, Price: 999.99, Category: Electronics
Product created successfully
This example shows how to use HttpClient with IHttpClientFactory for consuming APIs, including error handling.
Swagger/OpenAPI Documentation
What is Swagger/OpenAPI?
Swagger/OpenAPI is a standard for documenting APIs, providing an interactive UI to explore endpoints, parameters, and responses. In ASP.NET Core, Swashbuckle.AspNetCore integrates Swagger with your API.
Pros
Interactive: Allows testing APIs directly in the browser.
Standardized: Uses OpenAPI specification for compatibility.
Developer-Friendly: Simplifies API discovery and usage.
Cons
Overhead: Adds setup and configuration complexity.
Performance: Swagger UI can slow down in large APIs.
Security: Exposes API details, requiring careful access control.
Alternatives
Postman: For manual API documentation and testing.
Redoc: For static, lightweight API documentation.
Custom Docs: Hand-written documentation for simple APIs.
Example: Adding Swagger Documentation
Let’s add Swagger to the product API.
Step 1: Install Swashbuckle
dotnet add package Swashbuckle.AspNetCore
Step 2: Configure SwaggerUpdate Program.cs:
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MyApiApp.Data;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddApiVersioning(options =>
{
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new Microsoft.AspNetCore.Mvc.ApiVersion(1, 0);
options.ReportApiVersions = true;
});
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "MyApiApp v1", Version = "v1" });
c.SwaggerDoc("v2", new OpenApiInfo { Title = "MyApiApp v2", Version = "v2" });
});
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "MyApiApp v1");
c.SwaggerEndpoint("/swagger/v2/swagger.json", "MyApiApp v2");
});
}
else
{
app.UseExceptionHandler("/error");
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
app.Run();
Step 3: Test SwaggerRun the application and navigate to /swagger. You’ll see an interactive UI for testing the API endpoints.
This example demonstrates how to add Swagger documentation with support for multiple API versions.
Best Practices for Web APIs in ASP.NET Core
Security Best Practices
Use HTTPS: Encrypt all API communications.
Authenticate APIs: Use JWT or OAuth for secure access.
Validate Input: Use data annotations or FluentValidation to prevent invalid data.
Rate Limiting: Implement rate limiting to prevent abuse.
CORS: Configure Cross-Origin Resource Sharing carefully to restrict access.
Performance Best Practices
Optimize Queries: Use Select to retrieve only needed fields.
Caching: Implement response caching for frequently accessed endpoints.
Asynchronous Code: Use async/await for I/O-bound operations.
Pagination: Implement paging for large datasets.
Compression: Enable response compression (e.g., Gzip).
Error Handling Best Practices
Consistent Responses: Return standardized error objects (e.g., { "error": "message" }).
Status Codes: Use appropriate HTTP status codes (e.g., 400 for bad requests, 404 for not found).
Logging: Log errors using ILogger for debugging.
Exception Middleware: Use custom middleware to handle uncaught exceptions.
Real-Life Example: Building an E-Commerce Product API
Let’s build a secure, versioned, and documented e-commerce product API with authentication.
Step 1: Enhance the API ProjectAdd JWT authentication to the existing API project.
Step 2: Configure JWTUpdate Program.cs:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using MyApiApp.Data;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddApiVersioning(options =>
{
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new Microsoft.AspNetCore.Mvc.ApiVersion(1, 0);
options.ReportApiVersions = true;
});
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "E-Commerce API v1", Version = "v1" });
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Please enter JWT with Bearer into field",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } },
new string[] { }
}
});
});
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
});
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "E-Commerce API v1"));
}
else
{
app.UseExceptionHandler("/error");
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Update appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=MyApiAppDb;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Jwt": {
"Key": "your-secure-key-32-chars-long-at-least",
"Issuer": "ECommerceIssuer",
"Audience": "ECommerceAudience"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
Step 3: Add a Token ControllerCreate Controllers/TokenController.cs:
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using MyApiApp.Models;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace MyApiApp.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TokenController : ControllerBase
{
private readonly IConfiguration _configuration;
public TokenController(IConfiguration configuration)
{
_configuration = configuration;
}
[HttpPost]
public IActionResult GenerateToken([FromBody] LoginModel model)
{
if (model.Username == "admin" && model.Password == "password123") // Replace with real user validation
{
var claims = new[]
{
new Claim(ClaimTypes.Name, model.Username),
new Claim(ClaimTypes.Role, "Admin")
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
claims: claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds);
return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) });
}
return BadRequest(new { error = "Invalid credentials" });
}
}
public class LoginModel
{
public string Username { get; set; }
public string Password { get; set; }
}
}
Step 4: Update ProductsController.csAdd authentication and validation:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MyApiApp.Data;
using MyApiApp.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace MyApiApp.Controllers
{
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1.0")]
[Authorize(Roles = "Admin")]
public class ProductsController : ControllerBase
{
private readonly AppDbContext _context;
public ProductsController(AppDbContext context)
{
_context = context;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
{
try
{
return await _context.Products.ToListAsync();
}
catch (Exception ex)
{
return StatusCode(500, new { error = "Failed to retrieve products", details = ex.Message });
}
}
[HttpPost]
public async Task<ActionResult<Product>> PostProduct([FromBody] ProductDto productDto)
{
if (!ModelState.IsValid)
{
return BadRequest(new { error = "Invalid input", details = ModelState });
}
try
{
var product = new Product
{
Name = productDto.Name,
Price = productDto.Price,
Category = productDto.Category
};
_context.Products.Add(product);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetProducts), new { id = product.Id }, product);
}
catch (Exception ex)
{
return StatusCode(500, new { error = "Failed to create product", details = ex.Message });
}
}
}
}
Step 5: Test the E-Commerce API
Use Postman to get a JWT from /api/token.
Access /api/v1/products with the Authorization: Bearer <token> header.
Use the Swagger UI at /swagger to explore and test the API.
Real-Life Scenario: This e-commerce API supports:
Product Management: Admins can create and retrieve products securely.
Versioning: Allows future updates without breaking existing clients.
Documentation: Provides clear, interactive documentation for developers.
Security: Uses JWT to protect endpoints, ensuring only authorized users access sensitive operations.
Conclusion
In Module 7 of our ASP.NET Core Complete Course, we explored Working with Web APIs in ASP.NET Core, covering:
Creating RESTful APIs for scalable, interoperable services.
Attribute Routing for precise endpoint definitions.
Model Binding & Validation for robust data handling.
Versioning APIs to support backward compatibility.
Consuming APIs with HttpClient for client integration.
Swagger/OpenAPI Documentation for developer-friendly interfaces.
Through practical examples and a real-life e-commerce API, you’ve learned how to build secure, well-documented APIs with best practices for security, performance, and error handling. In the next module, we’ll dive into advanced topics like SignalR, Blazor, and performance optimization. Stay tuned!
FAQs
Q1: What is a RESTful API?
A RESTful API follows REST principles, using HTTP methods to perform CRUD operations on resources identified by URLs.
Q2: Why use attribute routing for APIs?
Attribute routing provides precise, action-level control over URL patterns, ideal for RESTful APIs.
Q3: How does model binding work in ASP.NET Core?
Model binding maps HTTP request data to action parameters or model objects, simplifying data handling.
Q4: Why is API versioning important?
Versioning ensures backward compatibility as APIs evolve, preventing breaking changes for clients.
Q5: How does Swagger improve API development?
Swagger provides interactive documentation, making it easier for developers to explore and test APIs.
No comments:
Post a Comment
Thanks for your valuable comment...........
Md. Mominul Islam