diff --git a/.gitignore b/.gitignore index 8a30d25..42bc5d0 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ *.userosscache *.sln.docstates +# Rider +.idea + # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/Directory.Build.props b/Directory.Build.props index 60adef7..5f07ef1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,7 +6,7 @@ - 2.2.0 + 3.0.0 diff --git a/src/ReflectionEventing.Autofac/AutofacConsumerProvider.cs b/src/ReflectionEventing.Autofac/AutofacConsumerProvider.cs index 72e89a9..c416972 100644 --- a/src/ReflectionEventing.Autofac/AutofacConsumerProvider.cs +++ b/src/ReflectionEventing.Autofac/AutofacConsumerProvider.cs @@ -13,7 +13,7 @@ namespace ReflectionEventing.Autofac; public class AutofacConsumerProvider(ILifetimeScope lifetimeScope) : IConsumerProvider { /// - public IEnumerable GetConsumerTypes(Type consumerType) + public IEnumerable GetConsumers(Type consumerType) { if (consumerType is null) { diff --git a/src/ReflectionEventing.Autofac/ContainerBuilderExtensions.cs b/src/ReflectionEventing.Autofac/ContainerBuilderExtensions.cs index a801b96..8867a5c 100644 --- a/src/ReflectionEventing.Autofac/ContainerBuilderExtensions.cs +++ b/src/ReflectionEventing.Autofac/ContainerBuilderExtensions.cs @@ -32,9 +32,8 @@ Action configure configure(autofacBuilder); _ = builder - .RegisterType() + .RegisterInstance(autofacBuilder.BuildTypesProvider()) .As() - .WithParameter("consumers", autofacBuilder.GetConsumers()) .SingleInstance(); _ = builder diff --git a/src/ReflectionEventing.Castle.Windsor/EventBusInstaller.cs b/src/ReflectionEventing.Castle.Windsor/EventBusInstaller.cs index 2cb8903..e42c97b 100644 --- a/src/ReflectionEventing.Castle.Windsor/EventBusInstaller.cs +++ b/src/ReflectionEventing.Castle.Windsor/EventBusInstaller.cs @@ -33,7 +33,7 @@ public void Install(IWindsorContainer container, IConfigurationStore store) _ = container.Register( Component .For() - .Instance(new HashedConsumerTypesProvider(builder.GetConsumers())) + .Instance(builder.BuildTypesProvider()) .LifestyleScoped(), Component .For() diff --git a/src/ReflectionEventing.Castle.Windsor/WindsorConsumerProvider.cs b/src/ReflectionEventing.Castle.Windsor/WindsorConsumerProvider.cs index 97f9667..b8b3ed7 100644 --- a/src/ReflectionEventing.Castle.Windsor/WindsorConsumerProvider.cs +++ b/src/ReflectionEventing.Castle.Windsor/WindsorConsumerProvider.cs @@ -13,7 +13,7 @@ namespace ReflectionEventing.Castle.Windsor; public class WindsorConsumerProvider(IWindsorContainer container) : IConsumerProvider { /// - public IEnumerable GetConsumerTypes(Type consumerType) + public IEnumerable GetConsumers(Type consumerType) { if (consumerType is null) { diff --git a/src/ReflectionEventing.Castle.Windsor/WindsorEventBusBuilder.cs b/src/ReflectionEventing.Castle.Windsor/WindsorEventBusBuilder.cs index b6b079e..8f8a23f 100644 --- a/src/ReflectionEventing.Castle.Windsor/WindsorEventBusBuilder.cs +++ b/src/ReflectionEventing.Castle.Windsor/WindsorEventBusBuilder.cs @@ -14,7 +14,7 @@ namespace ReflectionEventing.Castle.Windsor; public class WindsorEventBusBuilder(IWindsorContainer container) : EventBusBuilder { /// - public override void AddConsumer( + public override EventBusBuilder AddConsumer( #if NET5_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] #endif @@ -28,6 +28,6 @@ Type consumerType ); } - base.AddConsumer(consumerType); + return base.AddConsumer(consumerType); } } diff --git a/src/ReflectionEventing.Demo.Wpf/App.xaml.cs b/src/ReflectionEventing.Demo.Wpf/App.xaml.cs index 3766e77..d097e22 100644 --- a/src/ReflectionEventing.Demo.Wpf/App.xaml.cs +++ b/src/ReflectionEventing.Demo.Wpf/App.xaml.cs @@ -32,8 +32,9 @@ public partial class App : Application _ = services.AddEventBus(e => { - // _ = e.AddAllConsumers(Assembly.GetExecutingAssembly()); - _ = e.AddConsumer(); + e.Options.UseEventPolymorphism = true; + + _ = e.AddAllConsumers(Assembly.GetExecutingAssembly()); }); } ) diff --git a/src/ReflectionEventing.Demo.Wpf/AssemblyInfo.cs b/src/ReflectionEventing.Demo.Wpf/AssemblyInfo.cs index e5798bc..8eae719 100644 --- a/src/ReflectionEventing.Demo.Wpf/AssemblyInfo.cs +++ b/src/ReflectionEventing.Demo.Wpf/AssemblyInfo.cs @@ -3,11 +3,4 @@ // Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. // All Rights Reserved. -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located -//(used if a resource is not found in the page, -// app, or any theme specific resource dictionaries) -)] +[assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)] diff --git a/src/ReflectionEventing.Demo.Wpf/GlobalUsings.cs b/src/ReflectionEventing.Demo.Wpf/GlobalUsings.cs index 44774e7..e0f6c55 100644 --- a/src/ReflectionEventing.Demo.Wpf/GlobalUsings.cs +++ b/src/ReflectionEventing.Demo.Wpf/GlobalUsings.cs @@ -6,9 +6,12 @@ global using System; global using System.IO; global using System.Linq; +global using System.Reflection; global using System.Threading; global using System.Threading.Tasks; global using System.Windows; +global using CommunityToolkit.Mvvm.ComponentModel; global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; diff --git a/src/ReflectionEventing.Demo.Wpf/MainWindow.xaml b/src/ReflectionEventing.Demo.Wpf/MainWindow.xaml index 8669330..d5599dd 100644 --- a/src/ReflectionEventing.Demo.Wpf/MainWindow.xaml +++ b/src/ReflectionEventing.Demo.Wpf/MainWindow.xaml @@ -11,8 +11,13 @@ d:DataContext="{d:DesignInstance local:MainWindow, IsDesignTimeCreatable=True}" mc:Ignorable="d"> - - - - + + + + + + + + + diff --git a/src/ReflectionEventing.Demo.Wpf/Services/ApplicationHostService.cs b/src/ReflectionEventing.Demo.Wpf/Services/ApplicationHostService.cs index a8c8f8c..5fcd808 100644 --- a/src/ReflectionEventing.Demo.Wpf/Services/ApplicationHostService.cs +++ b/src/ReflectionEventing.Demo.Wpf/Services/ApplicationHostService.cs @@ -20,9 +20,9 @@ public async Task StartAsync(CancellationToken cancellationToken) /// Triggered when the application host is performing a graceful shutdown. /// /// Indicates that the shutdown process should no longer be graceful. - public async Task StopAsync(CancellationToken cancellationToken) + public Task StopAsync(CancellationToken cancellationToken) { - await Task.CompletedTask; + return Task.CompletedTask; } /// diff --git a/src/ReflectionEventing.Demo.Wpf/Services/BackgroundTickService.cs b/src/ReflectionEventing.Demo.Wpf/Services/BackgroundTickService.cs index 3e99563..4bfe253 100644 --- a/src/ReflectionEventing.Demo.Wpf/Services/BackgroundTickService.cs +++ b/src/ReflectionEventing.Demo.Wpf/Services/BackgroundTickService.cs @@ -28,15 +28,12 @@ public Task StopAsync(CancellationToken cancellationToken) private async Task TickInBackground(CancellationToken cancellationToken) { + await eventBus.PublishAsync(new OtherEvent(), cancellationToken); + Random random = new(); - while (true) + while (!cancellationToken.IsCancellationRequested) { - if (cancellationToken.IsCancellationRequested) - { - break; - } - await eventBus.PublishAsync( new BackgroundTicked(random.Next(10, 1001)), cancellationToken diff --git a/src/ReflectionEventing.Demo.Wpf/ViewModels/MainWindowViewModel.cs b/src/ReflectionEventing.Demo.Wpf/ViewModels/MainWindowViewModel.cs index 8e4680e..83fd8d6 100644 --- a/src/ReflectionEventing.Demo.Wpf/ViewModels/MainWindowViewModel.cs +++ b/src/ReflectionEventing.Demo.Wpf/ViewModels/MainWindowViewModel.cs @@ -3,33 +3,37 @@ // Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. // All Rights Reserved. -using CommunityToolkit.Mvvm.ComponentModel; using ReflectionEventing.Demo.Wpf.Events; namespace ReflectionEventing.Demo.Wpf.ViewModels; -public partial class MainWindowViewModel - : ObservableObject, +public partial class MainWindowViewModel(ILogger logger) + : ViewModel, IConsumer, IConsumer { [ObservableProperty] - private int _currentTick = 0; + private int _currentTick; /// public async Task ConsumeAsync(ITickedEvent payload, CancellationToken cancellationToken) { int tickValue = payload.Value; - await Application.Current.Dispatcher.InvokeAsync(() => - { - CurrentTick = tickValue; - }); + await DispatchAsync( + () => + { + CurrentTick = tickValue; + }, + cancellationToken + ); } /// public async Task ConsumeAsync(OtherEvent payload, CancellationToken cancellationToken) { + logger.LogInformation("Received {Event} event.", nameof(OtherEvent)); + await Task.CompletedTask; } } diff --git a/src/ReflectionEventing.Demo.Wpf/ViewModels/ViewModel.cs b/src/ReflectionEventing.Demo.Wpf/ViewModels/ViewModel.cs new file mode 100644 index 0000000..c403426 --- /dev/null +++ b/src/ReflectionEventing.Demo.Wpf/ViewModels/ViewModel.cs @@ -0,0 +1,31 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. +// All Rights Reserved. + +namespace ReflectionEventing.Demo.Wpf.ViewModels; + +/// +/// Represents an abstract ViewModel base class that provides functionality for dispatching actions on the UI thread. +/// +/// +/// This class extends the ObservableObject class to provide property change notification. +/// +public abstract class ViewModel : ObservableObject +{ + /// + /// Dispatches the specified action on the UI thread. + /// + /// The action to be dispatched. + /// A cancellation token that can be used to cancel the operation. + /// A task that represents the asynchronous operation. + protected static async Task DispatchAsync(Action action, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + await Application.Current.Dispatcher.InvokeAsync(action); + } +} diff --git a/src/ReflectionEventing.DependencyInjection/DependencyInjectionConsumerProvider.cs b/src/ReflectionEventing.DependencyInjection/DependencyInjectionConsumerProvider.cs index b298591..e89d4d0 100644 --- a/src/ReflectionEventing.DependencyInjection/DependencyInjectionConsumerProvider.cs +++ b/src/ReflectionEventing.DependencyInjection/DependencyInjectionConsumerProvider.cs @@ -12,7 +12,7 @@ public class DependencyInjectionConsumerProvider(IServiceProvider serviceProvide : IConsumerProvider { /// - public IEnumerable GetConsumerTypes(Type consumerType) + public IEnumerable GetConsumers(Type consumerType) { if (consumerType is null) { diff --git a/src/ReflectionEventing.DependencyInjection/DependencyInjectionEventBusBuilder.cs b/src/ReflectionEventing.DependencyInjection/DependencyInjectionEventBusBuilder.cs index 124d0d2..84fd2a1 100644 --- a/src/ReflectionEventing.DependencyInjection/DependencyInjectionEventBusBuilder.cs +++ b/src/ReflectionEventing.DependencyInjection/DependencyInjectionEventBusBuilder.cs @@ -13,7 +13,7 @@ namespace ReflectionEventing.DependencyInjection; public class DependencyInjectionEventBusBuilder(IServiceCollection services) : EventBusBuilder { /// - public override void AddConsumer( + public override EventBusBuilder AddConsumer( #if NET5_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] #endif @@ -29,6 +29,6 @@ Type consumerType ); } - base.AddConsumer(consumerType); + return base.AddConsumer(consumerType); } } diff --git a/src/ReflectionEventing.DependencyInjection/ServiceCollectionExtensions.cs b/src/ReflectionEventing.DependencyInjection/ServiceCollectionExtensions.cs index dfe04dc..2bc2b28 100644 --- a/src/ReflectionEventing.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/ReflectionEventing.DependencyInjection/ServiceCollectionExtensions.cs @@ -29,9 +29,7 @@ Action configure configure(builder); - _ = services.AddSingleton( - new HashedConsumerTypesProvider(builder.GetConsumers()) - ); + _ = services.AddSingleton(builder.BuildTypesProvider()); _ = services.AddScoped(); _ = services.AddScoped(); diff --git a/src/ReflectionEventing.Ninject/EventBusModule.cs b/src/ReflectionEventing.Ninject/EventBusModule.cs index 60acd6f..9495330 100644 --- a/src/ReflectionEventing.Ninject/EventBusModule.cs +++ b/src/ReflectionEventing.Ninject/EventBusModule.cs @@ -17,14 +17,18 @@ public class EventBusModule(Action configure) : NinjectM /// public override void Load() { + if (Kernel is null) + { + throw new InvalidOperationException("The kernel is not set."); + } + NinjectEventBusBuilder builder = new(Kernel); configure(builder); _ = Bind() - .To() - .InSingletonScope() - .WithConstructorArgument("consumers", builder.GetConsumers()); + .ToConstant(builder.BuildTypesProvider()) + .InSingletonScope(); _ = Bind().To().InTransientScope(); diff --git a/src/ReflectionEventing.Ninject/NinjectConsumerProvider.cs b/src/ReflectionEventing.Ninject/NinjectConsumerProvider.cs index 8ab1906..44b2349 100644 --- a/src/ReflectionEventing.Ninject/NinjectConsumerProvider.cs +++ b/src/ReflectionEventing.Ninject/NinjectConsumerProvider.cs @@ -13,7 +13,7 @@ namespace ReflectionEventing.Ninject; public class NinjectConsumerProvider(IKernel kernel) : IConsumerProvider { /// - public IEnumerable GetConsumerTypes(Type consumerType) + public IEnumerable GetConsumers(Type consumerType) { if (consumerType is null) { diff --git a/src/ReflectionEventing.Ninject/NinjectEventBusBuilder.cs b/src/ReflectionEventing.Ninject/NinjectEventBusBuilder.cs index e2b0b4f..b369317 100644 --- a/src/ReflectionEventing.Ninject/NinjectEventBusBuilder.cs +++ b/src/ReflectionEventing.Ninject/NinjectEventBusBuilder.cs @@ -13,13 +13,13 @@ namespace ReflectionEventing.Ninject; public class NinjectEventBusBuilder(IKernel kernel) : EventBusBuilder { /// - public override void AddConsumer(Type consumerType) + public override EventBusBuilder AddConsumer(Type consumerType) { if (!kernel.GetBindings(consumerType).Any()) { throw new InvalidOperationException("Event consumer must be registered in the kernel."); } - base.AddConsumer(consumerType); + return base.AddConsumer(consumerType); } } diff --git a/src/ReflectionEventing.Unity/UnityConsumerProvider.cs b/src/ReflectionEventing.Unity/UnityConsumerProvider.cs index 361b4d2..67b450b 100644 --- a/src/ReflectionEventing.Unity/UnityConsumerProvider.cs +++ b/src/ReflectionEventing.Unity/UnityConsumerProvider.cs @@ -13,7 +13,7 @@ namespace ReflectionEventing.Unity; public class UnityConsumerProvider(IUnityContainer container) : IConsumerProvider { /// - public IEnumerable GetConsumerTypes(Type consumerType) + public IEnumerable GetConsumers(Type consumerType) { if (consumerType is null) { diff --git a/src/ReflectionEventing.Unity/UnityContainerExtensions.cs b/src/ReflectionEventing.Unity/UnityContainerExtensions.cs index 089bfbc..b205a3b 100644 --- a/src/ReflectionEventing.Unity/UnityContainerExtensions.cs +++ b/src/ReflectionEventing.Unity/UnityContainerExtensions.cs @@ -28,12 +28,12 @@ public static IUnityContainer AddEventBus( Action configure ) { - UnityEventBusBuilder builder = new UnityEventBusBuilder(container); + UnityEventBusBuilder builder = new(container); configure(builder); - _ = container.RegisterInstance( - new HashedConsumerTypesProvider(builder.GetConsumers()), + _ = container.RegisterInstance( + builder.BuildTypesProvider(), new ContainerControlledLifetimeManager() ); diff --git a/src/ReflectionEventing/EventBus.cs b/src/ReflectionEventing/EventBus.cs index 8a24e01..a12dc17 100644 --- a/src/ReflectionEventing/EventBus.cs +++ b/src/ReflectionEventing/EventBus.cs @@ -16,37 +16,25 @@ public class EventBus( IConsumerTypesProvider consumerTypesProvider ) : IEventBus { - /// - public void Publish(TEvent eventItem) - { - Task.Run(() => - { - using CancellationTokenSource cancellationSource = new(); - - PublishAsync(eventItem, cancellationSource.Token) - .ConfigureAwait(false) - .GetAwaiter() - .GetResult(); - }) - .GetAwaiter() - .GetResult(); - } - /// public async Task PublishAsync(TEvent eventItem, CancellationToken cancellationToken) + where TEvent : class { - List tasks = new(); - IEnumerable consumerTypes = consumerTypesProvider.GetConsumerTypes(typeof(TEvent)); + if (eventItem is null) + { + throw new ArgumentNullException(nameof(eventItem)); + } + + Type eventType = typeof(TEvent); + List tasks = []; + IEnumerable consumerTypes = consumerTypesProvider.GetConsumerTypes(eventType); foreach (Type consumerType in consumerTypes) { - IEnumerable> consumerObjects = consumerProviders - .GetConsumerTypes(consumerType) - .OfType>(); - - tasks.AddRange( - consumerObjects.Select(x => x.ConsumeAsync(eventItem, cancellationToken)) - ); + foreach (object consumer in consumerProviders.GetConsumers(consumerType)) + { + tasks.Add(((IConsumer)consumer).ConsumeAsync(eventItem, cancellationToken)); + } } await Task.WhenAll(tasks).ConfigureAwait(false); diff --git a/src/ReflectionEventing/EventBusBuilder.cs b/src/ReflectionEventing/EventBusBuilder.cs index 2eacfb9..0dedff2 100644 --- a/src/ReflectionEventing/EventBusBuilder.cs +++ b/src/ReflectionEventing/EventBusBuilder.cs @@ -3,28 +3,39 @@ // Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. // All Rights Reserved. -using System.Diagnostics.CodeAnalysis; - namespace ReflectionEventing; /// -/// Represents a class that builds an event bus with a specific set of consumers. +/// Represents a class that builds an event bus with a specific set of classConsumers. /// /// -/// This class uses a dictionary of consumers where the key is the consumer type and the value is a collection of event types that the consumer can handle. +/// This class uses a dictionary of classConsumers where the key is the consumer type and the value is a collection of event types that the consumer can handle. /// public class EventBusBuilder { - private readonly IDictionary> consumers = + private readonly IDictionary> classConsumers = new Dictionary>(); /// - /// Gets the consumers that have been added to the builder. + /// Gets or sets a value indicating whether the event bus should use event polymorphism. + /// If set to true, the event bus will deliver events to classConsumers that handle the event type or any of its base types. + /// If set to false, the event bus will only deliver events to classConsumers that handle the exact event type. + /// The default value is false. /// - /// A dictionary of consumers where the key is the consumer type and the value is a collection of event types that the consumer can handle. - public IDictionary> GetConsumers() + public EventBusBuilderOptions Options { get; } = new(); + + /// + /// Builds and returns an instance of based on the current configuration. + /// + /// + /// An instance of . If is set to true, + /// it returns an instance of , otherwise it returns an instance of . + /// + public IConsumerTypesProvider BuildTypesProvider() { - return consumers; + return Options.UseEventPolymorphism + ? new HashedPolymorphicConsumerTypesProvider(classConsumers) + : new HashedConsumerTypesProvider(classConsumers); } /// @@ -34,9 +45,9 @@ public IDictionary> GetConsumers() /// /// This method checks if the consumer is registered in the service collection and if it is not transient. /// It then gets the interfaces of the consumer that are generic and have a generic type definition of . - /// For each of these interfaces, it gets the generic argument and adds it to the consumers dictionary. + /// For each of these interfaces, it gets the generic argument and adds it to the classConsumers dictionary. /// - public virtual void AddConsumer( + public virtual EventBusBuilder AddConsumer( #if NET5_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] #endif @@ -56,12 +67,14 @@ Type consumerType { Type consumedEventType = consumerInterface.GetGenericArguments()[0]; - if (!consumers.ContainsKey(consumerType)) + if (!classConsumers.ContainsKey(consumerType)) { - consumers[consumerType] = new HashSet(); + classConsumers[consumerType] = new HashSet(); } - _ = ((HashSet)consumers[consumerType]).Add(consumedEventType); + _ = ((HashSet)classConsumers[consumerType]).Add(consumedEventType); } + + return this; } } diff --git a/src/ReflectionEventing/EventBusBuilderExtensions.cs b/src/ReflectionEventing/EventBusBuilderExtensions.cs index f877b54..653dd37 100644 --- a/src/ReflectionEventing/EventBusBuilderExtensions.cs +++ b/src/ReflectionEventing/EventBusBuilderExtensions.cs @@ -3,9 +3,6 @@ // Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. // All Rights Reserved. -using System.Diagnostics.CodeAnalysis; -using System.Reflection; - namespace ReflectionEventing; /// @@ -56,15 +53,30 @@ params Assembly[] assemblies [RequiresUnreferencedCode("Calls System.Reflection.Assembly.GetTypes()")] private static IEnumerable ExtractConsumersFromAssembly(Assembly assembly) { - return assembly - .GetTypes() - .Where(t => - t is { IsClass: true, IsAbstract: false } - && t.GetInterfaces() - .Any(i => - i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IConsumer<>) - ) - ); + Type[] types = assembly.GetTypes(); + + foreach (Type type in types) + { + Type[] typeInterfaces = type.GetInterfaces(); + + if (type.IsAbstract || !type.IsClass) + { + continue; + } + + foreach (Type typeInterface in typeInterfaces) + { + if (!typeInterface.IsGenericType) + { + continue; + } + + if (typeInterface.GetGenericTypeDefinition() == typeof(IConsumer<>)) + { + yield return type; + } + } + } } private static void RegisterAllConsumers(EventBusBuilder builder, IEnumerable consumers) diff --git a/src/ReflectionEventing/EventBusBuilderOptions.cs b/src/ReflectionEventing/EventBusBuilderOptions.cs new file mode 100644 index 0000000..58aa90f --- /dev/null +++ b/src/ReflectionEventing/EventBusBuilderOptions.cs @@ -0,0 +1,21 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. +// All Rights Reserved. + +namespace ReflectionEventing; + +/// +/// Represents configuration options for the class. +/// These options control the behavior of the event bus built by the . +/// +public class EventBusBuilderOptions +{ + /// + /// Gets or sets a value indicating whether the event bus should use event polymorphism. + /// If set to true, the event bus will deliver events to consumers that handle the event type or any of its base types. + /// If set to false, the event bus will only deliver events to consumers that handle the exact event type. + /// The default value is false. + /// + public bool UseEventPolymorphism { get; set; } = false; +} diff --git a/src/ReflectionEventing/EventBusExtensions.cs b/src/ReflectionEventing/EventBusExtensions.cs new file mode 100644 index 0000000..a2678c4 --- /dev/null +++ b/src/ReflectionEventing/EventBusExtensions.cs @@ -0,0 +1,40 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. +// All Rights Reserved. + +namespace ReflectionEventing; + +/// +/// Provides extension methods for the . +/// +public static class EventBusExtensions +{ + /// + /// Publishes the specified event synchronously. + /// + /// The type of the event to publish. + /// The event bus to extend. + /// The event to publish. + [Obsolete($"May cause deadlock on UI threads, use {nameof(IEventBus.PublishAsync)} instead.")] + public static void Publish(this IEventBus eventBus, TEvent eventItem) + where TEvent : class + { + using CancellationTokenSource cancellationSource = new(); + + Task.Run( + () => + { + // ReSharper disable once AccessToDisposedClosure + eventBus + .PublishAsync(eventItem, cancellationSource.Token) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + }, + cancellationSource.Token + ) + .GetAwaiter() + .GetResult(); + } +} diff --git a/src/ReflectionEventing/GlobalUsings.cs b/src/ReflectionEventing/GlobalUsings.cs index 3266876..b59a06c 100644 --- a/src/ReflectionEventing/GlobalUsings.cs +++ b/src/ReflectionEventing/GlobalUsings.cs @@ -5,6 +5,8 @@ global using System; global using System.Collections.Generic; +global using System.Diagnostics.CodeAnalysis; global using System.Linq; +global using System.Reflection; global using System.Threading; global using System.Threading.Tasks; diff --git a/src/ReflectionEventing/HashedConsumerTypesProvider.cs b/src/ReflectionEventing/HashedConsumerTypesProvider.cs index db61e37..8565515 100644 --- a/src/ReflectionEventing/HashedConsumerTypesProvider.cs +++ b/src/ReflectionEventing/HashedConsumerTypesProvider.cs @@ -17,29 +17,16 @@ public class HashedConsumerTypesProvider(IDictionary> co /// public IEnumerable GetConsumerTypes(Type eventType) { + // ReSharper disable once LoopCanBeConvertedToQuery foreach (KeyValuePair> consumer in consumers) { - if (consumer.Value.Contains(eventType)) + foreach (Type consumedEventType in consumer.Value) { - yield return consumer.Key; - - continue; + if (consumedEventType == eventType) + { + yield return consumer.Key; + } } - - // Fallback reflection - if (!consumer.Value.Any(x => ExtendsEvent(x, eventType))) - { - continue; - } - - _ = ((HashSet)consumer.Value)?.Add(eventType); - - yield return consumer.Key; } } - - private static bool ExtendsEvent(Type consumerType, Type eventType) - { - return consumerType.IsAssignableFrom(eventType); - } } diff --git a/src/ReflectionEventing/HashedPolymorphicConsumerTypesProvider.cs b/src/ReflectionEventing/HashedPolymorphicConsumerTypesProvider.cs new file mode 100644 index 0000000..1c9f8dc --- /dev/null +++ b/src/ReflectionEventing/HashedPolymorphicConsumerTypesProvider.cs @@ -0,0 +1,61 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. +// All Rights Reserved. + +namespace ReflectionEventing; + +/// +/// Provides a mechanism for retrieving types of event consumers based on a specific event type. +/// +/// +/// This class uses a dictionary of consumers where the key is the consumer type and the value is a collection of event types that the consumer can handle. +/// +public class HashedPolymorphicConsumerTypesProvider(IDictionary> consumers) + : IConsumerTypesProvider +{ + /// + public IEnumerable GetConsumerTypes(Type eventType) + { + foreach (KeyValuePair> consumer in consumers) + { + bool consumerHasRelatedType = false; + + foreach (Type consumedEventType in consumer.Value) + { + if (consumedEventType == eventType) + { + yield return consumer.Key; + + continue; + } + + if (AreTypesRelated(consumedEventType, eventType)) + { + consumerHasRelatedType = true; + } + } + + if (!consumerHasRelatedType) + { + continue; + } + + if (consumer.Value is HashSet consumersHashSet) + { + _ = consumersHashSet.Add(eventType); + } + else if (consumer.Value is ICollection consumersCollection) + { + consumersCollection.Add(eventType); + } + + yield return consumer.Key; + } + } + + private static bool AreTypesRelated(Type type1, Type type2) + { + return type1.IsAssignableFrom(type2) || type2.IsAssignableFrom(type1); + } +} diff --git a/src/ReflectionEventing/IConsumerProvider.cs b/src/ReflectionEventing/IConsumerProvider.cs index 37611af..37fdbcb 100644 --- a/src/ReflectionEventing/IConsumerProvider.cs +++ b/src/ReflectionEventing/IConsumerProvider.cs @@ -15,15 +15,15 @@ namespace ReflectionEventing; public interface IConsumerProvider { /// - /// Gets the types of all consumers for the specified event type. + /// Gets the consumers objects for the specified event type. /// /// The type of the event that the consumers handle. - /// An enumerable of 's that are consumers of the specified event type. + /// An enumerable of 's that are consumers of the specified event type. /// /// /// Type consumerType = typeof(MyEvent); - /// IEnumerable<object> consumerTypes = consumerProvider.GetConsumerTypes(consumerType); + /// IEnumerable<object> consumerTypes = consumerProvider.GetConsumers(consumerType); /// /// - IEnumerable GetConsumerTypes(Type consumerType); + IEnumerable GetConsumers(Type consumerType); } diff --git a/src/ReflectionEventing/IConsumerTypesProvider.cs b/src/ReflectionEventing/IConsumerTypesProvider.cs index d6246d5..304b3ce 100644 --- a/src/ReflectionEventing/IConsumerTypesProvider.cs +++ b/src/ReflectionEventing/IConsumerTypesProvider.cs @@ -14,7 +14,7 @@ namespace ReflectionEventing; public interface IConsumerTypesProvider { /// - /// Gets the consumers for the specified event type. + /// Gets the consumer types for the specified event type. /// /// A collection of consumer types that can handle the specified event type. IEnumerable GetConsumerTypes(Type eventType); diff --git a/src/ReflectionEventing/IEventBus.cs b/src/ReflectionEventing/IEventBus.cs index e3dde9d..2dea8e3 100644 --- a/src/ReflectionEventing/IEventBus.cs +++ b/src/ReflectionEventing/IEventBus.cs @@ -10,14 +10,6 @@ namespace ReflectionEventing; /// public interface IEventBus { - /// - /// Publishes the specified event synchronously. - /// - /// The type of the event to publish. - /// The event to publish. - [Obsolete($"May cause deadlock on UI threads, use {nameof(PublishAsync)} instead.")] - void Publish(TEvent eventItem); - /// /// Publishes the specified event asynchronously. /// @@ -29,5 +21,6 @@ public interface IEventBus /// This method gets the consumers for the specified event type from the consumer provider and then uses the service provider to get the required service for each consumer. /// Each consumer is then used to consume the event asynchronously. /// - Task PublishAsync(TEvent eventItem, CancellationToken cancellationToken); + Task PublishAsync(TEvent eventItem, CancellationToken cancellationToken) + where TEvent : class; } diff --git a/tests/ReflectionEventing.Autofac.UnitTests/AutofacConsumerProviderTests.cs b/tests/ReflectionEventing.Autofac.UnitTests/AutofacConsumerProviderTests.cs index 28b35fa..2c338bc 100644 --- a/tests/ReflectionEventing.Autofac.UnitTests/AutofacConsumerProviderTests.cs +++ b/tests/ReflectionEventing.Autofac.UnitTests/AutofacConsumerProviderTests.cs @@ -15,7 +15,7 @@ public void GetConsumerTypes_ShouldThrowExceptionWhenConsumerTypeIsNull() ILifetimeScope lifetimeScope = Substitute.For(); AutofacConsumerProvider consumerProvider = new AutofacConsumerProvider(lifetimeScope); - Action act = () => consumerProvider.GetConsumerTypes(null!); + Action act = () => consumerProvider.GetConsumers(null!); _ = act.Should() .Throw() @@ -34,9 +34,7 @@ public void GetConsumerTypes_ShouldReturnResolvedConsumerType() AutofacConsumerProvider consumerProvider = new AutofacConsumerProvider(scope); - IEnumerable actualConsumers = consumerProvider.GetConsumerTypes( - typeof(TestConsumer) - ); + IEnumerable actualConsumers = consumerProvider.GetConsumers(typeof(TestConsumer)); _ = actualConsumers.First().Should().Be(testInstance); } diff --git a/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionConsumerProviderTests.cs b/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionConsumerProviderTests.cs index 381dc12..1e453c3 100644 --- a/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionConsumerProviderTests.cs +++ b/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionConsumerProviderTests.cs @@ -15,7 +15,7 @@ public void GetConsumerTypes_ShouldThrowExceptionWhenConsumerTypeIsNull() IServiceProvider serviceProvider = Substitute.For(); DependencyInjectionConsumerProvider consumerProvider = new(serviceProvider); - Action act = () => consumerProvider.GetConsumerTypes(null!); + Action act = () => consumerProvider.GetConsumers(null!); _ = act.Should() .Throw() @@ -30,9 +30,7 @@ public void GetConsumerTypes_ShouldReturnServicesOfConsumerType() _ = services.AddSingleton(); DependencyInjectionConsumerProvider consumerProvider = new(services.BuildServiceProvider()); - IEnumerable actualConsumers = consumerProvider.GetConsumerTypes( - typeof(TestConsumer) - ); + IEnumerable actualConsumers = consumerProvider.GetConsumers(typeof(TestConsumer)); _ = actualConsumers.Should().HaveCount(2); } diff --git a/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionEventBusBuilderTests.cs b/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionEventBusBuilderTests.cs index ea96d67..8bc0497 100644 --- a/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionEventBusBuilderTests.cs +++ b/tests/ReflectionEventing.DependencyInjection.UnitTests/DependencyInjectionEventBusBuilderTests.cs @@ -13,9 +13,7 @@ public sealed class DependencyInjectionEventBusBuilderTests public void AddConsumer_ShouldThrowExceptionWhenConsumerNotRegistered() { IServiceCollection services = new ServiceCollection(); - DependencyInjectionEventBusBuilder eventBusBuilder = new DependencyInjectionEventBusBuilder( - services - ); + DependencyInjectionEventBusBuilder eventBusBuilder = new(services); Action act = () => eventBusBuilder.AddConsumer(typeof(TestConsumer)); @@ -29,17 +27,20 @@ public void AddConsumer_ShouldAddConsumerToDictionaryWhenRegistered() { IServiceCollection services = new ServiceCollection(); _ = services.AddSingleton(); - DependencyInjectionEventBusBuilder eventBusBuilder = new DependencyInjectionEventBusBuilder( - services - ); + DependencyInjectionEventBusBuilder eventBusBuilder = new(services); eventBusBuilder.AddConsumer(typeof(TestConsumer)); - IDictionary> consumers = eventBusBuilder.GetConsumers(); - _ = consumers.Should().ContainKey(typeof(TestConsumer)); + IConsumerTypesProvider consumerTypesProvider = eventBusBuilder.BuildTypesProvider(); + _ = consumerTypesProvider + .GetConsumerTypes(typeof(TestEvent)) + .Should() + .Contain(typeof(TestConsumer)); } - public record TestEvent; + public interface ITestEvent; + + public record TestEvent : ITestEvent; public class TestConsumer : IConsumer { diff --git a/tests/ReflectionEventing.UnitTests/EventBusBuilderTests.cs b/tests/ReflectionEventing.UnitTests/EventBusBuilderTests.cs index aa396b0..3ff27da 100644 --- a/tests/ReflectionEventing.UnitTests/EventBusBuilderTests.cs +++ b/tests/ReflectionEventing.UnitTests/EventBusBuilderTests.cs @@ -3,41 +3,109 @@ // Copyright (C) Leszek Pomianowski and ReflectionEventing Contributors. // All Rights Reserved. +using FluentAssertions.Collections; + namespace ReflectionEventing.UnitTests; public sealed class EventBusBuilderTests { - private readonly EventBusBuilder _eventBusBuilder = new EventBusBuilder(); - [Fact] public void AddConsumer_ShouldAddConsumerToDictionary() { Type consumerType = typeof(MySampleConsumer); - _eventBusBuilder.AddConsumer(consumerType); + EventBusBuilder eventBusBuilder = new(); + eventBusBuilder.AddConsumer(consumerType); - IDictionary> consumers = _eventBusBuilder.GetConsumers(); - _ = consumers.Should().ContainKey(consumerType); + IConsumerTypesProvider typesProvider = eventBusBuilder.BuildTypesProvider(); + _ = typesProvider.GetConsumerTypes(typeof(TestEvent)).Should().Contain(consumerType); + _ = typesProvider.GetConsumerTypes(typeof(SecondaryEvent)).Should().Contain(consumerType); } [Fact] - public void AddConsumer_ShouldAddEventTypeToConsumerInDictionary() + public void AddConsumer_ShouldNotReturnConsumersTypes_WithoutPolymorphismEnabled() { Type consumerType = typeof(MySampleConsumer); - _eventBusBuilder.AddConsumer(consumerType); + EventBusBuilder eventBusBuilder = new(); + eventBusBuilder.AddConsumer(consumerType); - IDictionary> consumers = _eventBusBuilder.GetConsumers(); - _ = consumers[consumerType].Should().Contain(typeof(TestEvent)); + IConsumerTypesProvider typesProvider = eventBusBuilder.BuildTypesProvider(); + GenericCollectionAssertions? consumers = typesProvider + .GetConsumerTypes(typeof(ITestEvent)) + .Should(); + consumers.HaveCount(0); } - public sealed record TestEvent; + [Fact] + public void AddConsumer_ShouldReturnConsumersTypes_WhenPolymorphismEnabled() + { + Type consumerType = typeof(MySampleConsumer); + + EventBusBuilder eventBusBuilder = new(); + eventBusBuilder.Options.UseEventPolymorphism = true; + eventBusBuilder.AddConsumer(consumerType); + + IConsumerTypesProvider typesProvider = eventBusBuilder.BuildTypesProvider(); + + GenericCollectionAssertions? consumers = typesProvider + .GetConsumerTypes(typeof(ITestEvent)) + .Should(); + consumers.HaveCount(1).And.Contain(consumerType); + } - public sealed record MySampleConsumer : IConsumer + [Fact] + public void AddConsumer_ShouldReturnMultipleConsumersTypes_WhenPolymorphismEnabled() + { + Type primaryConsumerType = typeof(MySampleConsumer); + Type secondaryConsumerType = typeof(MySecondarySampleConsumer); + + EventBusBuilder eventBusBuilder = new(); + eventBusBuilder.Options.UseEventPolymorphism = true; + eventBusBuilder.AddConsumer(primaryConsumerType); + eventBusBuilder.AddConsumer(secondaryConsumerType); + + IConsumerTypesProvider typesProvider = eventBusBuilder.BuildTypesProvider(); + + GenericCollectionAssertions? consumers = typesProvider + .GetConsumerTypes(typeof(IBaseEvent)) + .Should(); + consumers.HaveCount(2).And.Contain(primaryConsumerType).And.Contain(secondaryConsumerType); + } + + public interface IBaseEvent; + + public interface ITestEvent; + + public sealed record TestEvent : ITestEvent, IBaseEvent; + + public sealed record SecondaryEvent : IBaseEvent; + + public sealed record MySampleConsumer : IConsumer, IConsumer { public Task ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) { return Task.CompletedTask; } + + /// + public Task ConsumeAsync(SecondaryEvent payload, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } + + public sealed record MySecondarySampleConsumer : IConsumer + { + public Task ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + /// + public Task ConsumeAsync(SecondaryEvent payload, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } } } diff --git a/tests/ReflectionEventing.UnitTests/EventBusTests.cs b/tests/ReflectionEventing.UnitTests/EventBusTests.cs index ef42a19..f73b84b 100644 --- a/tests/ReflectionEventing.UnitTests/EventBusTests.cs +++ b/tests/ReflectionEventing.UnitTests/EventBusTests.cs @@ -26,7 +26,7 @@ public async Task PublishAsync_ShouldCallConsumeAsyncOnAllConsumers() IConsumer consumer = Substitute.For>(); _ = _consumerTypesProvider.GetConsumerTypes().Returns([consumerType]); - _ = _consumerProvider.GetConsumerTypes(consumerType).Returns([consumer]); + _ = _consumerProvider.GetConsumers(consumerType).Returns([consumer]); await _eventBus.PublishAsync(testEvent, CancellationToken.None); @@ -34,6 +34,7 @@ public async Task PublishAsync_ShouldCallConsumeAsyncOnAllConsumers() } [Fact] +#pragma warning disable CS0618 // Type or member is obsolete public async Task Publish_ShouldCallPublishAsync() { TestEvent testEvent = new(); @@ -41,12 +42,13 @@ public async Task Publish_ShouldCallPublishAsync() IConsumer consumer = Substitute.For>(); _ = _consumerTypesProvider.GetConsumerTypes().Returns([consumerType]); - _ = _consumerProvider.GetConsumerTypes(consumerType).Returns([consumer]); + _ = _consumerProvider.GetConsumers(consumerType).Returns([consumer]); _eventBus.Publish(testEvent); await consumer.Received().ConsumeAsync(testEvent, Arg.Any()); } +#pragma warning restore CS0618 // Type or member is obsolete public sealed record TestEvent; } diff --git a/tests/ReflectionEventing.UnitTests/HashedConsumerTypesProviderTests.cs b/tests/ReflectionEventing.UnitTests/HashedConsumerTypesProviderTests.cs index 5276148..ad0fee6 100644 --- a/tests/ReflectionEventing.UnitTests/HashedConsumerTypesProviderTests.cs +++ b/tests/ReflectionEventing.UnitTests/HashedConsumerTypesProviderTests.cs @@ -14,28 +14,42 @@ public void GetConsumerTypes_ShouldReturnCollectionOfConsumers() new( new Dictionary> { - { typeof(PrimarySampleConsumer), [typeof(TestEvent)] }, - { typeof(SecondarySampleConsumer), [typeof(TestEvent)] }, + { typeof(PrimarySampleConsumer), [typeof(PrimaryTestEvent)] }, + { typeof(SecondarySampleConsumer), [typeof(PrimaryTestEvent)] }, + { typeof(TertiarySampleConsumer), [typeof(SecondaryTestEvent)] } } ); - IEnumerable consumers = testEvent.GetConsumerTypes(); + IEnumerable consumers = testEvent.GetConsumerTypes(); - _ = consumers.Should().HaveCount(2); - _ = consumers.Should().Contain(typeof(PrimarySampleConsumer)); + _ = consumers + .Should() + .HaveCount(2) + .And.Contain(typeof(PrimarySampleConsumer)) + .And.Contain(typeof(SecondarySampleConsumer)); } - public sealed record TestEvent; + private interface ITestEvent; - public sealed record PrimarySampleConsumer : IConsumer + private sealed record PrimaryTestEvent : ITestEvent; + + private sealed record SecondaryTestEvent : ITestEvent; + + private sealed record PrimarySampleConsumer : IConsumer + { + public Task ConsumeAsync(PrimaryTestEvent payload, CancellationToken cancellationToken) => + Task.CompletedTask; + } + + private sealed record SecondarySampleConsumer : IConsumer { - public Task ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => + public Task ConsumeAsync(PrimaryTestEvent payload, CancellationToken cancellationToken) => Task.CompletedTask; } - public sealed record SecondarySampleConsumer : IConsumer + private sealed record TertiarySampleConsumer : IConsumer { - public Task ConsumeAsync(TestEvent payload, CancellationToken cancellationToken) => + public Task ConsumeAsync(SecondaryTestEvent payload, CancellationToken cancellationToken) => Task.CompletedTask; } }