Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,15 +1,44 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",

"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:15887;http://localhost:15888",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:16175",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:17037",
"DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15270",
"applicationUrl": "http://localhost:15888",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16201"
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16175",
"DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:17038",
"DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true",
"ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true"
}
},
"generate-manifest": {
"commandName": "Project",
"launchBrowser": true,
"dotnetRunMessages": true,
"commandLineArgs": "--publisher manifest --output-path aspire-manifest.json",
"applicationUrl": "http://localhost:15888",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16175"
}
},
"generate-manifest": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<ItemGroup>
<ProjectReference Include="..\..\..\src\Components\Aspire.Azure.Messaging.EventHubs\Aspire.Azure.Messaging.EventHubs.csproj" />
<ProjectReference Include="..\..\..\src\Components\Aspire.Azure.Storage.Blobs\Aspire.Azure.Storage.Blobs.csproj" />
<ProjectReference Include="..\..\Playground.ServiceDefaults\Playground.ServiceDefaults.csproj" />
</ItemGroup>

Expand Down
4 changes: 3 additions & 1 deletion playground/AspireEventHub/EventHubsConsumer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
}
else
{
// required for checkpointing our position in the event stream
builder.AddAzureBlobClient("checkpoints");

builder.AddAzureEventProcessorClient("eventhubns",
settings =>
{
settings.EventHubName = "hub";
settings.BlobClientConnectionName = "checkpoints";
});
builder.Services.AddHostedService<Processor>();
Console.WriteLine("Starting EventProcessorClient...");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,11 @@ public static void AddKeyedAzureOpenAIClient(

private sealed class OpenAIComponent : AzureComponent<AzureOpenAISettings, OpenAIClient, OpenAIClientOptions>
{
protected override IAzureClientBuilder<OpenAIClient, OpenAIClientOptions> AddClient<TBuilder>(TBuilder azureFactoryBuilder, AzureOpenAISettings settings, string connectionName, string configurationSectionName)
protected override IAzureClientBuilder<OpenAIClient, OpenAIClientOptions> AddClient(
AzureClientFactoryBuilder azureFactoryBuilder, AzureOpenAISettings settings, string connectionName,
string configurationSectionName)
{
return azureFactoryBuilder.RegisterClientFactory<OpenAIClient, OpenAIClientOptions>((options, cred) =>
return azureFactoryBuilder.AddClient<OpenAIClient, OpenAIClientOptions>((options, _, _) =>
{
if (settings.Endpoint is null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,11 @@ public static void AddKeyedAzureTableClient(

private sealed class TableServiceComponent : AzureComponent<AzureDataTablesSettings, TableServiceClient, TableClientOptions>
{
protected override IAzureClientBuilder<TableServiceClient, TableClientOptions> AddClient<TBuilder>(TBuilder azureFactoryBuilder, AzureDataTablesSettings settings, string connectionName, string configurationSectionName)
protected override IAzureClientBuilder<TableServiceClient, TableClientOptions> AddClient(
AzureClientFactoryBuilder azureFactoryBuilder, AzureDataTablesSettings settings, string connectionName,
string configurationSectionName)
{
return azureFactoryBuilder.RegisterClientFactory<TableServiceClient, TableClientOptions>((options, cred) =>
return ((IAzureClientFactoryBuilderWithCredential)azureFactoryBuilder).RegisterClientFactory<TableServiceClient, TableClientOptions>((options, cred) =>
{
var connectionString = settings.ConnectionString;
if (string.IsNullOrEmpty(connectionString) && settings.ServiceUri is null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public static void AddAzureEventProcessorClient(
Action<AzureMessagingEventHubsProcessorSettings>? configureSettings = null,
Action<IAzureClientBuilder<EventProcessorClient, EventProcessorClientOptions>>? configureClientBuilder = null)
{
new EventProcessorClientComponent(builder.Configuration)
new EventProcessorClientComponent()
.AddClient(builder, DefaultConfigSectionName + nameof(EventProcessorClient),
configureSettings, configureClientBuilder, connectionName, serviceKey: null);
}
Expand All @@ -59,7 +59,7 @@ public static void AddKeyedAzureEventProcessorClient(
.GetKeyedConfigurationSectionName(name, DefaultConfigSectionName +
nameof(EventProcessorClient));

new EventProcessorClientComponent(builder.Configuration)
new EventProcessorClientComponent()
.AddClient(builder, configurationSectionName, configureSettings,
configureClientBuilder, connectionName: name, serviceKey: name);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,17 +120,20 @@ public sealed class AzureMessagingEventHubsConsumerSettings : AzureMessagingEven
public sealed class AzureMessagingEventHubsProcessorSettings : AzureMessagingEventHubsConsumerBaseSettings
{
/// <summary>
/// Gets or sets the connection name used to obtain a connection string for an Azure BlobContainerClient. This is required when the Event Processor is used.
/// Gets or sets the IServiceProvider service key used to obtain an Azure BlobServiceClient.
/// </summary>
/// <remarks>Applies only to <see cref="EventProcessorClient"/></remarks>
public string? BlobClientConnectionName { get; set; }
/// <remarks>
/// A BlobServiceClient is required when using the Event Processor. If a BlobClientServiceKey is not configured,
/// an un-keyed BlobServiceClient will be retrieved from the IServiceProvider. If a BlobServiceClient is not available in
/// the IServiceProvider, an exception is thrown.
/// </remarks>
public string? BlobClientServiceKey { get; set; }

/// <summary>
/// Get or sets the name of the blob container used to store the checkpoint data. If this container does not exist, Aspire will attempt to create it.
/// If this is not provided, Aspire will attempt to automatically create a container with a name based on the Namespace, Event Hub name and Consumer Group.
/// If a container is provided in the connection string, it will override this value and the container will be assumed to exist.
/// </summary>
/// <remarks>Applies only to <see cref="EventProcessorClient"/></remarks>
public string? BlobContainerName { get; set; }
}

Expand All @@ -142,13 +145,11 @@ public sealed class AzureMessagingEventHubsPartitionReceiverSettings : AzureMess
/// <summary>
/// Gets or sets the partition identifier.
/// </summary>
/// <remarks>Applies only to <see cref="PartitionReceiver"/></remarks>
public string? PartitionId { get; set; }

/// <summary>
/// Gets or sets the event position to start from in the bound partition. Defaults to <see cref="EventPosition.Earliest" />.
/// </summary>
/// <remarks>Applies only to <see cref="PartitionReceiver"/></remarks>
public EventPosition EventPosition { get; set; } = EventPosition.Earliest;
}

Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,9 @@
"EventProcessorClient": {
"type": "object",
"properties": {
"BlobClientConnectionName": {
"BlobClientServiceKey": {
"type": "string",
"description": "Gets or sets the connection name used to obtain a connection string for an Azure BlobContainerClient. This is required when the Event Processor is used."
"description": "Gets or sets the IServiceProvider service key used to obtain an Azure BlobServiceClient."
},
"BlobContainerName": {
"type": "string",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ protected override void BindSettingsToConfiguration(AzureMessagingEventHubsConsu
config.Bind(settings);
}

protected override IAzureClientBuilder<EventHubConsumerClient, EventHubConsumerClientOptions> AddClient<TBuilder>(TBuilder azureFactoryBuilder, AzureMessagingEventHubsConsumerSettings settings,
protected override IAzureClientBuilder<EventHubConsumerClient, EventHubConsumerClientOptions> AddClient(
AzureClientFactoryBuilder azureFactoryBuilder, AzureMessagingEventHubsConsumerSettings settings,
string connectionName, string configurationSectionName)
{
return azureFactoryBuilder.RegisterClientFactory<EventHubConsumerClient, EventHubConsumerClientOptions>((options, cred) =>
return ((IAzureClientFactoryBuilderWithCredential)azureFactoryBuilder).RegisterClientFactory<EventHubConsumerClient, EventHubConsumerClientOptions>((options, cred) =>
{
EnsureConnectionStringOrNamespaceProvided(settings, connectionName, configurationSectionName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ protected override void BindSettingsToConfiguration(AzureMessagingEventHubsProdu
config.Bind(settings);
}

protected override IAzureClientBuilder<EventHubProducerClient, EventHubProducerClientOptions> AddClient<TBuilder>(TBuilder azureFactoryBuilder, AzureMessagingEventHubsProducerSettings settings,
protected override IAzureClientBuilder<EventHubProducerClient, EventHubProducerClientOptions> AddClient(
AzureClientFactoryBuilder azureFactoryBuilder, AzureMessagingEventHubsProducerSettings settings,
string connectionName, string configurationSectionName)
{
return azureFactoryBuilder.RegisterClientFactory<EventHubProducerClient, EventHubProducerClientOptions>((options, cred) =>
return ((IAzureClientFactoryBuilderWithCredential)azureFactoryBuilder).RegisterClientFactory<EventHubProducerClient, EventHubProducerClientOptions>((options, cred) =>
{
EnsureConnectionStringOrNamespaceProvided(settings, connectionName, configurationSectionName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,79 +3,76 @@

using Aspire.Azure.Messaging.EventHubs;
using Azure;
using Azure.Core;
using Azure.Core.Extensions;
using Azure.Messaging.EventHubs;
using Azure.Messaging.EventHubs.Consumer;
using Azure.Storage.Blobs;
using Microsoft.Extensions.Azure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.Extensions.Hosting;

internal sealed class EventProcessorClientComponent(IConfiguration builderConfiguration)
internal sealed class EventProcessorClientComponent()
: EventHubsComponent<AzureMessagingEventHubsProcessorSettings, EventProcessorClient, EventProcessorClientOptions>
{
// cannot be in base class as source generator chokes on generic placeholders
protected override void BindClientOptionsToConfiguration(IAzureClientBuilder<EventProcessorClient, EventProcessorClientOptions> clientBuilder, IConfiguration configuration)
protected override void BindClientOptionsToConfiguration(
IAzureClientBuilder<EventProcessorClient, EventProcessorClientOptions> clientBuilder,
IConfiguration configuration)
{
#pragma warning disable IDE0200 // Remove unnecessary lambda expression - needed so the ConfigBinder Source Generator works
clientBuilder.ConfigureOptions(options => configuration.Bind(options));
#pragma warning restore IDE0200
}

protected override void BindSettingsToConfiguration(AzureMessagingEventHubsProcessorSettings settings, IConfiguration config)
protected override void BindSettingsToConfiguration(AzureMessagingEventHubsProcessorSettings settings,
IConfiguration config)
{
config.Bind(settings);
}

protected override IAzureClientBuilder<EventProcessorClient, EventProcessorClientOptions> AddClient<TBuilder>(TBuilder azureFactoryBuilder, AzureMessagingEventHubsProcessorSettings settings,
protected override IAzureClientBuilder<EventProcessorClient, EventProcessorClientOptions> AddClient(
AzureClientFactoryBuilder azureFactoryBuilder, AzureMessagingEventHubsProcessorSettings settings,
string connectionName, string configurationSectionName)
{
return azureFactoryBuilder.RegisterClientFactory<EventProcessorClient, EventProcessorClientOptions>(
(options, cred) =>
return azureFactoryBuilder.AddClient<EventProcessorClient, EventProcessorClientOptions>(
(options, cred, provider) =>
{
EnsureConnectionStringOrNamespaceProvided(settings, connectionName, configurationSectionName);

options.Identifier ??= GenerateClientIdentifier(settings);

var blobClient = GetBlobContainerClient(settings, cred, configurationSectionName);
var containerClient = GetBlobContainerClient(settings, provider, configurationSectionName);

var processor = !string.IsNullOrEmpty(settings.ConnectionString)
? new EventProcessorClient(blobClient,
settings.ConsumerGroup ?? EventHubConsumerClient.DefaultConsumerGroupName, settings.ConnectionString)
: new EventProcessorClient(blobClient,
? new EventProcessorClient(containerClient,
settings.ConsumerGroup ?? EventHubConsumerClient.DefaultConsumerGroupName,
settings.ConnectionString)
: new EventProcessorClient(containerClient,
settings.ConsumerGroup ?? EventHubConsumerClient.DefaultConsumerGroupName, settings.Namespace,
settings.EventHubName, cred, options);

return processor;

}, requiresCredential: false);
Copy link
Member

Choose a reason for hiding this comment

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

There is no available method that provides options, cred, provider and requiresCredential. The ultimate private call has these but there is no public combination that allows to access it. Ideally on AddClient since this is the one we wanted. I assume the expectation was that if you provide a factory with TokenCredential you'd want requiresCredential: true but here we don't always.

Copy link
Member

Choose a reason for hiding this comment

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

I was able to successfully use this using a connection string. So I'm not sure we need to specify requiresCredential: false. Since this is a new component, we shouldn't be breaking anyone. But let's keep the other components doing what they are doing now. This one can use AddClient.

@tg-msft - do you know what this requiresCredential bool does/means exactly? Why can't I specify requiresCredential: false when I use the overloads with IServiceProvider?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, this is the problem I had -- there was no public overload available that allowed specifying it when you want the service provider. I couldn't immediately see what the point of this flag even was, so I figured it was vestigial as everything seemed to work regardless.

Copy link
Contributor

Choose a reason for hiding this comment

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

requiresCredential is a niche feature for services that allow anonymous auth (i.e., reads on a Storage account used for hosting static websites). I believe it only controls whether or not we throw an exception when resolving a client if there was no credential provided. https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/extensions/Microsoft.Extensions.Azure/src/Internal/ClientRegistration.cs#L46

});
}

private BlobContainerClient GetBlobContainerClient(
AzureMessagingEventHubsProcessorSettings settings, TokenCredential cred, string configurationSectionName)
private static BlobContainerClient GetBlobContainerClient(
AzureMessagingEventHubsProcessorSettings settings, IServiceProvider provider, string configurationSectionName)
{
if (string.IsNullOrEmpty(settings.BlobClientConnectionName))
// look for keyed client if one is configured. Otherwise, get an unkeyed BlobServiceClient
var blobClient = !string.IsNullOrEmpty(settings.BlobClientServiceKey) ?
provider.GetKeyedService<BlobServiceClient>(settings.BlobClientServiceKey) :
provider.GetService<BlobServiceClient>();

if (blobClient is null)
{
// throw an invalid operation exception if the blob client connection name is not provided
throw new InvalidOperationException(
$"A EventProcessorClient could not be configured. Ensure a valid blob connection name was provided in " +
$"the '{configurationSectionName}:BlobClientConnectionName' configuration section.");
$"An EventProcessorClient could not be configured. Ensure a valid 'BlobServiceClient' is available in the ServiceProvider or " +
$"provide the service key of the 'BlobServiceClient' in " +
$"the '{configurationSectionName}:BlobClientServiceKey' configuration section, or use the settings callback to configure it in code.");
}

var blobConnectionString =
builderConfiguration.GetConnectionString(
settings.BlobClientConnectionName) ??
throw new InvalidOperationException(
"An EventProcessorClient could not be configured. " +
$"There is no connection string in Configuration with the name {settings.BlobClientConnectionName}. " +
"Ensure you have configured a connection for a Azure Blob Storage Account.");

// FIXME: ideally this should be pulled from services; but thar be dragons.
// There is no reliable way to get the blob service client from the services collection
var blobUriBuilder = new BlobUriBuilder(new Uri(blobConnectionString!));

// consumer group and blob container names have similar constraints (alphanumeric, hyphen) but we should sanitize nonetheless
var consumerGroup = (string.IsNullOrWhiteSpace(settings.ConsumerGroup)) ? "default" : settings.ConsumerGroup;

Expand All @@ -84,41 +81,40 @@ private BlobContainerClient GetBlobContainerClient(
// connection string themselves that includes a container name in the Uri already; in this case
// we assume it already exists and avoid the extra permission demand. The applies to any container
// name specified in the settings.
if (blobUriBuilder.BlobContainerName == string.Empty)
bool shouldTryCreateIfNotExists = false;

if (string.IsNullOrWhiteSpace(settings.BlobContainerName))
{
var ns = GetNamespaceFromSettings(settings);

// Do we have a container name provided in the settings?
if (string.IsNullOrWhiteSpace(settings.BlobContainerName))
{
// If not, we'll create a container name based on the namespace, event hub name and consumer group
blobUriBuilder.BlobContainerName = $"{ns}-{settings.EventHubName}-{consumerGroup}";
}
else
{
// If a container name is provided, we'll use that
blobUriBuilder.BlobContainerName = settings.BlobContainerName;
settings.BlobContainerName = $"{ns}-{settings.EventHubName}-{consumerGroup}";
shouldTryCreateIfNotExists = true;
}
}

var blobClient = new BlobContainerClient(blobUriBuilder.ToUri(), cred);
var containerClient = blobClient.GetBlobContainerClient(settings.BlobContainerName);

if (shouldTryCreateIfNotExists)
{
try
{
blobClient.CreateIfNotExists();

return blobClient;
containerClient.CreateIfNotExists();
}
catch (RequestFailedException ex)
{
throw new InvalidOperationException(
$"The configured container name of '{blobUriBuilder.BlobContainerName}' does not exist, " +
$"The configured container name of '{settings.BlobContainerName}' does not exist, " +
"so an attempt was made to create it automatically and this operation failed. Please ensure the container " +
"exists and is specified in the connection string, or if you have provided a BlobContainerName in settings, please " +
"ensure it exists. If you don't supply a container name, Aspire will attempt to create one with the name 'namespace-hub-consumergroup'.",
ex);
}
}

return new BlobContainerClient(blobUriBuilder.ToUri(), cred);
return containerClient;
}
}
Loading