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
ILoggerinstances 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 failuresDependency 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.json. 1. 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()inProgram.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 likeUseAuthorization):app.UseHttpLogging();Configuration: Adjust log levels without changing code by editing the
Loggingsection 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/usersvs/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
- URL Path Versioning:
Deprecation: Communicate to clients when old versions will be removed, allowing time for migration.
API Versioning Library:
Asp.Versioning(formerlyMicrosoft.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:
- URL Path:
/api/v1/users- Most discoverable, SEO-friendly - Query String:
/api/users?api-version=1- Clean URLs, flexible - Header: Custom header like
X-API-Version: 1- Headers-based, less visible - 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
Post a Comment