CSharp - Other

.env in .NET

Load environment variables from the file

using dotenv.net;

DotEnv.Load();


Include the file in the publish directory. (in case it’s not only used for local debugging)

<Content Include=".env" CopyToPublishDirectory="Always"/>


Add .env to .gitignore.

.NET Core - Dependency Injection (DI)

  • Singleton for services reused across the app's lifetime.
  • Scoped for request-specific services.
  • Transient for lightweight, short-lived services.


services.AddSingleton<IMyService, MyService>();

.NET Core - Enable response compression middleware

builder.Services.AddResponseCompression();
app.UseResponseCompression();

Advanced API Security Practices

Use HTTPS πŸ”’

Implement HTTPS to encrypt the communication between the client and server.

public class SecureApiController : ApiController
{
    // Use attribute to enforce HTTPS
    [RequireHttps]
    public HttpResponseMessage GetSensitiveData()
    {
        // Fetch sensitive data logic
        var sensitiveData = new { /* ... */ };
        return Request.CreateResponse(HttpStatusCode.OK, sensitiveData);
    }
}

// Custom attribute to enforce HTTPS
public class RequireHttpsAttribute : AuthorizationFilterAttribute
{
    public override void OnAuthorization(HttpActionContext actionContext)
    {
        if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps)
        {
            actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
            {
                ReasonPhrase = "HTTPS Required"
            };
        }
        else
        {
            base.OnAuthorization(actionContext);
        }
    }
}


Enforce HTTPS in the Startup.cs.

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpsRedirection(options =>
    {
        options.RedirectStatusCode = StatusCodes.Status308PermanentRedirect;
        options.HttpsPort = 443;
    });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 
{
    app.UseHttpsRedirection();
}


Use OAuth2 πŸ”

Implement OAuth2, a protocol for authorization, to provide secure restricted access tokens to clients.

// OAuth2 configuration in Startup.cs
public void ConfigureAuth(IAppBuilder app)
{
    // Configure the application for OAuth based flow
    PublicClientId = "self";
    OAuthOptions = new OAuthAuthorizationServerOptions
    {
        TokenEndpointPath = new PathString("/Token"),
        Provider = new ApplicationOAuthProvider(PublicClientId),
        AuthorizeEndpointPath = new PathString("/api/Account/Authorize"),
        AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
        // In production mode set AllowInsecureHttp = false
        AllowInsecureHttp = true
    };

    // Enable the application to use bearer tokens to authenticate users
    app.UseOAuthBearerTokens(OAuthOptions);
}


Implement the OAuth 2.0 authorization framework.

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(Configuration, "AzureAd");

services.AddAuthorization(options =>
{
    options.AddPolicy("RequireAdminRole", policy => 
    {
        policy.RequireRole("Admin");
    });
});


Implementing JWT Authentication

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = Configuration["Jwt:Issuer"],
                    ValidAudience = Configuration["Jwt:Issuer"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
                };
            });


Use Rate Limiting 🚦

Use middleware to enforce rate limiting rules based on IP, user, or action group.

// Middleware for rate limiting
public class RateLimitingMiddleware : OwinMiddleware
{
    public RateLimitingMiddleware(OwinMiddleware next) : base(next) { }

    public override async Task Invoke(IOwinContext context)
    {
        if (RateLimitReached(context))
        {
            context.Response.StatusCode = (int)HttpStatusCode.TooManyRequests;
            return;
        }

        await Next.Invoke(context);
    }

    private bool RateLimitReached(IOwinContext context)
    {
        // Implement your rate limiting logic here based on the context
        // For instance, check the IP address and limit the number of requests per minute
        return false;
    }
}


Implement rate limiting to cap the number of requests a client can make in a given time window. You can define rate limits based on various factors like client IP, user ID, API route, etc.

public void ConfigureServices(IServiceCollection services)
{
    services.AddOptions();
    services.AddMemoryCache();
    services.Configure<ClientRateLimitOptions>(options =>
    {
        options.GeneralRules = new List<RateLimitRule>
        {
            new RateLimitRule
            {
                Endpoint = "*",
                Period = "1m",
                Limit = 30,
            }
        };
    });
    services.AddSingleton<IClientPolicyStore, MemoryCacheClientPolicyStore>();
    services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();
}

public void Configure(IApplicationBuilder app)
{
    app.UseClientRateLimiting();
}


Use API Versioning πŸŒ€

Implement versioning in your API routes to allow clients to specify the version they are designed to work with.

// Web API Route configuration
public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Routes.MapHttpRoute(
            name: "VersionedApi",
            routeTemplate: "api/v{version}/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

public class UsersController : ApiController
{
    [HttpGet]
    public string GetV1(int id)
    {
        // Version 1 specific processing
        return "Data from version 1";
    }

    [HttpGet, Route("api/v2/users/{id}")]
    public string GetV2(int id)
    {
        // Version 2 specific processing
        return "Data from version 2";
    }
}


Implement API versioning to maintain backwards compatibility. Include a version indicator (like β€œv1”) in the API route and optionally in the request/response headers.

services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = new UrlSegmentApiVersionReader();
});

[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class UsersController : ControllerBase
{
    // Controller implementation
}


Input Validation βœ…

Use data annotations and the [ApiController] attribute for basic validations.

public class LoginModel
{
    [Required]
    [EmailAddress]
    public string Email { get; set; }
        
    [Required]
    [StringLength(100, MinimumLength = 6)]
    public string Password { get; set; }
}

[HttpPost("login")]
public IActionResult Login([FromBody] LoginModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    
    // Authenticate user
}


Implement input validation at the API gateway level to ensure that only valid requests are processed.

public class ValidateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!actionContext.ModelState.IsValid)
        {
            actionContext.Response = actionContext.Request.CreateErrorResponse(
                HttpStatusCode.BadRequest, actionContext.ModelState);
        }
    }
}

// Usage in a Controller
public class MyModel
{
    [Required]
    public string Property1 { get; set; }

    // Other properties and validation attributes
}

public class MyApiController : ApiController
{
    [ValidateModel]
    public IHttpActionResult Post(MyModel model)
    {
        // Proceed knowing the model is valid
        ProcessData(model);
        return Ok();
    }

    private void ProcessData(MyModel model)
    {
        // Processing logic
    }
}


Use Leveled API Keys πŸ—οΈ

Implement a system of leveled API keys with different access permissions. Each client gets their own unique key associated with specific roles or scopes.

public class ApiKey
{
    public int Id { get; set; }
    public string Key { get; set; }
    public string ClientName { get; set; }
    public List<string> Scopes { get; set; }
}

public class AuthorizationMiddleware
{
    private readonly RequestDelegate _next;

    public AuthorizationMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context, IApiKeyRepository apiKeyRepository)
    {
        string apiKey = context.Request.Headers["X-API-KEY"];
        
        if (apiKey == null)
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsync("API key is missing.");
            return;
        }

        ApiKey key = await apiKeyRepository.GetApiKey(apiKey);
        
        if (key == null)
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsync("Invalid API key.");
            return;
        }
        
        if (!key.Scopes.Contains(context.Request.Path.ToString()))
        {
            context.Response.StatusCode = 403;
            await context.Response.WriteAsync("Not authorized to access this resource.");
            return;
        }

        await _next(context);
    }
}


Implement leveled API keys with varying access rights.

public class ApiKeyHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // Validate API key
        if (!ValidateApiKey(request.Headers, out var apiKey))
        {
            return request.CreateResponse(HttpStatusCode.Forbidden, "Invalid API Key");
        }

        // Check access level of API key and set user's role
        SetUserRoleBasedOnApiKey(apiKey);

        // Continue down the pipeline
        return await base.SendAsync(request, cancellationToken);
    }

    private bool ValidateApiKey(HttpRequestHeaders headers, out string apiKey)
    {
        // Logic to validate API key
        apiKey = /* ... */;
        return true;
    }

    private void SetUserRoleBasedOnApiKey(string apiKey)
    {
        // Logic to set user role based on API key level
    }
}


Authorization πŸ”

Implement role-based access control (RBAC) and check user permissions on each API endpoint before allowing the request to proceed.

[Authorize(Roles = "Admin")]
[HttpDelete("users/{id}")]
public async Task<IActionResult> DeleteUser(int id)
{
    // Delete user logic

    return NoContent();
}


Implement authorization checks within your API to distinguish between different levels of access rights for users.

[Authorize(Roles = "Admin, Viewer")]
public class DataController : ApiController
{
    public IHttpActionResult GetData()
    {
        // Only users with role "Admin" or "Viewer" can access data
        var data = GetDataFromService();
        return Ok(data);
    }

    [Authorize(Roles = "Admin")]
    public IHttpActionResult UpdateData(MyDataModel model)
    {
        // Only users with role "Admin" can update data
        UpdateDataService(model);
        return Ok();
    }

    // Separate methods to get and update data
    private object GetDataFromService() { /*...*/ }
    private void UpdateDataService(MyDataModel model) { /*...*/ }
}


Allowlist βœ…

Use an allowlist (or whitelist) to explicitly define the permitted values for sensitive parameters.

[HttpGet("articles")]
public IActionResult GetArticles([FromQuery] string category)
{
    string[] allowedCategories = { "science", "technology", "business" };
    
    if (!allowedCategories.Contains(category))
    {
        return BadRequest("Invalid category.");
    }
    
    // Fetch and return articles in the specified category
}


Implement an IP allowlist that permits requests only from known and trusted IP addresses.

public class IPAllowlistHandler : DelegatingHandler
{
    private readonly string[] _trustedIPs;

    public IPAllowlistHandler(string[] trustedIPs)
    {
        _trustedIPs = trustedIPs ?? throw new ArgumentNullException(nameof(trustedIPs));
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var context = ((HttpContextBase)request.Properties["MS_HttpContext"]);
        var requestIP = context.Request.UserHostAddress;

        if (!_trustedIPs.Contains(requestIP))
        {
            return Task.FromResult(request.CreateResponse(HttpStatusCode.Forbidden, "Access denied from this IP address"));
        }

        return base.SendAsync(request, cancellationToken);
    }
}


OWASP API Security Risks πŸ”

// Example of checking for broken user authentication, which is a common OWASP risk
public class AuthenticationMiddleware : OwinMiddleware
{
    public AuthenticationMiddleware(OwinMiddleware next) : base(next) {}

    public override async Task Invoke(IOwinContext context)
    {
        if (!UserIsAuthenticated(context))
        {
            context.Response.StatusCode = 401; // Unauthorized
            await context.Response.WriteAsync("User authentication failed.");
            return;
        }

        await Next.Invoke(context);
    }

    private bool UserIsAuthenticated(IOwinContext context)
    {
        // Implement your authentication logic here
        // Make sure it's in line with OWASP recommendations
        return true; // Placeholder for actual authentication check
    }
}


Use an API Gateway πŸŒ‰

Use an API Gateway to act as a single-entry point for all client requests.

// Configure API Gateway routes
var routes = new List<RouteConfiguration>
{
    new RouteConfiguration
    {
        RouteId = "users-route",
        UpstreamPathTemplate = "/api/users/{everything}",
        DownstreamPathTemplate = "/api/users/{everything}",
        DownstreamScheme = "https",
        DownstreamHostAndPorts = new List<DownstreamHostAndPort>
        {
            new DownstreamHostAndPort
            {
                Host = "users-service",
                Port = 443
            }
        }
    },
    // Additional route configurations
};

var config = new OcelotPipelineConfiguration
{
    Routes = routes
};

// Configure authentication middleware
services.AddAuthentication()
    .AddJwtBearer("users-service", options =>
    {
        // JWT bearer configuration for users service
    })
    .AddJwtBearer("products-service", options =>
    {
        // JWT bearer configuration for products service
    });

await ocelotBuilder.AddOcelot(config)
    .AddDelegatingHandler<AuthenticationDelegatingHandler>()
    .Build()
    .StartAsync();


Implement an API Gateway as the single entry point to your microservices.

public class ApiGatewayHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // Pre-processing: authentication, logging, etc.
        AuthenticateRequest(request);

        // Route to the appropriate service
        var response = RouteToService(request);

        // Post-processing: modify response, add headers, etc.
        return await ProcessResponse(response);
    }

    private void AuthenticateRequest(HttpRequestMessage request)
    {
        // Authentication logic
    }

    private Task<HttpResponseMessage> RouteToService(HttpRequestMessage request)
    {
        // Logic to route to specific services
        // This is a placeholder for actual routing logic
        return Task.FromResult(new HttpResponseMessage());
    }

    private async Task<HttpResponseMessage> ProcessResponse(HttpResponseMessage response)
    {
        // Response processing logic
        return response;
    }
}


Error Handling 🚨

Default error handler

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }
}

public class GlobalExceptionFilter : IExceptionFilter
{
    private readonly ILogger<GlobalExceptionFilter> _logger;

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

    public void OnException(ExceptionContext context)
    {
        int statusCode = StatusCodes.Status500InternalServerError;
        string message = "An unexpected error occurred.";

        if (context.Exception is ArgumentException)
        {
            statusCode = StatusCodes.Status400BadRequest;
            message = "Invalid request data.";
        }
        else if (context.Exception is UnauthorizedAccessException)
        {
            statusCode = StatusCodes.Status401Unauthorized;
            message = "Authentication required.";
        }
        // Handle other specific exception types

        _logger.LogError(context.Exception, "Unhandled exception occurred.");

        context.Result = new ObjectResult(new ErrorDetails
        {
            StatusCode = statusCode,
            Message = message
        })
        {
            StatusCode = statusCode
        };

        context.ExceptionHandled = true;
    }
}

// Register the global exception filter
services.AddControllers(options =>
{
    options.Filters.Add<GlobalExceptionFilter>();
});


Custom error handler

public class GlobalExceptionHandler : ExceptionHandler
{
    public override void Handle(ExceptionHandlerContext context)
    {
        // Log the exception details for internal use
        LogException(context.Exception);

        // Provide a friendly error message to the client
        var result = new HttpResponseMessage(HttpStatusCode.InternalServerError)
        {
            Content = new StringContent("An unexpected error occurred. Please try again later."),
            ReasonPhrase = "Critical Exception"
        };

        context.Result = new ErrorMessageResult(context.Request, result);
    }

    private void LogException(Exception exception)
    {
        // Implement logging logic
    }
}

public class ErrorMessageResult : IHttpActionResult
{
    private readonly HttpRequestMessage _request;
    private readonly HttpResponseMessage _httpResponseMessage;

    public ErrorMessageResult(HttpRequestMessage request, HttpResponseMessage httpResponseMessage)
    {
        _request = request;
        _httpResponseMessage = httpResponseMessage;
    }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        return Task.FromResult(_httpResponseMessage);
    }
}

// Register in WebApiConfig
config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());


Input Validation πŸ›‘οΈ

Use data annotations and the [ApiController] attribute for basic validations.

public class CreateUserModel
{
    [Required]
    [StringLength(50)]
    public string Username { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    [StringLength(100, MinimumLength = 6)]
    public string Password { get; set; }
}

[HttpPost]
public IActionResult CreateUser([FromBody] CreateUserModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    // Create user logic

    return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
}


For more complex validation scenarios, consider using a dedicated validation library like FluentValidation.

public class CreateUserValidator : AbstractValidator<CreateUserModel>
{
    public CreateUserValidator()
    {
        RuleFor(x => x.Username)
            .NotEmpty()
            .MaximumLength(50);

        RuleFor(x => x.Email)
            .NotEmpty()
            .EmailAddress();

        RuleFor(x => x.Password)
            .NotEmpty()
            .Length(6, 100);
    }
}

[HttpPost]
public IActionResult CreateUser([FromBody] CreateUserModel model)
{
    var validator = new CreateUserValidator();
    var validationResult = validator.Validate(model);

    if (!validationResult.IsValid)
    {
        return BadRequest(validationResult.Errors);
    }

    // Create user logic

    return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
}


Implement input validation at the API gateway level to ensure that only valid requests are processed.

public class ValidateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!actionContext.ModelState.IsValid)
        {
            actionContext.Response = actionContext.Request.CreateErrorResponse(
                HttpStatusCode.BadRequest, actionContext.ModelState);
        }
    }
}

// Usage in a Controller
public class MyModel
{
    [Required]
    public string Property1 { get; set; }

    // Other properties and validation attributes
}

public class MyApiController : ApiController
{
    [ValidateModel]
    public IHttpActionResult Post(MyModel model)
    {
        // Proceed knowing the model is valid
        ProcessData(model);
        return Ok();
    }

    private void ProcessData(MyModel model)
    {
        // Processing logic
    }
}

Advanced Audit Logging in .NET Core Using Middleware

Supporting Classes

public class AuditLog
{
    public Guid Id { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime CreatedDate { get; set; }
    public string? CreatorId { get; set; }

    public string? CorrelationId { get; set; }
    public AuditLogEventTypes AuditLogEventType { get; set; }
    public string? EntityId { get; set; }
    public string? EntityName { get; set; }
    public string? UserName { get; set; }
    public string? UserRole { get; set; }
    public string? SessionId { get; set; }
    public string? ClientIpAddress { get; set; }
    public string? BrowserInfo { get; set; }
    public string? ApplicationName { get; set; }
    public string? MachineName { get; set; }
    public string? MachineVersion { get; set; }
    public string? MachineOsVersion { get; set; }
    public string? ServiceName { get; set; }
    public string? MethodName { get; set; }
    public string? HttpMethod { get; set; }
    public string? RequestUrl { get; set; }
    public string? ResponseStatus { get; set; }
    public DateTime? ExecutionTime { get; set; }
    public string? ExecutionDuration { get; set; }
    public string? RequestHeaders { get; set; }
    public string? QueryParameters { get; set; }
    public string? BodyParameters { get; set; }
    public string? Exception { get; set; }

    public ICollection<EntityPropertyChange> EntityPropertyChanges { get; set; } = new List<EntityPropertyChange>();
}

public class EntityPropertyChange
{
    public Guid Id { get; set; }
    public string? PropertyName { get; set; }
    public string? OriginalValue { get; set; }
    public string? NewValue { get; set; }
    public string? PropertyTypeFullName { get; set; }
    public Guid AuditLogId { get; set; }
}

public class AuditScope
{
    public List<AuditLog> Logs { get; set; } = new List<AuditLog>();
}


Audit Logging with Entity Tracking in DbContext

public class AppDbContext : DbContext
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly AuditScope _auditScope;

    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }

    public AppDbContext(DbContextOptions<AppDbContext> options, IHttpContextAccessor httpContextAccessor, AuditScope auditScope)
        : base(options)
    {
        _httpContextAccessor = httpContextAccessor;
        _auditScope = auditScope;
    }

    public override int SaveChanges()
    {
        var auditEntries = OnBeforeSaveChanges();
        var result = base.SaveChanges();
        OnAfterSaveChanges(auditEntries);
        return result;
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        var auditEntries = OnBeforeSaveChanges();
        var result = await base.SaveChangesAsync(cancellationToken);
        OnAfterSaveChanges(auditEntries);
        return result;
    }

    private List<EntityEntry> OnBeforeSaveChanges()
    {
        return ChangeTracker.Entries().ToList();
    }

    private void OnAfterSaveChanges(List<EntityEntry> auditEntries)
    {
        foreach (var entry in auditEntries)
        {
            if (entry.Entity is AuditLog) continue;
            
            var tableName = entry.Metadata.GetTableName();

            var auditLog = new AuditLog();
            auditLog.Id = Guid.NewGuid();
            auditLog.AuditLogEventType = entry.State switch
            {
                EntityState.Added => AuditLogEventTypes.Added,
                EntityState.Modified => AuditLogEventTypes.Modified,
                EntityState.Deleted => AuditLogEventTypes.Deleted,
                EntityState.Unchanged => AuditLogEventTypes.Unchanged,
                EntityState.Detached => AuditLogEventTypes.Detached,
                _ => AuditLogEventTypes.Unknown
            };
            auditLog.EntityId = entry.OriginalValues[entry.Metadata.FindPrimaryKey()!.Properties.First().Name]!.ToString();
            auditLog.EntityName = tableName;
            auditLog.UserName = _httpContextAccessor.HttpContext?.User?.GetUserName();
            auditLog.UserRole = _httpContextAccessor.HttpContext?.User?.GetUserRole() != null 
                ? string.Join(",", _httpContextAccessor.HttpContext.User.GetUserRole()!) 
                : null;

            foreach (var property in entry.Properties)
            {
                var columnName = property.Metadata.Name;
                var oldValue = property.OriginalValue?.ToString();
                var newValue = property.CurrentValue?.ToString();

                if (!Equals(oldValue, newValue))
                {
                    var entityPropertyChange = new EntityPropertyChange
                    {
                        Id = Guid.NewGuid(),
                        NewValue = newValue,
                        OriginalValue = oldValue,
                        PropertyName = columnName,
                        PropertyTypeFullName = property.Metadata.ClrType.FullName,
                        AuditLogId = auditLog.Id
                    };

                    auditLog.EntityPropertyChanges.Add(entityPropertyChange);
                }
            }
            
            _auditScope.Logs.Add(auditLog);
        }
    }
}


The AuditLogMiddleware Class

This middleware captures a wide range of information, including HTTP request/response details, execution times, and exception messages.

public class AuditLogMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IConfiguration _configuration;

    public AuditLogMiddleware(RequestDelegate next, IConfiguration configuration)
    {
        _next = next;
        _configuration = configuration;
    }

    public async Task InvokeAsync(HttpContext context, AuditScope auditScope)
    {
        var stopwatch = Stopwatch.StartNew();
        var executionTime = DateTime.UtcNow;
        var applicationName = _configuration.GetApplicationName();
        var correlationId = context.TraceIdentifier;
        
        var originalBodyStream = context.Response.Body;
        using var responseBody = new MemoryStream();
        context.Response.Body = responseBody;
        
        await _next(context);
        
        stopwatch.Stop();

        if (auditScope.Logs.Any())
        {
            foreach (var auditLog in auditScope.Logs)
            {
                var forwardedHeader = context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
                var ipAddress = string.IsNullOrEmpty(forwardedHeader)
                    ? context.Connection.RemoteIpAddress?.ToString()
                    : forwardedHeader.Split(',')[0];
                
                auditLog.CorrelationId = correlationId;
                auditLog.SessionId = context.User.GetUserCustomProperty("SessionId");
                auditLog.ClientIpAddress = ipAddress;
                auditLog.BrowserInfo = context.Request.Headers["User-Agent"];
                auditLog.ApplicationName = applicationName;
                auditLog.MachineName = Environment.MachineName;
                auditLog.MachineVersion = Environment.Version.ToString();
                auditLog.MachineOsVersion = Environment.OSVersion.ToString();
                var routeData = context.GetRouteData();
                auditLog.ServiceName = routeData?.Values["controller"]?.ToString();
                auditLog.MethodName = routeData?.Values["action"]?.ToString();
                auditLog.HttpMethod = context.Request.Method;
                auditLog.RequestUrl = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}";
                auditLog.ResponseStatus = context.Response.StatusCode.ToString();
                auditLog.ExecutionTime = executionTime;
                auditLog.ExecutionDuration = stopwatch.ElapsedMilliseconds.ToString();
                auditLog.RequestHeaders = JsonConvert.SerializeObject(
                    context.Request.Headers.ToDictionary(
                        header => header.Key, header => header.Value.ToString()
                        )
                    );
                auditLog.QueryParameters = JsonConvert.SerializeObject(
                    context.Request.Query.ToDictionary(
                        query => query.Key, query => query.Value.ToString()
                        )
                    );
                
                if (context.Request.Body.CanSeek == true)
                {
                    context.Request.Body.Position = 0;
                }
                else
                {
                    context.Request.EnableBuffering();
                }
                
                string originalContent;
                using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true))
                {
                    originalContent = await reader.ReadToEndAsync();
                    context.Request.Body.Position = 0;
                }
                
                var bodyParameters = JsonConvert.DeserializeObject<Dictionary<string, object>>(originalContent);
                if (bodyParameters != null && bodyParameters.Any())
                {
                    var bodyContent = JsonConvert.SerializeObject(bodyParameters);
                    auditLog.BodyParameters = bodyContent;
                }
                
                if (context.Response.StatusCode >= 400)
                {
                    responseBody.Seek(0, SeekOrigin.Begin);
                    var responseText = await new StreamReader(responseBody).ReadToEndAsync();
                    
                    auditLog.Exception = responseText;
                    
                    responseBody.Seek(0, SeekOrigin.Begin);
                    await responseBody.CopyToAsync(originalBodyStream);
                }
            }
        }
        
    }
}


Program.cs

app.UseMiddleware<AuditLogMiddleware>();
   app.UseRouting();
   app.UseAuthorization();
builder.Services.AddScoped<AuditScope>();

API Responses in .NET Controllers

JSON Response (JsonResult)

Returns a JSON response. Automatically sets the content type to application/json.

public JsonResult GetCustomer()
{
    return Json(new { Name = "Mike", Age = 30, Gender = "Male" });
}


Object Response (ActionResult<T> or IActionResult)

Uses Ok(), NotFound(), BadRequest(), etc.

public IActionResult GetCustomer()
{
    var customer = new { Name = "Mike", Age = 30, Gender = "Male" };
    return Ok(customer); // Returns HTTP 200 with JSON data
}


ActionResult<T> is a strongly typed response.

public ActionResult<Customer> GetCustomer()
{
    var customer = new Customer { Name = "Mike", Age = 30, Gender = "Male" };
    return customer; // Automatically returns JSON with HTTP 200
}


File Response (FileResult)

Returns a file for download.

public FileResult DownloadPdf()
{
    var fileBytes = System.IO.File.ReadAllBytes("Files/sample.pdf");
    return File(fileBytes, "application/pdf", "sample.pdf");
}


Content Response (ContentResult)

Returns raw content (e.g., HTML, plain text, JSON).

public ContentResult GetHtml()
{
    return Content("<h1>Hello, World!</h1>", "text/html");
}


Status Code Response (StatusCodeResult or ObjectResult)

Returns specific HTTP status codes.

public IActionResult UnauthorizedAccess()
{
    return StatusCode(401); // Returns HTTP 401 Unauthorized
}


With additional data:

public IActionResult CustomResponse()
{
    return StatusCode(500, new { Message = "Internal Server Error" });
}


Redirect Response (RedirectResult, RedirectToActionResult)

Redirects to another URL or action.

public IActionResult RedirectToGoogle()
{
    return Redirect("https://www.google.com");
}


Redirect to another action:

public IActionResult RedirectToHome()
{
    return RedirectToAction("Index", "Home");
}


Empty Response (EmptyResult)

Returns an empty response with HTTP 200 status.

public EmptyResult DoNothing()
{
    return new EmptyResult();
}


No Content Response (NoContentResult)

Returns HTTP 204 No Content.

public IActionResult DeleteCustomer()
{
    return NoContent();
}


Bad Request (BadRequestResult or BadRequestObjectResult)

Returns HTTP 400 Bad Request.

public IActionResult InvalidRequest()
{
    return BadRequest("Invalid request data");
}


Not Found (NotFoundResult or NotFoundObjectResult)

Returns HTTP 404 Not Found.

public IActionResult GetCustomerById(int id)
{
    if (id <= 0)
        return NotFound($"Customer with ID {id} not found");


    return Ok(new { Id = id, Name = "John Doe" });
}


Created (CreatedResult or CreatedAtActionResult)

Returns HTTP 201 Created.

public IActionResult CreateCustomer(Customer customer)
{
    return CreatedAtAction(nameof(GetCustomerById), new { id = customer.Id }, customer);
}


Accepted (AcceptedResult or AcceptedAtActionResult)

Returns HTTP 202 Accepted.

public IActionResult ProcessRequest() { return Accepted(new { Status = "Processing" }); }

Append

Append is used to add a single element to the end of a sequence.


If you have a list of products and want to append a new product to the end:

var products = new List<string> { "Product1", "Product2", "Product3" };
var newProduct = "Product4";


var updatedProducts = products.Append(newProduct);


foreach (var product in updatedProducts)
{
    Console.WriteLine(product);
}

Appsettings in .NET Core

Define Your appsettings.json

Create an appsettings.json file in the root of your project:

{
  "AppSettings": {
    "FeatureToggle": true,
    "ApplicationName": "MyApp",
    "MaxUsers": 1000
  }
}


Access Configuration in Code

Using IConfiguration

using Microsoft.Extensions.Configuration;
using System;

class Program
{
    static void Main(string[] args)
    {
        var config = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json")
            .Build();

        var appName = config["AppSettings:ApplicationName"];
        var maxUsers = config["AppSettings:MaxUsers"];

        Console.WriteLine($"Application Name: {appName}");
        Console.WriteLine($"Max Users: {maxUsers}");
    }
}


Using Strongly-Typed Classes

Define a Class:

public class AppSettings
{
    public bool FeatureToggle { get; set; }
    public string ApplicationName { get; set; }
    public int MaxUsers { get; set; }
}


Bind to the Class:

var builder = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .Build();

var appSettings = builder.GetSection("AppSettings").Get<AppSettings>();

Console.WriteLine($"FeatureToggle: {appSettings.FeatureToggle}");
Console.WriteLine($"Application Name: {appSettings.ApplicationName}");
Console.WriteLine($"Max Users: {appSettings.MaxUsers}");


Environment-Specific Configuration

Create environment-specific files:

  • appsettings.Development.json
  • appsettings.Production.json


Add settings specific to the environment:


appsettings.Development.json

{
  "AppSettings": {
    "FeatureToggle": false,
    "MaxUsers": 50
  }
}


Configure the environment in Program.cs:

var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";

var config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .AddJsonFile($"appsettings.{environment}.json", optional: true)
    .Build();


Inject Configuration into Services

Add Configuration to DI:

builder.Services.Configure<AppSettings>(builder.Configuration.GetSection("AppSettings"));


Access in Controllers or Services:

public class HomeController : Controller
{
    private readonly AppSettings _appSettings;

    public HomeController(IOptions<AppSettings> appSettings)
    {
        _appSettings = appSettings.Value;
    }

    public IActionResult Index()
    {
        ViewData["AppName"] = _appSettings.ApplicationName;
        return View();
    }
}

ASP.global_asax does not exist in the namespace ASP

Change

protected static ASP.global_asax ApplicationInstance {
    get {
        return ((ASP.global_asax)(Context.ApplicationInstance));
    }
}

to

protected static System.Web.HttpApplication ApplicationInstance {
    get {
        return ((System.Web.HttpApplication)(Context.ApplicationInstance));
    }
}

ASP.NET Core - A connection was successfully established with the server, but then an error occurred during the pre-login handshake

Add TrustServerCertificate=true to the end of the connection string.

ASP.NET Core - Database Provider

.UseSqlServer(connectionString)


.UseCosmos(connectionString, databaseName)


.UseSqlite(connectionString)


.UseInMemoryDatabase(databaseName)


.UseNpgsql(connectionString)


.UseMySql(connectionString)


.UseOracle(connectionString)


* These database providers are not shipped by Microsoft.

ASP.NET Core - MVC application cannot find view

Required nuget

  1. Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation


Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.AddControllersWithViews().AddRazorRuntimeCompilation();

ο»Ώο»Ώο»Ώ