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

Wednesday, October 11, 2023

.NET 8 RC 2: IFormFile Support in [FromForm] in ASP.NET Core Minimal APIs

.NET 8 RC 2 was released today and we are just one month away from GA release. 

With this release, in ASP.NET Core Minimal APIs, we can now pass IFormFile or IFormFileCollection when we are posting form fields that are getting bound via [FromFormattribute.

Let's have a look at the following example where we are uploading a Single file.
using Microsoft.AspNetCore.Mvc;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// TODO: Add services to the builder

WebApplication app = builder.Build();
// TODO: Add middleware to the app

app.MapPost("/upload", ([FromFormFileUploadModel model) =>
{
    return Results.Ok();
})
.DisableAntiforgery();

app.Run();

internal record FileUploadModel(stringDescriptionIFormFile File);
And we can call the endpoint as follows.
curl--location 'https://localhost:5001/upload' \
--form 'description="Single File Upload"' \
--form 'file=@"/C:/Users/Jaliya/Desktop/Image1.jpg"'
And I can see the file getting bound.
IFormFile
If we want to upload multiple files, I can bind the endpoint to the following model.
internal record FilesUploadModel(stringDescriptionIFormFileCollection Files);
We can call the endpoint as follows.
curl--location 'https://localhost:7208/upload' \
--form 'description="Multiple File Upload"' \
--form 'files=@"/C:/Users/Jaliya/Desktop/Image1.jpg"' \
--form 'files=@"/C:/Users/Jaliya/Desktop/Image2.png"'
And now both the files are getting bound.
IFormFileCollection
Hope this helps.

Do try .NET 8 RC 2.

Happy Coding.

Regards,
Jaliya

Friday, October 6, 2023

Calling an ASP.NET Core Web API Secured with Microsoft Entra ID using Azure Managed Identity

In this post, let's see how we can call an ASP.NET Core Web API Secured with Microsoft Entra ID (known as Azure AD) using an Azure Managed Identity.

This is our scenario: we are going to have 2 APIs.
  1. Internal API: This API is going to expose a GET: /claims endpoint. This endpoint will return ClaimsPrinciple Claims and will require Authorization. It's secured with Microsoft Entra ID.
  2. Public API: This API is going to expose a GET: /claims endpoint, but this DO NOT require any Authorization. This API will have an Azure User-Assigned Managed Identity assigned and this endpoint will call the Internal APIs' GET: /claims using the Managed Identity.
I have created the following already.
  • A User-Assigned Managed Identity with the name: mi-miauth-demo
  • Two brand new Azure App Services: app-miauth-internal-api and app-miauth-internal-api. To the Public API, I have assigned the User-Assigned Managed Identity: mi-miauth-demo.
Assign User-Assigned Managed Identity to Public API
Now let's start.

Creating App Registration


The first step is creating an App Registration in Microsoft Entra ID. You can do so by going into Microsoft Entra ID -> App registrations ->     + New registration. Give it a name, (in my case it's Managed Identity Auth Demo) and select an account type. In this case, for simplicity, I have selected Accounts in this organizational directory only. And then click on Create.

Once the App Registration is created (behind the scenes along with the App Registration, a new Enterprise Application will get created with the same name as in App Registration), I am clicking on Expose an API tab and adding the Application ID URI.
App Registration -> Set Application ID URI
I am proceeding with the default which looks something like: api://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.

Now go to App roles tab and create a new App role.
App Registration -> Create app role
So at this stage, we are basically done with the App Registration.

Configuring Enterprise Application (Service Principal)


Now we need to assign the Managed Identity access to the app role we created above. For that, I am running the following script from Windows PowerShell.
# Login to Azure and setting the subscription
Connect-AzAccount
Set-AzContext -SubscriptionId "<SubscriptionId>"

# Invoking Connect-MgGraph before any commands that access Microsoft Graph,
# Requesting scopes that we require during our session
$tenantID = '<TenantId>'
Connect-MgGraph -TenantId $tenantId -Scopes 'Application.Read.All', 'Application.ReadWrite.All', 'AppRoleAssignment.ReadWrite.All', 'Directory.AccessAsUser.All', 'Directory.Read.All', 'Directory.ReadWrite.All'
 
# App Registration Name
$appRegistrationName = 'Managed Identity Auth Demo'

# Install Microsoft.Graph Module if required using below command
# Install-Module Microsoft.Graph

 
# Retrieving Service Principal Id
$servicePrincipal = (Get-MgServicePrincipal -Filter "DisplayName eq '$appRegistrationName'")
$servicePrincipalObjectId = $servicePrincipal.Id

# Retrieving App role Id that the Managed Identity should be assigned to
$appRoleName = 'MI.Access'
$appRoleId = ($servicePrincipal.AppRoles | Where-Object {$_.Value -eq $appRoleName }).Id
 
# Managed Identity's Object (principal) ID.
$managedIdentityObjectId = '<ManagedIdentityObjectId>'
 
# Assign the managed identity access to the app role.
New-MgServicePrincipalAppRoleAssignment `
    
-ServicePrincipalId $servicePrincipalObjectId `
    -PrincipalId $managedIdentityObjectId `
    -ResourceId $servicePrincipalObjectId `

    -AppRoleId $appRoleId
Once the commands are completed, go to the relevant Enterprise Application -> Users and groups. Make sure you can see the Role assignment we just did.
Enterprise Application -> Users and groups
Now we are done with the Enterprise Application configuration.

Coding


Now let's write some code for our APIs starting with the Internal API.
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web; using System.Security.Claims;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddAuthorizationBuilder();

WebApplication app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/claims", (ClaimsPrincipal claimsPrincipal) =>
{
    return claimsPrincipal.Claims
        .Select(
claim => new
        (
            claim.Type,
            claim.Value
        ))
        .ToList();
})
.RequireAuthorization();

app.Run()
And update the appsettings.json adding AzureAd section as follows. Make sure to replace placeholders with your values.
{
  ...
  "AzureAd": {
    "Instance""https://login.microsoftonline.com/",
    "Domain""<Domain>",
    "TenantId""<TenantId>",
    "ClientId""<ClientId>" //Application (Client ID) of the App Registration
  }
}
Now Internal API is all good. I am deploying the code changes.

We are almost there now. Let's step is calling the above endpoint from our public API endpoint.
using Azure.Core;
using Azure.Identity;
using System.Net.Http.Headers;
using System.Text.Json.Nodes;
 
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

WebApplication app = builder.Build();

app.UseHttpsRedirection();

app.MapGet("/claims"async () =>
{
    DefaultAzureCredential credential = new();
    TokenRequestContext tokenRequestContext = new(new[]
    {
        "api://xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx/.default" // Application ID URI
    });
    AccessToken accessToken = await credential.GetTokenAsync(tokenRequestContext);

    HttpClient httpClient = new()
    {
        DefaultRequestHeaders =
        {
            Authorization = new AuthenticationHeaderValue("Bearer", accessToken.Token)
        }
    };

    string result = await httpClient.GetStringAsync("https://app-miauth-internal-api.azurewebsites.net/claims"); return JsonNode.Parse(result);
});

app.Run();
Public API is all good and I have deployed the code changes.

Now to the fun part, let's test the endpoints. Our expectation is the endpoint in Internal API should return 401 and the endpoint in Public API should return 200 with Claims of the Managed Idenity.
Result
Just like that, we are getting the expected output. Note: You can see the ClaimPrinciple has the Role "MI.Access" we assigned, so you can extend your authorization policies.

Hope this helps.

Happy Coding.

Regards,
Jaliya