Md Mominul Islam | Software and Data Enginnering | SQL Server, .NET, Power BI, Azure Blog

while(!(succeed=try()));

LinkedIn Portfolio Banner

Latest

Home Top Ad

Responsive Ads Here

Post Top Ad

Responsive Ads Here

Wednesday, August 20, 2025

ASP.NET Core Complete Course: Module 7 – Mastering Web APIs for Modern Web Development

 

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

  1. Introduction to Web APIs in ASP.NET Core

  2. 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

  3. Attribute Routing for APIs

    • What is Attribute Routing?

    • Pros, Cons, and Alternatives

    • Example: Implementing Attribute Routing

  4. Model Binding & Validation

    • Understanding Model Binding

    • Implementing Validation

    • Pros, Cons, and Alternatives

    • Example: Model Binding and Validation in an API

  5. Versioning APIs

    • Why Version APIs?

    • Pros, Cons, and Alternatives

    • Example: Implementing API Versioning

  6. Consuming APIs with HttpClient

    • What is HttpClient?

    • Pros, Cons, and Alternatives

    • Example: Consuming an API with HttpClient

  7. Swagger/OpenAPI Documentation

    • What is Swagger/OpenAPI?

    • Pros, Cons, and Alternatives

    • Example: Adding Swagger Documentation

  8. Best Practices for Web APIs in ASP.NET Core

    • Security Best Practices

    • Performance Best Practices

    • Error Handling Best Practices

  9. Real-Life Example: Building an E-Commerce Product API

  10. Conclusion

  11. 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:

  1. Create an API project using the webapi template.

  2. Define models and controllers.

  3. 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

Post Bottom Ad

Responsive Ads Here