Skip to content

Commit

Permalink
Added options for subscriber (pull model) (#80)
Browse files Browse the repository at this point in the history
Extension/Builder was used to follow what was already done for publisher / push model subscriber.

Added options plus validation for Subscriber (pull model)
Named options are supported so that we can register multiple subscribers to different topics
Added extension method and builder to register those options
  • Loading branch information
DArdouin authored and felpel committed Feb 26, 2024
1 parent 62c8655 commit 695f62a
Show file tree
Hide file tree
Showing 9 changed files with 328 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#nullable disable // To reproduce users that don't have nullable enabled

using Azure.Identity;

namespace Workleap.DomainEventPropagation.Subscription.PullDelivery.Tests;

public class EventPropagationSubscriptionOptionsValidatorTests
{
[Theory]

// Valid options
[InlineData("accessKey", false, "http://topicurl.com", "topicName", "subName", true)]

// Access key
[InlineData(" ", false, "http://topicurl.com", "topicName", "subName", false)]
[InlineData(null, false, "http://topicurl.com", "topicName", "subName", false)]

// Token credential
[InlineData("accessKey", true, "http://topicurl.com", "topicName", "subName", true)]

// Topic endpoint
[InlineData("accessKey", false, "invalid-url", "topicName", "subName", false)]
[InlineData("accessKey", false, null, "topicName", "subName", false)]
[InlineData("accessKey", false, " ", "topicName", "subName", false)]

// Topic name
[InlineData("accessKey", false, "http://topicurl.com", " ", "subName", false)]
[InlineData("accessKey", false, "http://topicurl.com", null, "subName", false)]

// Subscription name
[InlineData("accessKey", false, "http://topicurl.com", "topicName", "", false)]
[InlineData("accessKey", false, "http://topicurl.com", "topicName", null, false)]
public void GivenNamedConfiguration_WhenValidate_ThenOptionsAreValidated(string topicAccessKey, bool useTokenCredential, string topicEndpoint, string topicName, string subName, bool validationSucceeded)
{
var validator = new EventPropagationSubscriptionOptionsValidator();

var result = validator.Validate("namedOptions", new EventPropagationSubscriptionOptions
{
TokenCredential = useTokenCredential ? new DefaultAzureCredential() : default,
TopicEndpoint = topicEndpoint,
TopicAccessKey = topicAccessKey,
TopicName = topicName,
SubscriptionName = subName,
});

Assert.Equal(validationSucceeded, result.Succeeded);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace Workleap.DomainEventPropagation.Subscription.PullDelivery.Tests;

public class ServiceCollectionEventSubscriptionExtensionsTests
{
private const string AccessKey = "accessKey";
private const string TopicEndPoint = "http://topicurl.com";
private const string TopicName = "topic-name";
private const string SubscriptionName = "sub-name";

[Fact]
public void GivenUnnamedConfiguration_WhenAddSubscriber_ThenOptionsAreRegistered()
{
// Given
var services = new ServiceCollection();
GivenConfigurations(services, EventPropagationSubscriptionOptions.DefaultSectionName);

// When
services.AddPullDeliverySubscription().AddSubscriber();
var serviceProvider = services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptionsMonitor<EventPropagationSubscriptionOptions>>().Get(EventPropagationSubscriptionOptions.DefaultSectionName);

// Then
Assert.Equal(AccessKey, options.TopicAccessKey);
Assert.Equal(TopicEndPoint, options.TopicEndpoint);
Assert.Equal(TopicName, options.TopicName);
Assert.Equal(SubscriptionName, options.SubscriptionName);
}

[Fact]
public void GivenUnnamedConfiguration_WhenAddSubscriber_CanOverrideConfiguration()
{
// Given
var services = new ServiceCollection();
GivenConfigurations(services, EventPropagationSubscriptionOptions.DefaultSectionName);

// When
services.AddPullDeliverySubscription().AddSubscriber(options => { options.TopicEndpoint = "http://ovewrite.io"; });
var serviceProvider = services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptionsMonitor<EventPropagationSubscriptionOptions>>().Get(EventPropagationSubscriptionOptions.DefaultSectionName);

// Then
Assert.Equal("http://ovewrite.io", options.TopicEndpoint);
Assert.Equal(AccessKey, options.TopicAccessKey);
Assert.Equal(SubscriptionName, options.SubscriptionName);
Assert.Equal(TopicName, options.TopicName);
}

[Fact]
public void GivenNamedConfigurations_WhenAddSubscribers_ThenOptionsAreRegistered()
{
// Given
var services = new ServiceCollection();
const string sectionName1 = "EventPropagation:Sub1";
const string sectionName2 = "EventPropagation:Sub2";
GivenConfigurations(services, sectionName1, sectionName2);

// When
services.AddPullDeliverySubscription()
.AddSubscriber(sectionName1)
.AddSubscriber(sectionName2);
var serviceProvider = services.BuildServiceProvider();
var monitor = serviceProvider.GetRequiredService<IOptionsMonitor<EventPropagationSubscriptionOptions>>();
var options1 = monitor.Get(sectionName1);
var options2 = monitor.Get(sectionName2);

// Then
Assert.Equal(AccessKey, options1.TopicAccessKey);
Assert.Equal(TopicEndPoint, options1.TopicEndpoint);
Assert.Equal(TopicName, options1.TopicName);
Assert.Equal(SubscriptionName, options1.SubscriptionName);

Assert.Equal(AccessKey, options2.TopicAccessKey);
Assert.Equal(TopicEndPoint, options2.TopicEndpoint);
Assert.Equal(TopicName, options2.TopicName);
Assert.Equal(SubscriptionName, options2.SubscriptionName);
}

[Fact]
public void GivenNamedConfigurations_WhenAddSubscribers_CanOverrideConfigurations()
{
// Given
var services = new ServiceCollection();
const string sectionName1 = "EventPropagation:Sub1";
const string sectionName2 = "EventPropagation:Sub2";
GivenConfigurations(services, sectionName1, sectionName2);

// When
services.AddPullDeliverySubscription()
.AddSubscriber(options => { options.TopicEndpoint = "http://ovewrite1.io"; }, sectionName1)
.AddSubscriber(options => { options.TopicEndpoint = "http://ovewrite2.io"; }, sectionName2);
var serviceProvider = services.BuildServiceProvider();
var monitor = serviceProvider.GetRequiredService<IOptionsMonitor<EventPropagationSubscriptionOptions>>();
var options1 = monitor.Get(sectionName1);
var options2 = monitor.Get(sectionName2);

// Then
Assert.Equal("http://ovewrite1.io", options1.TopicEndpoint);
Assert.Equal(AccessKey, options1.TopicAccessKey);
Assert.Equal(TopicName, options1.TopicName);
Assert.Equal(SubscriptionName, options1.SubscriptionName);

Assert.Equal("http://ovewrite2.io", options2.TopicEndpoint);
Assert.Equal(AccessKey, options2.TopicAccessKey);
Assert.Equal(TopicName, options2.TopicName);
Assert.Equal(SubscriptionName, options2.SubscriptionName);
}

private static void GivenConfigurations(IServiceCollection services, params string[] sections)
{
var dictionnary = new Dictionary<string, string>();

foreach (var section in sections)
{
dictionnary[$"{section}:{nameof(EventPropagationSubscriptionOptions.TopicAccessKey)}"] = AccessKey;
dictionnary[$"{section}:{nameof(EventPropagationSubscriptionOptions.TopicEndpoint)}"] = TopicEndPoint;
dictionnary[$"{section}:{nameof(EventPropagationSubscriptionOptions.TopicName)}"] = TopicName;
dictionnary[$"{section}:{nameof(EventPropagationSubscriptionOptions.SubscriptionName)}"] = SubscriptionName;
}

var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(dictionnary)
.Build();
services.AddSingleton<IConfiguration>(configuration);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;

namespace Workleap.DomainEventPropagation;

internal sealed class EventPropagationSubscriberBuilder : IEventPropagationSubscriberBuilder
{
public EventPropagationSubscriberBuilder(IServiceCollection services)
{
this.Services = services;
}

public IServiceCollection Services { get; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Azure.Core;

namespace Workleap.DomainEventPropagation;

public class EventPropagationSubscriptionOptions
{
internal const string DefaultSectionName = "EventPropagation:Subscription";

public string TopicAccessKey { get; set; } = string.Empty;

public TokenCredential? TokenCredential { get; set; }

public string TopicEndpoint { get; set; } = string.Empty;

public string TopicName { get; set; } = string.Empty;

public string SubscriptionName { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Microsoft.Extensions.Options;

namespace Workleap.DomainEventPropagation;

public class EventPropagationSubscriptionOptionsValidator : IValidateOptions<EventPropagationSubscriptionOptions>
{
public ValidateOptionsResult Validate(string name, EventPropagationSubscriptionOptions options)
{
if (options.TokenCredential is null && string.IsNullOrWhiteSpace(options.TopicAccessKey))
{
return ValidateOptionsResult.Fail("A token credential or an access key is required");
}

if (string.IsNullOrWhiteSpace(options.TopicEndpoint))
{
return ValidateOptionsResult.Fail("A topic endpoint is required");
}

if (!Uri.TryCreate(options.TopicEndpoint, UriKind.Absolute, out _))
{
return ValidateOptionsResult.Fail("The topic endpoint must be an absolute URI");
}

if (string.IsNullOrWhiteSpace(options.TopicName))
{
return ValidateOptionsResult.Fail("A topic endpoint is required");
}

if (string.IsNullOrWhiteSpace(options.SubscriptionName))
{
return ValidateOptionsResult.Fail("A topic endpoint is required");
}

return ValidateOptionsResult.Success;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Microsoft.Extensions.DependencyInjection;

namespace Workleap.DomainEventPropagation;

public interface IEventPropagationSubscriberBuilder
{
IServiceCollection Services { get; }
}
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@
#nullable enable
#nullable enable
static Workleap.DomainEventPropagation.ServiceCollectionEventSubscriptionExtensions.AddPullDeliverySubscription(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Workleap.DomainEventPropagation.IEventPropagationSubscriberBuilder!
static Workleap.DomainEventPropagation.ServiceCollectionEventSubscriptionExtensions.AddSubscriber(this Workleap.DomainEventPropagation.IEventPropagationSubscriberBuilder! builder) -> Workleap.DomainEventPropagation.IEventPropagationSubscriberBuilder!
static Workleap.DomainEventPropagation.ServiceCollectionEventSubscriptionExtensions.AddSubscriber(this Workleap.DomainEventPropagation.IEventPropagationSubscriberBuilder! builder, string! optionsSectionName) -> Workleap.DomainEventPropagation.IEventPropagationSubscriberBuilder!
static Workleap.DomainEventPropagation.ServiceCollectionEventSubscriptionExtensions.AddSubscriber(this Workleap.DomainEventPropagation.IEventPropagationSubscriberBuilder! builder, System.Action<Workleap.DomainEventPropagation.EventPropagationSubscriptionOptions!>! configure, string! optionsSectionName = "EventPropagation:Subscription") -> Workleap.DomainEventPropagation.IEventPropagationSubscriberBuilder!
Workleap.DomainEventPropagation.EventPropagationSubscriptionOptions
Workleap.DomainEventPropagation.EventPropagationSubscriptionOptions.EventPropagationSubscriptionOptions() -> void
Workleap.DomainEventPropagation.EventPropagationSubscriptionOptions.SubscriptionName.get -> string!
Workleap.DomainEventPropagation.EventPropagationSubscriptionOptions.SubscriptionName.set -> void
Workleap.DomainEventPropagation.EventPropagationSubscriptionOptions.TokenCredential.get -> Azure.Core.TokenCredential?
Workleap.DomainEventPropagation.EventPropagationSubscriptionOptions.TokenCredential.set -> void
Workleap.DomainEventPropagation.EventPropagationSubscriptionOptions.TopicAccessKey.get -> string!
Workleap.DomainEventPropagation.EventPropagationSubscriptionOptions.TopicAccessKey.set -> void
Workleap.DomainEventPropagation.EventPropagationSubscriptionOptions.TopicEndpoint.get -> string!
Workleap.DomainEventPropagation.EventPropagationSubscriptionOptions.TopicEndpoint.set -> void
Workleap.DomainEventPropagation.EventPropagationSubscriptionOptions.TopicName.get -> string!
Workleap.DomainEventPropagation.EventPropagationSubscriptionOptions.TopicName.set -> void
Workleap.DomainEventPropagation.EventPropagationSubscriptionOptionsValidator
Workleap.DomainEventPropagation.EventPropagationSubscriptionOptionsValidator.EventPropagationSubscriptionOptionsValidator() -> void
Workleap.DomainEventPropagation.EventPropagationSubscriptionOptionsValidator.Validate(string! name, Workleap.DomainEventPropagation.EventPropagationSubscriptionOptions! options) -> Microsoft.Extensions.Options.ValidateOptionsResult!
Workleap.DomainEventPropagation.IEventPropagationSubscriberBuilder
Workleap.DomainEventPropagation.IEventPropagationSubscriberBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
Workleap.DomainEventPropagation.ServiceCollectionEventSubscriptionExtensions
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;

namespace Workleap.DomainEventPropagation;

public static class ServiceCollectionEventSubscriptionExtensions
{
public static IEventPropagationSubscriberBuilder AddPullDeliverySubscription(this IServiceCollection services)
{
return new EventPropagationSubscriberBuilder(services);
}

public static IEventPropagationSubscriberBuilder AddSubscriber(this IEventPropagationSubscriberBuilder builder)
=> builder.AddSubscriber(_ => { });

public static IEventPropagationSubscriberBuilder AddSubscriber(this IEventPropagationSubscriberBuilder builder, string optionsSectionName)
=> builder.AddSubscriber(_ => { }, optionsSectionName);

public static IEventPropagationSubscriberBuilder AddSubscriber(this IEventPropagationSubscriberBuilder builder, Action<EventPropagationSubscriptionOptions> configure, string optionsSectionName = EventPropagationSubscriptionOptions.DefaultSectionName)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}

if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}

builder.Services.AddOptions<EventPropagationSubscriptionOptions>(optionsSectionName)
.Configure<IConfiguration>((opt, cfg) => BindFromWellKnownConfigurationSection(opt, cfg, optionsSectionName))
.Configure(configure);

builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions<EventPropagationSubscriptionOptions>, EventPropagationSubscriptionOptionsValidator>());

return builder;
}

private static void BindFromWellKnownConfigurationSection(EventPropagationSubscriptionOptions options, IConfiguration configuration, string optionsSectionName)
{
var section = configuration.GetSection(optionsSectionName);
section.Bind(options);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.7.0" />
<PackageReference Include="Azure.Core" Version="1.36.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand All @@ -29,6 +31,7 @@

<ItemGroup>
<!-- Exposes internal symbols to test projects and mocking libraries -->
<InternalsVisibleTo Include="Workleap.DomainEventPropagation.Subscription.PullDelivery.Tests,PublicKey=002400000480000094000000060200000024000052534131000400000100010025301ce547647ab5ac9264ade0f9cdc0252796a257095add4791b0232c1def21bb9e0c87d218713f918565b23394362dbcb058e210c853a24ec33e6925ebedf654a0d65efb3828c855ff21eaaa67aeb9b24b81b8baff582a03df6ab04424c7e53cacbfe84d2765ce840389f900c55824d037d2c5b6b330ac0188a06ef6869dba" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7"/>
</ItemGroup>

Expand Down

0 comments on commit 695f62a

Please sign in to comment.