Showing posts with label .NET Core. Show all posts
Showing posts with label .NET Core. Show all posts

Monday, November 10, 2025

Running ASP.NET Core 3.1 Application Inside .NET 9 Container

Recently, I needed to run a set of applications targeting ASP.NET Core 3.1 inside .NET 9 containers. I know, it’s just a couple of days before .NET Conf 2025, and .NET Core 3.1 feels ancient at this point. But unfortunately, upgrading the applications to a newer .NET version wasn’t an option.

Had a bit of trouble getting things to run locally as well as in Azure DevOps Pipelines, so thought of sharing the experience.

First to get the things started, installed ASP.NET Core 3.1 Runtime.

FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
 
EXPOSE 8080 2222
 
RUN apt-get update && apt-get install -y \
    curl
 
# Install ASP.NET Core 3.1 runtime
RUN curl -SL --output aspnetcore-runtime-3.1.tar.gz https://dotnetcli.azureedge.net/dotnet/aspnetcore/Runtime/3.1.32/aspnetcore-runtime-3.1.32-linux-x64.tar.gz \
    && mkdir -p /usr/share/dotnet \
    && tar -zxf aspnetcore-runtime-3.1.tar.gz -C /usr/share/dotnet \
    && rm aspnetcore-runtime-3.1.tar.gz

Now ran the application on this and was expecting more errors. As expected container didn't even start.

The first error I got it related to ICU.

Process terminated. 
Couldn't find a valid ICU package installed on the system. 
Set the configuration flag System.Globalization.Invariant to true if you want to run with no globalization support.

I wanted to use Globalization, so installed the ICU package. Note: .NET Core 3.1 requires a specific version: libicu67. 

RUN apt-get update && apt-get install -y \
    curl \
    wget

# Download and install libicu67 from Debian Bullseye
RUN wget http://ftp.us.debian.org/debian/pool/main/i/icu/libicu67_67.1-7_amd64.deb \
    && dpkg -i libicu67_67.1-7_amd64.deb \
    && rm libicu67_67.1-7_amd64.deb

Once that is installed, the next error is related to libssl. 

No usable version of libssl was found

So installed that.

# Download and install libicu67 and libssl1.1 from Debian Bullseye
RUN wget http://ftp.us.debian.org/debian/pool/main/i/icu/libicu67_67.1-7_amd64.deb \
    && curl -fsSL http://ftp.us.debian.org/debian/pool/main/o/openssl/libssl1.1_1.1.1w-0+deb11u1_amd64.deb -o /tmp/libssl1.1.deb \
    && dpkg -i libicu67_67.1-7_amd64.deb \
    && dpkg -i /tmp/libssl1.1.deb \
    && rm libicu67_67.1-7_amd64.deb /tmp/libssl1.1.deb

And finally got a container up and running locally.

Next is to update the DevOps pipeline. In the pipeline, we were also running EF Core migrations.

We were using ubuntu-latest agent, and installed NET Core SDK 3.1.x and dotnet-ef tool version 3.1.x.

- task: UseDotNet@2
    displayName: Install .NET Core SDK 3.1.x
    inputs:
    version: 3.1.200
 
- script: |
    dotnet tool install --global dotnet-ef --version 3.1.32 || dotnet tool update --global dotnet-ef --version 3.1.32
    displayName: Install dotnet-ef tool version 3.1.x

And when the installing dotnet-ef --version 3.1.32, got the following error again.

No usable version of libssl was found

So installed libssl in the build agent before installing .NET.

# Ubuntu latest does not have libssl1.1 installed by default, which is required for .NET Core 3.1
- script: |
    echo "deb http://security.ubuntu.com/ubuntu focal-security main" | sudo tee /etc/apt/sources.list.d/focal-security.list
    sudo apt-get update
    sudo apt-get install -y libssl1.1
    displayName: 'Install libssl1.1 for .NET Core 3.1

And now migrations were executed and an image got built, pushed and deployed.

That was quite a pain.

Hope this helps.

Happy Coding.

Regards,
Jaliya

Sunday, September 8, 2024

Azure Pipelines: DurableTask.Netherite: Netherite backend requires 64bit, but current process is 32bit.

Recently one of our .NET Isolated Azure Durable Functions got switched to use Netherite as its storage provider and suddenly all the integration tests started to fail in the Azure DevOps CI pipeline. 

The underlying error was:  

DurableTask.Netherite: Netherite backend requires 64bit, but current process is 32bit

In the pipeline, we were using FuncToolsInstaller@0 to install Azure Functions Core Tools.

- task: FuncToolsInstaller@0
  displayName: Install Azure Func Core Tools

Upon investigating, noticed FuncToolsInstaller@0 is using x86 version of Azure Functions Core Tools.

FuncToolsInstaller@0
Checked choco (chocolatey), it was also using x86 as well, fortunately, npm was using x64 by default.
- bash: |
    npm i -g azure-functions-core-tools@4 --unsafe-perm true
  displayName: Install Azure Func Core Tools

And that solved the issue.

Created microsoft/azure-pipelines-tasks enhancement request for FuncToolsInstaller@0.
   [enhancement]: FuncToolsInstaller@0: Use win-x64 for Windows

Hope this helps.

Happy Coding.

Regards,
Jaliya

Friday, August 30, 2024

ASP.NET Core 9.0: HybridCache in ASP.NET Core

With ASP.NET Core 9.0, we have access to a new Caching API: HybridCache, and it's designed to replace both IDistributedCache and IMemoryCache.

Let's go through with an example code.

I have the following code:

public interface IDataService
{
    Task<ConfigurationData> GetConfigurationData(CancellationToken cancellationToken = default);
}

public abstract class DataServiceBase : IDataService
{
    protected const string CacheKey = "configuration-cache-key";

    public abstract Task<ConfigurationData> GetConfigurationData(CancellationToken cancellationToken = default);

    protected async Task<ConfigurationData> GetConfigurationFromSource(CancellationToken cancellationToken = default)
    {
        return await Task.FromResult(new ConfigurationData
        {
            SomeConfig1 = "Some Config1",
            SomeConfig2 = "Some Config2"
        });
    }
}
First, let's see how IDistributedCache works and then let's see how HybridCache can simplify it.
public class DataServiceWithIDistributedCache(IDistributedCache distributedCache)
    : DataServiceBase
{
    public async override Task<ConfigurationData> GetConfigurationData(CancellationToken cancellationToken = default)
    {
        byte[]? bytes = await distributedCache.GetAsync(CacheKey, cancellationToken); // Try to get from cache.

        // Cache hit; return the deserialized data.
        if (bytes is not null)
        {
            return JsonSerializer.Deserialize<ConfigurationData>(bytes)!;
        }

        // Cache miss; get the data from the real source and cache it.
        ConfigurationData configurationData = await GetConfigurationFromSource(cancellationToken);

        bytes = JsonSerializer.SerializeToUtf8Bytes(configurationData);

        await distributedCache.SetAsync(CacheKey, bytes, cancellationToken);

        return configurationData;
    }
}
I have IDistributedCache configured with Redis.
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "<ConnectionString>";
});
builder.Services.AddScoped<IDataService, DataServiceWithIDistributedCache>();
Now here in DataServiceWithIDistributedCache, we are first checking the cache to see whether the item exists, if it is we return the item from the cache, if not we retrieve the item from the original source, cache it, and then return the item.

There are potential problems here. Say the item does not exist in the cache and more than one thread attempts to read Configuration simultaneously. In that case, multiple threads are going to cache the item.

On top of that, we had to first check whether the item exists in the cache, and if not, we need to retrieve the item from original source, and cache it. When retrieving an item from a cache, almost all the time, that's something we will have to do.

Now let's see how HybridCache works. First, we need to install a new package: Microsoft.Extensions.Caching.Hybrid (it's still pre-release as of today)
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0-preview.7.24406.2" />
public class DataServiceWithHybridCache(HybridCache hybridCache)
    : DataServiceBase
{
    public async override Task<ConfigurationData> GetConfigurationData(CancellationToken cancellationToken = default)
    {
        return await hybridCache.GetOrCreateAsync(
            CacheKey, factory: async token => await GetConfigurationFromSource(token),
            cancellationToken: cancellationToken
        );
    }
}
Now we need to register HybridCache.
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "<ConnectionString>";
});

builder.Services.AddScoped<IDataService, DataServiceWithHybridCache>();
builder.Services.AddHybridCache(options =>
{
    // TODO: customize options if required
});
So here HybridCache is created with a primary cache and a secondary cache. 
HybridCache
HybridCache by default uses MemoryCache for its primary cache, and for secondary cache, it uses any IDistributedCache implementation that is configured. Since I have Redis configured, Redis is registered as the secondary cache here.

HybridCache exposes GetOrCreateAsync with two overloads, taking a key and: 
  1. A factory method.
  2. State, and a factory method.
The method uses the key to retrieve the item from the primary cache. If it's not there (cache miss), it then checks the secondary cache (if it's configured). If it doesn't find the item there (another cache miss), it calls the factory method to get the item from the original data source. It then caches the item in both primary and secondary caches. The factory method is never called if the item is found in the primary or secondary cache (a cache hit).

The HybridCache service ensures that only one concurrent caller for a given key calls the factory method, and all other callers wait for the result of that call. The CancellationToken passed to GetOrCreateAsync represents the combined cancellation of all concurrent callers.

I love this.

Hope this helps.

More read:

Happy Coding.

Regards,
Jaliya

Friday, August 9, 2024

Azure APIM as a Negotiate Server for Azure SignalR Service

In this post, let's see how to use Azure APIM as a Negioate Server for Azure SignalR Service.

Let's start with a background.

I have an Angular client application that uses microsoft/signalr to communicate with Azure SignalR Service and the negotiation is done over an Azure Function that uses SignalRConnectionInfoInput. 

[Function("Negotiate")]
public async Task<HttpResponseData> Negotiate(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequestData request,
    [SignalRConnectionInfoInput(HubName = SignalR.StreamlineHub, UserId = "{headers.x-ms-signalr-userid}")] string connectionInfo)
{
    // TODO: Read Authorization header and validate token

    HttpResponseData response = request.CreateResponse(HttpStatusCode.OK);
    await response.WriteStringAsync(connectionInfo);

    return response;
}

So basically before the client application can connect to Azure SignalR Service, it calls the above  endpoint which will return the Azure SignalR Service endpoint URL and a valid access token. Then it starts communicating with the Azure SignalR Service using the chosen Transport method, in my case it's WebSockets.

let options = {
    headers: {
        'x-ms-signalr-userid': this.tenantUserId,
        'x-authorization': 'Bearer ' + this.oidcSecurityService.getAccessToken()
    },
    transport: signalR.HttpTransportType.WebSockets,
};

this.hubConnection = new signalR.HubConnectionBuilder()
    .withUrl("https://{some-azure-function}.azurewebsites.net/api", options)
    .withAutomaticReconnect()
    .build();

await this.hubConnection.start();

The main flow of events from the client application side,

1. POST: https://{some-azure-function}.azurewebsites.net/api/negotiate, to retrieve the Azure SignalR Service service endpoint URL and a valid access token.

1. Reteieve Azure SignalR Service URL and an access token
2. POST: https://{some-signalr-service}.service.signalr.net/client/negotiate, the returned URL from previes call). This is where actual Negotiation happens with Azure SignalR Service. The response contains connectionId, which identifies the connection on the server and the list of transports that the server supports.

2. Negotiate with Azure SignalR Service
3. WebSocket connection to GET: wss://{some-signalr-service}.service.signalr.net/client/?hub={myHubName}&id={connectionToken}&access_token={accessToken}

3. WebSocket Connection

Now I needed to remove this Azure Function and instead expose Azure SignalR Service via APIM.

Let's start modifying APIM by adding the required APIs for WebSocket transport as follows.

1. Add a HTTP API:

  • Display name: SignalR negotiate
  • Web service URL: https://{some-signalr-service}.service.signalr.net/client/negotiate/
  • API URL suffix: client/negotiate/
  • Add two operations, and saving with the following parameters:
    • negotiate preflight
      • Display name: negotiate preflight
      • URL: OPTIONS /
    • negotiate
      • Display name: negotiate
      • URL: POST /

2. Add a WebSocket API:

  • Display name: SignalR connect
  • Web service URL: wss://{some-signalr-service}.service.signalr.net/client/
  • API URL suffix: client/

Now the APIs are added, go to the Settings tab in each of these APIs and uncheck Subscription required.

Now let's configure the policies for these APIs.

1. HTTP API:

All Operations

<policies>
  <inbound>
    <cors allow-credentials="true">
      <allowed-origins>
        <origin>https://localhost:4200</origin>
        <!-- TODO: Add other origins-->
      </allowed-origins>
      <allowed-methods>
        <method>*</method>
      </allowed-methods>
      <allowed-headers>
        <header>*</header>
      </allowed-headers>
      <expose-headers>
        <header>*</header>
      </expose-headers>
    </cors>
    <validate-jwt header-name="x-authorization" failed-validation-httpcode="401" failed-validation-error-message="Access token is missing or invalid." require-expiration-time="false">
      <!--Read Authorization header and validate token, not part of this-->
    </validate-jwt>
    <base />
  </inbound>
  <backend>
    <base />
  </backend>
  <outbound>
    <base />
  </outbound>
  <on-error>
    <base />
  </on-error>
</policies>

negotiate

<policies>
  <inbound>
    <base />
    <!--Step 1: Use a managed identity to get an access token for the SignalR service.-->
    <authentication-managed-identity resource="https://signalr.azure.com" 
                                     client-id="{Managed_Identity_Client_ID}" 
                                     output-token-variable-name="mi-access-token" 
                                     ignore-error="false" />
    <!--Step 2: Use the access token to get a SignalR client access token.--> <!--NOTE: In production environments, we don't want UserId to be in a HTTP header, instead extract from JWT etc.-->
    <send-request mode="new" response-variable-name="tokenResponse" timeout="20" ignore-error="false">
      <set-url>@("https://{some-signalr-service}.service.signalr.net/api/hubs/{my_hub_name}/:generateToken?api-version=2023-07-01&userId=" + context.Request.Headers.GetValueOrDefault("x-ms-signalr-userid",""))</set-url>
      <set-method>POST</set-method>
      <set-header name="Authorization" exists-action="override">
        <value>@("Bearer " + (string)context.Variables["mi-access-token"])</value>
      </set-header>
    </send-request>
    <!--Step 3: Extract the client access token from the response and set it as a variable.-->
    <set-variable name="client-access-token" value="@(((IResponse)context.Variables["tokenResponse"]).Body.As<JObject>()["token"].ToString())" />
    <!--Step 4: Set the client access token as a header in the request to the SignalR service.-->
    <set-header name="Authorization" exists-action="override">
      <value>@("Bearer " + (string)context.Variables["client-access-token"])</value>
    </set-header>
    <!--Step 5: Set the hub name in the query parameter.-->
    <set-query-parameter name="hub" exists-action="override">
      <value>{myhubName}</value>
    </set-query-parameter>
  </inbound>
  <backend>
    <base />
  </backend>
  <outbound>
    <base />
    <!--Step 6: Modify the response adding the client access token to the response body.-->
    <return-response>
      <set-status code="200" reason="OK" />
      <set-header name="Content-Type" exists-action="override">
        <value>application/json</value>
      </set-header>
      <set-body template="none">@{
        JToken body = context.Response.Body.As<JToken>();
        body["accessToken"] = (string)context.Variables["client-access-token"];
        return JsonConvert.SerializeObject(body, Newtonsoft.Json.Formatting.Indented);
      }</set-body>
    </return-response>
  </outbound>
  <on-error>
    <base />
  </on-error>
</policies>

Here in Step 1, I am using authentication-managed-identity. In order for this, I have modified Access Control (IAM) of my Azure SignalR Service granting SignalR Service Owner to the managed identity I am using.

So basically what's happening here as a summary,

  1. Acquire a token using a managed identity to communicate with SignalR Service
  2. Call SignalR Services'  generateToken endpoint using the token for managed identity (mi-access-token)
  3. Call the backend using the generated token (client-access-token)
  4. Once the response is received, modify the response by adding an accessToken property with the value of the generated token (client-access-token)

2. WebSocket API: 

SignalR connect

<policies>
  <inbound>
    <base />
    <set-query-parameter name="hub" exists-action="override">
      <value>{myhubName}</value>
    </set-query-parameter>
  </inbound>
  <backend>
    <base />
  </backend>
  <outbound>
    <base />
  </outbound>
  <on-error>
    <base />
  </on-error>
</policies>

And that's about it.

Now the final step is modifying Angular client code to point to APIM and making sure it's getting connected.

let options = {
    headers: {
        'x-ms-signalr-userid': this.tenantUserId,
        'x-authorization': 'Bearer ' + this.oidcSecurityService.getAccessToken()
    },
    transport: signalR.HttpTransportType.WebSockets,
};

this.hubConnection = new signalR.HubConnectionBuilder()
    .withUrl("https://{some-apim}.azure-api.net/client", options)
    .withAutomaticReconnect()
    .build();

await this.hubConnection.start();
And yes, it does.
1. Negotiate with Azure SignalR Service via APIM
2. WebSocket connection via APIM
SignalR Connected
Hope this helps.

More read:
   Azure SignalR Service: How to use Azure SignalR Service with Azure API Management
   Azure SignalR Service: Client negotiation

Happy Coding.

Regards,
Jaliya

Monday, August 5, 2024

Azure APIM Wildcard Operations

I recently had a requirement where I wanted to expose some endpoints in an ASP.NET Core Web API via an Azure APIM, but those were not included in the APIs OpenAPI specification. These endpoints/operations were dynamically being added by a 3rd party library. 

With these operations not defined in the APIM, the consumers cannot reach the API/Backend. And I didn't want to add each of these 3rd party operations individually, and fortunately APIM supports Wildcard operations.

We can put wildcard operations by suffixing the URL with /*, something like below:
APIM: Add operation
This will route any GET operation under /reports/viewer/ to corresponding API/Backend.

And now I can't have these operations manually added to APIM, and wanted to add them into applications OpenAPI specification. I was using Swagger, so I can easily add a IDocumentFilter, something like the following.
public class DevExpressReportingDocumentFilter : IDocumentFilter
{
    public void Apply(OpenApiDocument openApiDocument, DocumentFilterContext context)
    {
        var reportViewerOperation = new OpenApiOperation
        {
            Summary = "Report Viewer",
            Tags =
            {
                new OpenApiTag { Name = "Reporting" }
            },
            Responses =
            {
                { "200", new OpenApiResponse() }
            }
        };

        var reportDesignerOperation = new OpenApiOperation
        {
            Summary = "Report Designer",
            Tags =
            {
                new OpenApiTag { Name = "Reporting" }
            },
            Responses =
            {
                { "200", new OpenApiResponse() }
            }
        };

        openApiDocument?.Paths.Add("/reports/viewer/*", new OpenApiPathItem()
        {
            Operations =
            {
                { OperationType.Get, reportViewerOperation },
                { OperationType.Post, reportViewerOperation }
            }
        });

        openApiDocument?.Paths.Add("/reports/designer/*", new OpenApiPathItem()
        {
            Operations =
            {
                { OperationType.Get, reportDesignerOperation },
                { OperationType.Post, reportDesignerOperation }
            }
        });
    }
}
And once these endpoints are available in OpenAPI specification, I can update the deployment of the API to import the OpenAPI specification to APIM which would result in something like this.
APIM: Wildcard Operations
Hope this helps.

Happy Coding.

Regards,
Jaliya

Tuesday, July 30, 2024

Azure Container Apps: Scaling Rule based on Azure Service Bus Subscription

In this post, let's see how to set a scaling rule for Azure Container App to scale based on # of messages in an Azure Service Bus Subscription.

I am deploying an Azure Function App with a ServiceBusTrigger as a Container App.

# Windows PowerShell
az containerapp create `
--name <CONTAINER_APP_NAME> `
--resource-group <CONTAINER_APP_RESOURCE_GROUP>` --environment <CONTAINER_APP_ENVIRONMENT>
--image <CONTAINER_APP_IMAGE> `
--set-env-vars "AzureWebJobsServiceBus=<SERVICE_BUS_CONNECTION_STRING>" `
--min-replicas 0 `
--max-replicas 5 ` --secrets "service-bus-connectionstring=<SERVICE_BUS_CONNECTION_STRING>"
--scale-rule-name some-subsciption-name-rule `
--scale-rule-type azure-servicebus `
--scale-rule-metadata `
    "subscriptionName=some-subsciption-name" `
    "topicName=some-topic-name" `
    "messageCount=10" `
--scale-rule-auth "connection=service-bus-connectionstring"

Here I am creating a container app with a scale rule of type azure-servicebus. An important thing to notice is, I am creating a secret service-bus-connectionstring and using it for scale-rule-auth, so KEDA scaler can communicate with the Service Bus namespace.

Metrics: Replica Count

More reading:
   Set scaling rules in Azure Container Apps

Hope this helps.

Happy Coding.

Regards,
Jaliya