Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow specifying an Event Hub to use as the default EntityPath for namespace connection string #7105

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
50ee437
initial implementation
Jan 14, 2025
d398cf3
Update src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs
oising Jan 15, 2025
e236685
Update playground/AspireEventHub/EventHubs.AppHost/Program.cs
oising Jan 15, 2025
dd3146f
address review points
Jan 15, 2025
1f84b3a
update naming
Jan 15, 2025
0484621
update connectionstring to pass entitypath as a hint to clients; fix …
Jan 16, 2025
6c0e11c
update logic for healthcheck url for RunAsEmulator
Jan 16, 2025
c5a98fa
improve logic for WithDefaultEntity validation and throw early
Jan 16, 2025
dd17cba
fix some validation logic; add FQNS hint parsing in client base compo…
Jan 17, 2025
f438fd8
clean up emulator healthcheck code; add defines to playground to ease…
Jan 17, 2025
82c7622
missed a change
Jan 17, 2025
c7069f8
Update README.md
oising Jan 18, 2025
6ff7a82
replace systemweb cruft with lovely spans for davidfowl
Jan 18, 2025
8155a3b
Merge branch 'add-isdefaultentity-eventhub-hosting' of https://github…
Jan 18, 2025
bdccf10
Update src/Components/Aspire.Azure.Messaging.EventHubs/EventProcessor…
oising Jan 28, 2025
4f08582
Update src/Components/Aspire.Azure.Messaging.EventHubs/EventHubsCompo…
oising Jan 28, 2025
af0ac0c
Update src/Components/Aspire.Azure.Messaging.EventHubs/EventHubsCompo…
oising Jan 28, 2025
a2249a8
Update src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs
oising Jan 28, 2025
5098705
refactor BuildConnectionString to consolidate logic
Jan 28, 2025
c37c109
remove ifdefs in playground
Jan 28, 2025
6a9c272
collapse two linqs into one
Jan 28, 2025
d23a81d
Update AzureEventHubsResource.cs
oising Jan 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion playground/AspireEventHub/EventHubs.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
#define EMULATOR
oising marked this conversation as resolved.
Show resolved Hide resolved

var builder = DistributedApplication.CreateBuilder(args);

// required for the event processor client which will use the connectionName to get the connectionString.
var blob = builder.AddAzureStorage("ehstorage")
#if EMULATOR
.RunAsEmulator()
#endif
.AddBlobs("checkpoints");

var eventHub = builder.AddAzureEventHubs("eventhubns")
#if EMULATOR
.RunAsEmulator()
.WithHub("hub");
#endif
.WithHub("hub")
.WithHub("hub2")
.WithDefaultEntity("hub");

builder.AddProject<Projects.EventHubsConsumer>("consumer")
.WithReference(eventHub).WaitFor(eventHub)
Expand Down
13 changes: 9 additions & 4 deletions playground/AspireEventHub/EventHubsApi/Program.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
//#define AZCLI
#if AZCLI
using Azure.Identity;
#endif
using Azure.Messaging.EventHubs;
using Azure.Messaging.EventHubs.Producer;

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder.AddAzureEventHubProducerClient("eventhubns", settings =>
{
settings.EventHubName = "hub";
});
builder.AddAzureEventHubProducerClient("eventhubns"
#if AZCLI
, settings => settings.Credential = new AzureCliCredential()
oising marked this conversation as resolved.
Show resolved Hide resolved
#endif
);

var app = builder.Build();

Expand Down
32 changes: 20 additions & 12 deletions playground/AspireEventHub/EventHubsConsumer/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
//#define AZCLI
#if AZCLI
using Azure.Identity;
#endif
using EventHubsConsumer;

var builder = Host.CreateApplicationBuilder(args);
Expand All @@ -10,25 +14,29 @@

if (useConsumer)
{
builder.AddAzureEventHubConsumerClient("eventhubns",
settings =>
{
settings.EventHubName = "hub";
});

builder.AddAzureEventHubConsumerClient("eventhubns"
#if AZCLI
, settings => settings.Credential = new AzureCliCredential()
#endif
);
builder.Services.AddHostedService<Consumer>();
Console.WriteLine("Starting EventHubConsumerClient...");
}
else
{
// required for checkpointing our position in the event stream
builder.AddAzureBlobClient("checkpoints");
builder.AddAzureBlobClient("checkpoints"
#if AZCLI
, settings => settings.Credential = new AzureCliCredential()
#endif
);

builder.AddAzureEventProcessorClient("eventhubns"
#if AZCLI
, settings => settings.Credential = new AzureCliCredential()
#endif
);

builder.AddAzureEventProcessorClient("eventhubns",
settings =>
{
settings.EventHubName = "hub";
});
builder.Services.AddHostedService<Processor>();
Console.WriteLine("Starting EventProcessorClient...");
}
Expand Down
37 changes: 35 additions & 2 deletions src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using AzureProvisioning = Azure.Provisioning.EventHubs;
using Microsoft.Extensions.DependencyInjection;
using System.Text.Json.Nodes;
using Azure.Messaging.EventHubs;

namespace Aspire.Hosting;

Expand Down Expand Up @@ -116,6 +117,32 @@ public static IResourceBuilder<AzureEventHubsResource> WithHub(this IResourceBui
return builder;
}

/// <summary>
/// Specifies that the named EventHub should be used as the EntityPath in the resource's connection string.
/// <remarks>Only one EventHub can be set as the default entity. If more than one is configured as default, an Exception will be raised at runtime.</remarks>
/// </summary>
/// <param name="builder">The Azure Event Hubs resource builder.</param>
/// <param name="name">The name of the Event Hub.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<AzureEventHubsResource> WithDefaultEntity(this IResourceBuilder<AzureEventHubsResource> builder, [ResourceName] string name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we'd want to use this without having WithHub? Example: connecting to an existing azure resource without the need to call WithHub for each of the existing ones.

After all WithHub("foo") doesn't ensure it actually exists on the resource either. Almost like we are asking users twice in that case.

And related to this, as a user it might be nice to be able to call it twice, the second being one winning. That would mean store the hub name in the resource itself, instead of having a bool on each resource to verify the integrity (might be simpler too).

Is "Default" in DefaultEntity something concrete? Why not called WithEntityPath()? (ignore me if I already asked ;) )

Copy link
Contributor Author

@oising oising Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we'd want to use this without having WithHub? Example: connecting to an existing azure resource without the need to call WithHub for each of the existing ones.

When you mentioned this first, I had to think about it and I don't believe there's a scenario where we'd want to set a default entity path without having any associated hubs in the resource. If we want to use a pre-existing resource that already has hubs, we would use AddConnectionString. Anything that uses AddAzureEventHubs needs to provide at least one hub, or health checks will fail. I understand it is possible to want to create a namespace without hubs and have hubs created dynamically (i.e. some dapr patterns encourage dthis but you have to grant it management permissions.)

After all WithHub("foo") doesn't ensure it actually exists on the resource either. Almost like we are asking users twice in that case.

The resource itself doesn't neccessarily exist either -- all this does is mutate the model. At runtime, it's either projected into the emulator config, or emitted in the manifest. I don't think this is our concern?

And related to this, as a user it might be nice to be able to call it twice, the second being one winning. That would mean store the hub name in the resource itself, instead of having a bool on each resource to verify the integrity (might be simpler too).

I'm not sure how this scenario ("nice to be able to call it twice") would arise beyond a mistake?

Is "Default" in DefaultEntity something concrete? Why not called WithEntityPath()? (ignore me if I already asked ;) )

I say "default" because you can override it in the client settings.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sebastienros -- you're right -- it does make sense to allow someone to call it multiple times with different hubs, with last one winning. I've had a head cold all week and for some reason I didn't quite get it, lol. I'll fix that now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm renaming WithDefaultEntity to WithEntityPath since this now opens up:

var ns = builder.AddAzureEventHubs("eventhubns")
    .RunAsEmulator()
    .WithHub("hub")
    .WithHub("hub2");
    
builder.AddProject<Projects.EventHubsConsumerA>("consumera")
    .WithReference(ns.WithEntityPath("hub"));
    
builder.AddProject<Projects.EventHubsConsumerB>("consumerb")
    .WithReference(ns.WithEntityPath("hub2"));
 
 // ...

@davidfowl @eerhardt ^ ?

Copy link
Contributor Author

@oising oising Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although ... this approach could be troublesome due to it mutating the parent. Example:

ar ns = builder.AddAzureEventHubs("eventhubns")
    .RunAsEmulator()
    .WithHub("hub")
    .WithHub("hub2");

var hub = ns.WithEntityPath("hub");
var hub2 = ns.WithEntityPath("hub2");

builder.AddProject<Projects.EventHubsConsumerA>("consumera")
    .WithReference(hub);
    
builder.AddProject<Projects.EventHubsConsumerB>("consumerb")
    .WithReference(hub2);

So here, both get pointed at hub2. Oops. What are your thoughts? I like that With vs Add has well defined semantics. Is it clear enough though to dissuade people doing things like this? Maybe this isn't the only API that could fall foul to this, so it's an accepted tradeoff?

Copy link
Contributor Author

@oising oising Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The more I think about it, I think it's okay. There have to be many places where people might assign results from With* extensions and expect different semantics. This is why the distinction exists, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is getting at the comment @davidfowl made here: #7105 (comment). The problem we are running into is that EventHub "Hub" instances aren't a Resource in Aspire. Which means they aren't referencable/addressable. So that's the struggle with the above code. If instead it looked more like a database in typical DBMS systems (like SqlServer or Postgres) the above code would be:

IResourceBuilder<AzureEventHubsResource> ns = builder.AddAzureEventHubs("eventhubns")
    .RunAsEmulator();

IResourceBuilder<AzureEventHubsHubResource> hub = ns.AddHub("hub");
IResourceBuilder<AzureEventHubsHubResource> hub2 = ns.AddHub("hub2");

builder.AddProject<Projects.EventHubsConsumerA>("consumera")
    .WithReference(hub);
    
builder.AddProject<Projects.EventHubsConsumerB>("consumerb")
    .WithReference(hub2);

ConsumerA would get a connection string for eventhubns?EntityPath=hub and ConsumerB would get a connection string for eventhubns?EntityPath=hub2.

This feels more natural and consistent with the rest of Aspire.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely, and I completely agree. I had another PR open where I was creating child resources that could be deep-linked in this style, but it stalled because of refactoring work by the team in main. The thing is that we have obsolete APIs for AddEventHub that mutate the parent, so a stop-gap solution would be nice until the obsoleted API can be removed and then an AddHub added that does what you show. I guess the question is, do we go with this intermediary solution or do we throw it away and target 10.0 with AddHub. That's for you guys to decide.

{
// Only one event hub can be the default entity
if (builder.Resource.Hubs.Any(h => h.IsDefaultEntity && h.Name != name))
{
throw new DistributedApplicationException("Only one EventHub can be configured as the default entity.");
}

// We need to ensure that the hub exists before we can set it as the default entity.
if (builder.Resource.Hubs.Any(h => h.Name == name))
oising marked this conversation as resolved.
Show resolved Hide resolved
{
// WithHub is idempotent with respect to enrolling for creation of the hub, but configuration can be applied.
return WithHub(builder, name, hub => hub.IsDefaultEntity = true);
}

throw new DistributedApplicationException(
$"The specified EventHub does not exist in the Azure Event Hubs resource. Please ensure there is a call to WithHub(\"{name}\") before this call.");
}

/// <summary>
/// Configures an Azure Event Hubs resource to be emulated. This resource requires an <see cref="AzureEventHubsResource"/> to be added to the application model.
/// </summary>
Expand Down Expand Up @@ -200,7 +227,13 @@ public static IResourceBuilder<AzureEventHubsResource> RunAsEmulator(this IResou
// an event hub namespace without an event hub? :)
if (builder.Resource.Hubs is { Count: > 0 } && builder.Resource.Hubs[0] is { } hub)
oising marked this conversation as resolved.
Show resolved Hide resolved
{
var healthCheckConnectionString = $"{connectionString};EntityPath={hub.Name};";
// Endpoint=... format
var props = EventHubsConnectionStringProperties.Parse(connectionString);
oising marked this conversation as resolved.
Show resolved Hide resolved

var healthCheckConnectionString = string.IsNullOrEmpty(props.EventHubName)
? $"{connectionString};EntityPath={hub.Name};"
: connectionString;

client = new EventHubProducerClient(healthCheckConnectionString);
}
else
Expand Down Expand Up @@ -365,7 +398,7 @@ public static IResourceBuilder<AzureEventHubsEmulatorResource> WithHostPort(this
/// <param name="path">Path to the file on the AppHost where the emulator configuration is located.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
public static IResourceBuilder<AzureEventHubsEmulatorResource> WithConfigurationFile(this IResourceBuilder<AzureEventHubsEmulatorResource> builder, string path)
{
{
// Update the existing mount
var configFileMount = builder.Resource.Annotations.OfType<ContainerMountAnnotation>().LastOrDefault(v => v.Target == AzureEventHubsEmulatorResource.EmulatorConfigJsonPath);
if (configFileMount != null)
Expand Down
41 changes: 37 additions & 4 deletions src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,43 @@ public class AzureEventHubsResource(string name, Action<AzureResourceInfrastruct
/// <summary>
/// Gets the connection string template for the manifest for the Azure Event Hubs endpoint.
/// </summary>
public ReferenceExpression ConnectionStringExpression =>
IsEmulator
? ReferenceExpression.Create($"Endpoint=sb://{EmulatorEndpoint.Property(EndpointProperty.Host)}:{EmulatorEndpoint.Property(EndpointProperty.Port)};SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;")
: ReferenceExpression.Create($"{EventHubsEndpoint}");
public ReferenceExpression ConnectionStringExpression => BuildConnectionString();

private ReferenceExpression BuildConnectionString()
{
var builder = new ReferenceExpressionBuilder();

if (IsEmulator)
{
// ConnectionString: Endpoint=...
builder.Append($"Endpoint=sb://{EmulatorEndpoint.Property(EndpointProperty.Host)}:{EmulatorEndpoint.Property(EndpointProperty.Port)};SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true");
}
else
{
// FQNS: Uri format, e.g. https://...
builder.Append($"{EventHubsEndpoint}");
}

if (!Hubs.Any(hub => hub.IsDefaultEntity))
{
// Of zero or more hubs, none are flagged as default
return builder.Build();
}

// Of one or more hubs, only one may be flagged as default
var defaultEntity = Hubs.Single(hub => hub.IsDefaultEntity);
oising marked this conversation as resolved.
Show resolved Hide resolved

if (IsEmulator)
{
builder.Append($";EntityPath={defaultEntity.Name}");
}
else
{
builder.Append($"?EntityPath={defaultEntity.Name}");
oising marked this conversation as resolved.
Show resolved Hide resolved
oising marked this conversation as resolved.
Show resolved Hide resolved
}

return builder.Build();
}

void IResourceWithAzureFunctionsConfig.ApplyAzureFunctionsConfiguration(IDictionary<string, object> target, string connectionName)
{
Expand Down
2 changes: 2 additions & 0 deletions src/Aspire.Hosting.Azure.EventHubs/EventHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public EventHub(string name)
/// </summary>
public List<EventHubConsumerGroup> ConsumerGroups { get; } = [];

internal bool IsDefaultEntity { get; set; }

/// <summary>
/// Converts the current instance to a provisioning entity.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Hosting.Azure.EventHubs/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ static Aspire.Hosting.AzureEventHubsExtensions.RunAsEmulator(this Aspire.Hosting
static Aspire.Hosting.AzureEventHubsExtensions.WithConfigurationFile(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureEventHubsEmulatorResource!>! builder, string! path) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureEventHubsEmulatorResource!>!
static Aspire.Hosting.AzureEventHubsExtensions.WithDataBindMount(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureEventHubsEmulatorResource!>! builder, string? path = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureEventHubsEmulatorResource!>!
static Aspire.Hosting.AzureEventHubsExtensions.WithDataVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureEventHubsEmulatorResource!>! builder, string? name = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureEventHubsEmulatorResource!>!
static Aspire.Hosting.AzureEventHubsExtensions.WithDefaultEntity(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureEventHubsResource!>! builder, string! name) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureEventHubsResource!>!
static Aspire.Hosting.AzureEventHubsExtensions.WithGatewayPort(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureEventHubsEmulatorResource!>! builder, int? port) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureEventHubsEmulatorResource!>!
static Aspire.Hosting.AzureEventHubsExtensions.WithHostPort(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureEventHubsEmulatorResource!>! builder, int? port) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureEventHubsEmulatorResource!>!
static Aspire.Hosting.AzureEventHubsExtensions.WithHub(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureEventHubsResource!>! builder, string! name, System.Action<Aspire.Hosting.Azure.EventHubs.EventHub!>? configure = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.Azure.AzureEventHubsResource!>!
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Security.Cryptography;
using Aspire.Azure.Common;
using Aspire.Azure.Messaging.EventHubs;
Expand Down Expand Up @@ -58,7 +59,7 @@ protected static string GetNamespaceFromSettings(AzureMessagingEventHubsSettings
// This is likely to be similar to {yournamespace}.servicebus.windows.net or {yournamespace}.servicebus.chinacloudapi.cn
if (ns.Contains(".servicebus", StringComparison.OrdinalIgnoreCase))
{
ns = ns[..ns.IndexOf(".servicebus")];
ns = ns[..ns.IndexOf(".servicebus", StringComparison.OrdinalIgnoreCase)];
}
else
{
Expand Down Expand Up @@ -94,25 +95,47 @@ protected static void EnsureConnectionStringOrNamespaceProvided(AzureMessagingEv
{
// We have a connection string -- do we have an EventHubName?
if (string.IsNullOrWhiteSpace(settings.EventHubName))
{
// look for EntityPath
{
// look for EntityPath in the Endpoint style connection string
var props = EventHubsConnectionStringProperties.Parse(connectionString);

// if EntityPath is missing, throw
if (string.IsNullOrWhiteSpace(props.EventHubName))
// if EntityPath is found, capture it
if (!string.IsNullOrWhiteSpace(props.EventHubName))
{
throw new InvalidOperationException(
$"A {typeof(TClient).Name} could not be configured. Ensure a valid EventHubName was provided in " +
$"the '{configurationSectionName}' configuration section, or include an EntityPath in the ConnectionString.");
// this is used later to create the checkpoint blob container
oising marked this conversation as resolved.
Show resolved Hide resolved
settings.EventHubName = props.EventHubName;
}
}
}
// If we have a namespace and no connection string, ensure there's an EventHubName
// If we have a namespace and no connection string, ensure there's an EventHubName (also look for hint in FQNS)
else if (!string.IsNullOrWhiteSpace(settings.FullyQualifiedNamespace) && string.IsNullOrWhiteSpace(settings.EventHubName))
{
if (Uri.TryCreate(settings.FullyQualifiedNamespace, UriKind.Absolute, out var fqns))
{
var query = fqns.Query.AsSpan().TrimStart('?');
oising marked this conversation as resolved.
Show resolved Hide resolved

var key = "EntityPath=";
int startIndex = query.IndexOf(key);

if (startIndex != -1)
{
var valueSpan = query.Slice(startIndex + key.Length);
int endIndex = valueSpan.IndexOf('&');
var entityPath = endIndex == -1 ? valueSpan :
valueSpan.Slice(0, endIndex);

settings.EventHubName = entityPath.ToString();

Debug.Assert(!string.IsNullOrWhiteSpace(settings.EventHubName));
}
}
}

if (string.IsNullOrWhiteSpace(settings.EventHubName))
{
throw new InvalidOperationException(
$"A {typeof(TClient).Name} could not be configured. Ensure a valid EventHubName was provided in " +
$"the '{configurationSectionName}' configuration section.");
$"the '{configurationSectionName}' configuration section, or assign one in the settings callback for this client.");
}
}
}
2 changes: 2 additions & 0 deletions src/Components/Aspire.Azure.Messaging.EventHubs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ builder.AddAzureEventHubProducerClient("eventHubsConnectionName",
});
```

NOTE: Earlier versions of Aspire (&lt;9.1) required you to always set the EventHubName here because the Azure Event Hubs Hosting component did not provide a way to specify which Event Hub was to be included in the connection string. Beginning in 9.1, it is now possible to specify which Event Hub is to be used by way of calling `WithDefaultEntity(string)` with the name of a hub you have added via `WithHub(string)`. Only one Event Hub can be the default and attempts to flag multiple will elicit an Exception at runtime.

And then the connection information will be retrieved from the `ConnectionStrings` configuration section. Two connection formats are supported:

#### Fully Qualified Namespace
Expand Down
51 changes: 51 additions & 0 deletions tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,57 @@ public async Task VerifyWaitForOnEventHubsEmulatorBlocksDependentResources()
await app.StopAsync();
}

[Fact]
[RequiresDocker]
[ActiveIssue("https://github.com/dotnet/aspire/issues/7093")]
public async Task VerifyEntityPathInConnectionStringForIsDefaultEntity()
{
using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
var eventHub = builder.AddAzureEventHubs("eventhubns")
.RunAsEmulator()
.WithHub("hub")
.WithDefaultEntity("hub");

using var app = builder.Build();
await app.StartAsync();

// since we're running in Docker, this only tests the ConnectionString with the Emulator
// when using the real service, we pass a hint in the FQNS to the client. We can't test that here.
string? connectionString =
await eventHub.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None);

// has EntityPath?
Assert.Contains(";EntityPath=hub", connectionString);

// well-formed connection string?
var props = EventHubsConnectionStringProperties.Parse(connectionString);
Assert.NotNull(props);
Assert.Equal("hub", props.EventHubName);
}

[Fact]
[RequiresDocker]
[ActiveIssue("https://github.com/dotnet/aspire/issues/7093")]
public Task VerifyMultipleDefaultEntityThrowsException()
{
using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
var eventHub = builder.AddAzureEventHubs("eventhubns")
.RunAsEmulator()
.WithHub("hub")
.WithHub("hub2")
.WithDefaultEntity("hub");

// should throw for a second hub with default entity
Assert.Throws<DistributedApplicationException>(() => eventHub.WithDefaultEntity("hub2"));

// should not throw for same hub again
eventHub.WithDefaultEntity("hub");

using var app = builder.Build();

return Task.CompletedTask;
}

[Fact]
[RequiresDocker]
[ActiveIssue("https://github.com/dotnet/aspire/issues/6751")]
Expand Down