TestFramework.Azure 0.4.0

The owner has unlisted this package. This could mean that the package is deprecated, has security vulnerabilities or shouldn't be used anymore.
dotnet add package TestFramework.Azure --version 0.4.0
                    
NuGet\Install-Package TestFramework.Azure -Version 0.4.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TestFramework.Azure" Version="0.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TestFramework.Azure" Version="0.4.0" />
                    
Directory.Packages.props
<PackageReference Include="TestFramework.Azure" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TestFramework.Azure --version 0.4.0
                    
#r "nuget: TestFramework.Azure, 0.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TestFramework.Azure@0.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TestFramework.Azure&version=0.4.0
                    
Install as a Cake Addin
#tool nuget:?package=TestFramework.Azure&version=0.4.0
                    
Install as a Cake Tool

TestFramework.Azure

Introduction

TestFramework.Azure is an extension package for TestFramework.Core.

If you are new: TestFramework.Core runs your timeline, and this package adds Azure-specific triggers, events, and artifact helpers. You usually install and learn TestFramework.Core first, then add TestFramework.Azure when your tests need Azure resources.

Azure-focused test helpers for TestFramework.Core timelines.

TestFramework.Azure adds fluent building blocks for:

  • Azure Function App calls (remote and in-process)
  • Service Bus send/receive flows
  • Azure artifacts (SQL, Cosmos DB, Table Storage, Blob Storage)

Install

dotnet add package TestFramework.Azure

Start Here

If you are new to the Azure package, learn it in this order:

  1. Pick stable identifier names such as Default, MainDb, or MainSBQueue and keep those names consistent between timeline code and config.
  2. Register the smallest config shape that matches the resource you need.
  3. Start with one canonical flow per resource family before mixing features.

Recommended first flows:

  • Function App: AzureExt.Trigger.FunctionApp.Http("Default") ... .Call()
  • Service Bus: send with AzureExt.Trigger.ServiceBus.Send(...), then wait with AzureExt.Event.ServiceBus.MessageReceived(...)
  • Logic App: use CallAndCapture() for stateless workflows, Call() plus RunCompleted(...) for stateful workflows
  • Data systems: start with one artifact or one finder against a single named identifier before composing larger end-to-end scenarios

Once those three ideas are clear, the broader support matrix below becomes much easier to navigate.

Minimal Setup

using TestFramework.Azure.Extensions;
using TestFramework.Config;

ConfigInstance config = ConfigInstance.FromJsonFile("local.testSettings.json")
    .LoadAzureConfig()
    .Build();

How Azure Config Fits The Core Config Model

ConfigInstance is still the normal setup entry point for Azure timelines.

Use it to:

  • load JSON or create an empty base
  • call LoadAzureConfig() or related helpers
  • register extra services
  • build the IServiceProvider that you pass into SetupRun(...)

That means the usual shape stays:

  1. build the run provider with ConfigInstance
  2. let LoadAzureConfig() register the shapes that describe each section
  3. ask the run for a record when a step needs one: context.Configured<CosmosContainerDbConfig>("MainDb")

Configured<T> is the only way to read a configured resource, and it deliberately does not tell you where the answer came from. An entry someone wrote in a file and an address a container published while starting arrive the same way, so a timeline that runs against a deployed Cosmos and one that runs against the emulator are the same timeline.

If you are coming from ConfigStore<T>

Earlier versions registered a typed ConfigStore<T> per record and had you resolve it from the service provider. That type is gone. It could only ever hold what someone had written down, so a step reading it got a placeholder for anything a container had to start before its address existed.

// before
var config = provider.GetRequiredService<ConfigStore<CosmosContainerDbConfig>>().GetConfig("MainDb");

// now
var config = context.Configured<CosmosContainerDbConfig>("MainDb");

LoadAzureConfigs(...) no longer takes an IConfiguration either, for the same reason: the run holds the configuration its shapes read.

Output Binding Mental Model

When Azure-oriented APIs expose callback URLs, response payloads, workflow run identifiers, or similar values, treat them as explicit result extraction from a step.

  • they are not a second hidden execution model
  • they are not a replacement for normal TimelineRun assertions
  • they are the Azure-shaped way to surface a step result that the scenario can then register, assert, or pass forward explicitly

If a scenario starts to feel too Azure-magic-heavy, pull the extracted value into a named variable or artifact and keep the rest of the assertions generic.

Configuration Records

The Azure package reads named records from configuration sections through LoadAzureConfig() or LoadAzureConfigs(...). Each DSL identifier such as "MainDb" or "Default" maps to one child object inside the matching section.

Supported record types:

Section Record type Required fields Optional fields Used by
FunctionApp:{identifier} FunctionAppConfig BaseUrl, Code AdminCode, RoutePrefix Remote Function App HTTP triggers and Function App liveness checks
LogicApp:{identifier} LogicAppConfig no single global required field HostingMode, WorkflowName, nested Standard and Consumption settings Logic App request triggers, timer triggers, run events, and Logic App liveness checks
CosmosDb:{identifier} CosmosContainerDbConfig ConnectionString, DatabaseName, ContainerName None Cosmos artifacts, query finders, and Cosmos liveness checks
ServiceBus:{identifier} ServiceBusConfig ConnectionString plus either QueueName or TopicName SubscriptionName, RequiredSession Service Bus send triggers and message-received events
StorageAccount:{identifier} StorageAccountConfig ConnectionString QueueContainerName, BlobContainerName, TableContainerName Blob artifacts, table artifacts, and storage liveness checks
SqlDatabase:{identifier} SqlDatabaseConfig ConnectionString, DatabaseName SQL row artifacts, SQL query finders, and SQL liveness checks

Breaking change: FunctionAppConfig.BaseUrl is now strictly the site root and the api/ prefix comes from the new RoutePrefix property. If your BaseUrl currently ends in /api/, drop that segment - otherwise every HTTP call resolves to /api/api/.... A custom [HttpTrigger(Route = ...)] now keeps the host prefix instead of losing it.

Configuration expectations:

  • FunctionAppConfig.BaseUrl is the site root. Do not include the api/ route prefix in it - host endpoints such as admin/host/status are resolved against this value directly.

  • FunctionAppConfig.RoutePrefix defaults to api and mirrors extensions.http.routePrefix in host.json. It is applied exactly once, to every HTTP route the builders produce. Set it to "" for a host configured with an empty route prefix.

  • FunctionAppConfig.Code is the normal trigger key; AdminCode is the host master key, required by the admin/... endpoints the managed trigger and host-status checks use.

  • LogicAppConfig.HostingMode defaults to Standard.

  • LogicAppConfig.Standard contains the Standard-only values: BaseUrl, Code, and AdminCode.

  • LogicAppConfig.Consumption contains the Consumption-only values: InvokeUrl and WorkflowResourceId.

  • For Consumption request-trigger calls, Consumption.InvokeUrl is enough. Think of that as invoke-only Consumption.

  • For Consumption run polling, timer triggers, recurrence triggers, and management-backed liveness checks, set Consumption.WorkflowResourceId and register ILogicAppConsumptionManagementRequestAuthorizer in DI. Think of that as managed Consumption.

  • The framework still does not discover Consumption URLs, resource IDs, or tokens from Azure on the user's behalf.

  • CosmosContainerDbConfig is container-specific. One identifier points to one database/container pair.

  • ServiceBusConfig should define either queue mode or topic mode. Queue mode uses QueueName. Topic mode uses TopicName, and fixed-subscription receives also require SubscriptionName.

  • StorageAccountConfig only requires container names for the features you use. Blob and table liveness checks rely on BlobContainerName and TableContainerName respectively.

  • A DbContext is registered through the SQL registry and nowhere else. There is no AddDbContext beside it and no ContextType in configuration: the options handed to your callback already point at the database this run is using, whether you wrote its address down or a container published one while starting.

    services.AddSqlArtifactContexts(reg => reg
        .AddForIdentifier<AppDbContext>("MainSql", options => new AppDbContext(options)));
    

Logic App Support Matrix

Treat Logic App support as three capability levels instead of one large feature bucket:

Mode Required config Works without ARM access Supported features
Standard Standard.BaseUrl, optional Standard.Code / Standard.AdminCode yes invoke, run polling, timer/recurrence triggers, authenticated liveness
Consumption invoke-only Consumption.InvokeUrl yes manual request-trigger Call() and CallAndCapture(), basic reachability
Consumption managed Consumption.InvokeUrl, Consumption.WorkflowResourceId, ILogicAppConsumptionManagementRequestAuthorizer no everything from invoke-only plus RunCompleted(...), RunReachedStatus(...), timer/recurrence triggers, management-backed liveness

Example JSON:

{
    "FunctionApp": {
        "Default": {
            "BaseUrl": "https://my-functions.azurewebsites.net/",
            "Code": "function-key",
            "AdminCode": "admin-key"
        }
    },
    "LogicApp": {
        "StandardOrders": {
            "WorkflowName": "OrderProcessor",
            "Standard": {
                "BaseUrl": "https://my-logic.azurewebsites.net/",
                "Code": "workflow-key",
                "AdminCode": "host-admin-key"
            }
        },
        "ConsumptionOrders": {
            "HostingMode": "Consumption",
            "WorkflowName": "OrderProcessor",
            "Consumption": {
                "InvokeUrl": "https://prod-04.germanywestcentral.logic.azure.com/workflows/.../triggers/manual/paths/invoke?api-version=...&sp=...&sv=1.0&sig=...",
                "WorkflowResourceId": "/subscriptions/.../resourceGroups/.../providers/Microsoft.Logic/workflows/OrderProcessor"
            }
        }
    },
    "CosmosDb": {
        "MainDb": {
            "ConnectionString": "AccountEndpoint=...;AccountKey=...;",
            "DatabaseName": "AppDb",
            "ContainerName": "Orders"
        }
    },
    "ServiceBus": {
        "MainSBQueue": {
            "ConnectionString": "Endpoint=sb://...",
            "QueueName": "orders",
            "RequiredSession": false
        },
        "MainSBTopic": {
            "ConnectionString": "Endpoint=sb://...",
            "TopicName": "events",
            "SubscriptionName": "integration-tests",
            "RequiredSession": false
        }
    },
    "StorageAccount": {
        "MainStorage": {
            "ConnectionString": "DefaultEndpointsProtocol=https;...",
            "BlobContainerName": "exports",
            "TableContainerName": "OrderAudit"
        }
    },
    "SqlDatabase": {
        "MainSql": {
            "ConnectionString": "Server=...;Database=AppDb;...",
            "DatabaseName": "AppDb"
        }
    }
}

Sample: Function App HTTP Call

using TestFramework.Azure;
using TestFramework.Core.Timelines;

Timeline timeline = Timeline.Create()
    .Trigger(AzureExt.Trigger.FunctionApp.Http("Default").SelectEndpointWithMethod<HttpTests>(nameof(HttpTests.Run)).Call())
    .Build();

TimelineRun run = await timeline.SetupRun(config.BuildServiceProvider()).RunAsync();

run.EnsureRanToCompletion();

SelectEndpointWithMethod<T>(...) expects the target method to carry both a [Function(...)] attribute and a parameter marked with [HttpTrigger(...)].

Function App Execution Modes

AzureExt.Trigger.FunctionApp exposes three different execution styles:

Mode Use when Runtime dependency Typical benefit
Http(...) you want to call a deployed or container-hosted Function App over HTTP reachable HTTP host plus FunctionAppConfig closest to production wiring
Managed<T>(identifier, method) you want framework-managed invocation of a known function entry point the function type is available to the test process avoids hand-written HTTP request setup
InProcessHttp<T>(...) you want to execute the function handler directly in-process the function type and request delegate are available in the test process fastest feedback and easiest offline unit-style validation

Choose Http(...) for end-to-end behavior, Managed<T>(...) when you still want a Function App abstraction without a remote hop, and InProcessHttp<T>(...) when the test should stay entirely local to the current process.

InProcessHttp(...) Overload Guide

Use the smallest overload that matches what your function returns:

  • InProcessHttp<T>((request, context) => { ... }) for synchronous handlers that return no result.
  • InProcessHttp<T>((request, context) => Task.CompletedTask) for asynchronous handlers that return no result.
  • InProcessHttp<T>((request, context) => new OkResult()) for synchronous handlers that return IActionResult.
  • InProcessHttp<T>((request, context) => Task.FromResult<IActionResult>(...)) for asynchronous handlers that return IActionResult.

For remote calls, the equivalent decision point is different: start with Http(...), then choose either SelectEndpointWithMethod<T>(...) when the route can be inferred from the function metadata, SelectFunction(name, method) when you want the normal api/{functionName} route without spelling out the prefix yourself, or SelectEndpoint(path, method) when the test should supply the path and HTTP verb explicitly.

Example with explicit path, body, and headers:

Timeline timeline = Timeline.Create()
    .Trigger(
        AzureExt.Trigger.FunctionApp.Http("Default")
            .SelectEndpoint(Var.Const("orders/42"), Var.Const(HttpMethod.Post))
            .WithHeader(Var.Const("x-correlation-id"), Var.Const("order-42"))
            .WithHeaders(Var.Const(new Dictionary<string, string> { ["x-tenant"] = "lab" }))
            .WithBody(Var.Const("{\"id\":42}"))
            .Call())
    .Build();

Example with the default Function App route prefix:

Timeline timeline = Timeline.Create()
    .Trigger(
        AzureExt.Trigger.FunctionApp.Http("Default")
            .SelectFunction("HttpEchoTest", HttpMethod.Post)
            .WithBody(Var.Const("payload"))
            .Call())
    .Build();

Sample: Stateless Logic App Call And Capture

Use CallAndCapture() when the target workflow is stateless and completes inline with the callback response instead of exposing durable run history.

using TestFramework.Azure;
using TestFramework.Core.Timelines;
using TestFramework.Core.Variables;

Timeline timeline = Timeline.Create()
    .Trigger(
        AzureExt.Trigger.LogicApp.Http("logic")
            .Workflow("StatelessOrders")
            .Manual()
            .WithBody(Var.Const("{\"id\":42}"))
            .CallAndCapture())
    .Name("logic-call")
    .Build();

TimelineRun run = await timeline.SetupRun(config.BuildServiceProvider()).RunAsync();

run.EnsureRanToCompletion();
LogicAppExecutionResult result = Assert.IsType<LogicAppExecutionResult>(run.Step("logic-call").LastResult.Result);
Assert.Equal(LogicAppRunStatus.Succeeded, result.RunStatus);
Assert.Equal(HttpStatusCode.Accepted, result.StatusCode);

Use RunCompleted(...) and RunReachedStatus(...) only for stateful workflows. When Docker-hosted Logic App definitions are known to be stateless, the framework now fails fast with a message that points you to CallAndCapture().

Sample: Consumption Logic App Run Tracking

Consumption workflows now use a smaller config surface: provide the invoke URL for request triggers and add the workflow resource ID only when the test host should perform durable management operations.

using TestFramework.Azure.LogicApp;

Timeline timeline = Timeline.Create()
    .Trigger(
        AzureExt.Trigger.LogicApp.Http("ConsumptionOrders")
            .Workflow("OrderProcessor")
            .Manual()
            .WithBody(Var.Const("{\"id\":42}"))
            .Call())
    .Name("logic-call")
    .GetRunContext("logicRun")
    .WaitForEvent(
        AzureExt.Event.LogicApp.RunCompleted(
            "ConsumptionOrders",
            Var.Ref<LogicAppRunContext>("logicRun")))
    .Build();

This run-tracking flow is managed Consumption. It needs Consumption.InvokeUrl plus Consumption.WorkflowResourceId, and the host must register ILogicAppConsumptionManagementRequestAuthorizer so the framework can authenticate ARM management requests for RunCompleted(...) and timer/recurrence trigger operations.

Use CallAndCapture() when the Consumption workflow is effectively stateless and returns the meaningful result directly in the callback response. That is the invoke-only path and does not require ARM access.

Live Validation Notes

  • The live validation solution inside the repository now mirrors the same Consumption model: callback invoke URL plus workflow resource ID and an injected management-request authorizer.
  • SQL remains the heaviest live-validation surface because it needs both a connection string and a registered DbContext shape, which is a larger setup burden than the other Azure resources.

Service Bus Support Matrix

ServiceBusConfig supports three receive modes:

Mode Required fields Notes
Queue ConnectionString, QueueName SubscriptionName must be omitted. Works with session and non-session queues. Non-matching messages are abandoned so their lock is released.
Topic + subscription ConnectionString, TopicName, SubscriptionName Use when the test should receive from a fixed subscription. Non-matching messages are abandoned so their lock is released.
Topic + temp subscription ConnectionString, TopicName Call AzureExt.Event.ServiceBus.MessageReceived(..., createTempSubscription: true) to create and clean up a filtered temp subscription automatically. Filtering happens server-side, so nothing is abandoned.

Message settlement:

  • A matching message is completed by default. Pass completeMessage: false to opt out.
  • Leaving a matched message uncompleted returns it to the entity, where it can satisfy a later run's wait - a cross-run false pass. Only opt out when another consumer owns the message.
  • In the shared modes, a non-matching message is abandoned rather than ignored. Ignoring it would hold its lock for the SDK's renewal window and starve concurrent runs; abandoning increases the message's DeliveryCount, which is the accepted tradeoff. The first abandon of a run is logged as a warning.

Sample: Service Bus Queue Send + Wait

using Azure.Messaging.ServiceBus;
using TestFramework.Azure;
using TestFramework.Core.Timelines;

Timeline timeline = Timeline.Create()
    .Trigger(AzureExt.Trigger.ServiceBus.Send("MainSBQueue", new ServiceBusMessage("Test message") { CorrelationId = "order-42" }))
    .WaitForEvent(AzureExt.Event.ServiceBus.MessageReceived("MainSBQueue", correlationId: "order-42", completeMessage: true))
        .WithTimeOut(TimeSpan.FromSeconds(10))
    .Build();

Sample: Service Bus Topic Send + Temp Subscription Wait

Timeline timeline = Timeline.Create()
    .WaitForEvent(AzureExt.Event.ServiceBus.MessageReceived("MainSBTopic", correlationId: "topic-1234", createTempSubscription: true, completeMessage: true))
        .WithTimeOut(TimeSpan.FromSeconds(10))
    .Trigger(AzureExt.Trigger.ServiceBus.Send("MainSBTopic", new ServiceBusMessage("Test message") { CorrelationId = "topic-1234" }))
    .Build();

Cosmos Client Options Note

Use ConfigureCosmosClientOptions(...) when you need to customize the underlying Cosmos SDK client.

For AzureExt.Trigger.IsLive.Cosmos(...), timeout control comes from the normal timeline step timeout, for example via .WithTimeOut(...) on the builder. The optional AlivenessLevel lets you choose whether the check should stop at endpoint reachability, account authentication, or require the configured container to exist.

using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.DependencyInjection;
using TestFramework.Azure;
using TestFramework.Azure.Extensions;
using TestFramework.Config;
using TestFramework.Core.Timelines;

ConfigInstance config = ConfigInstance.FromJsonFile("local.testSettings.json")
    .AddService((services, configuration) =>
    {
        services.LoadAzureConfigs(configuration)
            .ConfigureCosmosClientOptions(_ => new CosmosClientOptions
            {
                ConnectionMode = ConnectionMode.Gateway,
            });
    })
    .Build();

Timeline timeline = Timeline.Create()
    .Trigger(AzureExt.Trigger.IsLive.Cosmos("MainDb", AlivenessLevel.Authenticated))
        .WithTimeOut(TimeSpan.FromSeconds(5))
    .Build();

Sample: Find Data Artifact

using Microsoft.Azure.Cosmos;
using TestFramework.Azure;

Timeline timeline = Timeline.Create()
    .FindArtifacts(
        "cosmosItemQuery",
        AzureExt.ArtifactFinder.DB.CosmosQuery<MyCosmosItem>(
            "MainDb",
            new QueryDefinition("SELECT * FROM c WHERE c.number = 1")))
    .Build();

Target Framework

  • .NET 8 (net8.0)
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated

BREAKING - teardown now deletes what a run discovered. An artifact this package produces is deletable, and the timeline decides whether it may be deleted: chain MarkReadonly() onto the FindArtifact / FindArtifacts / RegisterArtifact call for a resource the run only reads. Requires TestFramework.Core 0.3.0.
Before this release the Cosmos item finder and the Table Storage entity finder handed back references teardown deleted, while a SQL row located by the EF Core finder was never deleted. That split was invisible at the call site and inconsistent between resource families. Now all three behave the same way and the decision is written where the artifact is declared.
What to change: a timeline that discovers Azure SQL rows and relies on them surviving the run must add MarkReadonly(), or teardown will delete rows the application under test owns. A timeline that already treated discovery as deleting needs no change. Cosmos and Table discovery is unchanged in effect - it deleted before and it deletes now.