From a5c8c4fbd6705b10e1c8cbe49198a6bc085fd733 Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Mon, 19 Dec 2022 16:14:01 +0300 Subject: [PATCH] Support the use of IConfiguration (#487) This PR adds support for configuring the EventBus, its transports, events and consumers via `IConfiguration`. It eases the getting started process and allows overriding certain behavior without changing code for example changing the transport's connection string in production and development. A sample that uses `IConfiguration` from `appsettings.json` has also been added. --- README.md | 2 + Tingle.EventBus.sln | 7 ++ .../AzureManagedIdentity.csproj | 1 + samples/ConfigSample/ConfigSample.csproj | 16 +++++ samples/ConfigSample/ImageUploaded.cs | 15 ++++ samples/ConfigSample/Program.cs | 31 ++++++++ .../Properties/launchSettings.json | 11 +++ .../ConfigSample/VehicleDoorOpenedEvent.cs | 26 +++++++ .../VehicleTelemetryEventsConsumer.cs | 42 +++++++++++ .../ConfigSample/VisualsProducerService.cs | 42 +++++++++++ .../ConfigSample/VisualsUploadedConsumer.cs | 31 ++++++++ .../ConfigSample/appsettings.Development.json | 16 +++++ samples/ConfigSample/appsettings.json | 46 ++++++++++++ .../AmazonTransportConfigureOptions.cs | 15 +++- .../AmazonKinesisConfigureOptions.cs | 14 ++-- .../AmazonKinesisEventBusBuilderExtensions.cs | 7 +- .../AmazonSqsConfigureOptions.cs | 14 ++-- .../AmazonSqsEventBusBuilderExtensions.cs | 7 +- .../AzureTransportConfigureOptions.cs | 19 +++-- .../AzureBlobStorageCredentials.cs | 2 +- .../AzureEventHubsConfigureOptions.cs | 60 +++++++++++++--- ...AzureEventHubsEventBusBuilderExtensions.cs | 7 +- .../AzureEventHubsTransport.cs | 2 +- .../AzureQueueStorageConfigureOptions.cs | 40 +++++++++-- ...reQueueStorageEventBusBuilderExtensions.cs | 7 +- .../AzureServiceBusConfigureOptions.cs | 40 +++++++++-- ...zureServiceBusEventBusBuilderExtensions.cs | 7 +- .../InMemoryEventBusBuilderExtensions.cs | 3 +- .../InMemoryTransportConfigureOptions.cs | 15 +++- .../KafkaConfigureOptions.cs | 18 +++-- .../KafkaEventBusBuilderExtensions.cs | 7 +- .../RabbitMqConfigureOptions.cs | 18 +++-- .../RabbitMqEventBusBuilderExtensions.cs | 7 +- .../DefaultEventBusConfigurationProvider.cs | 20 ++++++ .../Configuration/DefaultEventConfigurator.cs | 23 ++++-- .../IEventBusConfigurationProvider.cs | 15 ++++ .../DependencyInjection/EventBusBuilder.cs | 29 +++++--- .../EventBusConfigureOptions.cs | 11 ++- .../EventBusNamingOptions.cs | 2 +- .../TransportOptionsConfigureOptions.cs | 27 ------- src/Tingle.EventBus/Tingle.EventBus.csproj | 1 + .../EventBusTransportConfigureOptions.cs | 72 +++++++++++++++++++ .../DefaultEventConfiguratorTests.cs | 39 ++++++---- 43 files changed, 677 insertions(+), 157 deletions(-) create mode 100644 samples/ConfigSample/ConfigSample.csproj create mode 100644 samples/ConfigSample/ImageUploaded.cs create mode 100644 samples/ConfigSample/Program.cs create mode 100644 samples/ConfigSample/Properties/launchSettings.json create mode 100644 samples/ConfigSample/VehicleDoorOpenedEvent.cs create mode 100644 samples/ConfigSample/VehicleTelemetryEventsConsumer.cs create mode 100644 samples/ConfigSample/VisualsProducerService.cs create mode 100644 samples/ConfigSample/VisualsUploadedConsumer.cs create mode 100644 samples/ConfigSample/appsettings.Development.json create mode 100644 samples/ConfigSample/appsettings.json create mode 100644 src/Tingle.EventBus/Configuration/DefaultEventBusConfigurationProvider.cs create mode 100644 src/Tingle.EventBus/Configuration/IEventBusConfigurationProvider.cs delete mode 100644 src/Tingle.EventBus/DependencyInjection/TransportOptionsConfigureOptions.cs create mode 100644 src/Tingle.EventBus/Transports/EventBusTransportConfigureOptions.cs diff --git a/README.md b/README.md index 0e05a2be..cd5107b9 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ A number fo the documents below are still a work in progress and would be added #### How to ... +* [Use configuration](docs/work-configuration.md) * [Work with Azure IoT Hub](docs/work-with-azure-iot-hub.md) * [Work with Azure Managed Identities](docs/work-with-azure-managed-identities.md) * [Advanced Service Bus options](docs/advanced-service-bus-options.md) @@ -50,6 +51,7 @@ A number fo the documents below are still a work in progress and would be added ## Samples +* [Using IConfiguration to configure the EventBus](./samples/ConfigSample) * [Simple Consumer](./samples/SimpleConsumer) * [Simple Publisher](./samples/SimplePublisher) * [Build a custom event serializer](./samples/CustomEventSerializer) diff --git a/Tingle.EventBus.sln b/Tingle.EventBus.sln index 99a04174..b4f51155 100644 --- a/Tingle.EventBus.sln +++ b/Tingle.EventBus.sln @@ -56,6 +56,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureIotHub", "samples\Azur EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureManagedIdentity", "samples\AzureManagedIdentity\AzureManagedIdentity.csproj", "{A9AA8DC8-F463-4BB2-AD7B-59060C758862}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConfigSample", "samples\ConfigSample\ConfigSample.csproj", "{8E115759-87CC-4F45-9679-A9EBBD59992B}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomEventConfigurator", "samples\CustomEventConfigurator\CustomEventConfigurator.csproj", "{8C0EE13F-701F-45EF-BADF-6B7A22AA6785}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomEventSerializer", "samples\CustomEventSerializer\CustomEventSerializer.csproj", "{2C55FABC-8C94-4104-BE05-42A477D2AD9E}" @@ -156,6 +158,10 @@ Global {A9AA8DC8-F463-4BB2-AD7B-59060C758862}.Debug|Any CPU.Build.0 = Debug|Any CPU {A9AA8DC8-F463-4BB2-AD7B-59060C758862}.Release|Any CPU.ActiveCfg = Release|Any CPU {A9AA8DC8-F463-4BB2-AD7B-59060C758862}.Release|Any CPU.Build.0 = Release|Any CPU + {8E115759-87CC-4F45-9679-A9EBBD59992B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8E115759-87CC-4F45-9679-A9EBBD59992B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8E115759-87CC-4F45-9679-A9EBBD59992B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8E115759-87CC-4F45-9679-A9EBBD59992B}.Release|Any CPU.Build.0 = Release|Any CPU {8C0EE13F-701F-45EF-BADF-6B7A22AA6785}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8C0EE13F-701F-45EF-BADF-6B7A22AA6785}.Debug|Any CPU.Build.0 = Debug|Any CPU {8C0EE13F-701F-45EF-BADF-6B7A22AA6785}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -216,6 +222,7 @@ Global {C369A8E1-F29D-4705-BD38-28C3DE80D8DB} = {62F603F3-FF36-4E36-AC0C-08D1883525BE} {3759B206-BF8D-4E46-9B04-1C19F156D295} = {62F603F3-FF36-4E36-AC0C-08D1883525BE} {A9AA8DC8-F463-4BB2-AD7B-59060C758862} = {62F603F3-FF36-4E36-AC0C-08D1883525BE} + {8E115759-87CC-4F45-9679-A9EBBD59992B} = {62F603F3-FF36-4E36-AC0C-08D1883525BE} {8C0EE13F-701F-45EF-BADF-6B7A22AA6785} = {62F603F3-FF36-4E36-AC0C-08D1883525BE} {2C55FABC-8C94-4104-BE05-42A477D2AD9E} = {62F603F3-FF36-4E36-AC0C-08D1883525BE} {C9293277-90BA-4F1A-BEA7-85CE41103B8D} = {62F603F3-FF36-4E36-AC0C-08D1883525BE} diff --git a/samples/AzureManagedIdentity/AzureManagedIdentity.csproj b/samples/AzureManagedIdentity/AzureManagedIdentity.csproj index b47b184f..d3589d97 100644 --- a/samples/AzureManagedIdentity/AzureManagedIdentity.csproj +++ b/samples/AzureManagedIdentity/AzureManagedIdentity.csproj @@ -7,4 +7,5 @@ + diff --git a/samples/ConfigSample/ConfigSample.csproj b/samples/ConfigSample/ConfigSample.csproj new file mode 100644 index 00000000..cc1b5d4f --- /dev/null +++ b/samples/ConfigSample/ConfigSample.csproj @@ -0,0 +1,16 @@ + + + + fdb14b87-4a29-455c-9912-67a1e0c64081 + + + + + + + + + + + + diff --git a/samples/ConfigSample/ImageUploaded.cs b/samples/ConfigSample/ImageUploaded.cs new file mode 100644 index 00000000..5548ee3a --- /dev/null +++ b/samples/ConfigSample/ImageUploaded.cs @@ -0,0 +1,15 @@ +namespace ConfigSample; + +internal class ImageUploaded +{ + public string? ImageId { get; set; } + public string? Url { get; set; } + public long SizeBytes { get; set; } +} + +internal class VideoUploaded +{ + public string? VideoId { get; set; } + public string? Url { get; set; } + public long SizeBytes { get; set; } +} diff --git a/samples/ConfigSample/Program.cs b/samples/ConfigSample/Program.cs new file mode 100644 index 00000000..071a266f --- /dev/null +++ b/samples/ConfigSample/Program.cs @@ -0,0 +1,31 @@ +using Azure.Identity; +using ConfigSample; +using Tingle.EventBus.Transports.Azure.ServiceBus; + +var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + var configuration = hostContext.Configuration; + + services.AddEventBus(builder => + { + builder.AddConsumer(); + builder.AddConsumer(); + + // Add transports + builder.AddAzureServiceBusTransport(); + builder.AddInMemoryTransport("in-memory-images"); + builder.AddInMemoryTransport("in-memory-videos"); + + // Transport specific configuration + var credential = new DefaultAzureCredential(); + builder.Services.PostConfigure( + name: AzureServiceBusDefaults.Name, + configureOptions: o => ((AzureServiceBusTransportCredentials)o.Credentials).TokenCredential = credential); + }); + + services.AddHostedService(); + }) + .Build(); + +await host.RunAsync(); diff --git a/samples/ConfigSample/Properties/launchSettings.json b/samples/ConfigSample/Properties/launchSettings.json new file mode 100644 index 00000000..544137c3 --- /dev/null +++ b/samples/ConfigSample/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "ConfigSample": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/ConfigSample/VehicleDoorOpenedEvent.cs b/samples/ConfigSample/VehicleDoorOpenedEvent.cs new file mode 100644 index 00000000..82d9fdcb --- /dev/null +++ b/samples/ConfigSample/VehicleDoorOpenedEvent.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace ConfigSample; + +internal class VehicleDoorOpenedEvent +{ + public string? VehicleId { get; set; } + public VehicleDoorKind Kind { get; set; } + public DateTimeOffset? Opened { get; set; } + public DateTimeOffset? Closed { get; set; } +} + +internal class VehicleTelemetryEvent +{ + public string? DeviceId { get; set; } + public DateTimeOffset Timestamp { get; set; } + public string? Action { get; set; } + public VehicleDoorKind? VehicleDoorKind { get; set; } + public VehicleDoorStatus? VehicleDoorStatus { get; set; } + [JsonExtensionData] + public JsonObject? Extras { get; set; } +} + +internal enum VehicleDoorStatus { Unknown, Open, Closed, } +internal enum VehicleDoorKind { FrontLeft, FrontRight, RearLeft, ReadRight, Hood, Trunk, } diff --git a/samples/ConfigSample/VehicleTelemetryEventsConsumer.cs b/samples/ConfigSample/VehicleTelemetryEventsConsumer.cs new file mode 100644 index 00000000..81e0b88c --- /dev/null +++ b/samples/ConfigSample/VehicleTelemetryEventsConsumer.cs @@ -0,0 +1,42 @@ +namespace ConfigSample; + +internal class VehicleTelemetryEventsConsumer : IEventConsumer +{ + private readonly ILogger logger; + + public VehicleTelemetryEventsConsumer(ILogger logger) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task ConsumeAsync(EventContext context, CancellationToken cancellationToken) + { + var telemetry = context.Event; + + var status = telemetry.VehicleDoorStatus; + if (status is not VehicleDoorStatus.Open and not VehicleDoorStatus.Closed) + { + logger.LogWarning("Vehicle Door status '{VehicleDoorStatus}' is not yet supported", status); + return; + } + + var kind = telemetry.VehicleDoorKind; + if (kind is null) + { + logger.LogWarning("Vehicle Door kind '{VehicleDoorKind}' cannot be null", kind); + return; + } + + var timestamp = telemetry.Timestamp; + var updateEvt = new VehicleDoorOpenedEvent + { + VehicleId = telemetry.DeviceId, + Kind = kind.Value, + Closed = status is VehicleDoorStatus.Closed ? timestamp : null, + Opened = status is VehicleDoorStatus.Open ? timestamp : null, + }; + + // the VehicleDoorOpenedEvent on a broadcast bus would notify all subscribers + await context.PublishAsync(updateEvt, cancellationToken: cancellationToken); + } +} diff --git a/samples/ConfigSample/VisualsProducerService.cs b/samples/ConfigSample/VisualsProducerService.cs new file mode 100644 index 00000000..4c708d98 --- /dev/null +++ b/samples/ConfigSample/VisualsProducerService.cs @@ -0,0 +1,42 @@ +namespace ConfigSample; + +internal class VisualsProducerService : BackgroundService +{ + private readonly IEventPublisher publisher; + private readonly ILogger logger; + + public VisualsProducerService(IEventPublisher publisher, ILogger logger) + { + this.publisher = publisher ?? throw new ArgumentNullException(nameof(publisher)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); // delays a little so that the logs are better visible in a better order (only ended for sample) + + logger.LogInformation("Starting production ..."); + + var delay = TimeSpan.FromSeconds(20); + var times = 10; + + var rnd = new Random(DateTimeOffset.UtcNow.Millisecond); + + for (var i = 0; i < times; i++) + { + var id = Convert.ToUInt32(rnd.Next()).ToString(); + var size = Convert.ToUInt32(rnd.Next()); + var image = (i % 2) == 0; + var url = $"https://localhost:8080/{(image ? "images" : "videos")}/{id}.{(image ? "png" : "flv")}"; + + _ = image + ? await DoPublishAsync(new VideoUploaded { VideoId = id, SizeBytes = size, Url = url, }, stoppingToken) + : await DoPublishAsync(new ImageUploaded { ImageId = id, SizeBytes = size, Url = url, }, stoppingToken); + + await Task.Delay(delay, stoppingToken); + } + } + + private async Task DoPublishAsync(T @event, CancellationToken cancellationToken) where T : class + => await publisher.PublishAsync(@event, cancellationToken: cancellationToken); +} diff --git a/samples/ConfigSample/VisualsUploadedConsumer.cs b/samples/ConfigSample/VisualsUploadedConsumer.cs new file mode 100644 index 00000000..3f8a31fd --- /dev/null +++ b/samples/ConfigSample/VisualsUploadedConsumer.cs @@ -0,0 +1,31 @@ +namespace ConfigSample; + +internal class VisualsUploadedConsumer : IEventConsumer, IEventConsumer +{ + private static readonly TimeSpan SimulationDuration = TimeSpan.FromSeconds(1.3f); + + private readonly ILogger logger; + + public VisualsUploadedConsumer(ILogger logger) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task ConsumeAsync(EventContext context, CancellationToken cancellationToken) + { + var id = context.Event.ImageId; + var thumbnailUrl = $"https://localhost:8080/thumbnails/{id}.jpg"; + + await Task.Delay(SimulationDuration, cancellationToken); + logger.LogInformation("Generated thumbnail from image '{ImageId}' at '{ThumbnailUrl}'.", id, thumbnailUrl); + } + + public async Task ConsumeAsync(EventContext context, CancellationToken cancellationToken = default) + { + var id = context.Event.VideoId; + var thumbnailUrl = $"https://localhost:8080/thumbnails/{id}.jpg"; + + await Task.Delay(SimulationDuration, cancellationToken); + logger.LogInformation("Generated thumbnail from video '{VideoId}' at '{ThumbnailUrl}'.", id, thumbnailUrl); + } +} diff --git a/samples/ConfigSample/appsettings.Development.json b/samples/ConfigSample/appsettings.Development.json new file mode 100644 index 00000000..d4ffd6f7 --- /dev/null +++ b/samples/ConfigSample/appsettings.Development.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Information", + "System": "Information" + }, + "Console": { + "FormatterName": "simple", + "FormatterOptions": { + "SingleLine": true, + "TimestampFormat": "HH:mm:ss " + } + } + } +} diff --git a/samples/ConfigSample/appsettings.json b/samples/ConfigSample/appsettings.json new file mode 100644 index 00000000..bbccc5b0 --- /dev/null +++ b/samples/ConfigSample/appsettings.json @@ -0,0 +1,46 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + + "EventBus": { + "WaitTransportStarted": false, + "Naming": { + "Convention": "DotCase", + "UseFullTypeNames": false + }, + "DefaultTransportName": "azure-service-bus", + "Transports": { // keyed by name of the transport + "azure-service-bus": { + "DefaultEntityKind": "Queue", // required if using the basic SKU (does not support topics) + "FullyQualifiedNamespace": "{your_namespace}.servicebus.windows.net" + }, + "in-memory-images": { + "DefaultEventIdFormat": "DoubleLongHex" + }, + "in-memory-videos": { + "DefaultEntityKind": "Queue" + } + }, + "Events": { + "ConfigSample.ImageUploaded": { // FullName of the type + "TransportName": "in-memory-images" + }, + "ConfigSample.VideoUploaded": { // FullName of the type + "TransportName": "in-memory-videos", + "Consumers": { + "ConfigSample.VisualsUploadedConsumer": { // FullName of the type + "UnhandledErrorBehaviour": "Discard", + "Metadata": { + "generation": "2022" + } + } + } + } + } + } +} diff --git a/src/Tingle.EventBus.Transports.Amazon.Abstractions/AmazonTransportConfigureOptions.cs b/src/Tingle.EventBus.Transports.Amazon.Abstractions/AmazonTransportConfigureOptions.cs index 8597e2e6..67f6e375 100644 --- a/src/Tingle.EventBus.Transports.Amazon.Abstractions/AmazonTransportConfigureOptions.cs +++ b/src/Tingle.EventBus.Transports.Amazon.Abstractions/AmazonTransportConfigureOptions.cs @@ -1,17 +1,26 @@ using Amazon; using Amazon.Runtime; -using Microsoft.Extensions.Options; +using Tingle.EventBus.Configuration; namespace Microsoft.Extensions.DependencyInjection; /// /// A class to finish the configuration of instances of derivatives. /// -public abstract class AmazonTransportConfigureOptions : IPostConfigureOptions where TOptions : AmazonTransportOptions +public abstract class AmazonTransportConfigureOptions : EventBusTransportConfigureOptions where TOptions : AmazonTransportOptions { + /// + /// Initializes a new given the configuration + /// provided by the . + /// + /// An instance.\ + public AmazonTransportConfigureOptions(IEventBusConfigurationProvider configurationProvider) : base(configurationProvider) { } + /// - public virtual void PostConfigure(string? name, TOptions options) + public override void PostConfigure(string? name, TOptions options) { + base.PostConfigure(name, options); + // Ensure the region is provided if (string.IsNullOrWhiteSpace(options.RegionName) && options.Region == null) { diff --git a/src/Tingle.EventBus.Transports.Amazon.Kinesis/AmazonKinesisConfigureOptions.cs b/src/Tingle.EventBus.Transports.Amazon.Kinesis/AmazonKinesisConfigureOptions.cs index 31e46b28..415364f4 100644 --- a/src/Tingle.EventBus.Transports.Amazon.Kinesis/AmazonKinesisConfigureOptions.cs +++ b/src/Tingle.EventBus.Transports.Amazon.Kinesis/AmazonKinesisConfigureOptions.cs @@ -11,15 +11,21 @@ internal class AmazonKinesisConfigureOptions : AmazonTransportConfigureOptions busOptionsAccessor) + /// + /// Initializes a new given the configuration + /// provided by the . + /// + /// An instance.\ + /// An for bus configuration.\ + public AmazonKinesisConfigureOptions(IEventBusConfigurationProvider configurationProvider, IOptions busOptionsAccessor) + : base(configurationProvider) { busOptions = busOptionsAccessor?.Value ?? throw new ArgumentNullException(nameof(busOptionsAccessor)); } + /// public override void PostConfigure(string? name, AmazonKinesisTransportOptions options) { - if (name is null) throw new ArgumentNullException(nameof(name)); - base.PostConfigure(name, options); // Ensure we have options for Kinesis and the region is set @@ -33,7 +39,7 @@ public override void PostConfigure(string? name, AmazonKinesisTransportOptions o } // Ensure the entity names are not longer than the limits - var registrations = busOptions.GetRegistrations(name); + var registrations = busOptions.GetRegistrations(name!); foreach (var reg in registrations) { // Set the IdFormat diff --git a/src/Tingle.EventBus.Transports.Amazon.Kinesis/AmazonKinesisEventBusBuilderExtensions.cs b/src/Tingle.EventBus.Transports.Amazon.Kinesis/AmazonKinesisEventBusBuilderExtensions.cs index 6bb8f216..35a386f1 100644 --- a/src/Tingle.EventBus.Transports.Amazon.Kinesis/AmazonKinesisEventBusBuilderExtensions.cs +++ b/src/Tingle.EventBus.Transports.Amazon.Kinesis/AmazonKinesisEventBusBuilderExtensions.cs @@ -20,10 +20,5 @@ public static EventBusBuilder AddAmazonKinesisTransport(this EventBusBuilder bui /// An to configure the transport options. /// public static EventBusBuilder AddAmazonKinesisTransport(this EventBusBuilder builder, string name, Action? configure = null) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); - - builder.Services.ConfigureOptions(); - return builder.AddTransport(name, configure); - } + => builder.AddTransport(name, configure); } diff --git a/src/Tingle.EventBus.Transports.Amazon.Sqs/AmazonSqsConfigureOptions.cs b/src/Tingle.EventBus.Transports.Amazon.Sqs/AmazonSqsConfigureOptions.cs index 24014172..5c350805 100644 --- a/src/Tingle.EventBus.Transports.Amazon.Sqs/AmazonSqsConfigureOptions.cs +++ b/src/Tingle.EventBus.Transports.Amazon.Sqs/AmazonSqsConfigureOptions.cs @@ -12,15 +12,21 @@ internal class AmazonSqsConfigureOptions : AmazonTransportConfigureOptions busOptionsAccessor) + /// + /// Initializes a new given the configuration + /// provided by the . + /// + /// An instance.\ + /// An for bus configuration.\ + public AmazonSqsConfigureOptions(IEventBusConfigurationProvider configurationProvider, IOptions busOptionsAccessor) + : base(configurationProvider) { busOptions = busOptionsAccessor?.Value ?? throw new ArgumentNullException(nameof(busOptionsAccessor)); } + /// public override void PostConfigure(string? name, AmazonSqsTransportOptions options) { - if (name is null) throw new ArgumentNullException(nameof(name)); - base.PostConfigure(name, options); // Ensure we have options for SQS and SNS and their regions are set @@ -30,7 +36,7 @@ public override void PostConfigure(string? name, AmazonSqsTransportOptions optio options.SnsConfig.RegionEndpoint ??= options.Region; // Ensure the entity names are not longer than the limits - var registrations = busOptions.GetRegistrations(name); + var registrations = busOptions.GetRegistrations(name!); foreach (var reg in registrations) { // Set the IdFormat diff --git a/src/Tingle.EventBus.Transports.Amazon.Sqs/AmazonSqsEventBusBuilderExtensions.cs b/src/Tingle.EventBus.Transports.Amazon.Sqs/AmazonSqsEventBusBuilderExtensions.cs index 02665976..d59bda6b 100644 --- a/src/Tingle.EventBus.Transports.Amazon.Sqs/AmazonSqsEventBusBuilderExtensions.cs +++ b/src/Tingle.EventBus.Transports.Amazon.Sqs/AmazonSqsEventBusBuilderExtensions.cs @@ -20,10 +20,5 @@ public static EventBusBuilder AddAmazonSqsTransport(this EventBusBuilder builder /// An to configure the transport options. /// public static EventBusBuilder AddAmazonSqsTransport(this EventBusBuilder builder, string name, Action? configure = null) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); - - builder.Services.ConfigureOptions(); - return builder.AddTransport(name, configure); - } + => builder.AddTransport(name, configure); } diff --git a/src/Tingle.EventBus.Transports.Azure.Abstractions/AzureTransportConfigureOptions.cs b/src/Tingle.EventBus.Transports.Azure.Abstractions/AzureTransportConfigureOptions.cs index 83b4d285..11794be3 100644 --- a/src/Tingle.EventBus.Transports.Azure.Abstractions/AzureTransportConfigureOptions.cs +++ b/src/Tingle.EventBus.Transports.Azure.Abstractions/AzureTransportConfigureOptions.cs @@ -1,23 +1,28 @@ using Microsoft.Extensions.Options; +using Tingle.EventBus.Configuration; namespace Microsoft.Extensions.DependencyInjection; /// /// A class to finish the configuration of instances of derivatives. /// -public abstract class AzureTransportConfigureOptions : IPostConfigureOptions, IValidateOptions +public abstract class AzureTransportConfigureOptions : EventBusTransportConfigureOptions where TCredential : AzureTransportCredentials where TOptions : AzureTransportOptions { - /// - public virtual void PostConfigure(string? name, TOptions options) - { - // intentionally left bank for future use - } + /// + /// Initializes a new given the configuration + /// provided by the . + /// + /// An instance.\ + public AzureTransportConfigureOptions(IEventBusConfigurationProvider configurationProvider) : base(configurationProvider) { } /// - public virtual ValidateOptionsResult Validate(string? name, TOptions options) + public override ValidateOptionsResult Validate(string? name, TOptions options) { + var result = base.Validate(name, options); + if (!result.Succeeded) return result; + // We should either have a token credential or a connection string if (options.Credentials == default || options.Credentials.CurrentValue is null) { diff --git a/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureBlobStorageCredentials.cs b/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureBlobStorageCredentials.cs index 28d1c456..7aafbe63 100644 --- a/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureBlobStorageCredentials.cs +++ b/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureBlobStorageCredentials.cs @@ -11,5 +11,5 @@ public class AzureBlobStorageCredentials : AzureTransportCredentials /// A referencing the blob service. /// This is likely to be similar to "https://{account_name}.blob.core.windows.net". /// - public Uri? BlobServiceUrl { get; set; } + public Uri? ServiceUrl { get; set; } } diff --git a/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureEventHubsConfigureOptions.cs b/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureEventHubsConfigureOptions.cs index 5df80658..7d7175b8 100644 --- a/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureEventHubsConfigureOptions.cs +++ b/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureEventHubsConfigureOptions.cs @@ -1,4 +1,6 @@ -using Microsoft.Extensions.Options; +using Azure.Messaging.EventHubs; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; using Tingle.EventBus.Configuration; namespace Microsoft.Extensions.DependencyInjection; @@ -6,20 +8,62 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// A class to finish the configuration of instances of . /// -internal class AzureEventHubsConfigureOptions : AzureTransportConfigureOptions +internal class AzureEventHubsConfigureOptions : AzureTransportConfigureOptions, + IConfigureNamedOptions { private readonly EventBusOptions busOptions; - public AzureEventHubsConfigureOptions(IOptions busOptionsAccessor) + /// + /// Initializes a new given the configuration + /// provided by the . + /// + /// An instance.\ + /// An for bus configuration.\ + public AzureEventHubsConfigureOptions(IEventBusConfigurationProvider configurationProvider, IOptions busOptionsAccessor) + : base(configurationProvider) { busOptions = busOptionsAccessor?.Value ?? throw new ArgumentNullException(nameof(busOptionsAccessor)); } /// - public override void PostConfigure(string? name, AzureEventHubsTransportOptions options) + protected override void Configure(IConfiguration configuration, AzureEventHubsTransportOptions options) { - if (name is null) throw new ArgumentNullException(nameof(name)); + base.Configure(configuration, options); + + if (options.Credentials == default || options.Credentials.CurrentValue is null) + { + var fullyQualifiedNamespace = configuration.GetValue(nameof(AzureEventHubsTransportCredentials.FullyQualifiedNamespace)) + ?? configuration.GetValue("Namespace"); + if (fullyQualifiedNamespace is not null) + { + options.Credentials = new AzureEventHubsTransportCredentials { FullyQualifiedNamespace = fullyQualifiedNamespace, }; + } + else + { + var connectionString = configuration.GetValue("ConnectionString"); + if (connectionString is not null) options.Credentials = connectionString; + } + } + if (options.BlobStorageCredentials == default || options.BlobStorageCredentials.CurrentValue is null) + { + var serviceUrl = configuration.GetValue("BlobStorageServiceUrl") + ?? configuration.GetValue("BlobStorageEndpoint"); + if (serviceUrl is not null) + { + options.BlobStorageCredentials = new AzureBlobStorageCredentials { ServiceUrl = serviceUrl, }; + } + else + { + var connectionString = configuration.GetValue("BlobStorageConnectionString"); + if (connectionString is not null) options.BlobStorageCredentials = connectionString; + } + } + } + + /// + public override void PostConfigure(string? name, AzureEventHubsTransportOptions options) + { base.PostConfigure(name, options); // ensure we have a FullyQualifiedNamespace when using AzureEventHubsTransportCredentials @@ -32,7 +76,7 @@ public override void PostConfigure(string? name, AzureEventHubsTransportOptions options.CheckpointInterval = Math.Max(options.CheckpointInterval, 1); // If there are consumers for this transport, we must check azure blob storage - var registrations = busOptions.GetRegistrations(name); + var registrations = busOptions.GetRegistrations(name!); if (registrations.Any(r => r.Consumers.Count > 0)) { // ensure the connection string for blob storage or token credential is provided @@ -42,9 +86,9 @@ public override void PostConfigure(string? name, AzureEventHubsTransportOptions } // ensure we have a BlobServiceUrl when using AzureBlobStorageCredential - if (options.BlobStorageCredentials.CurrentValue is AzureBlobStorageCredentials absc && absc.BlobServiceUrl is null) + if (options.BlobStorageCredentials.CurrentValue is AzureBlobStorageCredentials absc && absc.ServiceUrl is null) { - throw new InvalidOperationException($"'{nameof(AzureBlobStorageCredentials.BlobServiceUrl)}' must be provided when using '{nameof(AzureBlobStorageCredentials)}'."); + throw new InvalidOperationException($"'{nameof(AzureBlobStorageCredentials.ServiceUrl)}' must be provided when using '{nameof(AzureBlobStorageCredentials)}'."); } // ensure the blob container name is provided diff --git a/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureEventHubsEventBusBuilderExtensions.cs b/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureEventHubsEventBusBuilderExtensions.cs index 833c2dce..17ff95e8 100644 --- a/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureEventHubsEventBusBuilderExtensions.cs +++ b/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureEventHubsEventBusBuilderExtensions.cs @@ -20,10 +20,5 @@ public static EventBusBuilder AddAzureEventHubsTransport(this EventBusBuilder bu /// An to configure the transport options. /// public static EventBusBuilder AddAzureEventHubsTransport(this EventBusBuilder builder, string name, Action? configure = null) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); - - builder.Services.ConfigureOptions(); - return builder.AddTransport(name, configure); - } + => builder.AddTransport(name, configure); } diff --git a/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureEventHubsTransport.cs b/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureEventHubsTransport.cs index e70334b7..dd35e8b6 100644 --- a/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureEventHubsTransport.cs +++ b/src/Tingle.EventBus.Transports.Azure.EventHubs/AzureEventHubsTransport.cs @@ -287,7 +287,7 @@ async Task creator(string key, CancellationToken ct) // blobContainerUri has the format "https://{account_name}.blob.core.windows.net/{container_name}" which can be made using "{BlobServiceUri}/{container_name}". var cred_bs = Options.BlobStorageCredentials.CurrentValue; var blobContainerClient = cred_bs is AzureBlobStorageCredentials abstc - ? new BlobContainerClient(blobContainerUri: new Uri($"{abstc.BlobServiceUrl}/{Options.BlobContainerName}"), + ? new BlobContainerClient(blobContainerUri: new Uri($"{abstc.ServiceUrl}/{Options.BlobContainerName}"), credential: abstc.TokenCredential) : new BlobContainerClient(connectionString: (string)cred_bs, blobContainerName: Options.BlobContainerName); diff --git a/src/Tingle.EventBus.Transports.Azure.QueueStorage/AzureQueueStorageConfigureOptions.cs b/src/Tingle.EventBus.Transports.Azure.QueueStorage/AzureQueueStorageConfigureOptions.cs index 6279a1cf..95452e0e 100644 --- a/src/Tingle.EventBus.Transports.Azure.QueueStorage/AzureQueueStorageConfigureOptions.cs +++ b/src/Tingle.EventBus.Transports.Azure.QueueStorage/AzureQueueStorageConfigureOptions.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; using Tingle.EventBus.Configuration; namespace Microsoft.Extensions.DependencyInjection; @@ -6,20 +7,47 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// A class to finish the configuration of instances of . /// -internal class AzureQueueStorageConfigureOptions : AzureTransportConfigureOptions +internal class AzureQueueStorageConfigureOptions : AzureTransportConfigureOptions, + IConfigureNamedOptions { private readonly EventBusOptions busOptions; - public AzureQueueStorageConfigureOptions(IOptions busOptionsAccessor) + /// + /// Initializes a new given the configuration + /// provided by the . + /// + /// An instance.\ + /// An for bus configuration.\ + public AzureQueueStorageConfigureOptions(IEventBusConfigurationProvider configurationProvider, IOptions busOptionsAccessor) + : base(configurationProvider) { busOptions = busOptionsAccessor?.Value ?? throw new ArgumentNullException(nameof(busOptionsAccessor)); } /// - public override void PostConfigure(string? name, AzureQueueStorageTransportOptions options) + protected override void Configure(IConfiguration configuration, AzureQueueStorageTransportOptions options) { - if (name is null) throw new ArgumentNullException(nameof(name)); + base.Configure(configuration, options); + + if (options.Credentials == default || options.Credentials.CurrentValue is null) + { + var serviceUrl = configuration.GetValue(nameof(AzureQueueStorageTransportCredentials.ServiceUrl)) + ?? configuration.GetValue("Endpoint"); + if (serviceUrl is not null) + { + options.Credentials = new AzureQueueStorageTransportCredentials { ServiceUrl = serviceUrl }; + } + else + { + var connectionString = configuration.GetValue("ConnectionString"); + if (connectionString is not null) options.Credentials = connectionString; + } + } + } + /// + public override void PostConfigure(string? name, AzureQueueStorageTransportOptions options) + { base.PostConfigure(name, options); // ensure we have a ServiceUrl when using AzureQueueStorageTransportCredentials @@ -29,7 +57,7 @@ public override void PostConfigure(string? name, AzureQueueStorageTransportOptio } // Ensure there's only one consumer per event - var registrations = busOptions.GetRegistrations(name); + var registrations = busOptions.GetRegistrations(name!); var multiple = registrations.FirstOrDefault(r => r.Consumers.Count > 1); if (multiple is not null) { diff --git a/src/Tingle.EventBus.Transports.Azure.QueueStorage/AzureQueueStorageEventBusBuilderExtensions.cs b/src/Tingle.EventBus.Transports.Azure.QueueStorage/AzureQueueStorageEventBusBuilderExtensions.cs index 7e8b41dc..968f8d19 100644 --- a/src/Tingle.EventBus.Transports.Azure.QueueStorage/AzureQueueStorageEventBusBuilderExtensions.cs +++ b/src/Tingle.EventBus.Transports.Azure.QueueStorage/AzureQueueStorageEventBusBuilderExtensions.cs @@ -20,10 +20,5 @@ public static EventBusBuilder AddAzureQueueStorageTransport(this EventBusBuilder /// An to configure the transport options. /// public static EventBusBuilder AddAzureQueueStorageTransport(this EventBusBuilder builder, string name, Action? configure = null) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); - - builder.Services.ConfigureOptions(); - return builder.AddTransport(name, configure); - } + => builder.AddTransport(name, configure); } diff --git a/src/Tingle.EventBus.Transports.Azure.ServiceBus/AzureServiceBusConfigureOptions.cs b/src/Tingle.EventBus.Transports.Azure.ServiceBus/AzureServiceBusConfigureOptions.cs index b1bc0bb0..497fd767 100644 --- a/src/Tingle.EventBus.Transports.Azure.ServiceBus/AzureServiceBusConfigureOptions.cs +++ b/src/Tingle.EventBus.Transports.Azure.ServiceBus/AzureServiceBusConfigureOptions.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; using Tingle.EventBus.Configuration; namespace Microsoft.Extensions.DependencyInjection; @@ -6,20 +7,47 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// A class to finish the configuration of instances of . /// -internal class AzureServiceBusConfigureOptions : AzureTransportConfigureOptions +internal class AzureServiceBusConfigureOptions : AzureTransportConfigureOptions, + IConfigureNamedOptions { private readonly EventBusOptions busOptions; - public AzureServiceBusConfigureOptions(IOptions busOptionsAccessor) + /// + /// Initializes a new given the configuration + /// provided by the . + /// + /// An instance.\ + /// An for bus configuration.\ + public AzureServiceBusConfigureOptions(IEventBusConfigurationProvider configurationProvider, IOptions busOptionsAccessor) + : base(configurationProvider) { busOptions = busOptionsAccessor?.Value ?? throw new ArgumentNullException(nameof(busOptionsAccessor)); } /// - public override void PostConfigure(string? name, AzureServiceBusTransportOptions options) + protected override void Configure(IConfiguration configuration, AzureServiceBusTransportOptions options) { - if (name is null) throw new ArgumentNullException(nameof(name)); + base.Configure(configuration, options); + + if (options.Credentials == default || options.Credentials.CurrentValue is null) + { + var fullyQualifiedNamespace = configuration.GetValue(nameof(AzureServiceBusTransportCredentials.FullyQualifiedNamespace)) + ?? configuration.GetValue("Namespace"); + if (fullyQualifiedNamespace is not null) + { + options.Credentials = new AzureServiceBusTransportCredentials { FullyQualifiedNamespace = fullyQualifiedNamespace }; + } + else + { + var connectionString = configuration.GetValue("ConnectionString"); + if (connectionString is not null) options.Credentials = connectionString; + } + } + } + /// + public override void PostConfigure(string? name, AzureServiceBusTransportOptions options) + { base.PostConfigure(name, options); // ensure we have a FullyQualifiedNamespace when using AzureServiceBusTransportCredentials @@ -30,7 +58,7 @@ public override void PostConfigure(string? name, AzureServiceBusTransportOptions // Ensure the entity names are not longer than the limits // See https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-quotas#messaging-quotas - var registrations = busOptions.GetRegistrations(name); + var registrations = busOptions.GetRegistrations(name!); foreach (var reg in registrations) { // Set the IdFormat diff --git a/src/Tingle.EventBus.Transports.Azure.ServiceBus/AzureServiceBusEventBusBuilderExtensions.cs b/src/Tingle.EventBus.Transports.Azure.ServiceBus/AzureServiceBusEventBusBuilderExtensions.cs index 4cd3aaba..d94ff295 100644 --- a/src/Tingle.EventBus.Transports.Azure.ServiceBus/AzureServiceBusEventBusBuilderExtensions.cs +++ b/src/Tingle.EventBus.Transports.Azure.ServiceBus/AzureServiceBusEventBusBuilderExtensions.cs @@ -20,10 +20,5 @@ public static EventBusBuilder AddAzureServiceBusTransport(this EventBusBuilder b /// An to configure the transport options. /// public static EventBusBuilder AddAzureServiceBusTransport(this EventBusBuilder builder, string name, Action? configure = null) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); - - builder.Services.ConfigureOptions(); - return builder.AddTransport(name, configure); - } + => builder.AddTransport(name, configure); } diff --git a/src/Tingle.EventBus.Transports.InMemory/InMemoryEventBusBuilderExtensions.cs b/src/Tingle.EventBus.Transports.InMemory/InMemoryEventBusBuilderExtensions.cs index d118bdcb..9ffe0bff 100644 --- a/src/Tingle.EventBus.Transports.InMemory/InMemoryEventBusBuilderExtensions.cs +++ b/src/Tingle.EventBus.Transports.InMemory/InMemoryEventBusBuilderExtensions.cs @@ -24,9 +24,8 @@ public static EventBusBuilder AddInMemoryTransport(this EventBusBuilder builder, { if (builder == null) throw new ArgumentNullException(nameof(builder)); - builder.Services.ConfigureOptions(); builder.Services.AddSingleton(); - return builder.AddTransport(name, configure); + return builder.AddTransport(name, configure); } /// diff --git a/src/Tingle.EventBus.Transports.InMemory/InMemoryTransportConfigureOptions.cs b/src/Tingle.EventBus.Transports.InMemory/InMemoryTransportConfigureOptions.cs index b344d772..d8601324 100644 --- a/src/Tingle.EventBus.Transports.InMemory/InMemoryTransportConfigureOptions.cs +++ b/src/Tingle.EventBus.Transports.InMemory/InMemoryTransportConfigureOptions.cs @@ -6,17 +6,26 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// A class to finish the configuration of instances of . /// -internal class InMemoryTransportConfigureOptions : IPostConfigureOptions +internal class InMemoryTransportConfigureOptions : EventBusTransportConfigureOptions { private readonly EventBusOptions busOptions; - public InMemoryTransportConfigureOptions(IOptions busOptionsAccessor) + /// + /// Initializes a new given the configuration + /// provided by the . + /// + /// An instance.\ + /// An for bus configuration.\ + public InMemoryTransportConfigureOptions(IEventBusConfigurationProvider configurationProvider, IOptions busOptionsAccessor) + : base(configurationProvider) { busOptions = busOptionsAccessor?.Value ?? throw new ArgumentNullException(nameof(busOptionsAccessor)); } - public void PostConfigure(string? name, InMemoryTransportOptions options) + /// + public override void PostConfigure(string? name, InMemoryTransportOptions options) { + base.PostConfigure(name, options); if (name is null) throw new ArgumentNullException(nameof(name)); var registrations = busOptions.GetRegistrations(name); diff --git a/src/Tingle.EventBus.Transports.Kafka/KafkaConfigureOptions.cs b/src/Tingle.EventBus.Transports.Kafka/KafkaConfigureOptions.cs index 82198204..93f71492 100644 --- a/src/Tingle.EventBus.Transports.Kafka/KafkaConfigureOptions.cs +++ b/src/Tingle.EventBus.Transports.Kafka/KafkaConfigureOptions.cs @@ -7,18 +7,26 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// A class to finish the configuration of instances of . /// -internal class KafkaConfigureOptions : IPostConfigureOptions +internal class KafkaConfigureOptions : EventBusTransportConfigureOptions { private readonly EventBusOptions busOptions; - public KafkaConfigureOptions(IOptions busOptionsAccessor) + /// + /// Initializes a new given the configuration + /// provided by the . + /// + /// An instance. + /// An for bus configuration.\ + public KafkaConfigureOptions(IEventBusConfigurationProvider configurationProvider, IOptions busOptionsAccessor) + : base(configurationProvider) { busOptions = busOptionsAccessor?.Value ?? throw new ArgumentNullException(nameof(busOptionsAccessor)); } - public void PostConfigure(string? name, KafkaTransportOptions options) + /// + public override void PostConfigure(string? name, KafkaTransportOptions options) { - if (name is null) throw new ArgumentNullException(nameof(name)); + base.PostConfigure(name, options); if (options.BootstrapServers == null && options.AdminConfig == null) { @@ -45,7 +53,7 @@ public void PostConfigure(string? name, KafkaTransportOptions options) options.CheckpointInterval = Math.Max(options.CheckpointInterval, 1); // ensure there's only one consumer per event - var registrations = busOptions.GetRegistrations(name); + var registrations = busOptions.GetRegistrations(name!); var multiple = registrations.FirstOrDefault(r => r.Consumers.Count > 1); if (multiple is not null) { diff --git a/src/Tingle.EventBus.Transports.Kafka/KafkaEventBusBuilderExtensions.cs b/src/Tingle.EventBus.Transports.Kafka/KafkaEventBusBuilderExtensions.cs index 8a214fb0..1a180d0a 100644 --- a/src/Tingle.EventBus.Transports.Kafka/KafkaEventBusBuilderExtensions.cs +++ b/src/Tingle.EventBus.Transports.Kafka/KafkaEventBusBuilderExtensions.cs @@ -20,10 +20,5 @@ public static EventBusBuilder AddKafkaTransport(this EventBusBuilder builder, Ac /// An to configure the transport options. /// public static EventBusBuilder AddKafkaTransport(this EventBusBuilder builder, string name, Action? configure = null) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); - - builder.Services.ConfigureOptions(); - return builder.AddTransport(name, configure); - } + => builder.AddTransport(name, configure); } diff --git a/src/Tingle.EventBus.Transports.RabbitMQ/RabbitMqConfigureOptions.cs b/src/Tingle.EventBus.Transports.RabbitMQ/RabbitMqConfigureOptions.cs index 10aab357..cfc85ec7 100644 --- a/src/Tingle.EventBus.Transports.RabbitMQ/RabbitMqConfigureOptions.cs +++ b/src/Tingle.EventBus.Transports.RabbitMQ/RabbitMqConfigureOptions.cs @@ -7,21 +7,29 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// A class to finish the configuration of instances of . /// -internal class RabbitMqConfigureOptions : IPostConfigureOptions +internal class RabbitMqConfigureOptions : EventBusTransportConfigureOptions { private readonly EventBusOptions busOptions; - public RabbitMqConfigureOptions(IOptions busOptionsAccessor) + /// + /// Initializes a new given the configuration + /// provided by the . + /// + /// An instance. + /// An for bus configuration.\ + public RabbitMqConfigureOptions(IEventBusConfigurationProvider configurationProvider, IOptions busOptionsAccessor) + : base(configurationProvider) { busOptions = busOptionsAccessor?.Value ?? throw new ArgumentNullException(nameof(busOptionsAccessor)); } - public void PostConfigure(string? name, RabbitMqTransportOptions options) + /// + public override void PostConfigure(string? name, RabbitMqTransportOptions options) { - if (name is null) throw new ArgumentNullException(nameof(name)); + base.PostConfigure(name, options); // If there are consumers for this transport, confirm the right Bus options - var registrations = busOptions.GetRegistrations(name); + var registrations = busOptions.GetRegistrations(name!); if (registrations.Any(r => r.Consumers.Count > 0)) { // we need full type names diff --git a/src/Tingle.EventBus.Transports.RabbitMQ/RabbitMqEventBusBuilderExtensions.cs b/src/Tingle.EventBus.Transports.RabbitMQ/RabbitMqEventBusBuilderExtensions.cs index cd5823bd..bed2e16b 100644 --- a/src/Tingle.EventBus.Transports.RabbitMQ/RabbitMqEventBusBuilderExtensions.cs +++ b/src/Tingle.EventBus.Transports.RabbitMQ/RabbitMqEventBusBuilderExtensions.cs @@ -20,10 +20,5 @@ public static EventBusBuilder AddRabbitMqTransport(this EventBusBuilder builder, /// An to configure the transport options. /// public static EventBusBuilder AddRabbitMqTransport(this EventBusBuilder builder, string name, Action? configure = null) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); - - builder.Services.ConfigureOptions(); - return builder.AddTransport(name, configure); - } + => builder.AddTransport(name, configure); } diff --git a/src/Tingle.EventBus/Configuration/DefaultEventBusConfigurationProvider.cs b/src/Tingle.EventBus/Configuration/DefaultEventBusConfigurationProvider.cs new file mode 100644 index 00000000..f9c3d7c4 --- /dev/null +++ b/src/Tingle.EventBus/Configuration/DefaultEventBusConfigurationProvider.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Configuration; + +namespace Tingle.EventBus.Configuration; + +/// +/// Default implementation of . +/// +internal class DefaultEventBusConfigurationProvider : IEventBusConfigurationProvider +{ + private readonly IConfiguration configuration; + private const string EventBusKey = "EventBus"; + + public DefaultEventBusConfigurationProvider(IConfiguration configuration) + { + this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + } + + /// + public IConfiguration Configuration => configuration.GetSection(EventBusKey); +} \ No newline at end of file diff --git a/src/Tingle.EventBus/Configuration/DefaultEventConfigurator.cs b/src/Tingle.EventBus/Configuration/DefaultEventConfigurator.cs index c0e3d703..cbd5f9cd 100644 --- a/src/Tingle.EventBus/Configuration/DefaultEventConfigurator.cs +++ b/src/Tingle.EventBus/Configuration/DefaultEventConfigurator.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Tingle.EventBus.Serialization; @@ -10,10 +11,12 @@ namespace Tingle.EventBus.Configuration; internal class DefaultEventConfigurator : IEventConfigurator { private readonly IHostEnvironment environment; + private readonly IEventBusConfigurationProvider configurationProvider; - public DefaultEventConfigurator(IHostEnvironment environment) + public DefaultEventConfigurator(IHostEnvironment environment, IEventBusConfigurationProvider configurationProvider) { this.environment = environment ?? throw new ArgumentNullException(nameof(environment)); + this.configurationProvider = configurationProvider ?? throw new ArgumentNullException(nameof(configurationProvider)); } /// @@ -22,6 +25,14 @@ public void Configure(EventRegistration registration, EventBusOptions options) if (registration is null) throw new ArgumentNullException(nameof(registration)); if (options is null) throw new ArgumentNullException(nameof(options)); + // bind from IConfiguration + var configuration = configurationProvider.Configuration.GetSection($"Events:{registration.EventType.FullName}"); + configuration.Bind(registration); + foreach (var ecr in registration.Consumers) + { + configuration.GetSection($"Consumers:{ecr.ConsumerType.FullName}").Bind(ecr); + } + // set transport name ConfigureTransportName(registration, options); @@ -36,7 +47,7 @@ public void Configure(EventRegistration registration, EventBusOptions options) ConfigureSerializer(registration); } - internal static void ConfigureTransportName(EventRegistration reg, EventBusOptions options) + internal void ConfigureTransportName(EventRegistration reg, EventBusOptions options) { // If the event transport name has not been specified, attempt to get from the attribute var type = reg.EventType; @@ -60,7 +71,7 @@ internal static void ConfigureTransportName(EventRegistration reg, EventBusOptio } } - internal static void ConfigureEventName(EventRegistration reg, EventBusNamingOptions options) + internal void ConfigureEventName(EventRegistration reg, EventBusNamingOptions options) { // set the event name, if not set if (string.IsNullOrWhiteSpace(reg.EventName)) @@ -81,7 +92,7 @@ internal static void ConfigureEventName(EventRegistration reg, EventBusNamingOpt } } - internal static void ConfigureEntityKind(EventRegistration reg) + internal void ConfigureEntityKind(EventRegistration reg) { // set the entity kind, if not set and there is an attribute if (reg.EntityKind == null) @@ -135,7 +146,7 @@ internal void ConfigureConsumerNames(EventRegistration reg, EventBusNamingOption } } - internal static void ConfigureSerializer(EventRegistration reg) + internal void ConfigureSerializer(EventRegistration reg) { // If the event serializer has not been specified, attempt to get from the attribute var attrs = reg.EventType.GetCustomAttributes(false); diff --git a/src/Tingle.EventBus/Configuration/IEventBusConfigurationProvider.cs b/src/Tingle.EventBus/Configuration/IEventBusConfigurationProvider.cs new file mode 100644 index 00000000..72b3ccf3 --- /dev/null +++ b/src/Tingle.EventBus/Configuration/IEventBusConfigurationProvider.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.Configuration; + +namespace Tingle.EventBus.Configuration; + +/// +/// Provides an interface for implementing a construct that provides +/// access to EventBus-related configuration sections. +/// +public interface IEventBusConfigurationProvider +{ + /// + /// Gets the where EventBus options are stored. + /// + IConfiguration Configuration { get; } +} diff --git a/src/Tingle.EventBus/DependencyInjection/EventBusBuilder.cs b/src/Tingle.EventBus/DependencyInjection/EventBusBuilder.cs index 3f57e515..8bc2e889 100644 --- a/src/Tingle.EventBus/DependencyInjection/EventBusBuilder.cs +++ b/src/Tingle.EventBus/DependencyInjection/EventBusBuilder.cs @@ -29,6 +29,7 @@ public EventBusBuilder(IServiceCollection services) // Register necessary services Services.AddSingleton(); + Services.AddSingleton(); Services.AddSingleton(); Services.AddSingleton(); Services.AddTransient(); @@ -63,34 +64,40 @@ public EventBusBuilder ConfigureSerialization(ActionAdds a which can be used by the event bus. - /// The type to configure the transport."/>. /// The used to handle this transport. + /// The type to configure the transport."/>. + /// The type to configure the ."/>. /// The name of this transport. /// Used to configure the transport options. - public EventBusBuilder AddTransport(string name, Action? configureOptions) - where TOptions : EventBusTransportOptions, new() + public EventBusBuilder AddTransport(string name, Action? configureOptions) where THandler : EventBusTransport - => AddTransport(name, displayName: null, configureOptions: configureOptions); + where TOptions : EventBusTransportOptions, new() + where TConfigurator : EventBusTransportConfigureOptions + => AddTransport(name, displayName: null, configureOptions: configureOptions); /// Adds a which can be used by the event bus. - /// The type to configure the transport."/>. /// The used to handle this transport. + /// The type to configure the transport."/>. + /// The type to configure the ."/>. /// The name of this transport. /// The display name of this transport. /// Used to configure the transport options. - public EventBusBuilder AddTransport(string name, string? displayName, Action? configureOptions) - where TOptions : EventBusTransportOptions, new() + public EventBusBuilder AddTransport(string name, string? displayName, Action? configureOptions) where THandler : EventBusTransport - => AddTransportHelper(name, displayName, configureOptions); - - private EventBusBuilder AddTransportHelper(string name, string? displayName, Action? configureOptions) where TOptions : EventBusTransportOptions, new() + where TConfigurator : EventBusTransportConfigureOptions + => AddTransportHelper(name, displayName, configureOptions); + + private EventBusBuilder AddTransportHelper(string name, string? displayName, Action? configureOptions) where TTransport : class, IEventBusTransport + where TOptions : EventBusTransportOptions, new() + where TConfigurator : EventBusTransportConfigureOptions { Services.Configure(o => o.AddTransport(name, displayName)); + Services.ConfigureOptions(); + if (configureOptions is not null) Services.Configure(name, configureOptions); - Services.ConfigureOptions>(); // // transport is not registered because multiple separate instances are required per name // Services.AddSingleton(); return this; diff --git a/src/Tingle.EventBus/DependencyInjection/EventBusConfigureOptions.cs b/src/Tingle.EventBus/DependencyInjection/EventBusConfigureOptions.cs index 713ab056..0c136432 100644 --- a/src/Tingle.EventBus/DependencyInjection/EventBusConfigureOptions.cs +++ b/src/Tingle.EventBus/DependencyInjection/EventBusConfigureOptions.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Tingle.EventBus; using Tingle.EventBus.Configuration; @@ -16,17 +17,23 @@ internal class EventBusConfigureOptions : IConfigureOptions, IValidateOptions { private readonly IHostEnvironment environment; + private readonly IEventBusConfigurationProvider configurationProvider; private readonly IEnumerable configurators; - public EventBusConfigureOptions(IHostEnvironment environment, IEnumerable configurators) + public EventBusConfigureOptions(IHostEnvironment environment, + IEventBusConfigurationProvider configurationProvider, + IEnumerable configurators) { this.environment = environment ?? throw new ArgumentNullException(nameof(environment)); + this.configurationProvider = configurationProvider ?? throw new ArgumentNullException(nameof(configurationProvider)); this.configurators = configurators ?? throw new ArgumentNullException(nameof(configurators)); } /// public void Configure(EventBusOptions options) { + configurationProvider.Configuration.Bind(options); + // Set the default ConsumerNamePrefix options.Naming.ConsumerNamePrefix ??= environment.ApplicationName; } diff --git a/src/Tingle.EventBus/DependencyInjection/EventBusNamingOptions.cs b/src/Tingle.EventBus/DependencyInjection/EventBusNamingOptions.cs index 45e93f2c..8d60e8a4 100644 --- a/src/Tingle.EventBus/DependencyInjection/EventBusNamingOptions.cs +++ b/src/Tingle.EventBus/DependencyInjection/EventBusNamingOptions.cs @@ -128,7 +128,7 @@ internal string Join(params string[] args) NamingConvention.KebabCase => string.Join("-", args).ToLowerInvariant(), NamingConvention.SnakeCase => string.Join("_", args).ToLowerInvariant(), NamingConvention.DotCase => string.Join(".", args).ToLowerInvariant(), - _ => throw new ArgumentOutOfRangeException(nameof(Convention), $"'{Convention}' does not support joining"), + _ => throw new InvalidOperationException($"'{nameof(NamingConvention)}.{Convention}' does not support joining"), }; } } diff --git a/src/Tingle.EventBus/DependencyInjection/TransportOptionsConfigureOptions.cs b/src/Tingle.EventBus/DependencyInjection/TransportOptionsConfigureOptions.cs deleted file mode 100644 index 953c4849..00000000 --- a/src/Tingle.EventBus/DependencyInjection/TransportOptionsConfigureOptions.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.Extensions.Options; -using Tingle.EventBus.Transports; - -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// Implementation of -/// for shared settings in . -/// -/// -internal class TransportOptionsConfigureOptions : IPostConfigureOptions where T : EventBusTransportOptions -{ - public void PostConfigure(string? name, T options) - { - // check bounds for empty results delay - var ticks = options.EmptyResultsDelay.Ticks; - ticks = Math.Max(ticks, TimeSpan.FromSeconds(30).Ticks); // must be more than 30 seconds - ticks = Math.Min(ticks, TimeSpan.FromMinutes(10).Ticks); // must be less than 10 minutes - options.EmptyResultsDelay = TimeSpan.FromTicks(ticks); - - // ensure the dead-letter suffix name has been set - if (string.IsNullOrWhiteSpace(options.DeadLetterSuffix)) - { - throw new InvalidOperationException($"The '{nameof(options.DeadLetterSuffix)}' must be provided"); - } - } -} diff --git a/src/Tingle.EventBus/Tingle.EventBus.csproj b/src/Tingle.EventBus/Tingle.EventBus.csproj index 73d03caa..d77efca5 100644 --- a/src/Tingle.EventBus/Tingle.EventBus.csproj +++ b/src/Tingle.EventBus/Tingle.EventBus.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Tingle.EventBus/Transports/EventBusTransportConfigureOptions.cs b/src/Tingle.EventBus/Transports/EventBusTransportConfigureOptions.cs new file mode 100644 index 00000000..250409b3 --- /dev/null +++ b/src/Tingle.EventBus/Transports/EventBusTransportConfigureOptions.cs @@ -0,0 +1,72 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Tingle.EventBus.Configuration; +using Tingle.EventBus.Transports; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Implementation of +/// for shared settings in . +/// +/// +public abstract class EventBusTransportConfigureOptions : IConfigureNamedOptions, IPostConfigureOptions, IValidateOptions + where TOptions : EventBusTransportOptions +{ + private readonly IEventBusConfigurationProvider configurationProvider; + + /// + /// Initializes a new given the configuration + /// provided by the . + /// + /// An instance.\ + public EventBusTransportConfigureOptions(IEventBusConfigurationProvider configurationProvider) + { + this.configurationProvider = configurationProvider ?? throw new ArgumentNullException(nameof(configurationProvider)); + } + + /// + public virtual void Configure(string? name, TOptions options) + { + if (string.IsNullOrEmpty(name)) return; + + var configuration = configurationProvider.Configuration.GetSection($"Transports:{name}"); + if (configuration.GetChildren().Any()) Configure(configuration, options); + } + + /// + /// Invoked to configure an instance of with the relevant configuration + /// provided by for the specific transport by name. + /// + /// + /// + protected virtual void Configure(IConfiguration configuration, TOptions options) + { + configuration.Bind(options); + } + + /// + public virtual void Configure(TOptions options) => Configure(Options.Options.DefaultName, options); + + /// + public virtual void PostConfigure(string? name, TOptions options) + { + // check bounds for empty results delay + var ticks = options.EmptyResultsDelay.Ticks; + ticks = Math.Max(ticks, TimeSpan.FromSeconds(30).Ticks); // must be more than 30 seconds + ticks = Math.Min(ticks, TimeSpan.FromMinutes(10).Ticks); // must be less than 10 minutes + options.EmptyResultsDelay = TimeSpan.FromTicks(ticks); + } + + /// + public virtual ValidateOptionsResult Validate(string? name, TOptions options) + { + // ensure the dead-letter suffix name has been set + if (string.IsNullOrWhiteSpace(options.DeadLetterSuffix)) + { + return ValidateOptionsResult.Fail($"'{nameof(options.DeadLetterSuffix)}' must be provided."); + } + + return ValidateOptionsResult.Success; + } +} diff --git a/tests/Tingle.EventBus.Tests/Configurator/DefaultEventConfiguratorTests.cs b/tests/Tingle.EventBus.Tests/Configurator/DefaultEventConfiguratorTests.cs index 9c228695..f664ad98 100644 --- a/tests/Tingle.EventBus.Tests/Configurator/DefaultEventConfiguratorTests.cs +++ b/tests/Tingle.EventBus.Tests/Configurator/DefaultEventConfiguratorTests.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Tingle.EventBus.Configuration; using Tingle.EventBus.Serialization; @@ -9,35 +10,41 @@ public class DefaultEventConfiguratorTests [Fact] public void ConfigureSerializer_UsesDefault() { - var configurator = new DefaultEventConfigurator(new FakeHostEnvironment("app1")); + var configuration = new ConfigurationBuilder().Build(); + var configurationProvider = new DefaultEventBusConfigurationProvider(configuration); + var configurator = new DefaultEventConfigurator(new FakeHostEnvironment("app1"), configurationProvider); // when not set, use default var registration = new EventRegistration(typeof(TestEvent1)); Assert.Null(registration.EventSerializerType); - DefaultEventConfigurator.ConfigureSerializer(registration); + configurator.ConfigureSerializer(registration); Assert.Equal(typeof(IEventSerializer), registration.EventSerializerType); } [Fact] public void ConfigureSerializer_RespectsAttribute() { - var configurator = new DefaultEventConfigurator(new FakeHostEnvironment("app1")); + var configuration = new ConfigurationBuilder().Build(); + var configurationProvider = new DefaultEventBusConfigurationProvider(configuration); + var configurator = new DefaultEventConfigurator(new FakeHostEnvironment("app1"), configurationProvider); // attribute is respected var registration = new EventRegistration(typeof(TestEvent2)); Assert.Null(registration.EventSerializerType); - DefaultEventConfigurator.ConfigureSerializer(registration); + configurator.ConfigureSerializer(registration); Assert.Equal(typeof(FakeEventSerializer1), registration.EventSerializerType); } [Fact] public void ConfigureSerializer_Throws_InvalidOperationException() { - var configurator = new DefaultEventConfigurator(new FakeHostEnvironment("app1")); + var configuration = new ConfigurationBuilder().Build(); + var configurationProvider = new DefaultEventBusConfigurationProvider(configuration); + var configurator = new DefaultEventConfigurator(new FakeHostEnvironment("app1"), configurationProvider); // attribute is respected var registration = new EventRegistration(typeof(TestEvent3)); - var ex = Assert.Throws(() => DefaultEventConfigurator.ConfigureSerializer(registration)); + var ex = Assert.Throws(() => configurator.ConfigureSerializer(registration)); Assert.Equal("The type 'Tingle.EventBus.Tests.Configurator.FakeEventSerializer2' is used" + " as a serializer but does not implement 'Tingle.EventBus.Serialization.IEventSerializer'", ex.Message); @@ -56,14 +63,16 @@ public void ConfigureSerializer_Throws_InvalidOperationException() [InlineData(typeof(TestEvent2), true, "dev", NamingConvention.DotCase, "sample-event")] public void ConfigureEventName_Works(Type eventType, bool useFullTypeNames, string scope, NamingConvention namingConvention, string expected) { - var configurator = new DefaultEventConfigurator(new FakeHostEnvironment("app1")); + var configuration = new ConfigurationBuilder().Build(); + var configurationProvider = new DefaultEventBusConfigurationProvider(configuration); + var configurator = new DefaultEventConfigurator(new FakeHostEnvironment("app1"), configurationProvider); var options = new EventBusOptions { }; options.Naming.Scope = scope; options.Naming.Convention = namingConvention; options.Naming.UseFullTypeNames = useFullTypeNames; var registration = new EventRegistration(eventType); - DefaultEventConfigurator.ConfigureEventName(registration, options.Naming); + configurator.ConfigureEventName(registration, options.Naming); Assert.Equal(expected, registration.EventName); } @@ -140,7 +149,9 @@ public void SetConsumerName_Works(Type eventType, NamingConvention namingConvention, string expected) { - var configurator = new DefaultEventConfigurator(new FakeHostEnvironment("app1")); + var configuration = new ConfigurationBuilder().Build(); + var configurationProvider = new DefaultEventBusConfigurationProvider(configuration); + var configurator = new DefaultEventConfigurator(new FakeHostEnvironment("app1"), configurationProvider); var options = new EventBusOptions { }; options.Naming.Convention = namingConvention; @@ -153,7 +164,7 @@ public void SetConsumerName_Works(Type eventType, registration.Consumers.Add(new EventConsumerRegistration(consumerType)); var creg = Assert.Single(registration.Consumers); - DefaultEventConfigurator.ConfigureEventName(registration, options.Naming); + configurator.ConfigureEventName(registration, options.Naming); configurator.ConfigureConsumerNames(registration, options.Naming); Assert.Equal(expected, creg.ConsumerName); } @@ -164,10 +175,12 @@ public void SetConsumerName_Works(Type eventType, [InlineData(typeof(TestEvent3), EntityKind.Broadcast)] public void ConfigureEntityKind_Works(Type eventType, EntityKind? expected) { - var configurator = new DefaultEventConfigurator(new FakeHostEnvironment("app1")); + var configuration = new ConfigurationBuilder().Build(); + var configurationProvider = new DefaultEventBusConfigurationProvider(configuration); + var configurator = new DefaultEventConfigurator(new FakeHostEnvironment("app1"), configurationProvider); var registration = new EventRegistration(eventType); - DefaultEventConfigurator.ConfigureEntityKind(registration); + configurator.ConfigureEntityKind(registration); Assert.Equal(expected, registration.EntityKind); } }