Understanding RunAsEmulator() Behavior in Aspire
When working with Aspire for .NET cloud-native applications, it’s common to need flexible configuration that switches between production cloud resources and local emulators.
You might expect a consistent pattern across services like Azure Event Hubs and Azure Blob Storage, but subtle implementation differences can create confusion. Let’s explore how RunAsEmulator() seemingly works for Event Hubs, but not for Blob Storage—and why it’s important to recognize the pattern within this behavior because it might not behave how you’d otherwise expect.
The Problem: Inconsistent Use of RunAsEmulator()
Consider the following code:
var eventHubs = builder.ExecutionContext.IsPublishMode
? builder.AddConnectionString("event-hubs")
: builder.AddAzureEventHubs("event-hubs")
.RunAsEmulator();
This code works as intended. In publish mode, it uses a pre-defined connection string. Locally, it switches to an emulator, simulating Event Hubs.
But now, take a similar approach for Azure Blob Storage:
var blobStorage = builder.ExecutionContext.IsPublishMode
? builder.AddConnectionString("storage")
: builder.AddAzureStorage("storage")
.AddBlobs("blobs")
.RunAsEmulator();
This is a straightforward scenario: I want to use a connection string when deploying to the cloud, since the resource is already provisioned via Infrastructure-as-Code. However, when running locally, I prefer to use an emulator. The complication arises due to a quirk in polymorphism — AzureBlobStorageResource is not the same type as the one expected by the RunAsEmulator method.
This version fails. The error:
‘IResourceBuilder<AzureBlobStorageResource>’ does not contain a definition for ‘RunAsEmulator’ and the best extension method overload ‘AzureEventHubsExtensions.RunAsEmulator(IResourceBuilder<AzureEventHubsResource>, Action<IResourceBuilder<AzureEventHubsEmulatorResource>>?)’ requires a receiver of type ‘Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureEventHubsResource>’
This works because RunAsEmulator() is applied directly to the correct IResourceBuilder
Conceptual Model: Control Plane vs Data Plane
Think of RunAsEmulator() as something that applies at the control plane level—the resource account (like a storage account or Event Hubs namespace). Once you’re within a data-plane construct—like a blob container or event hub—the emulator context is already expected to be set up.
var blobStorage = builder.ExecutionContext.IsPublishMode
? builder.AddConnectionString("storage")
: builder.AddAzureStorage("storage")
.RunAsEmulator()
.AddBlobs("blobs");
This pattern becomes clearer with the Event Hubs example when you chain the .AddHub(“messages”) call:
var eventHubs = builder.ExecutionContext.IsPublishMode
? builder.AddConnectionString("event-hubs")
: builder.AddAzureEventHubs("event-hubs")
.RunAsEmulator()
.AddHub("messages");
You’re declaring a resource (event-hubs) and setting up an emulator for it, then declaring a child resource (messages). This sequence is logically correct—and also what the fluent API expects. It also parallels perfectly how it works with Azure Blob Storage. However, because I neglected to include the EventHub data plane resource with the AddHub extension method, I lost sight of this pattern and made a mistake when setting up Azure Blob Storage emulator.
It just goes to show you that sometimes if you vary how you declare local member variables or the initialization of such service endpoints and data plane resources this can become obfuscated — at least it was for me.
Conclusion
This subtle issue arises from the polymorphic nature of the Aspire resource builder API. Each fluent call potentially shifts the builder’s type, so RunAsEmulator() must be invoked at the correct moment—directly on the top-level resource builder, before any child resources are added. By understanding the intent behind the API design—distinguishing control plane resources from data plane constructs—you can write more robust and predictable configuration code.
It seems like when you are setting up a local emulator, you should always apply RunAsEmulator() immediately after adding the base Azure resource, before adding child resources like blobs or hubs. Remember this pattern and you can probably expect to apply this in a consistent fashion across any Azure Service that Aspire supports!