Monday, 29 September 2025

Configuring Entity Framework Core with Resiliency/retry and Logging in .NET

Configuring Entity Framework Core with Resiliency and Logging in .NET

When working with databases in enterprise applications, ensuring reliability and observability is just as important as building features. A typical challenge with relational databases like SQL Server is handling transient errors such as network hiccups, timeout issues, or temporary unavailability of resources.

In this blog, we’ll walk through how to configure Entity Framework Core (EF Core) in a way that adds resiliency with retry logic and enables SQL query logging for better debugging.


1. Centralized DbContext Configuration

To keep things clean and reusable, we often extract database configuration into a separate class. Below is an extension method added to IServiceCollection:

public static class DbConfiguration
{
        public static IServiceCollection AddDbConextConfiguration
                            (this IServiceCollection services
                            , IConfiguration configuration)
        {
           
            services.AddDbContext<AccessManagementDbContext>((serviceProvider, options) =>
            {
                options.UseSqlServer
                (configuration.GetConnectionString("DbConnection"), sqlOptions =>
                    {
                    sqlOptions.EnableRetryOnFailure(
                        maxRetryCount: 3, // retry will apply 3 times
                       
                        maxRetryDelay: TimeSpan.FromSeconds(10),//interval for retry
                        errorNumbersToAdd: null);
                    }
                );
//SQL logs for local debug purpose
#if DEBUG
                var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();

                options
                    .EnableSensitiveDataLogging()
                    .UseLoggerFactory(loggerFactory)
                    .LogTo(
                        log => loggerFactory.CreateLogger("SQL").LogInformation(log),
                        LogLevel.Information
                    );
#endif
            });

            return services;

        }
    }

2. Key Features Explained

a) Resiliency with Retry-on-Failure

The method EnableRetryOnFailure makes database calls more reliable by retrying transient failures automatically.

  • maxRetryCount: 3 → Retries up to three times.

  • maxRetryDelay: 10s → Maximum delay between retries.

  • errorNumbersToAdd → Can be customized for SQL error codes that should trigger retries.

This is especially useful in cloud environments (e.g., Azure SQL), where transient connectivity issues are common.

3. Why This Approach?

Centralized Configuration – Keeps database setup in one place.
Resiliency – Handles transient failures automatically without extra code.
Debug-Friendly – SQL logs help in analyzing queries and optimizing performance.
Environment-Specific – Logging is enabled only in development/debug mode.


4. How to Use It?

Once this configuration is in place, simply register it in your Program.cs or Startup.cs:

builder.Services.AddDbConextConfiguration(builder.Configuration);


Friday, 16 May 2025

MSAL (Microsoft Authentication Library)

 

🔐 What is MSAL?

MSAL (Microsoft Authentication Library) is a Microsoft-supported SDK used to authenticate users and acquire tokens from:

  • Azure AD

  • Azure AD B2C

  • Microsoft Identity Platform (v2 endpoint)

It supports:

  • Authorization Code Flow (for web apps)

  • Device Code Flow

  • Username/password (ROPC – discouraged for production)

  • Confidential client (server-to-Azure API access)

  • Public client (desktop/mobile apps)


✅ MSAL is used with OpenID Connect and OAuth 2.0 (not SAML)

MSAL is not a SAML library. It's used for OpenID Connect/OAuth2-based authentication flows, usually with Azure AD or Azure B2C.


🧰 MSAL Libraries for .NET

PlatformLibraryNuGet Package
Web apps / APIsMicrosoft.Identity.WebMicrosoft.Identity.Web
General useMicrosoft.Identity.ClientMicrosoft.Identity.Client (MSAL itself)
Blazor (WASM)Microsoft.Authentication.WebAssembly.MsalBlazor-specific

🔧 Example Usage in .NET Core / .NET 8

For Web Apps (OpenID Connect):

Use Microsoft.Identity.Web:

csharp
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApp(Configuration.GetSection("AzureAdB2C")) .EnableTokenAcquisitionToCallDownstreamApi() .AddMicrosoftGraph() .AddInMemoryTokenCaches();

Under the hood, Microsoft.Identity.Web uses MSAL.NET to acquire and cache tokens.

For APIs (Confidential client):

csharp
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder .Create(clientId) .WithClientSecret(clientSecret) .WithAuthority(new Uri(authority)) .Build(); AuthenticationResult result = await app .AcquireTokenForClient(scopes) .ExecuteAsync();

🎯 Where MSAL Fits

ScenarioUse MSAL?
Web app login (Azure AD/B2C)✅ Yes
Call Microsoft Graph API✅ Yes
Authenticate users with SAML❌ No
Acquire tokens for Azure APIs✅ Yes
Desktop/mobile app login✅ Yes
Integrate with Okta/ADFS (SAML only)❌ No

📌 Summary

  • MSAL is for token acquisition using OAuth 2.0/OpenID Connect, not SAML.

  • Use Microsoft.Identity.Web for ASP.NET Core web apps — it simplifies MSAL integration.

  • For backend-only APIs, use Microsoft.Identity.Client directly.

  • To troubleshoot token/cookie issues, MSAL logging can be enabled via:

Saturday, 19 April 2025

Building a KeyVaultService in .NET for Secure Secret Management

////Declare Interface
public interface IKeyVaultService
{
    public string GetSecret(string secretName, bool appendPrefix = true);

}
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Collections.Concurrent;
public class KeyVaultService : IKeyVaultService
{
    private const string _PREFIX = "XXX";
    private readonly string _keyVaultBaseUrl;
    protected  SecretClient _keyVaultClient;
    private static readonly ConcurrentDictionary<string, string> _keyVaultSecrets =
                         new ConcurrentDictionary<string, string>();
    private readonly KeyVaultConfigAppSetting _keyVaultLocalConfigAppSetting;
    private readonly ILogger _logger;
   
    /// <summary>
    /// initialize
    /// </summary>
    /// <param name="keyVaultLocalOptions"></param>
    /// <exception cref="Exception"></exception>
    public KeyVaultService(
        IOptions<KeyVaultConfigAppSetting> keyVaultLocalOptions
        ,ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<KeyVaultService>();
        _keyVaultLocalConfigAppSetting = keyVaultLocalOptions.Value;
        _keyVaultBaseUrl = _keyVaultLocalConfigAppSetting.BaseUrl;
        _keyVaultBaseUrl = _keyVaultLocalConfigAppSetting.BaseUrl
            ?? throw new InvalidOperationException
            ("KeyVaultBaseUrl is null or not configured");

        EnsureConfigured();

    }

    /// <summary>
    /// get secret value from keyvault
    /// </summary>
    /// <param name="secretName"></param>
    /// <param name="appendPrefix"></param>
    /// <returns></returns>
    public string GetSecret(string secretName, bool appendPrefix = true)
    {
        try
        {
            EnsureConfigured();
       
            if (appendPrefix && !secretName.StartsWith(_PREFIX))
                secretName = $"{_PREFIX}{secretName}";
            _keyVaultSecrets.TryGetValue(secretName, out var secretValue);

            if (string.IsNullOrEmpty(secretValue))
            {
                var secretBundle = _keyVaultClient.GetSecret(secretName);
                secretValue = secretBundle.Value?.Value;

                if (!string.IsNullOrEmpty(secretValue))
                    _keyVaultSecrets.TryAdd(secretName, secretValue);
            }

            return secretValue;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Keyvalut Error:{0} {1}", GetType()?.Name, ex.Message);
            throw new InvalidOperationException
            ("An error occurred while accessing the Keyvault.", ex);
        }

    }

    private void EnsureConfigured()
    {
        if (_keyVaultClient == null)
#if DEBUG
//Create azure key-vault instance using Application client id and client secret.
            ConfigureWithClientCredentials();
#else
            ConfigureAzureCredentials();
#endif
    }

    private void ConfigureWithClientCredentials()
    {


        var tenantId = _keyVaultLocalConfigAppSetting.TenantId;
        var sharedSecretKey = _keyVaultLocalConfigAppSetting.SharedSecretKey;
        var sharedSecretAppId = _keyVaultLocalConfigAppSetting.AppId;

        // If Shared Secret is present and not "Disabled",
        // use the shared secret (typically for local development).
        if (!string.IsNullOrWhiteSpace(sharedSecretKey)
        && sharedSecretKey != "Disabled")
        {
           
                ClientSecretCredential clientCredential =
                            new ClientSecretCredential
                            (tenantId, sharedSecretAppId, sharedSecretKey);
                _keyVaultClient = new SecretClient
                    (vaultUri: new Uri(_keyVaultBaseUrl), credential: clientCredential);
           
        }
        else
        {
            //fallback to the default credential authentication
            _keyVaultClient = new SecretClient(
                vaultUri: new Uri(_keyVaultBaseUrl)
                , credential: new DefaultAzureCredential());
        }

    }

    private void ConfigureAzureCredentials()
    {
        try
        {
            _keyVaultClient = new SecretClient(
                vaultUri: new Uri(_keyVaultBaseUrl)
                , credential: new DefaultAzureCredential());
        }
        catch (Exception ex)
        {
            _logger.LogError
            (ex,"Keyvalut Error: {0}  - {1}", GetType()?.Name, ex.Message);
            throw;
        }
    }
}

Middleware in .Net Core

Middleware is an software used to handle web request pipeline. 

Describe in simple term means it's organize the request and response using delegates and pipeline.

It is an decision maker to pass the next request to particular component.

Example:

3-tier Architecture (API, Business Layer, Data Layer)

we will receive n-number request at the time middleware anonymously  handle the request in pipeline to execute API --> Business Layer --> Data Layer and response vise versa.

Middleware Roles and Responsibility

    1.Chooses whether to pass the request to the next component in the pipeline.

    2.Can perform work before and after the next component in the pipeline.

Request delegates are used to build the request pipeline. The request delegates handle each HTTP request.


Request delegates are configured using RunMap, and Use extension methods. An individual request delegate can be specified in-line as an anonymous method (called in-line middleware), or it can be defined in a reusable class. These reusable classes and in-line anonymous methods are middleware, also called middleware components. Each middleware component in the request pipeline is responsible for invoking the next component in the pipeline or short-circuiting the pipeline. When a middleware short-circuits, it's called a terminal middleware because it prevents further middleware from processing the request.

Built-in middleware
MiddlewareDescription
HTTP Method OverrideAllows an incoming POST request to override the method.
HTTPS RedirectionRedirect all HTTP requests to HTTPS.
HTTP Strict Transport Security (HSTS)Security enhancement middleware that adds a special response header.

middleware workflow






Friday, 4 April 2025

Elegant HTTP Response Parsing in .NET

/// <summary>
/// Extension methods for parsing API responses into OperationalResult<T>.
/// </summary>
public static class ApiResponseParser
{
    /// <summary>
    /// Parses the HTTP response and returns a standardized OperationalResult<T>.
    /// Handles success, primitive types, and structured error responses.
    /// </summary>
    /// <typeparam name="T">The expected type of the result.</typeparam>
    /// <param name="apiResponse">The HTTP response message.</param>
    /// <returns>An OperationalResult containing the parsed result or an error.
    /// </returns>
    public static async Task<OperationalResult<T>> GetParsedApiResponse<T>
                        (this HttpResponseMessage apiResponse)
    {
        var responseBody = await apiResponse.Content.ReadAsStringAsync();

        if (ValidHttpStatusCodes.Contains(apiResponse.StatusCode)
                && apiResponse.StatusCode != HttpStatusCode.BadRequest)
        {
            var type = typeof(T);

            if (string.IsNullOrEmpty(responseBody))
            {
                return OperationalResult<T>.Successed();
            }

            if (type.IsPrimitive)
            {
                var primitiveValue = (T)Convert.ChangeType(apiResponse.Content,
                                     typeof(T));
                return OperationalResult<T>.Successed(primitiveValue);
            }

            var data = responseBody.JsonDeserializeObject<T>();
            return OperationalResult<T>.Successed(data);
        }
        else
        {
            var apiError = responseBody.JsonDeserializeObjectTryParse<ApiError>();

            if (apiError != null)
            {
                return OperationalResult<T>.Failed(
                    (ResponseStatusType)apiError.Code, apiError.Message);
            }

            return OperationalResult<T>.Failed(
                (ResponseStatusType)apiResponse.StatusCode,
                new ErrorDto($"Error: {apiResponse.StatusCode}"
                , apiResponse.ReasonPhrase)
            );
        }
    }

    /// <summary>
    /// A list of valid HTTP status codes considered as successful responses.
    /// </summary>
    public static readonly IEnumerable<HttpStatusCode> ValidHttpStatusCodes =
                            new List<HttpStatusCode>
                            {
                                HttpStatusCode.OK,
                                HttpStatusCode.Created,
                                HttpStatusCode.NoContent
                            };
}

Sunday, 26 January 2025

Apply Migration and DB schema if not exist while run local

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
  {
      if (env.IsDevelopment())
      {
          app.UseDeveloperExceptionPage();

          // Ensure the database is created and apply migrations if it is empty
          using (var scope = app.ApplicationServices.CreateScope())
          {
              var dbContext = scope.ServiceProvider.GetRequiredService<DbContext>();


              // Retrieve the IRelationalDatabaseCreator service from the service provider
              var databaseCreator = dbContext.Database
                                .GetService<IRelationalDatabaseCreator>();

              // Check if the database exists, and create it if it doesn't
              // Create the database If SQL server is local.Otherwise don't apply migration and database creation
              if (!databaseCreator.Exists())
              {
                      databaseCreator.Create();
                  // Check if the database has any tables
                  if (!databaseCreator.HasTables())
                  {
                      dbContext.Database.Migrate();
                  }// Apply all migrations
               
              }
             
          }
      }
}

Monday, 13 January 2025

EF Core 9 Generate Code First from Existing DB

1.Install EF Core, EF core Design, EF sql server using nuget packages.

2. Open package manger console 

3. Select the project where need to place entity and DbContext

4.Run below comments with modified inputs which is highlighted.

dotnet ef dbcontext scaffold "Server={Server};Database={DBname};User Id={db user name};Password={PWD};Encrypt=True;TrustServerCertificate=True;" Microsoft.EntityFrameworkCore.SqlServer --output-dir DbEntity --context-dir DbContext --context CoreDbContext --use-database-names --force --project D:/Source_vs2022/Core.Data/Core.Data.csproj

5. Now we can see Db context and db entity



6.Migrate to Full Code-First

If you want to start using migrations with the generated context execute below in same package mager console.

  1. Add migrations:
    dotnet ef migrations add InitialCreate --project Core.Data
  2. Update the database:
    dotnet ef database update --project Core.Data --verbose
Where :
  • InitialCreate is migration name
  • Core.Data is project name which contains EF.
NOTE: 
  • In case any other error occurs like missing OnConfigure then add below code in DBContext file.
  • after migration generated remove it below code due to security reason.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    if (!optionsBuilder.IsConfigured)
    {
        optionsBuilder.UseSqlServer("Server={DB-Server};Initial Catalog={DB};User ID={UserName};Password={PWD};");
    }
}