Author: sks

  • Setting up Azure AD Authentication for a React SPA and .NET API with Dataverse On-Behalf-Of Flow

    Setting up Azure AD Authentication for a React SPA and .NET API with Dataverse On-Behalf-Of Flow

    This guide covers how to configure Entra authentication for a React Single Page Application (SPA) and a .NET API backend, enabling access to Microsoft Dataverse using the On-Behalf-Of (OBO) flow.

    1. Register the Backend API Application

    Name: myapp-api

    • Navigate to Azure Portal → Azure Active Directory → App registrations → New registration.
    • Name: myapp-api
    • Supported account types: Single tenant
    • Redirect URI: leave blank.

    After registration:

    Screenshot of the Azure portal "Expose an API" configuration page for the myapp-api app registration. The Application ID URI is set to api://4ac715bd-612a-4830-89e0-b3ad0efe5a6e. One scope is defined: user_impersonation, with "Admins and users" able to consent, display name "Access Dataverse data," and the state set to "Enabled."
    1. Expose an API:
      • Set the Application ID URI to: api://<myapp-api-app-id>
      • Add a Scope:
        • Name: user_impersonation
        • Display Name/Description: “Access Dataverse data”
        • State: Enabled
    2. Grant permissions:
      • Go to API permissions → Add a permission.
      • Choose APIs my organization uses, search for Dataverse or Dynamics CRM.
      • Add the user_impersonation or .default scope.
      • Grant admin consent
    Azure Active Directory API permissions setup for Dataverse integration. "user_impersonation" delegated permission from Dynamics CRM API is configured and granted for tenant. No admin consent required. Used for On-Behalf-Of (OBO) authentication flow in Azure AD-secured .NET APIs accessing Dataverse.

    Note: Dataverse App ID: 00000007-0000-0000-c000-000000000000

    2. Register the Frontend SPA Application

    Name: myapp-frontend

    • Navigate to Azure Portal → Azure Active Directory → App registrations → New registration.
    • Name: myapp-frontend
    • Supported account types: Single tenant
    • Redirect URIs:
      • Platform: Single-page application (SPA)
      • Add: http://localhost:3000 or any URLs your frontend is running on.

    After registration:

    1. Enable Implicit grant (under Authentication):
      • ID tokens
      • Access tokens
    2. Assign API permissions:
      • My APIs → select myaapp-api → add user_impersonation, you can search for the Application ID of myapp-api
    Azure AD API permissions setup for React SPA using Microsoft Identity platform. Application configured with Microsoft Graph "User.Read" and custom myapp-api "user_impersonation" delegated permissions for Dataverse access via On-Behalf-Of authentication flow.

    3. Pre-authorize Frontend in Backend

    Azure AD authorized client applications setup showing pre-authorized React SPA frontend (Client ID: 3915cbe2-24b3-41f6-8337-4b0c7ccf4664) for backend API access using user_impersonation scope in On-Behalf-Of authentication flow.
    • In myapp-api, go to Expose an API → Add a client application.
    • – Add:
      • Client ID: 3915cbe2-24b3-41f6-8337-4b0c7ccf4664 (myapp-frontend App ID)
      • Scope: user_impersonation

    4. Register the Backend App as a Dataverse App User

    • Copy the myapp-api Application ID.
    • Go to Power Platform Admin Center → App Users → New App User
    • Paste the Application ID.
    • Assign the appropriate security role for Dataverse access.

    5. Backend Code Setup

    Program.cs

    // Choose credential source based on environment (development vs production)
    TokenCredential credential = builder.Environment.IsDevelopment() switch
    {
        true => new DefaultAzureCredential(),
        false => new DefaultAzureCredential()
    };
    
    // Configure authentication using Microsoft Identity Web
    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
        .EnableTokenAcquisitionToCallDownstreamApi()
        .AddDownstreamApi("DownstreamDataverseApi", builder.Configuration.GetSection("DownstreamDataverseApi"))
        .AddInMemoryTokenCaches();
    
    // Configure HTTP client for Dataverse API calls
    builder.Services.AddHttpClient("DataverseClient", client =>
    {
        client.BaseAddress = new Uri(dataverseUrl); // Dataverse environment URL
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        client.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
        client.DefaultRequestHeaders.Add("OData-Version", "4.0");
        client.DefaultRequestHeaders.Add("Prefer", "odata.include-annotations=\"*\""); // Request annotations
    })
    // Add authentication handler for injecting tokens
    .AddHttpMessageHandler(_ => new DataverseManagedIdentityAuthHandler())
    // Optional: Add resilience features (retry policies, etc.)
    .AddStandardResilienceHandler();
    
    // Register custom Dataverse client wrapper
    builder.Services.AddScoped<DataverseHttpClient>();
    
    var app = builder.Build();
    
    // Enable authentication and authorization middleware
    app.UseAuthentication();
    app.UseAuthorization();

    appsettings.json

    {
        "AzureAd": {
            "Audience": "api://4ac715bd-612a-4830-89e0-b3ad0efe5a6e", // Backend API Application ID URI
            "Authority": "https://login.microsoftonline.com/<your-tenant-id>", // Tenant Authority URL
            "Instance": "https://login.microsoftonline.com/", // Common Microsoft login endpoint
            "TenantId": "<your-tenant-id>", // Azure AD Tenant ID
            "Domain": "your-tenant-domain.com", // Tenant domain name
            "ClientId": "4ac715bd-612a-4830-89e0-b3ad0efe5a6e" // Backend API App Registration Client ID
        },
        "DownstreamDataverseApi": {
            "BaseUrl": "https://{your-environment}.api.crm.dynamics.com/", // Dataverse API base URL
            "Scopes": "https://{your-environment}.api.crm.dynamics.com/.default" // Dataverse API default scope
        }
    }
    

    DataverseManagedIdentityAuthHandler.cs

    // Custom HTTP message handler for authenticating Dataverse API requests
    public sealed class DataverseManagedIdentityAuthHandler(
        ITokenAcquisition tokenAcquisition,
        IHttpContextAccessor httpContextAccessor,
        IConfiguration configuration) : DelegatingHandler
    {
        // This method is called for every outgoing HTTP request
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            // Get the current authenticated user from HTTP context
            var user = httpContextAccessor.HttpContext?.User;
    
            // Reject if no authenticated user is found
            if (user?.Identity is not { IsAuthenticated: true })
            {
                throw new UnauthorizedAccessException("User is not authenticated");
            }
    
            // Load required scopes from configuration
            var scopesToAccessDataverseApi = configuration["DownstreamDataverseApi:Scopes"]
                ?? throw new Exception("Configuration 'DownstreamDataverseApi:Scopes' is missing.");
    
            // Acquire access token on behalf of the authenticated user
            var token = await tokenAcquisition.GetAccessTokenForUserAsync(
                [scopesToAccessDataverseApi],
                user: user);
    
            // Attach the token to the outgoing HTTP request
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
    
            // Proceed with sending the HTTP request
            return await base.SendAsync(request, cancellationToken);
        }
    }
    
  • Accessing Dataverse OData API from Azure Functions Using Managed Identity

    Accessing Dataverse OData API from Azure Functions Using Managed Identity

    When building serverless solutions in Azure, securely connecting to services like Microsoft Dataverse (formerly Common Data Service) is a common task. Azure Functions combined with Managed Identity offers a clean and secure way to authenticate without managing secrets. In this post, I’ll show you how to access the Dataverse OData API using a managed identity, and share a few gotchas—like needing the Application (client) ID, not the Object ID, when registering the app in Power Platform.

    Why Managed Identity?

    Managed Identity lets your Azure Function authenticate against Azure AD and access resources like Dataverse without needing any client secrets or certificates.

    Step 1: Configure Azure Function for Managed Identity

    In your Azure Function:

    1. Go to the Identity blade in the portal.
    2. Enable the System-assigned managed identity.
    3. Copy the Object ID (you’ll need this in the next step).

    Step 2: Register the Managed Identity as an App User in Power Platform

    Here’s the tricky part: Power Platform (Dataverse) requires the Application (client) ID of an Entra ID Enterprise Application, not the Object ID.

    Here’s how to get it:

    1. Go to Microsoft Entra ID → Enterprise Applications.
    2. Search for the Object ID from your Azure Function’s Identity blade.
    3. Open that Enterprise App and copy the Application ID (this is what Dataverse wants).
    4. In Power Platform Admin Center, create a new App User, paste this Application ID, and assign the correct security role.

    Step 3: Implement a Custom Auth Handler in Your Azure Function

    Now, let’s get to code.

    Create a delegating handler that uses DefaultAzureCredential to fetch a token for Dataverse:

    public sealed class DataverseManagedIdentityAuthHandler : DelegatingHandler
    {
        private readonly DefaultAzureCredential _credential = new();
    
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            var scopes = new[] { "https://{your-environment}.api.crm.dynamics.com/" };
    
            var token = await _credential.GetTokenAsync(
                new TokenRequestContext(scopes),
                cancellationToken);
    
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
    
            return await base.SendAsync(request, cancellationToken);
        }
    }

    Step 4: Register the HTTP Client with Dependency Injection

    Here’s a clean way to set up your Dataverse client using IHttpClientFactory:

    public static IServiceCollection AddDataverseHttpClient(this IServiceCollection services, string dataverseUrl)
    {
        services.AddHttpClient("DataverseClient", client =>
            {
                client.BaseAddress = new Uri(dataverseUrl);
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                client.DefaultRequestHeaders.Add("OData-MaxVersion", "4.0");
                client.DefaultRequestHeaders.Add("OData-Version", "4.0");
                client.DefaultRequestHeaders.Add("Prefer", "odata.include-annotations=\"*\"");
            })
            .AddHttpMessageHandler(_ => new DataverseManagedIdentityAuthHandler())
            .AddStandardResilienceHandler(); // optional
    
        services.AddScoped<DataverseHttpClient>(); // your custom wrapper
    
        return services;
    }

  • Catching common C# string performance fixes with Semgrep

    Catching common C# string performance fixes with Semgrep

    Following up on my previous post, I’ve put together a new set of Semgrep rules focused specifically on string-related performance issues in C#.

    These are the kinds of things that rarely show up in code reviews, and not everything is covered by Resharper or IDEs. The issues adds up tho, especially if they are in a hot path. Things related to strings can also cause a lot of allocations, leading to plenty of work for the garbage collector. These rules are designed to be lightweight, easy to integrate into your workflow, and catch the kind of subtle inefficiencies that can quietly degrade performance over time. Some of the have auto fixes, meaning you can apply the rules to your code base, and it will sort it out. This is still work in progress, but let’s go through the rules.

    1. String Comparison

    Calling ToLower() or ToUpper() just to compare strings is wasteful, it allocates a new string, converts every character, and then compares. Use string.Equals(str1, str2, StringComparison.OrdinalIgnoreCase) to compare the strings without creating any temporary strings. Resharper does not flag this.

     public bool ToLower_Different()
     {
         // Here ToLower allocates a new string.
         return TestString1.ToLower().Equals(TestString2);
     }
     public bool StringEquals_OrdinalIgnoreCase_SameIgnoreCase()
    {
          // Here we compare without allocating new strings
          return string.Equals(TestString1, TestString2,       
                               StringComparison.OrdinalIgnoreCase);
    }

    csharp-inefficient-string-comparison.yaml

    rules:
      - id: csharp-inefficient-string-comparison
        patterns:
          - pattern-either:
              - pattern: $STR.ToLower().Equals($OTHER)
              - pattern: $STR.ToLowerInvariant().Equals($OTHER)
              - pattern: $STR.ToUpper().Equals($OTHER)
              - pattern: $STR.ToUpperInvariant().Equals($OTHER)
          - pattern-not: String.Equals($STR, $OTHER, StringComparison.OrdinalIgnoreCase)
        message: >
          Inefficient string comparison. Use String.Equals(s1, s2, StringComparison.OrdinalIgnoreCase) 
          instead of ToLower()/ToUpper().Equals() for better performance and clarity.
        fix: String.Equals($STR, $OTHER, StringComparison.OrdinalIgnoreCase)
        languages: [csharp]
        severity: WARNING
        metadata:
          category: performance
          subcategory:
          - easyfix
          - strings
          references:
          - "https://blog.smistad.me/semgrep-rules-for-c-performance/"

    2. Avoid string.Format for cases where interpolation is enough

    string.Format adds overhead and is harder to read. Interpolation ($"...") is faster and alloc-free in simple cases. For more complex formatting you should continue to use string.Format, but where it is used for basic string concatenation you should switch to string interpolation. Resdharper suggests fixing this if you use string.Format, but not in cases where you use string.Concat.

    public string Format()
    {
        // Resharper suggests switching to interpolation
        return string.Format("{0} {1} {2}", Left, Right, Middle);
    }
    
    public string Interpolation()
    {
        return $"{Left} {Right} {Middle}";
    }
    
    public string Concat()
    {
        // No suggestion to fix this from Resharper
        return string.Concat(Left, " ", Right, " ", Middle);
    }
    MethodMeanRationAllocated
    Interpolation0.4472 ns1.00
    Concat19.2632 ns43.1656 B
    Format44.2375 ns99.1256 B

    Interpolation is much faster than the alternatives. The benchmark here is the code shown above, so I guess the interpolation just gets optimized away in the end. This will also apply to your actual code in situations where you use it for simple string concatenations. It also causes less allocations than the alternatives.

    The reason is that we have to avoid parsing the format parameters, in the cases where you just refer to a variable. So this improvement only really works in simple use cases.

    csharp-string-format-to-interpolation.yaml

    The regex for detecting more complex format parameters is not quite working. So this rule currently picks up some false-positives.

    rules:
      - id: csharp-string-format-to-interpolation
        languages: [csharp]
        severity: WARNING
        message: "Use string interpolation ($\"...\") instead of string.Format for simple cases"
        metadata:
          description: "Detects simple string.Format calls that could be replaced with string interpolation"
          category: "performance"
          references:
            - "https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/interpolated"
            - "https://blog.smistad.me/semgrep-rules-for-c-performance/"
          technology:
            - csharp
          subcategory:
            - easyfix
            - strings
        pattern-either:
          - pattern: string.Format("$FMT", $A1)
          - pattern: string.Format("$FMT", $A1, $A2)
          - pattern: string.Format("$FMT", $A1, $A2, $A3)
          - pattern: string.Format("$FMT", $A1, $A2, $A3, $A4)
        pattern-not-regex: \{\d+:[^}]+\}
    

    3. Use AsSpan() Instead of Substring()

    In some cases we can avoid allocating a new string with string.Substring() and instead use .AsSpan().

    Some typical cases we can avoid is inputs to int/double/Guid.Parse() methods, or comparing a substring to a string literal.

    // allocates new string
    int.Parse(tName.Substring("VariantArray".Length),;
    if (s.Substring(i) == "INF")
    
    // Using AsSpan()
    int.Parse(tName.AsSpan("VariantArray".Length));
    if (s.AsSpan(i).SequenceEqual("INF"))

    Here Resharp will suggest to use a range index instead of substring, but this is actually slower than using the substring method. You do not avoid any allocations either, as you do if you use .AsSpan().

    [Benchmark(Baseline = true)]
    public string Substring()
    {
        return "this is my wonderful string".Substring("this".Length);
    }
    
    [Benchmark]
    public ReadOnlySpan<char> AsSpan()
    {
        return "this is my wonderful string".AsSpan("this".Length);
    }
    
    [Benchmark]
    public string RangeIndex()
    {
        // Resharper will sugest changing your code to this
        return "this is my wonderful string"["this".Length..];
    }
    MethodMeanRatioAllocated
    AsSpan0.2085 ns0.05
    Substring4.5797 ns1.0072 B
    RangeIndex6.5919 ns1.4472 B

    csharp-substring.yaml

    rules:
      - id: csharp-avoid-substring-for-span-accepting-methods
        languages: [csharp]
        message: Use AsSpan instead of Substring to avoid string allocations when passing to methods accepting ReadOnlySpan<char>.
        severity: WARNING
        metadata:
          category: performance
          subcategory:
            - easyfix
            - strings
          likelihood: LOW
          impact: LOW
        patterns:
          - pattern: $METHOD($STR.Substring($IDX))
          - metavariable-regex:
              metavariable: $METHOD
              regex: >
                (int|float|double|decimal|uint|long|bool|Guid|DateTime|DateTimeOffset)\.(Parse(Exact)?|TryParse(Exact)?)
        fix: $METHOD($STR.AsSpan($IDX))
      - id: csharp-avoid-substring-for-suffix
        pattern: $STR.Substring($IDX)
        message: Use AsSpan instead of Substring to avoid string allocations.
        languages: [csharp]
        severity: INFO
        metadata:
          category: performance
          subcategory:
          - easyfix
          - strings
          likelihood: LOW
          impact: LOW
        fix: $STR.AsSpan($IDX)
    
      - id: csharp-avoid-substring-equals
        pattern: $STR.Substring($IDX) == "$SUFFIX"
        message: Use AsSpan(...).SequenceEqual("...") instead of Substring == "..." for performance.
        languages: [csharp]
        severity: WARNING
        metadata:
          category: performance
          subcategory:
          - easyfix
          - strings
          likelihood: LOW
          impact: LOW
        fix: $STR.AsSpan($IDX).SequenceEqual("$SUFFIX")

    4. Optimize UTF-8 Transcoding

    Not avoiding any allocations with this one, but you save some CPU cycles. This is also caught by Resharper

    return Encoding.UTF8.GetBytes("ThIs A StRiNG");
    // Can be shortend to this:
    return "ThIs A StRiNG"u8.ToArray();

    csharp-avoid-transcoding.yaml

    rules:
    - id: csharp-avoid-transcoding
      patterns:
      - pattern-either:
        - pattern: Encoding.UTF8.GetBytes("$STR")
      message: Use u8 to avoid csharp-avoid-transcoding
      fix: \"$STR\"u8.ToArray()
      languages: [csharp]
      severity: WARNING
      metadata:
        category: performance
        subcategory:
        - easyfix
        - strings
        likelihood: LOW
        impact: LOW

    Want to see all the rules and benchmarks? Check out csharp-semgrep-performance on GitHub.

  • Semgrep rules for C# performance

    Semgrep rules for C# performance

    Performance isn’t just about making users happy, though. It makes our lives as developers way better too. Think about your typical day, you’re constantly running your code, debugging, testing, and then doing it all over again. When your code runs faster, you spend less time waiting and more time actually coding. Nobody enjoys staring at a spinning wheel while your tests run or the debugger loads up. Those small delays mess with your flow and make development less fun.

    I was thinking of using semgrep to catch a lot of small easy to fix performance improvements. So I want to just share the rules I make, so maybe somebody else can use them to.

    These rules will be covering the small cases, but sometimes the performance issues can be a death of a thousand cuts. Garbage Collection can be a real killer for performance, so a lot of the rules will try to cover things where there is some alternative that requires less or no allocations.

    Stop Converting Strings Just to Compare Them

    Strings are everywhere in our code, and the way we compare them can make a surprising difference in performance. Here’s our first rule that catches a really common mistake.

    We’ve all done this at some point:

    // The slow way
    if (someString.ToLower().Equals(otherString))
    {
        // Do something
    }
    

    Or maybe this version:

    // Also slow
    if (someString.ToUpper().Equals(otherString))
    {
        // Do something
    }
    

    What’s wrong with this? A few things:

    • It creates a whole new string just for the comparison
    • It wastes memory for this temporary string
    • It has to convert every character before it even starts comparing

    The Better Way

    There’s a much faster way to do the same thing in C#:

    if (String.Equals(someString, otherString, StringComparison.OrdinalIgnoreCase))
    {
        // Do something
    }
    

    This skips creating new strings completely and just does the comparison directly.

    The Semgrep Rule

    Here’s the rule I made to catch this in your code:

    rules:
      - id: csharp-inefficient-string-comparison
        patterns:
          - pattern-either:
              - pattern: $STR.ToLower().Equals($OTHER)
              - pattern: $STR.ToLowerInvariant().Equals($OTHER)
              - pattern: $STR.ToUpper().Equals($OTHER)
              - pattern: $STR.ToUpperInvariant().Equals($OTHER)
          - pattern-not: String.Equals($STR, $OTHER, StringComparison.OrdinalIgnoreCase)
        message: >
          Inefficient string comparison. Use String.Equals(s1, s2, StringComparison.OrdinalIgnoreCase) 
          instead of ToLower()/ToUpper().Equals() for better performance and clarity.
        languages: [csharp]
        severity: WARNING
        metadata:
          category: performance
          subcategory:
          - easyfix
          - strings
    

    This catches all four ways people typically do the slow comparison, but it won’t bug you if you’re already doing it the right way.

    How Much Faster Is It Really?

    I ran some benchmarks to see exactly how big the difference is:

    | Method                                        | Mean       | Allocated |
    |---------------------------------------------- |-----------:|----------:|
    | StringEquals_OrdinalIgnoreCase_SameIgnoreCase | 0.0138 ns | - |
    | StringEquals_OrdinalIgnoreCase_Different | 0.0327 ns | - |
    | ToUpperInvariant_Different | 16.4798 ns | 56 B |
    | ToLowerInvariant_Different | 16.6340 ns | 56 B |
    | ToLowerInvariant_SameIgnoreCase | 17.4380 ns | 56 B |
    | ToUpper_Different | 18.7223 ns | 56 B |
    | ToLower_Different | 19.5512 ns | 56 B |
    | ToLower_SameIgnoreCase | 21.0037 ns | 56 B |
    | ToUpperInvariant_SameIgnoreCase | 29.8586 ns | 112 B |
    | ToUpper_SameIgnoreCase | 34.5027 ns | 112 B |

    Why Should You Care?

    “But it’s just nanoseconds,” you might say. True, but:

    • In a busy app, you might do these comparisons millions of times
    • Every little memory allocation makes the garbage collector work harder
    • These tiny slowdowns add up across your whole codebase

    This is just the first of several performance-boosting rules I’m working on. If you add these to your workflow, you’ll catch these speed bumps before they slow down your code.

    Want to see all the rules and benchmarks? Check out csharp-semgrep-performance on GitHub.

  • Speeding up DefaultAzureCredential for local development

    Speeding up DefaultAzureCredential for local development

    If you’ve ever found yourself waiting for your local .NET application to authenticate with Azure services, you’re not alone. That delay is often caused by DefaultAzureCredential trying authentication methods that simply don’t exist on your development machine.

    Why Your Local Development Is Slow

    When you use DefaultAzureCredential without any customization, it tries a whole series of authentication methods one after another until something works. For local development, this is painfully inefficient.

    Think about it: your development machine doesn’t have managed identities or workload identities – those only exist in Azure environments. Yet DefaultAzureCredential will still stubbornly try to use them, waiting for timeouts before giving up and moving to the next method. Those wasted seconds add up, especially when you’re frequently restarting your application during development.

    As shown in Microsoft’s documentation on credential chains, DefaultAzureCredential tries these credential types in order:

    1. Environment
    2. Workload Identity
    3. Managed Identity
    4. Visual Studio
    5. Azure CLI
    6. Azure PowerShell
    7. Azure Developer CLI
    8. Interactive browser (when explicitly enabled)

    The first three are the biggest culprits for slowing down local development.

    Diagram showing the Azure authentication flow between a .NET app, Credential chain, and TokenCredential instance. The flow shows six numbered steps: 1) .NET app authenticates to Microsoft Entra ID, 2) Credential chain gets token, 3) Inside a loop, it fetches token from TokenCredential instance, 4) TokenCredential gets token, 5) Result is returned when an AccessToken is received, 6) AccessToken is returned to the .NET app. The diagram illustrates how the credential chain traverses the TokenCredential collection until a valid access token is received.

    The Simple Fix: Tell It What to Skip

    The good news is that fixing this is easy. You don’t need to abandon DefaultAzureCredential entirely, you just need to tell it which methods to skip when you’re developing locally:

    var isDevelopment = builder.Environment.IsDevelopment();
    TokenCredential credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions
    {
        ExcludeEnvironmentCredential = isDevelopment,
        ExcludeManagedIdentityCredential = isDevelopment,
        ExcludeWorkloadIdentityCredential = isDevelopment,
    });
    

    This skips the 3 first parts of the flow, directly trying a development credential.

    Integration with HttpClient

    Here’s how you can integrate this optimized credential with an HttpClient used to call an API:

    builder.Services.AddTransient<MyApiClientClientBearerTokenHandler>();
    
    builder.Services.AddHttpClient("MyApiClient", httpClient =>
    {
        httpClient.BaseAddress = new Uri(builder.Configuration["MyApiClient:BaseUrl"] ??
                                         throw new Exception("Missing configuration 'MyApiClient:BaseUrl'"));
    })
        .AddHttpMessageHandler<MyApiClientClientBearerTokenHandler>();
    

    The handler will take care of adding the authentication token to your requests:

    public sealed class MyApiClientClientBearerTokenHandler(ITokenAcquisition tokenAcquisition, IConfiguration configuration)
        : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            var token = await GetUserTokenAsync();
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
            return await base.SendAsync(request, cancellationToken);
        }
    
        private async Task<string> GetUserTokenAsync()
        {
            var s = configuration["MyApiClientClient:Scope"];
            return await tokenAcquisition.GetAccessTokenForUserAsync([
                s ?? throw new Exception("Missing configuration 'MyApiClientClient:Scope'")
            ]);
        }
    }
    

    Putting It All Together

    Here’s how you might use this optimized credential with your HTTP clients:

    builder.Services.AddTransient&lt;MyApiClientClientBearerTokenHandler>();
    
    builder.Services.AddHttpClient("MyApiClient", httpClient =>
    {
        httpClient.BaseAddress = new Uri(builder.Configuration["MyApiClient:BaseUrl"] ??
                                         throw new Exception("Missing configuration 'MyApiClient:BaseUrl'"));
    })
        .AddHttpMessageHandler&lt;MyApiClientClientBearerTokenHandler>();
    

    And the handler to attach the token:

    public sealed class MyApiClientClientBearerTokenHandler(ITokenAcquisition tokenAcquisition, IConfiguration configuration)
        : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            var token = await GetUserTokenAsync();
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
            return await base.SendAsync(request, cancellationToken);
        }
    
        private async Task<string> GetUserTokenAsync()
        {
            var s = configuration["MyApiClientClient:Scope"];
            return await tokenAcquisition.GetAccessTokenForUserAsync([
                s ?? throw new Exception("Missing configuration 'MyApiClientClient:Scope'")
            ]);
        }
    }
    

    Things to Remember

    • Only skip these credentials in development. Your production code should still use the full chain.
    • The beauty of this approach is that it’s environment-aware – your app will automatically use the right authentication strategy based on where it’s running.
    • If your development process involves environment variables for authentication, you might want to keep EnvironmentCredential enabled.

    Wrap Up

    I have been experimenting with keeping app start times fast. Especially when you need to access a service like Azure Key Vault, or Azure App Config during application startup, then you want the authentication to be fast.