diff --git a/JustSaying.IntegrationTests/Fluent/MessagingBusBuilderTests.cs b/JustSaying.IntegrationTests/Fluent/MessagingBusBuilderTests.cs new file mode 100644 index 000000000..0d1979e99 --- /dev/null +++ b/JustSaying.IntegrationTests/Fluent/MessagingBusBuilderTests.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JustSaying.Fluent; +using JustSaying.Messaging; +using JustSaying.Messaging.MessageHandling; +using JustSaying.Messaging.Monitoring; +using JustSaying.Models; +using JustSaying.TestingFramework; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Shouldly; +using Xunit.Abstractions; + +namespace JustSaying.IntegrationTests +{ + public class MessagingBusBuilderTests + { + public MessagingBusBuilderTests(ITestOutputHelper outputHelper) + { + OutputHelper = outputHelper; + } + + private ITestOutputHelper OutputHelper { get; } + + [AwsFact] + public async Task Can_Create_Messaging_Bus_Fluently_For_A_Queue() + { + // Arrange + var services = new ServiceCollection() + .AddLogging((p) => p.AddXUnit(OutputHelper)) + .AddJustSaying( + (builder) => + { + builder.Client((options) => options.WithBasicCredentials("accessKey", "secretKey").WithServiceUri(TestEnvironment.SimulatorUrl)) + .Messaging((options) => options.WithRegions("eu-west-1")) + .Publications((options) => options.WithQueue()) + .Subscriptions((options) => options.ForQueue()) + .Services((options) => options.WithMessageMonitoring(() => new MyMonitor())); + }) + .AddJustSayingHandler(); + + IServiceProvider serviceProvider = services.BuildServiceProvider(); + + IMessagePublisher publisher = serviceProvider.GetRequiredService(); + IMessagingBus listener = serviceProvider.GetRequiredService(); + + using (var source = new CancellationTokenSource(TimeSpan.FromSeconds(20))) + { + // Act + listener.Start(source.Token); + + var message = new QueueMessage(); + + await publisher.PublishAsync(message, source.Token); + + // Assert + while (!source.IsCancellationRequested && !QueueHandler.MessageIds.Contains(message.Id)) + { + await Task.Delay(TimeSpan.FromSeconds(0.2), source.Token); + } + + QueueHandler.MessageIds.ShouldContain(message.Id); + } + } + + [AwsFact] + public async Task Can_Create_Messaging_Bus_Fluently_For_A_Topic() + { + // Arrange + var services = new ServiceCollection() + .AddLogging((p) => p.AddXUnit(OutputHelper)) + .AddJustSaying( + (builder) => + { + builder.Client((options) => options.WithBasicCredentials("accessKey", "secretKey").WithServiceUri(TestEnvironment.SimulatorUrl)) + .Messaging((options) => options.WithRegions("eu-west-1")) + .Publications((options) => options.WithTopic()) + .Subscriptions((options) => options.ForTopic()); + }) + .AddJustSayingHandler(); + + IServiceProvider serviceProvider = services.BuildServiceProvider(); + + IMessagePublisher publisher = serviceProvider.GetRequiredService(); + IMessagingBus listener = serviceProvider.GetRequiredService(); + + using (var source = new CancellationTokenSource(TimeSpan.FromSeconds(20))) + { + // Act + listener.Start(source.Token); + + var message = new TopicMessage(); + + await publisher.PublishAsync(message, source.Token); + + // Assert + while (!source.IsCancellationRequested && !TopicHandler.MessageIds.Contains(message.Id)) + { + await Task.Delay(TimeSpan.FromSeconds(0.2), source.Token); + } + + TopicHandler.MessageIds.ShouldContain(message.Id); + } + } + + [AwsFact] + public void Can_Create_Messaging_Bus() + { + // Arrange + var services = new ServiceCollection() + .AddLogging((p) => p.AddXUnit(OutputHelper)) + .AddJustSaying("eu-west-1") + .AddJustSayingHandler(); + + IServiceProvider serviceProvider = services.BuildServiceProvider(); + + IMessagePublisher publisher = serviceProvider.GetRequiredService(); + IMessagingBus listener = serviceProvider.GetRequiredService(); + + using (var source = new CancellationTokenSource(TimeSpan.FromSeconds(20))) + { + // Act + listener.Start(source.Token); + } + } + + [AwsFact] + public async Task Can_Create_Messaging_Bus_With_Contributors() + { + // Arrange + var services = new ServiceCollection() + .AddLogging((p) => p.AddXUnit(OutputHelper)) + .AddJustSaying() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddJustSayingHandler() + .AddSingleton(); + + IServiceProvider serviceProvider = services.BuildServiceProvider(); + + IMessagePublisher publisher = serviceProvider.GetRequiredService(); + IMessagingBus listener = serviceProvider.GetRequiredService(); + + using (var source = new CancellationTokenSource(TimeSpan.FromSeconds(20))) + { + // Act + listener.Start(source.Token); + + var message = new QueueMessage(); + + await publisher.PublishAsync(message, source.Token); + + // Assert + while (!source.IsCancellationRequested && !QueueHandler.MessageIds.Contains(message.Id)) + { + await Task.Delay(TimeSpan.FromSeconds(0.2), source.Token); + } + + QueueHandler.MessageIds.ShouldContain(message.Id); + } + } + + private sealed class AwsContributor : IMessageBusConfigurationContributor + { + public void Configure(MessagingBusBuilder builder) + { + builder.Client( + (options) => options.WithSessionCredentials("accessKeyId", "secretKeyId", "token") + .WithServiceUri(TestEnvironment.SimulatorUrl)); + } + } + + private sealed class MessagingContributor : IMessageBusConfigurationContributor + { + public MessagingContributor(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + private IServiceProvider ServiceProvider { get; } + + public void Configure(MessagingBusBuilder builder) + { + builder.Services((p) => p.WithMessageMonitoring(ServiceProvider.GetRequiredService)); + } + } + + private sealed class QueueContributor : IMessageBusConfigurationContributor + { + public void Configure(MessagingBusBuilder builder) + { + builder.Publications((p) => p.WithQueue()) + .Subscriptions((p) => p.ForQueue()); + } + } + + private sealed class RegionContributor : IMessageBusConfigurationContributor + { + public void Configure(MessagingBusBuilder builder) + { + builder.Messaging((p) => p.WithRegion("eu-west-1")); + } + } + + private sealed class QueueMessage : Message + { + } + + private sealed class QueueHandler : IHandlerAsync + { + internal static ConcurrentBag MessageIds { get; } = new ConcurrentBag(); + + public Task Handle(QueueMessage message) + { + MessageIds.Add(message.Id); + return Task.FromResult(true); + } + } + + private sealed class TopicMessage : Message + { + } + + private sealed class TopicHandler : IHandlerAsync + { + internal static ConcurrentBag MessageIds { get; } = new ConcurrentBag(); + + public Task Handle(TopicMessage message) + { + MessageIds.Add(message.Id); + return Task.FromResult(true); + } + } + + private sealed class MyMonitor : IMessageMonitor + { + public void HandleException(Type messageType) + { + } + + public void HandleThrottlingTime(long handleTimeMs) + { + } + + public void HandleTime(long handleTimeMs) + { + } + + public void IncrementThrottlingStatistic() + { + } + + public void IssuePublishingMessage() + { + } + + public void PublishMessageTime(long handleTimeMs) + { + } + + public void ReceiveMessageTime(long handleTimeMs, string queueName, string region) + { + } + } + } +} diff --git a/JustSaying.IntegrationTests/JustSaying.IntegrationTests.csproj b/JustSaying.IntegrationTests/JustSaying.IntegrationTests.csproj index 07fcb0c5c..6e1cf27d1 100644 --- a/JustSaying.IntegrationTests/JustSaying.IntegrationTests.csproj +++ b/JustSaying.IntegrationTests/JustSaying.IntegrationTests.csproj @@ -1,9 +1,9 @@ - + netcoreapp2.2 - $(NoWarn);CA1001;CA1034;CA1052;CA1054;CA1063;CA1307;CA1816;CA1822;CA2007 + $(NoWarn);CA1001;CA1034;CA1052;CA1054;CA1063;CA1307;CA1707;CA1812;CA1816;CA1822;CA2007 @@ -15,6 +15,7 @@ + diff --git a/JustSaying.TestingFramework/LocalAwsClientFactory.cs b/JustSaying.TestingFramework/LocalAwsClientFactory.cs index 4d35dd20a..077af89bd 100644 --- a/JustSaying.TestingFramework/LocalAwsClientFactory.cs +++ b/JustSaying.TestingFramework/LocalAwsClientFactory.cs @@ -21,6 +21,7 @@ public IAmazonSimpleNotificationService GetSnsClient(RegionEndpoint region) var credentials = new AnonymousAWSCredentials(); var clientConfig = new AmazonSimpleNotificationServiceConfig { + RegionEndpoint = region, ServiceURL = ServiceUrl.ToString() }; @@ -32,6 +33,7 @@ public IAmazonSQS GetSqsClient(RegionEndpoint region) var credentials = new AnonymousAWSCredentials(); var clientConfig = new AmazonSQSConfig { + RegionEndpoint = region, ServiceURL = ServiceUrl.ToString() }; diff --git a/JustSaying/CreateMeABus.cs b/JustSaying/CreateMeABus.cs index 046e961ac..c688d6256 100644 --- a/JustSaying/CreateMeABus.cs +++ b/JustSaying/CreateMeABus.cs @@ -12,7 +12,7 @@ public static class CreateMeABus public static Func DefaultClientFactory { get; set; } = () => new DefaultAwsClientFactory(); - public static JustSayingFluentlyDependencies WithLogging(ILoggerFactory loggerFactory) => + public static JustSayingFluentlyDependencies WithLogging(ILoggerFactory loggerFactory) => new JustSayingFluentlyDependencies { LoggerFactory = loggerFactory}; } } diff --git a/JustSaying/Fluent/AwsClientFactoryBuilder.cs b/JustSaying/Fluent/AwsClientFactoryBuilder.cs new file mode 100644 index 000000000..5c2c3f8fa --- /dev/null +++ b/JustSaying/Fluent/AwsClientFactoryBuilder.cs @@ -0,0 +1,178 @@ +using System; +using Amazon.Runtime; +using JustSaying.AwsTools; + +namespace JustSaying.Fluent +{ + /// + /// A class representing a builder for instances of . This class cannot be inherited. + /// + public sealed class AwsClientFactoryBuilder + { + /// + /// Initializes a new instance of the class. + /// + /// The that owns this instance. + internal AwsClientFactoryBuilder(MessagingBusBuilder busBuilder) + { + BusBuilder = busBuilder; + } + + /// + public MessagingBusBuilder BusBuilder { get; } + + /// + /// Gets or sets a delegate to use to create the to use. + /// + private Func ClientFactory { get; set; } + + /// + /// Gets or sets the to use. + /// + private AWSCredentials Credentials { get; set; } + + /// + /// Gets or sets the URI for the AWS services to use. + /// + private Uri ServiceUri { get; set; } + + /// + /// Creates a new instance of . + /// + /// + /// The created instance of . + /// + public IAwsClientFactory Build() + { + if (ClientFactory != null) + { + return ClientFactory(); + } + + DefaultAwsClientFactory factory; + + if (Credentials == null) + { + factory = new DefaultAwsClientFactory(); + } + else + { + factory = new DefaultAwsClientFactory(Credentials); + } + + factory.ServiceUri = ServiceUri; + + return factory; + } + + /// + /// Specifies the to use. + /// + /// A delegate to a method to use to create an . + /// + /// The current . + /// + public AwsClientFactoryBuilder WithClientFactory(Func clientFactory) + { + ClientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); + return this; + } + + /// + /// Specifies the to use. + /// + /// The to use. + /// + /// The current . + /// + public AwsClientFactoryBuilder WithCredentials(AWSCredentials credentials) + { + Credentials = credentials ?? throw new ArgumentNullException(nameof(credentials)); + return this; + } + + /// + /// Specifies the basic AWS credentials to use. + /// + /// The access key to use. + /// The secret key to use. + /// + /// The current . + /// + public AwsClientFactoryBuilder WithBasicCredentials(string accessKey, string secretKey) + { + Credentials = new BasicAWSCredentials(accessKey, secretKey); + return this; + } + + /// + /// Specifies the basic AWS credentials to use. + /// + /// The access key Id to use. + /// The secret access key to use. + /// The session token to use. + /// + /// The current . + /// + public AwsClientFactoryBuilder WithSessionCredentials(string accessKeyId, string secretAccessKey, string token) + { + Credentials = new SessionAWSCredentials(accessKeyId, secretAccessKey, token); + return this; + } + + /// + /// Specifies the AWS service URL to use. + /// + /// The URL to use for AWS services. + /// + /// The current . + /// + /// + /// is . + /// + /// + /// is not an absolute URI. + /// +#pragma warning disable CA1054 // Uri parameters should not be strings + public AwsClientFactoryBuilder WithServiceUrl(string url) +#pragma warning restore CA1054 // Uri parameters should not be strings + { + if (url == null) + { + throw new ArgumentNullException(nameof(url)); + } + + return WithServiceUri(new Uri(url, UriKind.Absolute)); + } + + /// + /// Specifies the AWS service URI to use. + /// + /// The URI to use for AWS services. + /// + /// The current . + /// + /// + /// is . + /// + /// + /// is not an absolute URI. + /// + public AwsClientFactoryBuilder WithServiceUri(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (!uri.IsAbsoluteUri) + { + throw new ArgumentException("The AWS service URI must be an absolute URI.", nameof(uri)); + } + + ServiceUri = uri; + + return this; + } + } +} diff --git a/JustSaying/Fluent/DefaultServiceResolver.cs b/JustSaying/Fluent/DefaultServiceResolver.cs new file mode 100644 index 000000000..e1917763e --- /dev/null +++ b/JustSaying/Fluent/DefaultServiceResolver.cs @@ -0,0 +1,89 @@ +using System; +using JustSaying.AwsTools; +using JustSaying.Messaging.MessageSerialization; +using JustSaying.Messaging.Monitoring; +using Microsoft.Extensions.Logging; + +namespace JustSaying.Fluent +{ + /// + /// A class representing the built-in implementation of . This class cannot be inherited. + /// + internal sealed class DefaultServiceResolver : IServiceResolver + { + /// + public T ResolveService() + => (T)ResolveService(typeof(T)); + + private object ResolveService(Type desiredType) + { + if (desiredType == typeof(ILoggerFactory)) + { + return new NullLoggerFactory(); + } + else if (desiredType == typeof(IAwsClientFactoryProxy)) + { + return new AwsClientFactoryProxy(); + } + else if (desiredType == typeof(IHandlerResolver)) + { + return null; // Special case - must be provided by the consumer + } + else if (desiredType == typeof(IMessagingConfig)) + { + return new MessagingConfig(); + } + else if (desiredType == typeof(IMessageMonitor)) + { + return new NullOpMessageMonitor(); + } + else if (desiredType == typeof(IMessageSerializationFactory)) + { + return new NewtonsoftSerializationFactory(); + } + else if (desiredType == typeof(IMessageSerializationRegister)) + { + return new MessageSerializationRegister(ResolveService()); + } + else if (desiredType == typeof(IMessageSubjectProvider)) + { + return new NonGenericMessageSubjectProvider(); + } + + throw new NotSupportedException($"Resolving a service of type {desiredType.Name} is not supported."); + } + + private sealed class NullLoggerFactory : ILoggerFactory + { + public void AddProvider(ILoggerProvider provider) + { + } + + public ILogger CreateLogger(string categoryName) + { + return new NullLogger(); + } + + public void Dispose() + { + } + + private sealed class NullLogger : ILogger + { + public IDisposable BeginScope(TState state) + { + return null; + } + + public bool IsEnabled(LogLevel logLevel) + { + return false; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + } + } + } + } +} diff --git a/JustSaying/Fluent/IMessageBusConfigurationContributor.cs b/JustSaying/Fluent/IMessageBusConfigurationContributor.cs new file mode 100644 index 000000000..24bac3d02 --- /dev/null +++ b/JustSaying/Fluent/IMessageBusConfigurationContributor.cs @@ -0,0 +1,16 @@ +namespace JustSaying.Fluent +{ + //// TODO I don't like this name, but we can give it a better name later + + /// + /// Defines a method for configuring an instance of . + /// + public interface IMessageBusConfigurationContributor + { + /// + /// Configures an . + /// + /// The builder to configure. + void Configure(MessagingBusBuilder builder); + } +} diff --git a/JustSaying/Fluent/IPublicationBuilder`1.cs b/JustSaying/Fluent/IPublicationBuilder`1.cs new file mode 100644 index 000000000..26becdcb8 --- /dev/null +++ b/JustSaying/Fluent/IPublicationBuilder`1.cs @@ -0,0 +1,19 @@ +using JustSaying.Models; + +namespace JustSaying.Fluent +{ + /// + /// Defines a builder for a publication. + /// + /// + /// The type of the messages to publish. + /// + internal interface IPublicationBuilder + where T : Message + { + /// + /// Configures the publication for the . + /// + void Configure(JustSayingFluently bus); + } +} diff --git a/JustSaying/Fluent/IServiceResolver.cs b/JustSaying/Fluent/IServiceResolver.cs new file mode 100644 index 000000000..7d68f9c43 --- /dev/null +++ b/JustSaying/Fluent/IServiceResolver.cs @@ -0,0 +1,19 @@ +namespace JustSaying.Fluent +{ + /// + /// Defines a method for resolving instances of types from a depenency injection container. + /// + public interface IServiceResolver + { + /// + /// Resolves an instance of the specified type. + /// + /// + /// The type to resolve an instance of. + /// + /// + /// The resolved instance of . + /// + T ResolveService(); + } +} diff --git a/JustSaying/Fluent/ISubscriptionBuilder`1.cs b/JustSaying/Fluent/ISubscriptionBuilder`1.cs new file mode 100644 index 000000000..90ae595fd --- /dev/null +++ b/JustSaying/Fluent/ISubscriptionBuilder`1.cs @@ -0,0 +1,20 @@ +using JustSaying.Models; + +namespace JustSaying.Fluent +{ + /// + /// Defines a builder for a subscription. + /// + /// + /// The type of the messages to subscribe to. + /// + internal interface ISubscriptionBuilder + where T : Message + { + /// + /// Configures the subscription for the . + /// + /// The to configure the subscription for. + void Configure(JustSayingFluently bus, IHandlerResolver resolver); + } +} diff --git a/JustSaying/Fluent/MessagingConfigurationBuilder.cs b/JustSaying/Fluent/MessagingConfigurationBuilder.cs new file mode 100644 index 000000000..d7cbec1b0 --- /dev/null +++ b/JustSaying/Fluent/MessagingConfigurationBuilder.cs @@ -0,0 +1,374 @@ +using System; +using System.Collections.Generic; +using Amazon; +using JustSaying.AwsTools.MessageHandling; +using JustSaying.Messaging.MessageSerialization; +using JustSaying.Models; + +namespace JustSaying.Fluent +{ + /// + /// A class representing a builder for instances of . This class cannot be inherited. + /// + public sealed class MessagingConfigurationBuilder + { + /// + /// Initializes a new instance of the class. + /// + /// The that owns this instance. + internal MessagingConfigurationBuilder(MessagingBusBuilder busBuilder) + { + BusBuilder = busBuilder; + } + + /// + public MessagingBusBuilder BusBuilder { get; } + + /// + /// Gets or sets the optional value to use for + /// + private List AdditionalSubscriberAccounts { get; set; } + + /// + /// Gets or sets the optional value to use for + /// + private Action MessageResponseLogger { get; set; } + + /// + /// Gets or sets the optional value to use for + /// + private TimeSpan? PublishFailureBackoff { get; set; } + + /// + /// Gets or sets the optional value to use for + /// + private int? PublishFailureReAttempts { get; set; } + + /// + /// Gets or sets the optional value to use for + /// + private Func GetActiveRegion { get; set; } + + /// + /// Gets or sets the optional value to use for + /// + private List Regions { get; set; } + + /// + /// Gets or sets the optional value to use for + /// + private IMessageSubjectProvider MessageSubjectProvider { get; set; } + + /// + /// Specifies the active AWS region to use. + /// + /// The active AWS region to use. + /// + /// The current . + /// + /// + /// is . + /// + public MessagingConfigurationBuilder WithActiveRegion(RegionEndpoint region) + { + if (region == null) + { + throw new ArgumentNullException(nameof(region)); + } + + return WithActiveRegion(region.SystemName); + } + + /// + /// Specifies the active AWS region to use. + /// + /// The active AWS region to use. + /// + /// The current . + /// + /// + /// is . + /// + public MessagingConfigurationBuilder WithActiveRegion(string region) + { + if (region == null) + { + throw new ArgumentNullException(nameof(region)); + } + + return WithActiveRegion(() => region); + } + + /// + /// Specifies a delgate which evaluates the current active AWS region to use. + /// + /// A delegate to a method with evaluates the active AWS region to use. + /// + /// The current . + /// + /// + /// is . + /// + public MessagingConfigurationBuilder WithActiveRegion(Func evaluator) + { + GetActiveRegion = evaluator ?? throw new ArgumentNullException(nameof(evaluator)); + return this; + } + + /// + /// Specifies additional subscriber account(s) to use. + /// + /// The AWS account Id(s) to additionally subscribe to. + /// + /// The current . + /// + /// + /// is . + /// + public MessagingConfigurationBuilder WithAdditionalSubscriberAccounts(params string[] regions) + => WithAdditionalSubscriberAccounts(regions as IEnumerable); + + /// + /// Specifies additional subscriber account(s) to use. + /// + /// The AWS account Id(s) to additionally subscribe to. + /// + /// The current . + /// + /// + /// is . + /// + public MessagingConfigurationBuilder WithAdditionalSubscriberAccounts(IEnumerable accountIds) + { + if (accountIds == null) + { + throw new ArgumentNullException(nameof(accountIds)); + } + + AdditionalSubscriberAccounts = new List(accountIds); + return this; + } + + /// + /// Specifies an additional subscriber account to use. + /// + /// The AWS account Id to additionally subscribe to. + /// + /// The current . + /// + /// + /// is . + /// + public MessagingConfigurationBuilder WithAdditionalSubscriberAccount(string accountId) + { + if (accountId == null) + { + throw new ArgumentNullException(nameof(accountId)); + } + + if (AdditionalSubscriberAccounts == null) + { + AdditionalSubscriberAccounts = new List(); + } + + AdditionalSubscriberAccounts.Add(accountId); + return this; + } + + /// + /// Specifies a delegate to use to log message responses. + /// + /// A delegate to a method to use to log message responses. + /// + /// The current . + /// + /// + /// is . + /// + public MessagingConfigurationBuilder WithMessageResponseLogger(Action logger) + { + MessageResponseLogger = logger ?? throw new ArgumentNullException(nameof(logger)); + return this; + } + + /// + /// Specifies the to use. + /// + /// The to use. + /// + /// The current . + /// + /// + /// is . + /// + public MessagingConfigurationBuilder WithMessageSubjectProvider(IMessageSubjectProvider subjectProvider) + { + MessageSubjectProvider = subjectProvider ?? throw new ArgumentNullException(nameof(subjectProvider)); + return this; + } + + /// + /// Specifies the back-off period to use if message publishing fails. + /// + /// The back-off period to use. + /// + /// The current . + /// + public MessagingConfigurationBuilder WithPublishFailureBackoff(TimeSpan value) + { + PublishFailureBackoff = value; + return this; + } + + /// + /// Specifies the number of publish re-attempts to use if message publishing fails. + /// + /// The number of re-attempts. + /// + /// The current . + /// + public MessagingConfigurationBuilder WithPublishFailureReattempts(int value) + { + PublishFailureReAttempts = value; + return this; + } + + /// + /// Specifies the AWS region(s) to use. + /// + /// The AWS region(s) to use. + /// + /// The current . + /// + /// + /// is . + /// + public MessagingConfigurationBuilder WithRegions(params string[] regions) + => WithRegions(regions as IEnumerable); + + /// + /// Specifies the AWS region(s) to use. + /// + /// The AWS region(s) to use. + /// + /// The current . + /// + /// + /// is . + /// + public MessagingConfigurationBuilder WithRegions(IEnumerable regions) + { + if (regions == null) + { + throw new ArgumentNullException(nameof(regions)); + } + + Regions = new List(regions); + return this; + } + + /// + /// Specifies an AWS region to use. + /// + /// The AWS region to use. + /// + /// The current . + /// + /// + /// is . + /// + public MessagingConfigurationBuilder WithRegion(string region) + { + if (region == null) + { + throw new ArgumentNullException(nameof(region)); + } + + if (Regions == null) + { + Regions = new List(); + } + + if (!Regions.Contains(region)) + { + Regions.Add(region); + } + + return this; + } + + /// + /// Specifies an AWS region to use. + /// + /// The AWS region to use. + /// + /// The current . + /// + /// + /// is . + /// + public MessagingConfigurationBuilder WithRegion(RegionEndpoint region) + { + if (region == null) + { + throw new ArgumentNullException(nameof(region)); + } + + return WithRegion(region.SystemName); + } + + /// + /// Creates a new instance of . + /// + /// + /// The created instance of . + /// + public IMessagingConfig Build() + { + var config = BusBuilder.ServiceResolver.ResolveService(); + + if (Regions?.Count > 0) + { + foreach (string region in Regions) + { + if (!config.Regions.Contains(region)) + { + config.Regions.Add(region); + } + } + } + + if (AdditionalSubscriberAccounts?.Count > 0) + { + config.AdditionalSubscriberAccounts = AdditionalSubscriberAccounts; + } + + if (GetActiveRegion != null) + { + config.GetActiveRegion = GetActiveRegion; + } + + if (MessageResponseLogger != null) + { + config.MessageResponseLogger = MessageResponseLogger; + } + + if (MessageSubjectProvider != null) + { + config.MessageSubjectProvider = MessageSubjectProvider; + } + + if (PublishFailureBackoff.HasValue) + { + config.PublishFailureBackoff = PublishFailureBackoff.Value; + } + + if (PublishFailureReAttempts.HasValue) + { + config.PublishFailureReAttempts = PublishFailureReAttempts.Value; + } + + return config; + } + } +} diff --git a/JustSaying/Fluent/PublicationsBuilder.cs b/JustSaying/Fluent/PublicationsBuilder.cs new file mode 100644 index 000000000..21b99b1c3 --- /dev/null +++ b/JustSaying/Fluent/PublicationsBuilder.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using JustSaying.Models; + +namespace JustSaying.Fluent +{ + /// + /// A class representing a builder for publications. This class cannot be inherited. + /// + public sealed class PublicationsBuilder + { + /// + /// Initializes a new instance of the class. + /// + /// The that owns this instance. + internal PublicationsBuilder(MessagingBusBuilder parent) + { + Parent = parent; + } + + /// + /// Gets the parent of this builder. + /// + internal MessagingBusBuilder Parent { get; } + + /// + /// Gets the configured publication builders. + /// + private IList> Publications { get; } = new List>(); + + /// + /// Configures a publisher for a queue. + /// + /// + /// The current . + /// + /// + /// is . + /// + public PublicationsBuilder WithQueue() + where T : Message + { + Publications.Add(new QueuePublicationBuilder()); + return this; + } + + /// + /// Configures a publisher for a queue. + /// + /// A delegate to a method to use to configure a queue. + /// + /// The current . + /// + /// + /// is . + /// + public PublicationsBuilder WithQueue(Action> configure) + where T : Message + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var builder = new QueuePublicationBuilder(); + + configure(builder); + + Publications.Add(builder); + + return this; + } + + /// + /// Configures a publisher for a topic. + /// + /// + /// The current . + /// + /// + /// is . + /// + public PublicationsBuilder WithTopic() + where T : Message + { + Publications.Add(new TopicPublicationBuilder()); + return this; + } + + /// + /// Configures a publisher for a topic. + /// + /// A delegate to a method to use to configure a topic. + /// + /// The current . + /// + /// + /// is . + /// + public PublicationsBuilder WithTopic(Action> configure) + where T : Message + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var builder = new TopicPublicationBuilder(); + + configure(builder); + + Publications.Add(builder); + + return this; + } + + /// + /// Configures the publications for the . + /// + /// The to configure publications for. + internal void Configure(JustSayingFluently bus) + { + foreach (IPublicationBuilder builder in Publications) + { + builder.Configure(bus); + } + } + } +} diff --git a/JustSaying/Fluent/QueuePublicationBuilder`1.cs b/JustSaying/Fluent/QueuePublicationBuilder`1.cs new file mode 100644 index 000000000..71ef211fd --- /dev/null +++ b/JustSaying/Fluent/QueuePublicationBuilder`1.cs @@ -0,0 +1,75 @@ +using System; +using JustSaying.AwsTools.QueueCreation; +using JustSaying.Models; + +namespace JustSaying.Fluent +{ + /// + /// A class representing a builder for a queue publication. This class cannot be inherited. + /// + /// + /// The type of the message published to the queue. + /// + public sealed class QueuePublicationBuilder : IPublicationBuilder + where T : Message + { + /// + /// Initializes a new instance of the class. + /// + internal QueuePublicationBuilder() + { + } + + /// + /// Gets or sets a delegate to a method to use to configure SQS writes. + /// + private Action ConfigureWrites { get; set; } + + /// + /// Configures the SQS write configuration. + /// + /// A delegate to a method to use to configure SQS writes. + /// + /// The current . + /// + /// + /// is . + /// + public QueuePublicationBuilder WithWriteConfiguration(Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var builder = new SqsWriteConfigurationBuilder(); + + configure(builder); + + ConfigureWrites = builder.Configure; + return this; + } + + /// + /// Configures the SQS write configuration. + /// + /// A delegate to a method to use to configure SQS writes. + /// + /// The current . + /// + /// + /// is . + /// + public QueuePublicationBuilder WithWriteConfiguration(Action configure) + { + ConfigureWrites = configure ?? throw new ArgumentNullException(nameof(configure)); + return this; + } + + /// + void IPublicationBuilder.Configure(JustSayingFluently bus) + { + bus.WithSqsMessagePublisher(ConfigureWrites); + } + } +} diff --git a/JustSaying/Fluent/QueueSubscriptionBuilder`1.cs b/JustSaying/Fluent/QueueSubscriptionBuilder`1.cs new file mode 100644 index 000000000..36769a99e --- /dev/null +++ b/JustSaying/Fluent/QueueSubscriptionBuilder`1.cs @@ -0,0 +1,112 @@ +using System; +using JustSaying.AwsTools.QueueCreation; +using JustSaying.Models; + +namespace JustSaying.Fluent +{ + /// + /// A class representing a builder for a queue subscription. This class cannot be inherited. + /// + /// + /// The type of the message. + /// + public sealed class QueueSubscriptionBuilder : ISubscriptionBuilder + where T : Message + { + /// + /// Initializes a new instance of the class. + /// + internal QueueSubscriptionBuilder() + { + } + + /// + /// Gets or sets the queue name. + /// + private string QueueName { get; set; } = string.Empty; + + /// + /// Gets or sets a delegate to a method to use to configure SQS reads. + /// + private Action ConfigureReads { get; set; } + + /// + /// Configures that the default queue name should be used. + /// + /// + /// The current . + /// + public QueueSubscriptionBuilder WithDefaultQueue() + => WithName(string.Empty); + + /// + /// Configures the name of the queue. + /// + /// The name of the queue to subscribe to. + /// + /// The current . + /// + /// + /// is . + /// + public QueueSubscriptionBuilder WithName(string name) + { + QueueName = name ?? throw new ArgumentNullException(nameof(name)); + return this; + } + + /// + /// Configures the SQS read configuration. + /// + /// A delegate to a method to use to configure SQS reads. + /// + /// The current . + /// + /// + /// is . + /// + public QueueSubscriptionBuilder WithReadConfiguration(Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var builder = new SqsReadConfigurationBuilder(); + + configure(builder); + + ConfigureReads = builder.Configure; + return this; + } + + /// + /// Configures the SQS read configuration. + /// + /// A delegate to a method to use to configure SQS reads. + /// + /// The current . + /// + /// + /// is . + /// + public QueueSubscriptionBuilder WithReadConfiguration(Action configure) + { + ConfigureReads = configure ?? throw new ArgumentNullException(nameof(configure)); + return this; + } + + /// + void ISubscriptionBuilder.Configure(JustSayingFluently bus, IHandlerResolver resolver) + { + var queue = bus.WithSqsPointToPointSubscriber() + .IntoQueue(QueueName) + .WithMessageHandler(resolver); + + if (ConfigureReads != null) + { + queue.ConfigureSubscriptionWith(ConfigureReads); + } + } + } +} diff --git a/JustSaying/Fluent/ServiceProviderResolver.cs b/JustSaying/Fluent/ServiceProviderResolver.cs new file mode 100644 index 000000000..43a70fd23 --- /dev/null +++ b/JustSaying/Fluent/ServiceProviderResolver.cs @@ -0,0 +1,35 @@ +using System; +using JustSaying.Messaging.MessageHandling; +using Microsoft.Extensions.DependencyInjection; + +namespace JustSaying.Fluent +{ + /// + /// A class that implements and + /// for . This class cannot be inherited. + /// + internal sealed class ServiceProviderResolver : IServiceResolver, IHandlerResolver + { + /// + /// Initializes a new instance of the class. + /// + /// The to use. + internal ServiceProviderResolver(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + /// + /// Gets the to use. + /// + private IServiceProvider ServiceProvider { get; } + + /// + public IHandlerAsync ResolveHandler(HandlerResolutionContext context) + => ServiceProvider.GetRequiredService>(); + + /// + public T ResolveService() + => ServiceProvider.GetRequiredService(); + } +} diff --git a/JustSaying/Fluent/ServicesBuilder.cs b/JustSaying/Fluent/ServicesBuilder.cs new file mode 100644 index 000000000..e1fa6a0f0 --- /dev/null +++ b/JustSaying/Fluent/ServicesBuilder.cs @@ -0,0 +1,156 @@ +using System; +using JustSaying.Messaging.MessageHandling; +using JustSaying.Messaging.MessageSerialization; +using JustSaying.Messaging.Monitoring; +using Microsoft.Extensions.Logging; + +namespace JustSaying.Fluent +{ + /// + /// Defines a builder for services used by JustSaying. This class cannot be inherited. + /// + public sealed class ServicesBuilder + { + /// + /// Initializes a new instance of the class. + /// + /// The that owns this instance. + internal ServicesBuilder(MessagingBusBuilder busBuilder) + { + BusBuilder = busBuilder; + } + + /// + internal MessagingBusBuilder BusBuilder { get; } + + /// + /// Gets or sets a delegate to a method to create the to use. + /// + internal Func LoggerFactory { get; private set; } + + /// + /// Gets or sets a delegate to a method to create the to use. + /// + internal Func MessageMonitoring { get; private set; } + + /// + /// Gets or sets a delegate to a method to create the to use. + /// + internal Func MessageLock { get; private set; } + + /// + /// Gets or sets a delegate to a method to create the to use. + /// + internal Func MessageSerializationFactory { get; private set; } + + /// + /// Gets or sets a delegate to a method to create the to use. + /// + internal Func NamingStrategy { get; private set; } + + /// + /// Gets or sets a delegate to a method to create the to use. + /// + internal Func SerializationRegister { get; private set; } + + /// + /// Specifies the to use. + /// + /// The to use. + /// + /// The current . + /// + /// + /// is . + /// + public ServicesBuilder WithLoggerFactory(ILoggerFactory loggerFactory) + { + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + return WithLoggerFactory(() => loggerFactory); + } + + /// + /// Specifies the to use. + /// + /// A delegate to a method to get the to use. + /// + /// The current . + /// + /// + /// is . + /// + public ServicesBuilder WithLoggerFactory(Func loggerFactory) + { + LoggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); + return this; + } + + /// + /// Specifies the to use. + /// + /// A delegate to a method to get the to use. + /// + /// The current . + /// + /// + /// is . + /// + public ServicesBuilder WithMessageLock(Func messageLock) + { + MessageLock = messageLock ?? throw new ArgumentNullException(nameof(messageLock)); + return this; + } + + /// + /// Specifies the to use. + /// + /// A delegate to a method to get the to use. + /// + /// The current . + /// + /// + /// is . + /// + public ServicesBuilder WithMessageMonitoring(Func monitoring) + { + MessageMonitoring = monitoring ?? throw new ArgumentNullException(nameof(monitoring)); + return this; + } + + /// + /// Specifies the to use. + /// + /// A delegate to a method to get the to use. + /// + /// The current . + /// + /// + /// is . + /// + public ServicesBuilder WithNamingStrategy(Func strategy) + { + NamingStrategy = strategy ?? throw new ArgumentNullException(nameof(strategy)); + return this; + } + + /// + /// Specifies the to use. + /// + /// A delegate to a method to get the to use. + /// + /// The current . + /// + /// + /// is . + /// + public ServicesBuilder WithMessageSerializationFactory(Func factory) + { + MessageSerializationFactory = factory ?? throw new ArgumentNullException(nameof(factory)); + return this; + } + } +} diff --git a/JustSaying/Fluent/SnsWriteConfigurationBuilder.cs b/JustSaying/Fluent/SnsWriteConfigurationBuilder.cs new file mode 100644 index 000000000..61f32e06d --- /dev/null +++ b/JustSaying/Fluent/SnsWriteConfigurationBuilder.cs @@ -0,0 +1,45 @@ +using System; +using JustSaying.AwsTools.QueueCreation; +using JustSaying.Models; + +namespace JustSaying.Fluent +{ + /// + /// A class representing a builder for configuring instances of . This class cannot be inherited. + /// + public sealed class SnsWriteConfigurationBuilder + { + /// + /// Gets or sets the error callback to use. + /// + private Func Handler { get; set; } + + /// + /// Configures an error handler to use. + /// + /// A delegate to a method to call when an error occurs. + /// + /// The current . + /// + /// + /// is . + /// + public SnsWriteConfigurationBuilder WithErrorHandler(Func handler) + { + Handler = handler ?? throw new ArgumentNullException(nameof(handler)); + return this; + } + + /// + /// Configures the specified . + /// + /// The configuration to configure. + internal void Configure(SnsWriteConfiguration config) + { + if (Handler != null) + { + config.HandleException = Handler; + } + } + } +} diff --git a/JustSaying/Fluent/SqsConfigurationBuilder.cs b/JustSaying/Fluent/SqsConfigurationBuilder.cs new file mode 100644 index 000000000..e4ca67d3c --- /dev/null +++ b/JustSaying/Fluent/SqsConfigurationBuilder.cs @@ -0,0 +1,137 @@ +using System; +using JustSaying.AwsTools.QueueCreation; + +namespace JustSaying.Fluent +{ + /// + /// Defines the base class for a builder for instances of . + /// + /// The type of the configuration. + /// The type of the builder. + public abstract class SqsConfigurationBuilder + where TConfiguration : SqsBasicConfiguration + where TBuilder : SqsConfigurationBuilder + { + /// + /// Initializes a new instance of the class. + /// + internal SqsConfigurationBuilder() + { + } + + /// + /// Gets the current . + /// + protected abstract TBuilder Self { get; } + + /// + /// Gets or sets a value indicating whether to opt-out of error queues. + /// + private bool? ErrorQueueOptOut { get; set; } + + /// + /// Gets or sets the message retention value to use. + /// + private TimeSpan? MessageRetention { get; set; } + + /// + /// Gets or sets the visibility timeout value to use. + /// + private TimeSpan? VisibilityTimeout { get; set; } + + /// + /// Configures that an error queue should be used. + /// + /// + /// The current . + /// + public TBuilder WithErrorQueue() + => WithErrorQueueOptOut(false); + + /// + /// Configures that no error queue should be used. + /// + /// + /// The current . + /// + public TBuilder WithNoErrorQueue() + => WithErrorQueueOptOut(true); + + /// + /// Configures whether to opt-out of an error queue. + /// + /// Whether or not to opt-out of an error queue. + /// + /// The current . + /// + public TBuilder WithErrorQueueOptOut(bool value) + { + ErrorQueueOptOut = value; + return Self; + } + + /// + /// Configures the message retention period to use. + /// + /// The value to use for the message retention. + /// + /// The current . + /// + public TBuilder WithMessageRetention(TimeSpan value) + { + MessageRetention = value; + return Self; + } + + /// + /// Configures the visibility timeout to use. + /// + /// The value to use for the visibility timeout. + /// + /// The current . + /// + public TBuilder WithVisibilityTimeout(TimeSpan value) + { + VisibilityTimeout = value; + return Self; + } + + /// + /// Configures the specified . + /// + /// The configuration to configure. + internal virtual void Configure(TConfiguration config) + { + // TODO Which ones should be configurable? All, or just the important ones? + // config.BaseQueueName = default; + // config.BaseTopicName = default; + // config.DeliveryDelay = default; + // config.ErrorQueueRetentionPeriod = default; + // config.FilterPolicy = default; + // config.MessageBackoffStrategy = default; + // config.MessageProcessingStrategy = default; + // config.PublishEndpoint = default; + // config.QueueName = default; + // config.RetryCountBeforeSendingToErrorQueue = default; + // config.ServerSideEncryption = default; + // config.Topic = default; + + if (ErrorQueueOptOut.HasValue) + { + config.ErrorQueueOptOut = ErrorQueueOptOut.Value; + } + + if (MessageRetention.HasValue) + { + config.MessageRetention = MessageRetention.Value; + } + + if (VisibilityTimeout.HasValue) + { + config.VisibilityTimeout = VisibilityTimeout.Value; + } + + config.Validate(); + } + } +} diff --git a/JustSaying/Fluent/SqsReadConfigurationBuilder.cs b/JustSaying/Fluent/SqsReadConfigurationBuilder.cs new file mode 100644 index 000000000..c7a4aee98 --- /dev/null +++ b/JustSaying/Fluent/SqsReadConfigurationBuilder.cs @@ -0,0 +1,156 @@ +using System; +using Amazon.SQS.Model; +using JustSaying.AwsTools.QueueCreation; + +namespace JustSaying.Fluent +{ + /// + /// A class representing a builder for configuring instances of . This class cannot be inherited. + /// + public sealed class SqsReadConfigurationBuilder : SqsConfigurationBuilder + { + /// + protected override SqsReadConfigurationBuilder Self => this; + + /// + /// Gets or sets the instance position value to use. + /// + private int? InstancePosition { get; set; } + + /// + /// Gets or sets the maximum number of messages that can be inflight. + /// + private int? MaximumAllowedMessagesInflight { get; set; } + + /// + /// Gets or sets the error callback to use. + /// + private Action OnError { get; set; } + + /// + /// Gets or sets the topic source account Id to use. + /// + private string TopicSourceAccountId { get; set; } + + /// + /// Configures an error handler to use. + /// + /// A delegate to a method to call when an error occurs. + /// + /// The current . + /// + /// + /// is . + /// + public SqsReadConfigurationBuilder WithErrorHandler(Action action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + OnError = (exception, _) => action(exception); + return this; + } + + /// + /// Configures an error handler to use. + /// + /// A delegate to a method to call when an error occurs. + /// + /// The current . + /// + /// + /// is . + /// + public SqsReadConfigurationBuilder WithErrorHandler(Action action) + { + OnError = action ?? throw new ArgumentNullException(nameof(action)); + return this; + } + + /// + /// Configures the instance position to use. + /// + /// The value to use for the instance position. + /// + /// The current . + /// + public SqsReadConfigurationBuilder WithInstancePosition(int value) + { + InstancePosition = value; + return this; + } + + /// + /// Configures the maximum number of messages that can be inflight at any time. + /// + /// The value to use for maximum number of inflight messages. + /// + /// The current . + /// + public SqsReadConfigurationBuilder WithMaximumMessagesInflight(int value) + { + MaximumAllowedMessagesInflight = value; + return this; + } + + /// + /// Configures the account Id to use for the topic source. + /// + /// The Id of the AWS account which is the topic's source. + /// + /// The current . + /// + public SqsReadConfigurationBuilder WithTopicSourceAccount(string id) + { + TopicSourceAccountId = id; + return this; + } + + /// + /// Configures the specified . + /// + /// The configuration to configure. + internal override void Configure(SqsReadConfiguration config) + { + // TODO Which ones should be configurable? All, or just the important ones? + // config.BaseQueueName = default; + // config.BaseTopicName = default; + // config.DeliveryDelay = default; + // config.ErrorQueueRetentionPeriod = default; + // config.FilterPolicy = default; + // config.MessageBackoffStrategy = default; + // config.MessageProcessingStrategy = default; + // config.PublishEndpoint = default; + // config.QueueName = default; + // config.RetryCountBeforeSendingToErrorQueue = default; + // config.ServerSideEncryption = default; + // config.Topic = default; + + base.Configure(config); + + if (InstancePosition.HasValue) + { + config.InstancePosition = InstancePosition.Value; + } + + if (MaximumAllowedMessagesInflight.HasValue) + { + config.MaxAllowedMessagesInFlight = MaximumAllowedMessagesInflight.Value; + } + + if (OnError != null) + { + config.OnError = OnError; + } + + if (TopicSourceAccountId != null) + { + config.TopicSourceAccount = TopicSourceAccountId; + } + + config.Validate(); + } + } +} diff --git a/JustSaying/Fluent/SqsWriteConfigurationBuilder.cs b/JustSaying/Fluent/SqsWriteConfigurationBuilder.cs new file mode 100644 index 000000000..e7c5579ae --- /dev/null +++ b/JustSaying/Fluent/SqsWriteConfigurationBuilder.cs @@ -0,0 +1,51 @@ +using System; +using JustSaying.AwsTools.QueueCreation; + +namespace JustSaying.Fluent +{ + /// + /// A class representing a builder for configuring instances of . This class cannot be inherited. + /// + public sealed class SqsWriteConfigurationBuilder : SqsConfigurationBuilder + { + /// + protected override SqsWriteConfigurationBuilder Self => this; + + /// + /// Gets or sets the queue name to use. + /// + private string QueueName { get; set; } + + /// + /// Configures the queue name to use. + /// + /// The value to use for the message retention. + /// + /// The current . + /// + /// + /// is . + /// + public SqsWriteConfigurationBuilder WithQueueName(string name) + { + QueueName = name ?? throw new ArgumentNullException(nameof(name)); + return this; + } + + /// + /// Configures the specified . + /// + /// The configuration to configure. + internal override void Configure(SqsWriteConfiguration config) + { + base.Configure(config); + + if (QueueName != null) + { + config.QueueName = QueueName; + } + + config.Validate(); + } + } +} diff --git a/JustSaying/Fluent/SubscriptionsBuilder.cs b/JustSaying/Fluent/SubscriptionsBuilder.cs new file mode 100644 index 000000000..d3f55fe02 --- /dev/null +++ b/JustSaying/Fluent/SubscriptionsBuilder.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using JustSaying.Models; + +namespace JustSaying.Fluent +{ + /// + /// A class representing a builder for subscriptions. This class cannot be inherited. + /// + public sealed class SubscriptionsBuilder + { + /// + /// Initializes a new instance of the class. + /// + /// The that owns this instance. + internal SubscriptionsBuilder(MessagingBusBuilder parent) + { + Parent = parent; + } + + /// + /// Gets the parent of this builder. + /// + internal MessagingBusBuilder Parent { get; } + + /// + /// Gets the configured subscription builders. + /// + private IList> Subscriptions { get; } = new List>(); + + /// + /// Configures a queue subscription for the default queue. + /// + /// + /// The current . + /// + /// + /// is . + /// + public SubscriptionsBuilder ForQueue() + where T : Message + { + return ForQueue((p) => p.WithDefaultQueue()); + } + + /// + /// Configures a queue subscription. + /// + /// A delegate to a method to use to configure a queue subscription. + /// + /// The current . + /// + /// + /// is . + /// + public SubscriptionsBuilder ForQueue(Action> configure) + where T : Message + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var builder = new QueueSubscriptionBuilder(); + + configure(builder); + + Subscriptions.Add(builder); + + return this; + } + + /// + /// Configures a topic subscription for the default topic name. + /// + /// + /// The current . + /// + public SubscriptionsBuilder ForTopic() + where T : Message + { + return ForTopic((p) => p.IntoDefaultTopic()); + } + + /// + /// Configures a topic subscription. + /// + /// A delegate to a method to use to configure a topic subscription. + /// + /// The current . + /// + /// + /// is . + /// + public SubscriptionsBuilder ForTopic(Action> configure) + where T : Message + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var builder = new TopicSubscriptionBuilder(); + + configure(builder); + + Subscriptions.Add(builder); + + return this; + } + + /// + /// Configures the subscriptions for the . + /// + /// The to configure subscriptions for. + /// + /// No instance of could be resolved. + /// + internal void Configure(JustSayingFluently bus) + { + var resolver = Parent.ServiceResolver.ResolveService(); + + if (resolver == null) + { + throw new InvalidOperationException($"No {nameof(IHandlerResolver)} is registered."); + } + + foreach (ISubscriptionBuilder builder in Subscriptions) + { + builder.Configure(bus, resolver); + } + } + } +} diff --git a/JustSaying/Fluent/TopicPublicationBuilder`1.cs b/JustSaying/Fluent/TopicPublicationBuilder`1.cs new file mode 100644 index 000000000..1e36b175a --- /dev/null +++ b/JustSaying/Fluent/TopicPublicationBuilder`1.cs @@ -0,0 +1,75 @@ +using System; +using JustSaying.AwsTools.QueueCreation; +using JustSaying.Models; + +namespace JustSaying.Fluent +{ + /// + /// A class representing a builder for a topic publication. This class cannot be inherited. + /// + /// + /// The type of the message. + /// + public sealed class TopicPublicationBuilder : IPublicationBuilder + where T : Message + { + /// + /// Initializes a new instance of the class. + /// + internal TopicPublicationBuilder() + { + } + + /// + /// Gets or sets a delegate to a method to use to configure SNS writes. + /// + private Action ConfigureWrites { get; set; } + + /// + /// Configures the SNS write configuration. + /// + /// A delegate to a method to use to configure SNS writes. + /// + /// The current . + /// + /// + /// is . + /// + public TopicPublicationBuilder WithWriteConfiguration(Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var builder = new SnsWriteConfigurationBuilder(); + + configure(builder); + + ConfigureWrites = builder.Configure; + return this; + } + + /// + /// Configures the SNS write configuration. + /// + /// A delegate to a method to use to configure SNS writes. + /// + /// The current . + /// + /// + /// is . + /// + public TopicPublicationBuilder WithWriteConfiguration(Action configure) + { + ConfigureWrites = configure ?? throw new ArgumentNullException(nameof(configure)); + return this; + } + + /// + void IPublicationBuilder.Configure(JustSayingFluently bus) + { + bus.WithSnsMessagePublisher(ConfigureWrites); + } + } +} diff --git a/JustSaying/Fluent/TopicSubscriptionBuilder`1.cs b/JustSaying/Fluent/TopicSubscriptionBuilder`1.cs new file mode 100644 index 000000000..168ec5511 --- /dev/null +++ b/JustSaying/Fluent/TopicSubscriptionBuilder`1.cs @@ -0,0 +1,112 @@ +using System; +using JustSaying.AwsTools.QueueCreation; +using JustSaying.Models; + +namespace JustSaying.Fluent +{ + /// + /// A class representing a builder for a topic subscription. This class cannot be inherited. + /// + /// + /// The type of the message. + /// + public sealed class TopicSubscriptionBuilder : ISubscriptionBuilder + where T : Message + { + /// + /// Initializes a new instance of the class. + /// + internal TopicSubscriptionBuilder() + { + } + + /// + /// Gets or sets the topic name. + /// + private string TopicName { get; set; } = string.Empty; + + /// + /// Gets or sets a delegate to a method to use to configure SNS reads. + /// + private Action ConfigureReads { get; set; } + + /// + /// Configures that the default topic name should be used. + /// + /// + /// The current . + /// + public TopicSubscriptionBuilder IntoDefaultTopic() + => WithName(string.Empty); + + /// + /// Configures the name of the topic. + /// + /// The name of the topic to subscribe to. + /// + /// The current . + /// + /// + /// is . + /// + public TopicSubscriptionBuilder WithName(string name) + { + TopicName = name ?? throw new ArgumentNullException(nameof(name)); + return this; + } + + /// + /// Configures the SNS read configuration. + /// + /// A delegate to a method to use to configure SNS reads. + /// + /// The current . + /// + /// + /// is . + /// + public TopicSubscriptionBuilder WithReadConfiguration(Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + var builder = new SqsReadConfigurationBuilder(); + + configure(builder); + + ConfigureReads = builder.Configure; + return this; + } + + /// + /// Configures the SNS read configuration. + /// + /// A delegate to a method to use to configure SNS reads. + /// + /// The current . + /// + /// + /// is . + /// + public TopicSubscriptionBuilder WithReadConfiguration(Action configure) + { + ConfigureReads = configure ?? throw new ArgumentNullException(nameof(configure)); + return this; + } + + /// + void ISubscriptionBuilder.Configure(JustSayingFluently bus, IHandlerResolver resolver) + { + var topic = bus.WithSqsTopicSubscriber() + .IntoQueue(TopicName) + .WithMessageHandler(resolver); + + if (ConfigureReads != null) + { + topic.ConfigureSubscriptionWith(ConfigureReads); + } + } + } +} diff --git a/JustSaying/IMessagingBus.cs b/JustSaying/IMessagingBus.cs new file mode 100644 index 000000000..485d20f92 --- /dev/null +++ b/JustSaying/IMessagingBus.cs @@ -0,0 +1,16 @@ +using System.Threading; + +namespace JustSaying +{ + /// + /// Defines a messaging bus. + /// + public interface IMessagingBus + { + /// + /// Starts the message bus. + /// + /// A which will stop the bus when signalled. + void Start(CancellationToken cancellationToken); + } +} diff --git a/JustSaying/IMessagingConfig.cs b/JustSaying/IMessagingConfig.cs index ae5d66482..a8363075a 100644 --- a/JustSaying/IMessagingConfig.cs +++ b/JustSaying/IMessagingConfig.cs @@ -8,7 +8,7 @@ public interface IMessagingConfig : IPublishConfiguration //ToDo: This vs publis { IList Regions { get; } Func GetActiveRegion { get; set; } - IMessageSubjectProvider MessageSubjectProvider { get; } + IMessageSubjectProvider MessageSubjectProvider { get; set; } void Validate(); } diff --git a/JustSaying/JustSaying.csproj b/JustSaying/JustSaying.csproj index 99fc66104..f6d7bda39 100644 --- a/JustSaying/JustSaying.csproj +++ b/JustSaying/JustSaying.csproj @@ -12,9 +12,11 @@ + + diff --git a/JustSaying/JustSayingBus.cs b/JustSaying/JustSayingBus.cs index a364621a3..27734a2fc 100644 --- a/JustSaying/JustSayingBus.cs +++ b/JustSaying/JustSayingBus.cs @@ -15,7 +15,7 @@ namespace JustSaying { - public sealed class JustSayingBus : IAmJustSaying, IAmJustInterrogating + public sealed class JustSayingBus : IAmJustSaying, IAmJustInterrogating, IMessagingBus { private readonly Dictionary> _subscribersByRegionAndQueue; private readonly Dictionary> _publishersByRegionAndTopic; diff --git a/JustSaying/JustSayingFluently.cs b/JustSaying/JustSayingFluently.cs index 37a3a2b61..4837092fd 100644 --- a/JustSaying/JustSayingFluently.cs +++ b/JustSaying/JustSayingFluently.cs @@ -122,7 +122,7 @@ public IHaveFulfilledPublishRequirements WithSqsMessagePublisher(Action + /// A class representing a builder for instances of + /// and . This class cannot be inherited. + /// + public sealed class MessagingBusBuilder + { + /// + /// Gets the to use. + /// + internal IServiceResolver ServiceResolver { get; private set; } = new DefaultServiceResolver(); + + /// + /// Gets or sets the builder to use for creating an AWS client factory. + /// + private AwsClientFactoryBuilder ClientFactoryBuilder { get; set; } + + /// + /// Gets or sets the builder to use to configure messaging. + /// + private MessagingConfigurationBuilder MessagingConfig { get; set; } + + /// + /// Gets or sets the builder to use for publications. + /// + private PublicationsBuilder PublicationsBuilder { get; set; } + + /// + /// Gets or sets the builder to use for services. + /// + private ServicesBuilder ServicesBuilder { get; set; } + + /// + /// Gets or sets the builder to use for subscriptions. + /// + private SubscriptionsBuilder SubscriptionBuilder { get; set; } + + /// + /// Configures the factory for AWS clients. + /// + /// A delegate to a method to use to configure the AWS clients. + /// + /// The current . + /// + /// + /// is . + /// + public MessagingBusBuilder Client(Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + if (ClientFactoryBuilder == null) + { + ClientFactoryBuilder = new AwsClientFactoryBuilder(this); + } + + configure(ClientFactoryBuilder); + + return this; + } + + /// + /// Configures messaging. + /// + /// A delegate to a method to use to configure messaging. + /// + /// The current . + /// + /// + /// is . + /// + public MessagingBusBuilder Messaging(Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + if (MessagingConfig == null) + { + MessagingConfig = new MessagingConfigurationBuilder(this); + } + + configure(MessagingConfig); + + return this; + } + + /// + /// Configures the publications. + /// + /// A delegate to a method to use to configure publications. + /// + /// The current . + /// + /// + /// is . + /// + public MessagingBusBuilder Publications(Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + if (PublicationsBuilder == null) + { + PublicationsBuilder = new PublicationsBuilder(this); + } + + configure(PublicationsBuilder); + + return this; + } + + /// + /// Configures the services. + /// + /// A delegate to a method to use to configure JustSaying services. + /// + /// The current . + /// + /// + /// is . + /// + public MessagingBusBuilder Services(Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + if (ServicesBuilder == null) + { + ServicesBuilder = new ServicesBuilder(this); + } + + configure(ServicesBuilder); + + return this; + } + + /// + /// Configures the subscriptions. + /// + /// A delegate to a method to use to configure subscriptions. + /// + /// The current . + /// + /// + /// is . + /// + public MessagingBusBuilder Subscriptions(Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + if (SubscriptionBuilder == null) + { + SubscriptionBuilder = new SubscriptionsBuilder(this); + } + + configure(SubscriptionBuilder); + + return this; + } + + /// + /// Specifies the to use. + /// + /// The to use. + /// + /// The current . + /// + /// + /// is . + /// + public MessagingBusBuilder WithServiceResolver(IServiceResolver serviceResolver) + { + ServiceResolver = serviceResolver ?? throw new ArgumentNullException(nameof(serviceResolver)); + return this; + } + + /// + /// Creates a new instance of . + /// + /// + /// The created instance of + /// + public IMessagePublisher BuildPublisher() + { + IMessagingConfig config = CreateConfig(); + + config.Validate(); + + ILoggerFactory loggerFactory = + ServicesBuilder?.LoggerFactory?.Invoke() ?? ServiceResolver.ResolveService(); + + JustSayingBus publisher = CreateBus(config, loggerFactory); + JustSayingFluently fluent = CreateFluent(publisher, loggerFactory); + + if (ServicesBuilder?.NamingStrategy != null) + { + fluent.WithNamingStrategy(ServicesBuilder.NamingStrategy); + } + + if (PublicationsBuilder != null) + { + PublicationsBuilder.Configure(fluent); + } + + return publisher; + } + + /// + /// Creates a new instance of . + /// + /// + /// The created instance of + /// + public IMessagingBus BuildSubscribers() + { + IMessagingConfig config = CreateConfig(); + + config.Validate(); + + ILoggerFactory loggerFactory = + ServicesBuilder?.LoggerFactory?.Invoke() ?? ServiceResolver.ResolveService(); + + JustSayingBus bus = CreateBus(config, loggerFactory); + JustSayingFluently fluent = CreateFluent(bus, loggerFactory); + + if (ServicesBuilder?.NamingStrategy != null) + { + fluent.WithNamingStrategy(ServicesBuilder.NamingStrategy); + } + + if (SubscriptionBuilder != null) + { + SubscriptionBuilder.Configure(fluent); + } + + return bus; + } + + private JustSayingBus CreateBus(IMessagingConfig config, ILoggerFactory loggerFactory) + { + IMessageSerializationRegister register = + ServicesBuilder?.SerializationRegister?.Invoke() ?? ServiceResolver.ResolveService(); + + return new JustSayingBus(config, register, loggerFactory); + } + + private IMessagingConfig CreateConfig() + { + return MessagingConfig != null ? + MessagingConfig.Build() : + ServiceResolver.ResolveService(); + } + + private IAwsClientFactoryProxy CreateFactoryProxy() + { + return ClientFactoryBuilder != null ? + new AwsClientFactoryProxy(new Lazy(ClientFactoryBuilder.Build)) : + ServiceResolver.ResolveService(); + } + + private IMessageMonitor CreateMessageMonitor() + { + return ServicesBuilder?.MessageMonitoring?.Invoke() ?? ServiceResolver.ResolveService(); + } + + private IMessageSerializationFactory CreateMessageSerializationFactory() + { + return ServicesBuilder?.MessageSerializationFactory?.Invoke() ?? ServiceResolver.ResolveService(); + } + + private JustSayingFluently CreateFluent(JustSayingBus bus, ILoggerFactory loggerFactory) + { + IAwsClientFactoryProxy proxy = CreateFactoryProxy(); + IVerifyAmazonQueues queueCreator = new AmazonQueueCreator(proxy, loggerFactory); + + var fluent = new JustSayingFluently(bus, queueCreator, proxy, loggerFactory); + + IMessageSerializationFactory serializationFactory = CreateMessageSerializationFactory(); + IMessageMonitor messageMonitor = CreateMessageMonitor(); + + fluent.WithSerializationFactory(serializationFactory) + .WithMonitoring(messageMonitor); + + if (ServicesBuilder?.MessageLock != null) + { + fluent.WithMessageLockStoreOf(ServicesBuilder.MessageLock()); + } + + return fluent; + } + } +} diff --git a/JustSaying/Microsoft/Extensions/DependencyInjection/IServiceCollectionExtensions.cs b/JustSaying/Microsoft/Extensions/DependencyInjection/IServiceCollectionExtensions.cs new file mode 100644 index 000000000..0a0c8f9e2 --- /dev/null +++ b/JustSaying/Microsoft/Extensions/DependencyInjection/IServiceCollectionExtensions.cs @@ -0,0 +1,200 @@ +using System; +using System.ComponentModel; +using JustSaying; +using JustSaying.AwsTools; +using JustSaying.AwsTools.QueueCreation; +using JustSaying.Fluent; +using JustSaying.Messaging.MessageHandling; +using JustSaying.Messaging.MessageSerialization; +using JustSaying.Messaging.Monitoring; +using JustSaying.Models; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// A class containing extension methods for the interface. This class cannot be inherited. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static class IServiceCollectionExtensions + { + // TODO This is here for convenience while protyping, would probably live elsewhere + // so we don't need to force the dependency on MS' DI types + + /// + /// Adds JustSaying services to the service collection. + /// + /// The to add JustSaying services to. + /// + /// The specified by . + /// + /// + /// is . + /// + public static IServiceCollection AddJustSaying(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + return services.AddJustSaying((_) => { }); + } + + /// + /// Adds JustSaying services to the service collection. + /// + /// The to add JustSaying services to. + /// The AWS region(s) to configure. + /// + /// The specified by . + /// + /// + /// or is . + /// + public static IServiceCollection AddJustSaying(this IServiceCollection services, params string[] regions) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (regions == null) + { + throw new ArgumentNullException(nameof(regions)); + } + + return services.AddJustSaying( + (builder) => builder.Messaging( + (options) => options.WithRegions(regions))); + } + + /// + /// Adds JustSaying services to the service collection. + /// + /// The to add JustSaying services to. + /// A delegate to a method to use to configure JustSaying. + /// + /// The specified by . + /// + /// + /// or is . + /// + public static IServiceCollection AddJustSaying(this IServiceCollection services, Action configure) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + return services.AddJustSaying((builder, _) => configure(builder)); + } + + /// + /// Adds JustSaying services to the service collection. + /// + /// The to add JustSaying services to. + /// A delegate to a method to use to configure JustSaying. + /// + /// The specified by . + /// + /// + /// or is . + /// + public static IServiceCollection AddJustSaying(this IServiceCollection services, Action configure) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + // Register as self so the same singleton instance implements two different interfaces + services.TryAddSingleton((p) => new ServiceProviderResolver(p)); + services.TryAddSingleton((p) => p.GetRequiredService()); + services.TryAddSingleton((p) => p.GetRequiredService()); + + services.TryAddSingleton(); + services.TryAddSingleton((p) => new AwsClientFactoryProxy(p.GetRequiredService)); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton( + (p) => + { + var config = p.GetRequiredService(); + return new MessageSerializationRegister(config.MessageSubjectProvider); + }); + + services.TryAddSingleton( + (serviceProvider) => + { + var builder = new MessagingBusBuilder() + .WithServiceResolver(new ServiceProviderResolver(serviceProvider)); + + configure(builder, serviceProvider); + + var contributors = serviceProvider.GetServices(); + + foreach (var contributor in contributors) + { + contributor.Configure(builder); + } + + return builder; + }); + + services.TryAddSingleton( + (serviceProvider) => + { + var builder = serviceProvider.GetRequiredService(); + return builder.BuildPublisher(); + }); + + services.TryAddSingleton( + (serviceProvider) => + { + var builder = serviceProvider.GetRequiredService(); + return builder.BuildSubscribers(); + }); + + return services; + } + + /// + /// Adds a JustSaying message handler to the service collection. + /// + /// The type of the message handled. + /// The type of the message handler to register. + /// The to add the message handler to. + /// + /// The specified by . + /// + /// + /// is . + /// + public static IServiceCollection AddJustSayingHandler(this IServiceCollection services) + where TMessage : Message + where THandler : class, IHandlerAsync + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.TryAddTransient, THandler>(); + return services; + } + } +}