Tuesday, February 20, 2024

.NET 8.0 Isolated Azure Functions: Binding Expressions that uses Azure App Configuration

In this post let's see how we can use binding expressions in an .NET 8.0 Isolated Azure Function and how to consume the binding expression values from Azure App Configuration (AAC).

Binding expressions are basically something like this. Let's take a simple ServiceBus trigger function. 
[Function(nameof(ServiceBusTrigger))]
public static void ServiceBusTrigger( [ServiceBusTrigger("%Messaging:Topic%", "%Messaging:Subscription%")] ServiceBusReceivedMessage serviceBusReceivedMessage)
{
    // TODO: Process the received message
}
Here the  %Messaging:Topic% and %Messaging:Subscription% are binding expressions and its value doesn't have to be a compile time constant.

In In-Process Azure functions, it's pretty straightforward, you can just add Azure App Configuration as another configuration provider in the Startup, and it will work.

But in Isolated functions at least as of today (20th February 2024), you can't do that (Support expression resolution from configuration sources registered by the worker #1253). While it's a bit disappointing (after having Isolated functions available for a couple of years), you can use the following workaround.

Let's say I have the following values in my Azure App Configuration.
Azure App Configuration
Azure App Configuration Values
I can use the following notation to access AAC values.
@Microsoft.AppConfiguration(Endpoint=https://aac-temp-001.azconfig.io; Key=<key>)
// if you want to choose a particular Label
@Microsoft.AppConfiguration(Endpoint=https://aac-temp-001.azconfig.io; Key=<key>; Label=<label>)
So I can update Function App settings in Azure as follows. Make sure the identity of the function app (system-assigned managed identity or user-assigned managed identity) can read the configuration from AAC.
Function App Configuration
More read:
   Use App Configuration references for App Service and Azure Functions (preview)

Friday, February 9, 2024

Azure DevOps Self-hosted Agent: NETSDK1045: The current .NET SDK does not support targeting .NET 8.0

Recently I have faced this issue in one of our Self-hosted agents in Azure DevOps when a pipeline is trying to build a .NET 8.0 application.
C:\vsts-agent\_work\_tool\dotnet\sdk\5.0.405\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.TargetFrameworkInference.targets(141,5): 

error NETSDK1045: The current .NET SDK does not support targeting .NET 8.0.  
Either target .NET 5.0 or lower, or use a version of the .NET SDK that supports .NET 8.0.  [C:\vsts-agent\_work\67\s\xxxxx.csproj]
The error was happening in a NuGetCommand@2 task while doing a restore. I replaced that with a DotNetCoreCLI@2. Then that step succeeded but eventually failed again in a VSBuild@1 task (that was using vsVersion: '17.0' which is the latest) for the same reason. 

This was strange because the pipeline was specifically requesting for .NET 8.0.
task: UseDotNet@2
  displayName: Use .NET
  inputs:
    packageType: 'sdk'
    version: '8.0.x'
The pipeline had no reason to use .NET SDK 5.0.405 and had no idea where this specific version was coming from.

Then I started digging, and after scratching my head for a couple of hours, noticed the following in agent worker logs (usually inside C:\vsts-agent\_diag). To my surprise, the pipeline is getting executed with the following.
{
  ...
  "variables": {
    "DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR": {
      "value""C:\\vsts-agent\\_work\\_tool\\dotnet\\sdk\\5.0.405\\Sdks"
    },
    "DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER": {
      "value""5.0.405"
    },
    ...
  }
  ...
}
DOTNET_MSBUILD_SDK_RESOLVER_* are .NET environment variables that are used to force the resolved SDK tasks and targets to come from a given base directory and report a given version to MSBuild.
  • DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR: Overrides the .NET SDK directory.
  • DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER: Overrides the .NET SDK version.
  • DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR: Overrides the dotnet.exe directory path.
And that kind of answered where  .NET SDK 5.0.405 was coming from, but the question remains why. Submitted an Issue #19520: Self hosted agent uses incorrect DOTNET_MSBUILD_SDK_RESOLVER_SDKS_*.

To get past the issue, I had to override these variables. To test the concept, I have overridden these variables by passing .NET 8.0 counterpart values to the pipeline execution.
Passing variables to the pipeline execution
and that finally worked. But we can't be manually overriding these for each run, so I have overridden them in YAML as follows.
variables:
name: DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR
  value: 'C:\vsts-agent\_work\_tool\dotnet\sdk\8.0.101\Sdks'
name: DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER
  value: '8.0.101'
...
Now the pipeline builds and publishes .NET 8 apps successfully, but I still have no idea why the older SDK was being forced.

Hopefully, we will find it here soon:

Hope this helps.

Happy Coding.

Regards,
Jaliya

Saturday, February 3, 2024

Azure AD B2C: Validating Output Claim from a Non-Self-Asserted Technical Profile

I had a requirement where I wanted to do an additional validation on a boolean claim value in an AAD B2C user journey. If the boolean claim value is true, I wanted to move forward in the user journey. If the value is false, I wanted to short circuit the user journey and return an error. 

I couldn't use Validation Technical Profiles, because the output claim I am validating upon was in a non-self-asserted technical profile (the claim was retrieved by calling an external REST endpoint)  and Validation Technical Profiles doesn't support non-self-asserted technical profiles.

In such cases, we can add an additional OrchestrationStep, do a Precondition in that particular step, assert and navigate the user to a self-asserted technical profile and display the error there.

So how do we do that? 

1. Define a ClaimType for a self-asserted technical profile.

<BuildingBlocks>
  <ClaimsSchema>
    ...
    <ClaimType Id="errorMessage">
      <DisplayName>Please contact support.</DisplayName>
      <DataType>string</DataType>
      <UserInputType>Paragraph</UserInputType>
    </ClaimType>
  </ClaimsSchema>
  ...
</BuildingBlocks>

2. Define a ClaimsTransformation.

<BuildingBlocks>
  ...
  <ClaimsTransformations> ...
    <ClaimsTransformation Id="CreateApplicationUserNotActiveErrorMessage" TransformationMethod="CreateStringClaim">
      <InputParameters>
        <InputParameter Id="value" DataType="string" Value="Application user is not active." />
      </InputParameters>
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="errorMessage" TransformationClaimType="createdClaim" />
      </OutputClaims>
    </ClaimsTransformation>
  </ClaimsTransformations>
</BuildingBlocks>

3. Define a self-asserted TechnicalProfile. Use the above ClaimsTransformation as a InputClaimsTransformation. Reference the ClaimType created in the first step.

<ClaimsProviders>
  <ClaimsProvider>
    <DisplayName>...</DisplayName>
    <TechnicalProfiles> ...
      <TechnicalProfile Id="SelfAsserted-ApplicationUserNotActiveError">
        <DisplayName>Error message</DisplayName>
        <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
        <Metadata>
          <Item Key="ContentDefinitionReferenceId">api.selfasserted</Item>
          <Item Key="setting.showContinueButton">false</Item>
          <Item Key="setting.showCancelButton">true</Item>
        </Metadata>
        <InputClaimsTransformations>
          <InputClaimsTransformation ReferenceId="CreateApplicationUserNotActiveErrorMessage" />
        </InputClaimsTransformations>
        <InputClaims>
          <InputClaim ClaimTypeReferenceId="errorMessage"/>
        </InputClaims>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="errorMessage"/>
        </OutputClaims>
      </TechnicalProfile>
    </TechnicalProfiles>
  </ClaimsProvider>
</ClaimsProviders>

4. Introduce an additional OrchestrationStep with a Precondition before the last the OrchestrationStep. If the condition is not satisfied, use the created self-asserted TechnicalProfile.

<UserJourneys>
  ...
  <UserJourney Id="...">
    <OrchestrationSteps>
      ...
      <OrchestrationStep Order="9" Type="ClaimsExchange">
        <Preconditions>
          <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
            <Value>isActive</Value> <!-- this claim is forwarded from a previous step -->
            <Value>True</Value>
            <Action>SkipThisOrchestrationStep</Action>
          </Precondition>
        </Preconditions>
        <ClaimsExchanges>
          <ClaimsExchange Id="SelfAssertedApplicationUserNotActiveError" TechnicalProfileReferenceId="SelfAsserted-ApplicationUserNotActiveError" />
        </ClaimsExchanges>
      </OrchestrationStep>
      ...
      <OrchestrationStep Order="11" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
    </OrchestrationSteps>
  </UserJourney>
  ...
</UserJourneys>

And this is what happens when isActive claim is false. When it's true, the above OrchestrationStep will get skipped and the user journey will continue.
Self-Asserted Technical Profile
Hope this helps.

Happy Coding.

Regards,
Jaliya

Thursday, February 1, 2024

.NET 8.0 Isolated Azure Durable Functions: Preserve Stack Order When Passing Between Orchestrators, Activities etc

In this post let's see how we can preserve Stack<T> order when it's getting passed between Orchestrators/Activities in a .NET Isolated Azure Durable Function. 

In Durable Functions in the .NET isolated worker, the Serialization default behavior has changed from Newtonsoft.Json to System.Text.Json.

I have already written a post about preserving Stack Order in an In-Process Azure Durable Functionshere. I am using the same code example, instead converted it to isolated worker. So I am not going to write down the entire example code to describe the issue here, you can have a look at the previous post.

You can see in the below screenshot, the order of Stack<T> is not preserved with default Serializer options.

Incorrect Result
With Isolated Durable Functions, we can easily configure the JsonSerializerOptions. We need to add a custom JsonConverter that correctly serializes and deserializes a Stack<T>. There is already JsonConverterFactoryForStackOfT shared by the .NET team that we can use in our Isolated Durable Function as follows.
using DurableFunctions.Isolated.StackSerialization.Converters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Text.Json;

IHost host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices(services =>
    {
        services.Configure<JsonSerializerOptions>(options =>
        {
// Add custom converter to serialize and deserialize a Stack<T>
            options.Converters.Add(new JsonConverterFactoryForStackOfT());
        });
    })
    .Build();

host.Run();
And now once the Serializer options are configured, we can see Stack<T> is getting serialized/deserialized correctly.
Correct Result

Hope this helps.

Happy Coding.

Regards,
Jaliya