Showing posts with label ASp.NET Core. Show all posts
Showing posts with label ASp.NET Core. Show all posts

Caching in ASP.NET Core.

Caching is the technique of storing frequently accessed data in temporary fast storage so that future requests can be served faster without repeatedly calling a slow source like a database or external API.

👉 In simple words: Cache = Fast memory that avoids repeated expensive operations

Without caching:

  • Every request hits the database
  • Higher response time
  • Increased load on DB
  • Poor scalability

With caching:

  • Faster response time
  • Reduced database calls
  • Better scalability
  • Lower infrastructure cost

Imagine a restaurant menu:
  • ❌ Without cache → Chef cooks every dish from scratch every time
  • ✅ With cache → Popular dishes are pre-prepared and served instantly
That pre-prepared dish = Cache

How does caching work?
  1. Client requests data
  2. Application checks the cache first
  3. If found → return from cache (cache hit)
  4. If not found → fetch from DB, store in cache (cache miss)

Types of Caching in ASP.NET Core

There are two main types you must know as a .NET developer:
  • In-Memory Cache
  • Distributed Cache

In-Memory Caching.

In-Memory Cache stores data in the RAM of the application server.

  • Lives inside the application process
  • Very fast
  • Data is lost when the app restarts
How To Implement In-Memory Caching?

Step 1: Register Memory Cache.
builder.Services.AddMemoryCache();

Step 2: Inject IMemoryCache
using Microsoft.Extensions.Caching.Memory;

public class ProductService
{
    private readonly IMemoryCache _cache;

    public ProductService(IMemoryCache cache)
    {
        _cache = cache;
    }
}

Step 3: Cache Data Using Cache-Aside Pattern
public List<string> GetProducts()
{
    const string cacheKey = "products";

    if (!_cache.TryGetValue(cacheKey, out List<string> products))
    {
        // Simulate DB call
        products = GetProductsFromDatabase();

        var cacheOptions = new MemoryCacheEntryOptions()
            .SetAbsoluteExpiration(TimeSpan.FromMinutes(5))
            .SetSlidingExpiration(TimeSpan.FromMinutes(2));

        _cache.Set(cacheKey, products, cacheOptions);
    }

    return products;
}
When to Use In-Memory Cache:
  • Small applications
  • Single server apps
  • Lookup / static data
  • Config values
  • Not suitable for load-balanced systems

Distributed Caching.

Distributed Cache stores data in a separate cache server shared across multiple application instances.
Common implementations:
  • Redis
  • SQL Server Cache
  • NCache
Let's understand in detail, with Redis as an example, because it is one of the most popular Caching System.

Redis is an in-memory distributed cache used to:
  • Share cached data across multiple app instances
  • Improve performance
  • Reduce database load
👉 Unlike in-memory cache, Redis works in load-balanced & microservice environments.

Prerequisites
  • Option 1: Run Redis using Docker (Port: 6379)
  • Option 2: Local Redis
Step 1: Install Required NuGet Packages
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

Step 2: Configure Redis in ASP.NET Core
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration =
        "my-redis.xxxxxx.use1.cache.amazonaws.com:6379";

    options.InstanceName = "MyApp:";
});

Step 3: Use Redis via IDistributedCache (Service Layer).
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;

public class ProductService
{
    private readonly IDistributedCache _cache;

    public ProductService(IDistributedCache cache)
    {
        _cache = cache;
    }

Step 4: Cache-Aside Pattern Implementation.
public async Task<List<Product>> GetProductsAsync()
{
    const string cacheKey = "products";

    var cachedData = await _cache.GetStringAsync(cacheKey);

    if (cachedData != null)
    {
        return JsonSerializer.Deserialize<List<Product>>(cachedData);
    }

    // Simulate DB call
    var products = GetProductsFromDatabase();

    var options = new DistributedCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
        SlidingExpiration = TimeSpan.FromMinutes(2)
    };

    await _cache.SetStringAsync(
        cacheKey,
        JsonSerializer.Serialize(products),
        options
    );

    return products;
}

Step 5: Cache Invalidation
public async Task UpdateProductAsync(Product product)
{
    // Update DB logic

    await _cache.RemoveAsync("products");
}
Note: Caching without invalidation is a bug, not a feature.

How To Implement Logging in ASP.NET Core.

Logging is the process of recording application events so you can:

  • Debug issues
  • Monitor behavior
  • Trace requests
  • Audit actions in production

Without proper logging:

  • Bugs are hard to reproduce
  • Production failures are invisible
  • Root cause analysis becomes guesswork

ASP.NET Core has ILogger built in via Microsoft.Extensions.Logging. Supported Providers (by default)
  • Console
  • Debug
  • EventSource
  • Application Insights (Azure)
Rule: Never log everything as Information.

How To Use ILogger?

Step 1: Inject ILogger.
public class OrderController : ControllerBase
{
    private readonly ILogger<OrderController> _logger;

    public OrderController(ILogger<OrderController> logger)
    {
        _logger = logger;
    }
}

Step 2: Log Messages
_logger.LogInformation("Order creation started");

_logger.LogWarning("Order {OrderId} has no items", orderId);

_logger.LogError(exception, "Failed to create order {OrderId}", orderId);

Good Option 1: Logging Exception Correctly.
catch (Exception ex)
{
    _logger.LogError(ex, "Error while processing order {OrderId}", orderId);
    throw;
}
👉 Always pass the exception object to preserve stthe ack trace.

Good Option 2: Logging in Middleware.
public async Task InvokeAsync(HttpContext context)
{
    _logger.LogInformation(
        "Request {Method} {Path}",
        context.Request.Method,
        context.Request.Path);

    await _next(context);

    _logger.LogInformation(
        "Response {StatusCode}",
        context.Response.StatusCode);
}

Built-in logging is good, but Serilog is preferred in production because it provides:
  • Rich structured logging
  • Multiple sinks (File, DB, Seq, Elastic)
  • Better performance & flexibility
Step 1: Install Packages
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File

Step 2: Configure Serilog in Program.cs
using Serilog;

Log.Logger = new LoggerConfiguration()
    .Enrich.FromLogContext()
    .WriteTo.Console()
    .WriteTo.File("Logs/app-.log", rollingInterval: RollingInterval.Day)
    .CreateLogger();

builder.Host.UseSerilog();

Step 3: Use ILogger as Usual.
_logger.LogInformation("Invoice {InvoiceId} generated", invoiceId);
You still use ILoggerSerilog works behind the scenes.

Interview Answer: In ASP.NET Core, I use ILogger for application logging and Serilog for structured, production-grade logging. I follow proper log levels, structured messages, correlation IDs, and centralized log storage to ensure observability and easy debugging.

Implement Custom Global Exception Handling in ASP.NET Core.

Exception handling is one of the most important non-functional requirements (NFRs) in any backend application. In ASP.NET Core, poor exception handling leads to:
  • Application crashes
  • Inconsistent error responses
  • Security risks (stack traces exposed)
  • Difficult debugging and monitoring
To solve this, ASP.NET Core promotes Global Exception Handling, where all unhandled exceptions are caught at a single place, logged, and converted into a consistent HTTP response.

This article explains:
  • Why is global exception handling needed
  • How to implement a custom global exception middleware
  • Best practices used in production systems

Why Do We Need Global Exception Handling?

Before understanding Global Exception, we need to know what an exception is and why we need to handle it properly.

An exception is a runtime error that occurs when the application cannot continue normal execution.
Examples:
  • Database connection failure
  • Null reference access
  • Invalid user input
  • External API timeout

If exceptions are not handled properly, they can:
  • Crash the application
  • Leak sensitive information
  • Return inconsistent responses
  • Make debugging extremely difficult
Global Exception Handling is a centralized mechanism that:
  • Catches all unhandled exceptions in one place
  • Logs them consistently
  • Converts them into a standard HTTP response
  • Prevents sensitive details from reaching clients
Instead of handling errors locally in every controller or method, the application handles them globally.

Let's understand the benefits of Global Exception Handling in detail with an example:

Example 1: Error Message Without Global Exception Handler.
{
  "error": "Object reference not set to an instance of an object"
}

Worse Case:
System.NullReferenceException at OrderService.cs line 42
Issues
  • Internal code details exposed
  • Frontend doesn’t know how to handle different formats
  • Logs may be missing or incomplete
Example 2: Error Message With Global Exception Handler.
{
  "statusCode": 400,
  "message": "Invalid request",
  "traceId": "abc123"
}
APIs always return a consistent response.

Imagine a building security desk:
  • Without global handling → every room handles its own security
  • With global handling → one central security desk handles all issues
This is exactly how Global Exception Handling works.
Interview Answer: Global exception handling is needed to centrally catch all unhandled exceptions, return consistent and secure error responses, improve maintainability, and enable reliable logging without duplicating try–catch blocks across the application.

 How To Handle Exceptions in ASP.NET Core?

ASP.NET Core provides multiple ways to handle exceptions, but not all are equal. A senior .NET developer is expected to choose the right approach based on maintainability, security, and scalability.

1. Try-Catch Block in Controller: Handling exceptions inside each controller action using try–catch.

[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
    try
    {
        var product = _service.GetProduct(id);
        return Ok(product);
    }
    catch (Exception ex)
    {
        return StatusCode(500, ex.Message);
    }
}
Problem:
  • Code duplication: try–catch in every action
  • Inconsistent responses: Different error formats
  • Security risk: Exception messages exposed
  • Poor separation: Business + error handling mixed
  • Hard to maintain: Changes needed everywhere

2. UseExceptionHandler() Middleware: Built-in middleware provided by ASP.NET Core for basic global exception handling.
app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        context.Response.StatusCode = 500;
        await context.Response.WriteAsync("Something went wrong");
    });
});
Problem:
  • Limited customization
  • No exception type mapping
  • No structured response
  • Not ideal for APIs
3. Custom Exception Middleware: A custom middleware that intercepts all unhandled exceptions, logs them, and returns a standard response.

Step 1: Create Custom Exceptions.
public class NotFoundException : Exception
{
    public NotFoundException(string message) : base(message) { }
}

Step 2: Create Error Response Model.
public class ErrorResponse
{
    public int StatusCode { get; set; }
    public string Message { get; set; }
    public string TraceId { get; set; }
}

Step 3: Create Middleware
public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionMiddleware> _logger;

    public GlobalExceptionMiddleware(RequestDelegate next, ILogger logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unhandled exception");

            int statusCode = ex switch
            {
                NotFoundException => 404,
                _ => 500
            };

            var response = new ErrorResponse
            {
                StatusCode = statusCode,
                Message = statusCode == 500 ? "Internal error" : ex.Message,
                TraceId = context.TraceIdentifier
            };

            context.Response.StatusCode = statusCode;
            context.Response.ContentType = "application/json";
            await context.Response.WriteAsJsonAsync(response);
        }
    }
}

Step 4: Register Middleware
app.UseMiddleware<GlobalExceptionMiddleware>();

4. IExceptionHandler: A new interface-based global exception handling mechanism introduced in .NET 7.

Step 1: Step 1: Implement IExceptionHandler.
public class GlobalExceptionHandler : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext context,
        Exception exception,
        CancellationToken cancellationToken)
    {
        context.Response.StatusCode = exception switch
        {
            NotFoundException => 404,
            _ => 500
        };

        await context.Response.WriteAsJsonAsync(new
        {
            message = "Error occurred",
            traceId = context.TraceIdentifier
        }, cancellationToken);

        return true;
    }
}

Step 2: Register Service.
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

Step 3: Enable Middleware.
app.UseExceptionHandler();

Benefits:
  • Clean & structured
  • Official Microsoft approach
  • Easy integration with ProblemDetails
  • Less boilerplate
Interview Answer: For production-grade ASP.NET Core APIs, I prefer custom global exception middleware because it gives full control over exception mapping, logging, and response structure. For newer .NET versions, IExceptionHandler is also a clean and modern alternative.

Difference Between Middleware and Filter in C#.

Middleware Vs Filter

In ASP.NET Core, both Middleware and Filters are used to handle cross-cutting concerns such as logging, authentication, authorization, and exception handling. Although they may appear similar, they operate at different levels of the request pipeline and serve different purposes.

Understanding this difference is very important for real-world projects and is a frequently asked interview topic.

What is Middleware?

Middleware is a component that sits in the ASP.NET Core request processing pipeline and handles HTTP requests and responses.

Every incoming request passes through a series of middleware components, and each middleware can:

  • Inspect the request
  • Modify the request or response
  • Decide whether to pass control to the next middleware

Middleware is mainly used for cross-cutting concerns that apply to many or all requests.

When a request comes to an ASP.NET Core application, it flows through the middleware pipeline in the order in which they are registered.

Request Flow:

Request
Middleware 1
Middleware 2
Controller / Endpoint
Middleware 2
Middleware 1
Response

Each middleware:
  1. Executes some logic before calling the next middleware
  2. Calls the next middleware using await _next(context)
  3. Executes logic after the next middleware completes (optional)
Example of Built-in Middleware:
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
Each of these is a middleware component registered in the pipeline.

Why do we need a Middleware?

Middleware helps to:
  • Handle requests globally.
  • Keep controllers clean.
  • Centralize logic like logging, security, and error handling.
  • Improve maintainability.

How To Create Custom Middleware?

There are multiple conditions where we need to create our own custom middleware to handle incoming requests and out going response. Let's learn how to create a custom middleware and register it in the existing pipeline.

Step 1: Create a Custom Middleware Class.

In this step, we create a middleware class that contains the actual logic to handle HTTP requests and responses.
public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;

    public RequestLoggingMiddleware(
        RequestDelegate next,
        ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        _logger.LogInformation(
            "Request Started: {Method} {Path}",
            context.Request.Method,
            context.Request.Path);

        await _next(context);

        _logger.LogInformation(
            "Response Finished: {StatusCode}",
            context.Response.StatusCode);
    }
}

Key Terms Explained
  • RequestDelegate: A delegate representing the next middleware in the pipeline. It is calling _next(context) passes control forward.
  • HttpContext: Represents the current HTTP request and response. It contains headers, body, method, status code, user info, etc.
  • InvokeAsync Method: The method ASP.NET Core automatically calls for each request. This is where middleware logic lives.
  • ILogger: Built-in logging abstraction used instead of Console.WriteLine. Supports logging levels and providers.

Step 2: Creating an Extension Method to Register Middleware.

This step makes your middleware look and behave like a built-in middleware, allowing clean and readable registration in Program.cs.
public static class RequestLoggingMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestLogging(
        this IApplicationBuilder app)
    {
        return app.UseMiddleware<RequestLoggingMiddleware>();
    }
}
  • Extension Method: A static method that adds new functionality to an existing type without modifying it.
  • IApplicationBuilder: Used to build the HTTP request pipeline. It provides methods like Use, Run, and Map.
  • UseMiddleware<T>(): Registers a middleware type into the pipeline and lets the framework manage its lifetime and dependencies.
  • Fluent API: Returning app allows chaining multiple middleware registrations.

Step 3: Register the Middleware in the Program.cs

Here, the middleware is added to the request pipeline, and its position determines when it runs.
var app = builder.Build();

app.UseRequestLogging(); // Custom middleware

app.MapControllers();

app.Run();

Now our custom Middleware is registered successfully and ready to execute for each http request and response.

What is the use of the Run, Use, and Map keywords in the Middleware pipeline?

In ASP.NET Core, Use, Run, and Map are used to configure the middleware pipeline. Each keyword has a specific purpose and behavior.

1. Use: Use is used to add middleware that can pass control to the next middleware.
app.Use(async (context, next) =>
{
    Console.WriteLine("Before next middleware");
    await next();
    Console.WriteLine("After next middleware");
});

2. Run: Run adds a terminal middleware that does not call the next middleware.
app.Run(async context =>
{
    await context.Response.WriteAsync("Request handled here");
});

3. Map: A map is used to branch the request pipeline based on URL path.
app.Map("/admin", adminApp =>
{
    adminApp.Run(async context =>
    {
        await context.Response.WriteAsync("Admin area");
    });
});
Only requests starting with /admin go through this branch.

Common Use Cases of Middleware
  • Global exception handling
  • Authentication & authorization
  • Logging and auditing
  • Request/response modification
  • CORS
  • Rate limiting

What is a Filter?

A Filter is a component in ASP.NET Core that allows you to run custom logic before or after a controller action executes.
Filters are part of the MVC/Web API pipeline, not the global middleware pipeline.

Filters are mainly used for action-level cross-cutting concerns, such as:
  • Authorization
  • Validation
  • Logging
  • Exception handling
  • Modifying responses

Types of Filters.

ASP.NET Core provides five types of filters, each executing at a specific stage of the MVC request pipeline. Filters help handle cross-cutting concerns such as authorization, validation, logging, caching, and exception handling.

1. Authorization Filters

Authorization filters run first in the filter pipeline. They determine whether a user is allowed to access a controller or action.

If authorization fails, the request is short-circuited, and the action never executes.

2. Resource Filters

Resource filters run before model binding and after action execution. They are useful for caching, short-circuiting, or resource initialization.

3. Action Filters (Most Common)

Action filters run before and after controller action methods. They are commonly used for logging, validation, and auditing.

4. Result Filters

Result filters run before and after the action result executes (like Ok(), View()). They are useful for modifying responses.

5. Exception Filters

Exception filters handle exceptions thrown by controller actions.

How do filters work?

In ASP.NET Core, filters execute around a controller action and allow you to run logic before and after specific stages of request processing. Unlike middleware, filters are MVC-specific and are tightly coupled with controllers and actions.

Let’s understand the execution flow step by step using a real example.

Filter Execution Flow:
When a request reaches an MVC controller, it passes through filters in the following order:
Request
Authorization Filter
Resource Filter
Action Filter (Before)
Controller Action
Action Filter (After)
Result Filter
Response
If an exception occurs, Exception Filters are invoked.

Example: Logging Action Filter.

Step 1: Create a Custom Action Filter.
using Microsoft.AspNetCore.Mvc.Filters;

public class LogActionFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        Console.WriteLine("Before Action Method");
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        Console.WriteLine("After Action Method");
    }
}

Step 2: Apply Filter to Controller
[LogActionFilter]
public class ProductsController : Controller
{
    public IActionResult Get()
    {
        Console.WriteLine("Inside Controller Action");
        return Ok("Products");
    }
}

Step-by-Step Execution (What Happens Internally)

1. Request arrives at MVC: The request has already passed through middleware and is now handled by MVC.
2. Authorization Filters run: ASP.NET Core checks whether the request is authorized to access the action.
3. Resource Filters run: These execute before model binding and can short-circuit the request.
4. Action Filter – OnActionExecuting
Before Action Method
This runs just before the controller action executes.

5. Controller Action Executes
Inside Controller Action

6. Action Filter – OnActionExecuted
After Action Method
This runs immediately after the action method completes.

7. Result Filters execute: These run before and after the action result (e.g., Ok()).
8. Response is returned to the client.

Use Filters when:
  • Logic is tied to MVC actions
  • You need access to model/action parameters
  • Behavior is controller/action specific

Middleware Vs Filter.

Feature Middleware Filter
Scope Global (entire application) MVC / Controller / Action
Execution Level Request pipeline (before MVC) Inside the MVC pipeline
Access to Action Context No Yes
Order Control Registration order in the Program.cs Filter type and order attribute
Can Short-Circuit Request Yes Yes
Dependency on MVC No Yes
Runs for Static Files Yes No
Common Use Cases Logging, Auth, Exception handling Validation, Authorization, Action logging
Performance Faster (framework-level) Slightly slower (MVC-specific)

Use Middleware when you need to handle cross-cutting concerns globally for every HTTP request, such as authentication, logging, CORS, or exception handling, and when the logic is not tied to MVC controllers. Use Filters when the logic is specific to MVC actions or controllers, requires access to action parameters, model state, or results, and should run before or after controller execution, such as validation, authorization at the action level, or action-specific logging.

DON'T MISS

Tech News
© all rights reserved
made with by AlgoLesson