Logging & Versioning and ASP.NET Core

Logging in .NET and ASP.NET Core

Logging in an ASP.NET Web API relies on the built-in ILogger interface provided by Microsoft, which supports various built-in and third-party logging providers (sinks). Core Concepts

  • ILogger: The interface used within your application code to write log messages.

  • ILoggerFactory / ILoggerProvider: These manage the creation of ILogger instances and determine where logs are sent (e.g., Console, Debug window, files, databases, or cloud services).

  • Log Levels: Messages are categorized by severity: Trace, Debug, Information, Warning, Error, and Critical.

Severities are ranked from most to least detailed:

  • Trace (0): Very detailed, often sensitive data.

  • Debug (1): Short-term usefulness during development.

  • Information (2): General flow of the application.

  • Warning (3): Abnormal flow that doesn't stop the app.

  • Error (4): Failures that stop the current operation.

  • Critical (5): Total system failures

  • Dependency Injection (DI): In modern ASP.NET Core, the logging infrastructure is automatically available via DI, making it easy to inject ILogger<T> into controllers and services.

Step-by-Step Implementation (ASP.NET Core)

The default web API template automatically sets up console, debug, and EventSource providers, with configuration managed in appsettings.json1. Configuration in appsettings.json You can control the minimum log level for different categories here.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information", // Default minimum level for all categories
      "Microsoft": "Warning"    // Overrides the default for Microsoft framework logs
    }
  }
}

2. Injecting and Using ILogger in a Controller Inject the logger into your controller's constructor. The category name defaults to the class's fully qualified name, aiding organization.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

[ApiController]
[Route("api/[controller]")]
public class SampleController : ControllerBase
{
    private readonly ILogger<SampleController> _logger;

    // Inject ILogger via constructor
    public SampleController(ILogger<SampleController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IActionResult Get()
    {
        // Use extension methods to log messages at specific levels
        _logger.LogInformation("GET request received for SampleController.");

        try
        {
            // ... application logic ...
            return Ok();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An error occurred during the GET request.");
            return StatusCode(500, "Internal server error");
        }
    }
}

Built-in vs. Third-Party Providers

By default, logs are sent to the Console and Debug output. For more robust needs like writing to files or databases, popular third-party libraries include:

  • Serilog: Known for structured logging (logs as searchable JSON rather than plain text).
  • NLog: Highly flexible with many targets (File, Email, Database).
  • log4net: A classic, modular framework often used in legacy systems.

Implementation of log in file using Serilog

1. Install Required Packages

Use the NuGet Package Manager or the .NET CLI to install the essential library:

  • Serilog.AspNetCore
  • Serilog.Sinks.File

2. Configure and Register Serilog

Method1: Configure and Register Serilog in Program.cs

// configure serilog to write in a file 
Log.Logger = new LoggerConfiguration().MinimumLevel.Debug()
    .WriteTo.File("log/villaLog.txt", rollingInterval: RollingInterval.Day)
    .CreateLogger();

// register Serilog
builder.Host.UseSerilog();

Method2: Configure via appsettings.json (Recommended)

Add a Serilog section to your appsettings.json file. This allows you to change logging settings without recompiling the code

//JSON
{
  "Serilog": {
    "Using": [ "Serilog.Sinks.File" ],
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "File",
        "Args": {
          "path": "Logs/webapi-.log",
          "rollingInterval": "Day",
          "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
        }
      }
    ],
    "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ]
  }
}

  • path: Specifies the folder and filename (e.g., Logs/webapi-20241027.log).

  • rollingInterval: Set to Day to create a new log file every 24 hours.

Initialize in Program.cs Update your Program.cs to use Serilog as the primary logging provider.

using Serilog;

var builder = WebApplication.CreateBuilder(args);

// Configure Serilog to read from appsettings.json
builder.Host.UseSerilog((context, configuration) => 
    configuration.ReadFrom.Configuration(context.Configuration));

builder.Services.AddControllers();
// ... other services

var app = builder.Build();

// Optional: Streamlined request logging (logs every HTTP request)
app.UseSerilogRequestLogging(); 

app.UseAuthorization();
app.MapControllers();
app.Run();

3. Use the Logger in Controllers

Integrate ILogger<T> via dependency injection in your controllers to use Serilog

Advanced Features

  • HTTP Logging Middleware: You can log entire HTTP requests and responses (headers, bodies, status codes) by adding app.UseHttpLogging() in Program.cs.

  • Step1: - Add Service

   builder.Services.AddHttpLogging(options =>
   {
       options.LoggingFields = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.All;
       options.RequestBodyLogLimit = 4096; // Set limits to avoid performance issues
       options.ResponseBodyLogLimit = 4096;
   });

  • Step2: Use Middleware (in Program.cs, before other middleware like UseAuthorization):

    app.UseHttpLogging();
    
    
  • Configuration: Adjust log levels without changing code by editing the Logging section in appsettings.json.

  • Redaction: Prevent sensitive data (like passwords or PII) from appearing in logs using data redaction tools.

Custom Logging

You can use DI for Logging vai custom class & its interfce

step by step Implementation using Example

1. Create ILogging Interface and Logging class

// ILogging Interface 
namespace WebApplication1.Logging
{
    public interface ILogging
    {
        public void Log(string message, string type);
    }
}

// Logging class which implement ILogging
namespace WebApplication1.Logging
{
    public class Logging : ILogging
    {
        public void Log(string message, string type)
        {
            if(type == "error")
            {
                Console.WriteLine("Error - " +message);
            }
            else
            {
                Console.WriteLine(message);
            }
        }
    }
}

2. register a service using lifetime

// In Program.cs 
builder.Services.AddSingleton<ILogging, Logging>();

3. Use in controller

namespace WebApplication1.Controllers
{
    
    [Route("api/[controller]")]
    [ApiController]
    public class VillaController : ControllerBase
    {
        // Inject logging via constrouctor
        private readonly ILogging _logger;

        public VillaController(ILogging logger)
        {
            _logger = logger;
        }

        [ProducesResponseType(StatusCodes.Status200OK)]
        [HttpGet]
        public ActionResult<IEnumerable<VillaDTO>>GetVillas()
        {
            // Implement in Http Method 
            _logger.Log("Getting all Villa.", "");
            // _logger.Log("Get villa Error with id: " + id, "error");
            return Ok(VillaStore.VillaList);
        }
        

more details: in i want to use another Logging class LoggingV2 we can do as well

step1: Create LoggingV2 class which Implemets same ILogging Interface.

namespace WebApplication1.Logging
{
    public class LoggingV2 : ILogging
    {
        public void Log(string message, string type)
        {
            if(type == "error")
            {
                Console.BackgroundColor = ConsoleColor.Red;
                Console.WriteLine("Error - "+ message);
                Console.BackgroundColor= ConsoleColor.Black;
            }else if(type == "warning")
            {
                Console.BackgroundColor = ConsoleColor.DarkYellow;
                Console.WriteLine("Warning - " + message);
                Console.BackgroundColor = ConsoleColor.Black;
            }
            else
            {
                Console.WriteLine(message);
            }
        }
    }
}

step 2: register service using lifecycle

//builder.Services.AddSingleton<ILogging, Logging>();
builder.Services.AddSingleton<ILogging, LoggingV2>();

DONE ✅✅


API Versioning in ASP.NET Core

API versioning is a strategy to manage changes to your REST API while maintaining backward compatibility. It allows multiple versions of an API to coexist, enabling clients to upgrade at their own pace.

Core Concepts

  • Why Versioning?: APIs evolve. Without versioning, breaking changes would immediately affect all clients. Versioning lets you maintain old and new versions simultaneously.

  • Versioning Strategies: Different ways to specify the API version in requests:

    • URL Path Versioning: /api/v1/users vs /api/v2/users
    • Query String Versioning: /api/users?api-version=1
    • Header Versioning: Add header X-API-Version: 1
    • Media Type Versioning: Accept header with version info
  • Deprecation: Communicate to clients when old versions will be removed, allowing time for migration.

  • API Versioning Library: Asp.Versioning (formerly Microsoft.AspNetCore.Mvc.Versioning) simplifies implementation.

Strategy 1: URL Path Versioning (Most Common)

Version specified directly in the URL path.

Setup in Program.cs

using Asp.Versioning;

var builder = WebApplication.CreateBuilder(args);

// Add API Versioning
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0); // Default to v1
    options.ReportApiVersions = true;
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ApiVersionReader = new UrlSegmentApiVersionReader(); // Read from URL
});

builder.Services.AddControllers();
var app = builder.Build();

app.MapControllers();
app.Run();

Controller Implementation

using Asp.Versioning;

// Version 1 Controller
[ApiVersion("1.0")]
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class UsersController : ControllerBase
{
    [HttpGet]
    public IActionResult GetUsers()
    {
        return Ok(new { message = "Users from API v1", data = new[] { "User1", "User2" } });
    }

    [HttpGet("{id}")]
    public IActionResult GetUser(int id)
    {
        return Ok(new { id, name = "User1", email = "user1@example.com" });
    }
}

// Version 2 Controller - Enhanced response
[ApiVersion("2.0")]
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class UsersController : ControllerBase
{
    [HttpGet]
    public IActionResult GetUsers()
    {
        return Ok(new 
        { 
            message = "Users from API v2", 
            data = new[] 
            { 
                new { id = 1, name = "User1", email = "user1@example.com", status = "active" },
                new { id = 2, name = "User2", email = "user2@example.com", status = "inactive" }
            } 
        });
    }

    [HttpGet("{id}")]
    public IActionResult GetUser(int id)
    {
        return Ok(new { id, name = "User1", email = "user1@example.com", status = "active", createdDate = "2024-01-15" });
    }
}

Request Examples

GET /api/v1/users  → Returns v1 response
GET /api/v2/users  → Returns v2 response

Strategy 2: Query String Versioning

Version specified as query parameter.

Setup in Program.cs

using Asp.Versioning;

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.ReportApiVersions = true;
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ApiVersionReader = new QueryStringApiVersionReader("api-version"); // Query string
});

builder.Services.AddControllers();

Controller Implementation

// Both v1 and v2 use the same route path, version from query string
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [MapToApiVersion("1.0")]
    [HttpGet]
    public IActionResult GetProductsV1()
    {
        return Ok(new { version = "1.0", data = new[] { "Product1", "Product2" } });
    }

    [MapToApiVersion("2.0")]
    [HttpGet]
    public IActionResult GetProductsV2()
    {
        return Ok(new 
        { 
            version = "2.0", 
            data = new[] 
            { 
                new { id = 1, name = "Product1", price = 29.99, discount = 10 },
                new { id = 2, name = "Product2", price = 49.99, discount = 5 }
            } 
        });
    }
}

Request Examples

GET /api/products?api-version=1.0  → Returns v1 response
GET /api/products?api-version=2.0  → Returns v2 response
GET /api/products                  → Returns default version (v1.0)

Strategy 3: Header Versioning

Version specified in HTTP request header.

Setup in Program.cs

using Asp.Versioning;

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.ReportApiVersions = true;
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ApiVersionReader = new HeaderApiVersionReader("X-API-Version"); // Custom header
});

builder.Services.AddControllers();

Controller Implementation

[ApiVersion("1.0")]
[ApiVersion("2.0")]
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    [MapToApiVersion("1.0")]
    [HttpGet]
    public IActionResult GetOrdersV1()
    {
        return Ok(new { version = "1.0", orders = new[] { "Order#001", "Order#002" } });
    }

    [MapToApiVersion("2.0")]
    [HttpGet]
    public IActionResult GetOrdersV2()
    {
        return Ok(new 
        { 
            version = "2.0", 
            orders = new[] 
            { 
                new { id = 1, number = "Order#001", total = 150.50, status = "delivered" },
                new { id = 2, number = "Order#002", total = 200.00, status = "pending" }
            } 
        });
    }
}

Request Examples

GET /api/orders -H "X-API-Version: 1"   → Returns v1 response
GET /api/orders -H "X-API-Version: 2"   → Returns v2 response
GET /api/orders                         → Returns default version

Strategy 4: Media Type Versioning

Version specified in Accept header using content negotiation.

Setup in Program.cs

using Asp.Versioning;

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.ReportApiVersions = true;
    options.ApiVersionReader = new MediaTypeApiVersionReader("version");
});

builder.Services.AddControllers();

Controller Implementation

[ApiController]
[Route("api/[controller]")]
public class CategoriesController : ControllerBase
{
    [HttpGet]
    [Produces("application/json;version=1")]
    [ApiVersion("1.0", Deprecated = true)] // Mark v1 as deprecated
    public IActionResult GetCategoriesV1()
    {
        return Ok(new { version = "1.0", categories = new[] { "Cat1", "Cat2" } });
    }

    [HttpGet]
    [Produces("application/json;version=2")]
    [ApiVersion("2.0")]
    public IActionResult GetCategoriesV2()
    {
        return Ok(new 
        { 
            version = "2.0", 
            categories = new[] 
            { 
                new { id = 1, name = "Cat1", itemCount = 15 },
                new { id = 2, name = "Cat2", itemCount = 8 }
            } 
        });
    }
}

Request Examples

GET /api/categories -H "Accept: application/json;version=1"  → Returns v1
GET /api/categories -H "Accept: application/json;version=2"  → Returns v2

Advanced Features

Mark API as Deprecated

[ApiVersion("1.0", Deprecated = true)] // Indicates clients should upgrade
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class LegacyController : ControllerBase
{
    [HttpGet]
    public IActionResult GetData()
    {
        return Ok(new { warning = "API v1 is deprecated. Please use v2." });
    }
}

Sunset Header (HTTP Deprecation)

Automatically add deprecation headers to responses:

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.ReportApiVersions = true;
});

// In middleware
app.Use(async (context, next) =>
{
    var version = context.GetRequestedApiVersion()?.ToString() ?? "1.0";
    
    if (version == "1.0")
    {
        // Notify client that v1 will be removed
        context.Response.Headers.Add("Sunset", new DateTime(2025, 12, 31).ToString("R"));
        context.Response.Headers.Add("Deprecation", "true");
        context.Response.Headers.Add("Link", "</api/v2/resource>; rel=\"successor-version\"");
    }

    await next();
});

Multiple Versioning Strategies

Support multiple strategies simultaneously:

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.ReportApiVersions = true;
    
    // Support URL path AND query string
    options.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new QueryStringApiVersionReader("api-version")
    );
});

Custom Versioning Implementation

Create custom version reader for specialized needs:

public class CustomApiVersionReader : IApiVersionReader
{
    public ApiVersion Read(HttpRequest request)
    {
        // Read version from custom header or logic
        if (request.Headers.TryGetValue("X-Custom-Version", out var version))
        {
            if (ApiVersion.TryParse(version, out var apiVersion))
                return apiVersion;
        }

        return new ApiVersion(1, 0); // Default fallback
    }

    public string ToString() => "Custom-Header";
}

// Register in Program.cs
builder.Services.AddApiVersioning(options =>
{
    options.ApiVersionReader = new CustomApiVersionReader();
});

Versioning Best Practices

✅ Use URL path versioning for public APIs (most discoverable)
✅ Maintain backward compatibility for at least 2-3 versions
✅ Communicate deprecation clearly with sunrise/sunset headers
✅ Version your data models, not just routes
✅ Document version differences thoroughly
✅ Set expiration dates for old API versions
✅ Test all versions to ensure consistency
✅ Consider semantic versioning (Major.Minor.Patch)
✅ Use deprecated flag to warn clients
✅ Provide migration guides for version upgrades


Interview Q&A - API Versioning

Q1: Why is API versioning important?

Answer: API versioning allows you to maintain backward compatibility while evolving your API. Without versioning, breaking changes would immediately affect all clients. With versioning, clients can upgrade at their own pace, and you can deprecate old versions gradually.


Q2: What are the main API versioning strategies?

Answer:

  1. URL Path: /api/v1/users - Most discoverable, SEO-friendly
  2. Query String: /api/users?api-version=1 - Clean URLs, flexible
  3. Header: Custom header like X-API-Version: 1 - Headers-based, less visible
  4. Media Type: Accept header - Content negotiation based

Each has trade-offs; URL path is most common for REST APIs.


Q3: How do you deprecate an old API version?

Answer:

// Mark as deprecated
[ApiVersion("1.0", Deprecated = true)]

// Add HTTP headers
context.Response.Headers.Add("Sunset", expirationDate);
context.Response.Headers.Add("Deprecation", "true");
context.Response.Headers.Add("Link", "</api/v2>; rel=\"successor-version\"");

// Document with warnings and migration guides

Q4: Can you combine multiple versioning strategies?

Answer: Yes, using ApiVersionReader.Combine():

options.ApiVersionReader = ApiVersionReader.Combine(
    new UrlSegmentApiVersionReader(),
    new QueryStringApiVersionReader("api-version"),
    new HeaderApiVersionReader("X-API-Version")
);

Q5: What's the difference between old and new API versioning packages?

Answer: Microsoft deprecated Microsoft.AspNetCore.Mvc.Versioning in favor of Asp.Versioning. The new package has better support for .NET 6+ and minimal APIs, plus improved deprecation handling.

Comments

Popular posts from this blog