Skip to content

Commit

Permalink
Support the use of IConfiguration (#487)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mburumaxwell authored Dec 19, 2022
1 parent 146a63c commit a5c8c4f
Show file tree
Hide file tree
Showing 43 changed files with 677 additions and 157 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions Tingle.EventBus.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
1 change: 1 addition & 0 deletions samples/AzureManagedIdentity/AzureManagedIdentity.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Tingle.EventBus.Transports.Azure.ServiceBus\Tingle.EventBus.Transports.Azure.ServiceBus.csproj" />
</ItemGroup>

</Project>
16 changes: 16 additions & 0 deletions samples/ConfigSample/ConfigSample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">

<PropertyGroup>
<UserSecretsId>fdb14b87-4a29-455c-9912-67a1e0c64081</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.8.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Tingle.EventBus.Transports.Azure.ServiceBus\Tingle.EventBus.Transports.Azure.ServiceBus.csproj" />
<ProjectReference Include="..\..\src\Tingle.EventBus.Transports.InMemory\Tingle.EventBus.Transports.InMemory.csproj" />
</ItemGroup>

</Project>
15 changes: 15 additions & 0 deletions samples/ConfigSample/ImageUploaded.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
31 changes: 31 additions & 0 deletions samples/ConfigSample/Program.cs
Original file line number Diff line number Diff line change
@@ -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<VehicleTelemetryEventsConsumer>();
builder.AddConsumer<VisualsUploadedConsumer>();

// Add transports
builder.AddAzureServiceBusTransport();
builder.AddInMemoryTransport("in-memory-images");
builder.AddInMemoryTransport("in-memory-videos");

// Transport specific configuration
var credential = new DefaultAzureCredential();
builder.Services.PostConfigure<AzureServiceBusTransportOptions>(
name: AzureServiceBusDefaults.Name,
configureOptions: o => ((AzureServiceBusTransportCredentials)o.Credentials).TokenCredential = credential);
});

services.AddHostedService<VisualsProducerService>();
})
.Build();

await host.RunAsync();
11 changes: 11 additions & 0 deletions samples/ConfigSample/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"profiles": {
"ConfigSample": {
"commandName": "Project",
"dotnetRunMessages": true,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}
26 changes: 26 additions & 0 deletions samples/ConfigSample/VehicleDoorOpenedEvent.cs
Original file line number Diff line number Diff line change
@@ -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, }
42 changes: 42 additions & 0 deletions samples/ConfigSample/VehicleTelemetryEventsConsumer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace ConfigSample;

internal class VehicleTelemetryEventsConsumer : IEventConsumer<VehicleTelemetryEvent>
{
private readonly ILogger logger;

public VehicleTelemetryEventsConsumer(ILogger<VehicleTelemetryEventsConsumer> logger)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

public async Task ConsumeAsync(EventContext<VehicleTelemetryEvent> 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);
}
}
42 changes: 42 additions & 0 deletions samples/ConfigSample/VisualsProducerService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace ConfigSample;

internal class VisualsProducerService : BackgroundService
{
private readonly IEventPublisher publisher;
private readonly ILogger logger;

public VisualsProducerService(IEventPublisher publisher, ILogger<VisualsProducerService> 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<ScheduledResult?> DoPublishAsync<T>(T @event, CancellationToken cancellationToken) where T : class
=> await publisher.PublishAsync(@event, cancellationToken: cancellationToken);
}
31 changes: 31 additions & 0 deletions samples/ConfigSample/VisualsUploadedConsumer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace ConfigSample;

internal class VisualsUploadedConsumer : IEventConsumer<ImageUploaded>, IEventConsumer<VideoUploaded>
{
private static readonly TimeSpan SimulationDuration = TimeSpan.FromSeconds(1.3f);

private readonly ILogger logger;

public VisualsUploadedConsumer(ILogger<VisualsUploadedConsumer> logger)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

public async Task ConsumeAsync(EventContext<ImageUploaded> 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<VideoUploaded> 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);
}
}
16 changes: 16 additions & 0 deletions samples/ConfigSample/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft": "Information",
"System": "Information"
},
"Console": {
"FormatterName": "simple",
"FormatterOptions": {
"SingleLine": true,
"TimestampFormat": "HH:mm:ss "
}
}
}
}
46 changes: 46 additions & 0 deletions samples/ConfigSample/appsettings.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
using Amazon;
using Amazon.Runtime;
using Microsoft.Extensions.Options;
using Tingle.EventBus.Configuration;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// A class to finish the configuration of instances of <see cref="AmazonTransportOptions"/> derivatives.
/// </summary>
public abstract class AmazonTransportConfigureOptions<TOptions> : IPostConfigureOptions<TOptions> where TOptions : AmazonTransportOptions
public abstract class AmazonTransportConfigureOptions<TOptions> : EventBusTransportConfigureOptions<TOptions> where TOptions : AmazonTransportOptions
{
/// <summary>
/// Initializes a new <see cref="AmazonTransportConfigureOptions{TOptions}"/> given the configuration
/// provided by the <paramref name="configurationProvider"/>.
/// </summary>
/// <param name="configurationProvider">An <see cref="IEventBusConfigurationProvider"/> instance.</param>\
public AmazonTransportConfigureOptions(IEventBusConfigurationProvider configurationProvider) : base(configurationProvider) { }

/// <inheritdoc/>
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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,21 @@ internal class AmazonKinesisConfigureOptions : AmazonTransportConfigureOptions<A
{
private readonly EventBusOptions busOptions;

public AmazonKinesisConfigureOptions(IOptions<EventBusOptions> busOptionsAccessor)
/// <summary>
/// Initializes a new <see cref="AmazonKinesisConfigureOptions"/> given the configuration
/// provided by the <paramref name="configurationProvider"/>.
/// </summary>
/// <param name="configurationProvider">An <see cref="IEventBusConfigurationProvider"/> instance.</param>\
/// <param name="busOptionsAccessor">An <see cref="IOptions{TOptions}"/> for bus configuration.</param>\
public AmazonKinesisConfigureOptions(IEventBusConfigurationProvider configurationProvider, IOptions<EventBusOptions> busOptionsAccessor)
: base(configurationProvider)
{
busOptions = busOptionsAccessor?.Value ?? throw new ArgumentNullException(nameof(busOptionsAccessor));
}

/// <inheritdoc/>
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
Expand All @@ -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
Expand Down
Loading

0 comments on commit a5c8c4f

Please sign in to comment.