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