Wednesday, December 20, 2023

.NET 8.0: [LogProperties] Attribute

There are a lot of improvements to Logging in .NET 8.0, and in this post, let's have a look at the new LogProperties attribute.

Now we can use LogProperties attribute in log methods attributed with LoggerMessage attribute (introduced with .NET 6.0). It's available through Microsoft.Extensions.Telemetry.Abstractions NuGet package.

Consider the below sample console application code.

using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

using var channel = new InMemoryChannel();

try
{
// Setup Application Insights
    IServiceCollection services = new ServiceCollection();
    services.Configure<TelemetryConfiguration>(config => config.TelemetryChannel = channel);
    services.AddLogging(builder =>
    {
        builder.AddApplicationInsights(
            configureTelemetryConfiguration: (config) =>
            {
                config.ConnectionString = "<ConnectionString>";
            },
            configureApplicationInsightsLoggerOptions: (options) =>
            {
            }
        );
    });

    IServiceProvider serviceProvider = services.BuildServiceProvider();
    ILogger<Program> logger = serviceProvider.GetRequiredService<ILogger<Program>>();
    User user = new("John""Doe");
    Console.WriteLine($"Hello {user.FirstName}!");     // Structured logging with [LogProperties]
    logger.SaidHello(user.FirstName, user);
}
finally
{
    // Explicitly call Flush() followed by Delay, as required in console apps.
    // This ensures that even if the application terminates, telemetry is sent to the back end.

    channel.Flush();

    await Task.Delay(TimeSpan.FromMilliseconds(1000));
}

public record User(string FirstName, string LastName);

public static partial class LoggerExtensions
{
    [LoggerMessage(EventId = 1,  Level = LogLevel.Information,  Message = "Saying hello to {firstName}.")]
    public static partial void SaidHello(this ILogger logger,  string firstName,  [LogProperties] User user);
}

Here you can see the usage of [LogProperties] inside the LoggerExtensions.SaidHello method.

And this one would get logged in Application Insights as follows (in that case in any telemetry collector):
Structured Logging with LogProperties

More read:
   High-performance logging in .NET
   Watch: Improving your application telemetry using .NET 8 and Open Telemetry | .NET Conf 2023

Hope this helps.

Happy Coding.

Regards.
Jaliya

Tuesday, December 12, 2023

LINQ: let Clause

In this post, let's see what let is in LINQ query-syntax queries. I think it's an overlooked feature in LINQ.

Let's consider the following query.
IQueryable<string> wfhEmployees = from e in context.Employees
                                  where context.Departments
                                      .Where(d => d.IsWfhAllowed)
                                      .Select(d => d.Id)
                                      .Contains(e.DepartmentId)
                                  select e.FirstName + " " + e.LastName;
Here some might find it hard to understand the where condition immediately, we can use the let clause to make it more readable.
IQueryable<string> wfhEmployees = from e in context.Employees
                                  let wfhDepartments = context.Departments
                                      .Where(d => d.IsWfhAllowed)
                                      .Select(d => d.Id)
                                      .ToList()
                                  where wfhDepartments
                                      .Contains(e.DepartmentId)
                                  let fullName = e.FirstName + " " + e.LastName
                                  select fullName;
Here I have used let to store the result of a subexpression to use it in subsequent clauses. This is really handy when you have complex queries, we can break it into multiple sub-expressions.

Hope this helps!

Happy Coding.

Regards,
Jaliya

Sunday, December 10, 2023

EF Core 8.0: Better Use of IN Queries

When we are using  Contains LINQ operator in an EF subquery, EF Core now generates better queries using SQL IN instead of EXISTS. This can result in dramatically faster queries. 

Let's go by an example.

Consider the following DbContext.
public class MyDbContext : DbContext
{
    public DbSet<Employee> Employees { get; set; }

    public DbSet<Department> Departments { get; set; }
}

public record Employee(string Name, int DepartmentId)
{
    public int Id { get; init; }
}

public record Department(string Name)
{
    public int Id { get; init; }

    public bool IsWfhAllowed { get; set; }
}
Now say, I want to get all the employees who are allowed to work from home. I can write the following LINQ query for my requirement.
List<Employee> wfhEmployees = await context.Employees
    .Where(e => context.Departments
        .Where(d => d.IsWfhAllowed)
        .Select(d => d.Id)
        .Contains(e.DepartmentId))
    .ToListAsync();
If I am on an earlier EF version before EF 8.0 (like EF 7.x),  EF generates the following query.
SELECT [e].[Id], [e].[DepartmentId], [e].[Name]
FROM [Employees] AS [e]
WHERE EXISTS (
    SELECT 1
    FROM [Departments] AS [d]
    WHERE [d].[IsWfhAllowed] = CAST(AS bit) AND [d].[Id] = [e].[DepartmentId])
Here the subquery is referencing the outer [Employees] table, so the subquery must be executed for each row in the [Employees] table (correlated subquery).

With EF 8.0, EF generates the following query.
SELECT [e].[Id], [e].[DepartmentId], [e].[Name]
FROM [Employees] AS [e]
WHERE [e].[DepartmentId] IN (
    SELECT [d].[Id]
    FROM [Departments] AS [d]
    WHERE [d].[IsWfhAllowed] = CAST(AS bit))
Here the subquery no longer references the outer table, meaning it can be evaluated once, yielding massive performance improvements on most database systems. 

Note: On Microsoft SQL Server, the database can optimize the first query to the second query so that the performance is likely the same.

Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, December 6, 2023

ASP.NET Core 8.0: Securing Swagger UI Endpoints

With ASP.NET Core 8.0, now you can secure Swagger UI endpoints by calling MapSwagger().RequireAuthorization.

Consider the following code example.

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddAuthorization();
builder.Services.AddAuthentication("Bearer").AddJwtBearer();

WebApplication app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI();

app.MapSwagger().RequireAuthorization();

app.MapGet("/status", () =>
{
    return "ONLINE";
})
.WithName("GetStatus")
.WithOpenApi();

app.Run();

Here, /status endpoint will not require any authorization, but the Swagger endpoints will require authorization.

Swagger: 401

Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, November 22, 2023

ASP.NET Core 8.0: Keyed Services in Dependency Injection

There are often times that we have different implementations of an interface and we need to resolve a particular implementation when doing dependency injection.

With ASP.NET Core 8.0, we now have support for Keyed Services.

Consider the following interface and its implementations.
public interface IMyService
{
    string GetValue();
}

public class MyService1 : IMyService
{
    public string GetValue() => "MyService1";
}

public class MyService2 : IMyService
{
    public string GetValue() => "MyService2";
}
Now we can register these Services with keys as follows.
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedScoped<IMyService, MyService1>("service1");
builder.Services.AddKeyedScoped<IMyService, MyService2>("service2");
You can register Keyed services with other lifetimes as well (Transient and Singleton).

Now we can resolve a specific implementation by key using [FromKeyedServices] attribute.

From the constructor
// Note:
//    I am using Primary Constructor which is new with .NET 8.0
//    You can use the other constructor as well

public class MyClass([FromKeyedServices("service1")] IMyService myService)
{
    public string GetValue() => myService.GetValue();
}
From an API Endpoint
app.MapGet("/", ([FromKeyedServices("service2")] IMyService myService) =>
{
    return myService.GetValue();
});
Hope this helps.

Happy Coding.

Regards,
Jaliya

Tuesday, November 21, 2023

Visual Studio 2022 17.9.0 Preview 1.0: Experimental Control Styles

Last week during .NET Conf there were some great announcements in the world of .NET, and also Microsoft has released the latest preview of Visual Studio 2022 17.9.0 Preview 1.0.

With the latest Visual Studio 2022 Preview, we now get to experience the refreshed Visual Studio UI that the Visual Studio team has been working on for some time.

To enable the UI Refresh, from Visual Studio go to Tools -> Environment -> Preview Features and then select  "Experimental control styles". You’ll need to restart Visual Studio for changes to be applied.
Enable Experimental control styles
The first thing you will notice is, now we have an entirely different set of Color Themes.
Visual Studio: Color Themes
If you are used to the Blue theme like I am, unfortunately, it isn't there under new themes.


Do try it out and please leave your feedback on Visual Studio UI Refresh.
Happy Coding.

Regards,
Jaliya

Monday, November 6, 2023

Azure API Management: Self-Signed Certificate Authentication with ASP.NET Core Web API Running in an Azure Web Apps for Containers

I recently spent quite an interesting time trying to get self-signed Certificate Authentication working with ASP.NET Core Web API Running in Azure Web Apps for Containers. The caller was an Azure API Management, that was making a send-request as part of its policy, something like the following.

<send-request mode="new" response-variable-name="httpresponse" timeout="10" ignore-error="true">
    <set-url>@("https://app-something-001.azurewebsites.net/api/values)</set-url>
    <set-method>GET</set-method>
    <authentication-certificate certificate-id="self-signed-certificate" />
</send-request>

There were a couple of important things that had to be in place in order for it to finally work. So in this post, let's have a look at those, so hopefully someone might find it helpful.

The first piece is to get the Certificate Authentication working locally.

1: Configure Kestrel configuration options to AllowCertificates and if required introduce additional  client certificate validation

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(serverOptions =>
{
    serverOptions.ConfigureHttpsDefaults(options =>
    {
        options.ClientCertificateMode = ClientCertificateMode.AllowCertificate;
        options.ClientCertificateValidation = (certificatechainerrors) =>
        {
            // TODO: Additional client certificate validation logic here
            return true;
        };
    });
});

2: Customize Certificate Authentication Options to allow self-signed certificates

builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
    {
     // Default is Chained only, All is required Chained | SelfSigned
        options.AllowedCertificateTypes = CertificateTypes.All;
        options.RevocationMode = X509RevocationMode.NoCheck;
        options.ValidateCertificateUse = false;

        options.Events = new CertificateAuthenticationEvents
        {
          OnCertificateValidated = context =>
             { // TODO: Further validation if required
              Claim[] claims = new[]
                 {
                  new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer),
                     new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer)
                 };

                 context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claimscontext.Scheme.Name));

                 context.Success();

                 return Task.CompletedTask;
             },
             OnAuthenticationFailed = context =>
             {
              context.Fail("Invalid certificate");

                 return Task.CompletedTask;
             }
        };
});
builder.Services.AddAuthorization();

3: Check whether everything is working locally. 

Send a request to an endpoint that requires authorization using a Tool like Postman and configure the certificate to be sent along.

Postman Certificate Authentication
So when a request matches the HOST, Postman will automatically associate the certificate to the request.

Once the Certificate Authentication is working locally, the next step is getting it to work when running on Azure Web Apps for Containers.

Here there are some very important things. 

4: Configure Incoming client certificates in Azure App Service Configuration -> General Settings.

Incoming Client Certificates

5: The most important thing: in Azure App Services, TLS termination of the request happens at the frontend load balancer. When forwarding the request to your app code with client certificates enabled, App Service injects an X-ARR-ClientCert request header with the client certificate. App Service does not do anything with this client certificate other than forwarding it to your app. Your app code is responsible for validating the client certificate.

So we need to do some code changes as follows. 

First, configure Services as below.

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    // Only loopback proxies are allowed by default. Clear that restriction to enable this explicit configuration.
    options.KnownNetworks.Clear();
    options.KnownProxies.Clear();
});

// Configure the application to client certificate forwarded the frontend load balancer
builder.Services.AddCertificateForwarding(options => 

    options.CertificateHeader = "X-ARR-ClientCert"
}); // builder.Services.AddAuthentication...

Then we need to update the middleware order adding UseForwardedHeaders and UseCertificateForwarding to the beginning of the execution order.

WebApplication app = builder.Build();

app.UseForwardedHeaders();
app.UseCertificateForwarding();

// Note: HttpLogging should be after UseForwardedHeaders and UseCertificateForwarding. // otherwise those won't get logged
app.UseHttpLogging();

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization(); // ...

And hopefully, that should get the things to work.

The certificate was successfully authenticated
If things aren't working as expected, enable Debug logging in the application and start troubleshooting from there.

Another important note: If you try to route a request with certificate authentication to your local machine using Tunneling in Visual Studio, it will always fail because it's still not supported (as of today). Follow the feature request here: Support forwarding HTTPS client certificates.

Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, November 1, 2023

Azure API Management: Enriching Requests with Additional Headers and Use of Caching

In this post let's see how we can customize Azure API Management (APIM) Policy to enrich requests with additional headers and also let's look at how we can make use of caching.

The demo scenario is as follows:
  • I have an example BE API, which requires a specific HTTP Header: X-Tenant-Code, in order for it to serve.
  • A client sends a request with JWT Token, but the client has no knowledge of X-Tenant-Code HTTP header.
  • Inside the APIM, we need to inspect the JWT Token, and based on a Claim in the token (in this case, it's AppId/ClientId), we need to find out Tenant information by calling another API endpoint, let's say Tenant API. And then populate X-Tenant-Code HTTP header from the response data.
  • We don't need to be calling the Tenants API all the time, instead, we can cache Tenant information in APIM itself (while we can use the inbuilt cache, it's recommended to use external Cache, currently only Redis is supported)
Now let's start.

1: The first step is accessing the JWT token and extracting claims. (Read more on how to access JWT token: API Management policy expressions)
<!-- Get ClientId From JWT Token -->
<set-variable  name="clientid" 
    value="@(context.Request.Headers.GetValueOrDefault("Authorization","").Split(' ')[1].AsJwt()?.Claims.GetValueOrDefault("appid"))" />
2: Now I am checking whether the Tenant Information already exists in APIM Cache for the given ClientId.
<!--Look for tenantinfo for this specific client in the cache and set it to context variable 'tenantinfo' -->
<cache-lookup-value  key="@("tenantinfo-" + context.Variables["clientid"])"  variable-name="tenantinfo" />
3: Now I need to have a conditional expression. That is, if the item is not in the cache, then I need to call another API endpoint, passing in the ClientId to get the Tenant information. Let's assume, the endpoints return a  JSON response.
<choose>
<!-- Didn't find tenantinfo in cache -->
    <when condition="@(!context.Variables.ContainsKey("tenantinfo"))">
     <!-- Make a HTTP request to GET Tenant, copying current HTTP request headers -->
        <!-- Store the response in context variable 'tenantinforesponse' -->
        <send-request mode="copy" response-variable-name="tenantinforesponse" timeout="10" ignore-error="true">
         <set-url>@("https://app-azarch-tenants-service-001.azurewebsites.net/" + context.Variables["clientid"])</set-url>
            <set-method>GET</set-method>
        </send-request>

        <!-- Store response body in context variable 'tenantinfo' as a JObject -->
        <set-variable  name="tenantinfo"  value="@(((IResponse)context.Variables["tenantinforesponse"]).Body.As<JObject>())" />

        <!-- Store result in cache for 1 day (86400 seconds) -->
        <cache-store-value  key="@("tenantinfo-" + context.Variables["clientid"])"  value="@((JObject)context.Variables["tenantinfo"])"  duration="86400" />
    </when>
</choose>
4: At this state, the context variable: tenantinfo , is populated. And now we can use it to populate X-Tenant-Code header.
<!-- Set the X-Tenant-Code header to the tenantCode value from the context variable 'tenantinfo' -->
<set-header name="X-Tenant-Code" exists-action="override">
<value>@((string)((JObject)context.Variables["tenantinfo"]).GetValue("tenantCode"))</value>
</set-header>
The complete policy looks as follows.
<policies>
    <inbound>
        <!-- Get ClientId From JWT Token -->
        <set-variable name="clientid" 
            value="@(context.Request.Headers.GetValueOrDefault("Authorization","").Split(' ')[1].AsJwt()?.Claims.GetValueOrDefault("appid"))" />

        <!--Look for tenantinfo for this specific client in the cache and set it to context variable 'tenantinfo' -->
        <cache-lookup-value key="@("tenantinfo-" + context.Variables["clientid"])" variable-name="tenantinfo" />

        <choose>
            <!-- Didn't find tenantinfo in cache -->
            <when condition="@(!context.Variables.ContainsKey("tenantinfo"))">
              
                <!-- Make a HTTP request to GET Tenant, copying current HTTP request headers -->
                <!-- Store the response in context variable 'tenantinforesponse' -->
                <send-request mode="copy" response-variable-name="tenantinforesponse" timeout="10" ignore-error="true">
                    <set-url>@("https://app-azarch-tenants-service-001.azurewebsites.net/" + context.Variables["clientid"])</set-url>
                    <set-method>GET</set-method>
                </send-request>

                <!-- Store response body in context variable 'tenantinfo' as a JObject -->
                <set-variable 
                    name="tenantinfo" 
                    value="@(((IResponse)context.Variables["tenantinforesponse"]).Body.As<JObject>())" />

                <!-- Store result in cache for 1 day (86400 seconds) -->
                <cache-store-value 
                     key="@("tenantinfo-" + context.Variables["clientid"])" 
                     value="@((JObject)context.Variables["tenantinfo"])" 
                     duration="86400" />
                      
            </when>
        </choose>

        <!-- Set the X-Tenant-Code header to the tenantCode value from the context variable 'tenantinfo' -->
        <set-header name="X-Tenant-Code" exists-action="override">
            <value>@((string)((JObject)context.Variables["tenantinfo"]).GetValue("tenantCode"))</value>
        </set-header>
        <base />
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>
And that's it.

You can set up External Cache, from the following menu item.
APIM: External Cache
Using an external cache can be really handy if you want to inspect the cache items more visually. Something like below (I wrote a post a while ago on Connecting to Azure Cache for Redis Instance from RedisInsight, do check it out).
APIM Cache
Hope this helps.

Happy Coding.

Regards,
Jaliya

Tuesday, October 24, 2023

C# 12.0: Collection Expressions and Spread Operator

We are less than a month away from .NET 8 official release (.NET Conf 2023: November 14-16, 2023) and in this post, let's have a look at a nice feature in   C# 12.0 which is getting shipped along with .NET 8.

You can try this feature with the latest preview of Visual Studio 2022 (as of today it's 17.8.0 Preview 4.0). And remember to set LangVersion as preview.

We can now express a collection as follows.
string[] workingDays = [ "Monday""Tuesday""Wednesday""Thursday""Friday" ];
string[] weekEnds = [ "Saturday""Sunday" ];
A collection expression contains a sequence of elements between [ and ] brackets.

You use a spread element .. to spread out collection values in a collection expression.
IEnumerable<stringdays = [.. workingDays, .. weekEnds];

Console.WriteLine(days.Count());
// 7

foreach (string day in days)
{
    Console.WriteLine(day);
}
// Monday
// Tuesday
// Wednesday
// Thursday
// Friday
// Saturday
// Sunday
You can even pass collection expressions into a method as follows.
IEnumerable<EmployeeitEmployees = [new Employee("John""IT"), new Employee("Jane""IT")];
IEnumerable<EmployeehrEmployees = [new Employee("Joe""HR")];

Console.WriteLine(Count([.. itEmployees, .. hrEmployees]));
// 3

int Count(IEnumerable<Employeevalues) => values.Count();

record Employee(string Namestring Department);

Happy Coding.

Regards,
Jaliya

Thursday, October 19, 2023

Azure API Management: Adding Local Self-hosted Gateway

Azure API Management is made up of an API gateway, a management plane, and a developer portal. In this post, let's see how we can add another API gateway that is going to run locally on Docker to an APIM. So if we can have the API gateway running locally, we can basically run it anywhere that supports Containers, for example in Kubernetes, Azure Container Apps, etc.

Here what we are going to be doing is,

  • Have a demo API running locally (let's say customers API)
    • Since I am a .NET person, I will have an ASP.NET Core Minimal API with a single endpoint. And this is going to be running using the local IP Address and will NOT use localhost. If you are wondering why, I will explain it later.
  • Have a Developer Tier APIM created, there I am going to expose our demo customers' API which is running locally. So obviously, from Azure Portal, I won't be able to test the API.
  • Add an API gateway to our APIM and specify which APIs it can expose. In this case, the API gateway will be running locally, and it will be communicating with Azure to report its status, get configuration updates, etc. 
  • Once the API gateway is up, I am going to try calling the customers API through the local API gateway endpoint, and hopefully, it should work,
Let's start with getting a demo API (customers' API) up and running. I have the following Minimal API.
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

WebApplication app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.MapGet("/info", () =>
{
    return "Customers";
})
.WithName("GetApiInfo")
.WithOpenApi();

app.Run();

And I am running it on http://192.168.1.5:5192.

dotnet run --urls=http://192.168.1.5:5192

Demo Customers API running locally on http://192.168.1.5:5192
Now I have a Developer Tier APIM created and have added a single API.
Adding an API into APIM
Note: for the URL scheme, I have used HTTP(s), because I am going to expose the Self-hosted gateway via HTTP for demo purposes. And I am not asking for a subscription key for this API.

So at this stage, our API is not really usable. If you try to call it through the Azure APIM gateway URL, it will try to call http://192.168.1.5:5192/info, and it's not reachable.

Now let's add the Self-hosted gateway.
Add Self-hosted Gateway
I am giving it a Name, Location & optional Description, selecting our API and adding it.
Add Self-hosted Gateway
Once it's added, now comes the interesting part.
Self-hosted Gateway Deployment Options
Look at the Deployment scripts section. We are given instructions to run the gateway on Docker, K8s and Helm.

For Docker, we need to create an env.conf file, copy-paste the content, and then execute the docker run command. I have modified the docker command a bit, so I am not running the gateway on port 80 which could already being used most of the time.

docker run -d -p 8085:8080 --name apimgw-ravana-tpp15 --env-file env.conf mcr.microsoft.com/azure-api-management/gateway:v2
And once the command is executed, I can check the logs and make sure it's up and running.
Self-hosted Gateway Running
And now if I check the Azure Portal, I can see the gateway is up and running and it's communicating with Azure.
Self-hosted Gateway Linked to Azure
And now the moment of truth. Let's call our API through the Self-hosted gateway. The URL of the Self-hosted gateway is http://localhost:8085.
Calling Self-hosted Gateway

And that's it. Here the gateway is calling our locally running API on http://192.168.1.5:5192. I used the IP address instead of localhost because otherwise, the gateway would be looking inside the container to find the API.

You can check the gateway container logs to see all the information, like requests, how configurations are getting updated, etc.

Hope this helps.

More read:
   Self-hosted gateway overview

Happy Coding.

Regards,
Jaliya