From 2d875c960db4e98decb7ee76760a50b6f5e43e21 Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 13 Nov 2023 10:27:59 +0800 Subject: [PATCH 01/37] Add extension methods to determines whether the specified type is implements a interface. --- Directory.Packages.props | 29 +- .../Euonia.Core/Extensions/Extensions.Type.cs | 329 ++++++++++-------- 2 files changed, 197 insertions(+), 161 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ce1aa4e..397101c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,15 +2,16 @@ true true - 6.0.23 - 7.0.12 + 6.0.24 + 7.0.13 + 2.59.0 - + @@ -22,7 +23,7 @@ - + @@ -31,13 +32,13 @@ - + - + @@ -97,20 +98,22 @@ - + - - - + + + + + - - + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Source/Euonia.Core/Extensions/Extensions.Type.cs b/Source/Euonia.Core/Extensions/Extensions.Type.cs index f8769b9..0775c60 100644 --- a/Source/Euonia.Core/Extensions/Extensions.Type.cs +++ b/Source/Euonia.Core/Extensions/Extensions.Type.cs @@ -3,152 +3,185 @@ public static partial class Extensions { - /// - /// - /// - /// - /// - public static string GetFullNameWithAssemblyName(this Type type) - { - return type.FullName + ", " + type.Assembly.GetName().Name; - } - - /// - /// Determines whether an instance of this type can be assigned to - /// an instance of the . - /// Internally uses . - /// - /// Target type (as reverse). - public static bool IsAssignableTo([NotNull] this Type type) - { - Check.EnsureNotNull(type, nameof(type)); - - return type.IsAssignableTo(typeof(TTarget)); - } - - /// - /// Determines whether an instance of this type can be assigned to - /// an instance of the . - /// Internally uses (as reverse). - /// - /// this type - /// Target type - public static bool IsAssignableTo([NotNull] this Type type, [NotNull] Type targetType) - { - Check.EnsureNotNull(type, nameof(type)); - Check.EnsureNotNull(targetType, nameof(targetType)); - - return targetType.IsAssignableFrom(type); - } - - /// - /// Gets all base classes of this type. - /// - /// The type to get its base classes. - /// True, to include the standard type in the returned array. - public static Type[] GetBaseClasses([NotNull] this Type type, bool includeObject = true) - { - Check.EnsureNotNull(type, nameof(type)); - - var types = new List(); - AddTypeAndBaseTypesRecursively(types, type.BaseType, includeObject); - return types.ToArray(); - } - - /// - /// Gets all base classes of this type. - /// - /// The type to get its base classes. - /// A type to stop going to the deeper base classes. This type will be be included in the returned array - /// True, to include the standard type in the returned array. - public static Type[] GetBaseClasses([NotNull] this Type type, Type stoppingType, bool includeObject = true) - { - Check.EnsureNotNull(type, nameof(type)); - - var types = new List(); - AddTypeAndBaseTypesRecursively(types, type.BaseType, includeObject, stoppingType); - return types.ToArray(); - } - - private static void AddTypeAndBaseTypesRecursively([NotNull] ICollection types, Type type, bool includeObject, Type stoppingType = null) - { - if (type == null || type == stoppingType) - { - return; - } - - if (!includeObject && type == typeof(object)) - { - return; - } - - AddTypeAndBaseTypesRecursively(types, type.BaseType, includeObject, stoppingType); - types.Add(type); - } - - /// - /// Detect whether the method is async method. - /// - /// - /// - /// - public static bool IsAsync([NotNull] this MethodInfo method) - { - if (method == null) - { - throw new NullReferenceException("The method instance is null."); - } - - var returnType = method.ReturnType; - return returnType == typeof(Task) || (returnType.IsGenericType && returnType.GetInterfaces().Any(type => type == typeof(IAsyncResult))); - } - - /// - /// Gets the property type. - /// - /// - /// - public static Type GetPropertyType(this Type propertyType) - { - if (propertyType.IsGenericType && (propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))) - { - return Nullable.GetUnderlyingType(propertyType); - } - - return propertyType; - } - - /// - /// Detect whether the specified type is extends the target type. - /// - /// - /// - /// - public static bool IsExtends(this Type type) - { - return type.IsExtends(typeof(T)); - } - - /// - /// Detect whether the specified type is extends the target type. - /// - /// - /// - /// - public static bool IsExtends(this Type type, Type targetType) - { - var baseType = type.BaseType; - - while (baseType != typeof(object)) - { - if (baseType == targetType) - { - return true; - } - - baseType = type.BaseType; - } - - return false; - } + /// + /// + /// + /// + /// + public static string GetFullNameWithAssemblyName(this Type type) + { + return type.FullName + ", " + type.Assembly.GetName().Name; + } + + /// + /// Determines whether an instance of this type can be assigned to + /// an instance of the . + /// Internally uses . + /// + /// Target type (as reverse). + public static bool IsAssignableTo([NotNull] this Type type) + { + Check.EnsureNotNull(type, nameof(type)); + + return type.IsAssignableTo(typeof(TTarget)); + } + + /// + /// Determines whether an instance of this type can be assigned to + /// an instance of the . + /// Internally uses (as reverse). + /// + /// this type + /// Target type + public static bool IsAssignableTo([NotNull] this Type type, [NotNull] Type targetType) + { + Check.EnsureNotNull(type, nameof(type)); + Check.EnsureNotNull(targetType, nameof(targetType)); + + return targetType.IsAssignableFrom(type); + } + + /// + /// Gets all base classes of this type. + /// + /// The type to get its base classes. + /// True, to include the standard type in the returned array. + public static Type[] GetBaseClasses([NotNull] this Type type, bool includeObject = true) + { + Check.EnsureNotNull(type, nameof(type)); + + var types = new List(); + AddTypeAndBaseTypesRecursively(types, type.BaseType, includeObject); + return types.ToArray(); + } + + /// + /// Gets all base classes of this type. + /// + /// The type to get its base classes. + /// A type to stop going to the deeper base classes. This type will be be included in the returned array + /// True, to include the standard type in the returned array. + public static Type[] GetBaseClasses([NotNull] this Type type, Type stoppingType, bool includeObject = true) + { + Check.EnsureNotNull(type, nameof(type)); + + var types = new List(); + AddTypeAndBaseTypesRecursively(types, type.BaseType, includeObject, stoppingType); + return types.ToArray(); + } + + private static void AddTypeAndBaseTypesRecursively([NotNull] ICollection types, Type type, bool includeObject, Type stoppingType = null) + { + if (type == null || type == stoppingType) + { + return; + } + + if (!includeObject && type == typeof(object)) + { + return; + } + + AddTypeAndBaseTypesRecursively(types, type.BaseType, includeObject, stoppingType); + types.Add(type); + } + + /// + /// Detect whether the method is async method. + /// + /// + /// + /// + public static bool IsAsync([NotNull] this MethodInfo method) + { + if (method == null) + { + throw new NullReferenceException("The method instance is null."); + } + + var returnType = method.ReturnType; + return returnType == typeof(Task) || (returnType.IsGenericType && returnType.GetInterfaces().Any(type => type == typeof(IAsyncResult))); + } + + /// + /// Gets the property type. + /// + /// + /// + public static Type GetPropertyType(this Type propertyType) + { + if (propertyType.IsGenericType && (propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))) + { + return Nullable.GetUnderlyingType(propertyType); + } + + return propertyType; + } + + /// + /// Detect whether the specified type is extends the target type. + /// + /// + /// + /// + public static bool IsExtends(this Type type) + { + return type.IsExtends(typeof(T)); + } + + /// + /// Detect whether the specified type is extends the target type. + /// + /// + /// + /// + public static bool IsExtends(this Type type, Type targetType) + { + var baseType = type.BaseType; + + while (baseType != typeof(object)) + { + if (baseType == targetType) + { + return true; + } + + baseType = type.BaseType; + } + + return false; + } + + /// + /// Determines whether the specified type is implements the target type. + /// + /// The implementing type. + /// The target type. + /// + public static bool IsImplements(this Type type) + { + return type.IsAssignableTo(typeof(T)); + } + + /// + /// Determines whether the specified type is implements the target type. + /// + /// The implementing type. + /// The target type. + /// + public static bool IsImplements(this Type type, Type targetType) + { + return type.IsAssignableTo(targetType); + } + + /// + /// Determines whether the specified type is implements the interface with generic type. + /// + /// The implementing type. + /// The target type. + /// + public static bool IsImplementsGeneric(this Type type, Type targetType) + { + return type.GetInterfaces().Any(f => f.IsGenericType && f.GetGenericTypeDefinition() == targetType); + } } \ No newline at end of file From 0afac20fbd760803fb852cf738da609dff36873c Mon Sep 17 00:00:00 2001 From: damon Date: Wed, 15 Nov 2023 21:51:58 +0800 Subject: [PATCH 02/37] Add extension method to determines primitive type. --- .../Euonia.Core/Extensions/Extensions.Type.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Source/Euonia.Core/Extensions/Extensions.Type.cs b/Source/Euonia.Core/Extensions/Extensions.Type.cs index 0775c60..eb19490 100644 --- a/Source/Euonia.Core/Extensions/Extensions.Type.cs +++ b/Source/Euonia.Core/Extensions/Extensions.Type.cs @@ -3,6 +3,20 @@ public static partial class Extensions { + private static readonly Type[] _primitiveTypes = + { + typeof(string), + typeof(decimal), + typeof(DateTime), + typeof(DateTimeOffset), + typeof(TimeSpan), + typeof(Guid), +#if NET5_0_OR_GREATER + typeof(DateOnly), + typeof(TimeOnly), +#endif + }; + /// /// /// @@ -184,4 +198,17 @@ public static bool IsImplementsGeneric(this Type type, Type targetType) { return type.GetInterfaces().Any(f => f.IsGenericType && f.GetGenericTypeDefinition() == targetType); } + + /// + /// Determines whether the specified type is primitive type. + /// + /// The type to detect. + /// + /// true if the specified type is primitive type; otherwise, false. + /// + public static bool IsPrimitiveType(this Type type) + { + Check.EnsureNotNull(type, nameof(type)); + return type.IsPrimitive || type.IsEnum || type.IsIn(_primitiveTypes); + } } \ No newline at end of file From 9302221d18b3ce00a3abec8d244dc7a6d44c10ee Mon Sep 17 00:00:00 2001 From: damon Date: Sun, 19 Nov 2023 15:35:15 +0800 Subject: [PATCH 03/37] Add Euonia.Bus.Abstract.csproj & Euonia.Bus.ActiveMq.csproj --- Euonia.sln | 18 ++++ .../Euonia.Bus.Abstract.csproj | 25 +++++ .../Properties/AssemblyInfo.cs | 3 + .../Properties/Resources.resx | 101 ++++++++++++++++++ .../Euonia.Bus.ActiveMq.csproj | 33 ++++++ .../Properties/Resources.resx | 101 ++++++++++++++++++ .../Properties/Resources.zh-CN.resx | 101 ++++++++++++++++++ 7 files changed, 382 insertions(+) create mode 100644 Source/Euonia.Bus.Abstract/Euonia.Bus.Abstract.csproj create mode 100644 Source/Euonia.Bus.Abstract/Properties/AssemblyInfo.cs create mode 100644 Source/Euonia.Bus.Abstract/Properties/Resources.resx create mode 100644 Source/Euonia.Bus.ActiveMq/Euonia.Bus.ActiveMq.csproj create mode 100644 Source/Euonia.Bus.ActiveMq/Properties/Resources.resx create mode 100644 Source/Euonia.Bus.ActiveMq/Properties/Resources.zh-CN.resx diff --git a/Euonia.sln b/Euonia.sln index a47e93b..957716f 100644 --- a/Euonia.sln +++ b/Euonia.sln @@ -117,6 +117,10 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Euonia.Mapping.Tests.Shared EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Euonia.Core.Tests", "Tests\Euonia.Core.Tests\Euonia.Core.Tests.csproj", "{81D1403E-24B7-47DD-BD55-1D22B8E7756B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Euonia.Bus.ActiveMq", "Source\Euonia.Bus.ActiveMq\Euonia.Bus.ActiveMq.csproj", "{EFABA5DF-BD24-4880-A9FE-242AACF5B599}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Euonia.Bus.Abstract", "Source\Euonia.Bus.Abstract\Euonia.Bus.Abstract.csproj", "{31CDB3D0-A7CE-435F-A2A8-CD5F9E7EB8B2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -340,6 +344,18 @@ Global {81D1403E-24B7-47DD-BD55-1D22B8E7756B}.Product|Any CPU.Build.0 = Release|Any CPU {81D1403E-24B7-47DD-BD55-1D22B8E7756B}.Release|Any CPU.ActiveCfg = Release|Any CPU {81D1403E-24B7-47DD-BD55-1D22B8E7756B}.Release|Any CPU.Build.0 = Release|Any CPU + {EFABA5DF-BD24-4880-A9FE-242AACF5B599}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFABA5DF-BD24-4880-A9FE-242AACF5B599}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFABA5DF-BD24-4880-A9FE-242AACF5B599}.Product|Any CPU.ActiveCfg = Debug|Any CPU + {EFABA5DF-BD24-4880-A9FE-242AACF5B599}.Product|Any CPU.Build.0 = Debug|Any CPU + {EFABA5DF-BD24-4880-A9FE-242AACF5B599}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFABA5DF-BD24-4880-A9FE-242AACF5B599}.Release|Any CPU.Build.0 = Release|Any CPU + {31CDB3D0-A7CE-435F-A2A8-CD5F9E7EB8B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31CDB3D0-A7CE-435F-A2A8-CD5F9E7EB8B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31CDB3D0-A7CE-435F-A2A8-CD5F9E7EB8B2}.Product|Any CPU.ActiveCfg = Debug|Any CPU + {31CDB3D0-A7CE-435F-A2A8-CD5F9E7EB8B2}.Product|Any CPU.Build.0 = Debug|Any CPU + {31CDB3D0-A7CE-435F-A2A8-CD5F9E7EB8B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31CDB3D0-A7CE-435F-A2A8-CD5F9E7EB8B2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -391,6 +407,8 @@ Global {34B067D7-7126-4B02-A4E8-E1AFC77F3485} = {0A6E75E4-2AD5-49F3-9120-467A71B471B0} {DE31E135-48A1-40D8-AFEF-768CEBB4819A} = {0A6E75E4-2AD5-49F3-9120-467A71B471B0} {81D1403E-24B7-47DD-BD55-1D22B8E7756B} = {E048931D-EC51-448A-A737-3C62CF100813} + {EFABA5DF-BD24-4880-A9FE-242AACF5B599} = {273D1F47-F6AF-4ED5-AAB5-977BD9906B2E} + {31CDB3D0-A7CE-435F-A2A8-CD5F9E7EB8B2} = {273D1F47-F6AF-4ED5-AAB5-977BD9906B2E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {84CDDCF4-F3D0-45FC-87C5-557845F58F55} diff --git a/Source/Euonia.Bus.Abstract/Euonia.Bus.Abstract.csproj b/Source/Euonia.Bus.Abstract/Euonia.Bus.Abstract.csproj new file mode 100644 index 0000000..58ebc95 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Euonia.Bus.Abstract.csproj @@ -0,0 +1,25 @@ + + + + + + Nerosoft.Euonia.Bus + disable + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/Source/Euonia.Bus.Abstract/Properties/AssemblyInfo.cs b/Source/Euonia.Bus.Abstract/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..f01cb4d --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Euonia.Bus")] \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Properties/Resources.resx b/Source/Euonia.Bus.Abstract/Properties/Resources.resx new file mode 100644 index 0000000..4fdb1b6 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Properties/Resources.resx @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Source/Euonia.Bus.ActiveMq/Euonia.Bus.ActiveMq.csproj b/Source/Euonia.Bus.ActiveMq/Euonia.Bus.ActiveMq.csproj new file mode 100644 index 0000000..60e8711 --- /dev/null +++ b/Source/Euonia.Bus.ActiveMq/Euonia.Bus.ActiveMq.csproj @@ -0,0 +1,33 @@ + + + + + + disable + + + + + + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/Source/Euonia.Bus.ActiveMq/Properties/Resources.resx b/Source/Euonia.Bus.ActiveMq/Properties/Resources.resx new file mode 100644 index 0000000..4fdb1b6 --- /dev/null +++ b/Source/Euonia.Bus.ActiveMq/Properties/Resources.resx @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Source/Euonia.Bus.ActiveMq/Properties/Resources.zh-CN.resx b/Source/Euonia.Bus.ActiveMq/Properties/Resources.zh-CN.resx new file mode 100644 index 0000000..4fdb1b6 --- /dev/null +++ b/Source/Euonia.Bus.ActiveMq/Properties/Resources.zh-CN.resx @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file From 450ae20794ebc200dbc2202a020110104a9637af Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 20 Nov 2023 00:00:10 +0800 Subject: [PATCH 04/37] Config ActiveMq project. --- Directory.Packages.props | 3 ++- .../Euonia.Bus.ActiveMq.csproj | 25 +++++++++++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 397101c..685b4c0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,6 +7,7 @@ 2.59.0 + @@ -112,7 +113,7 @@ - + all diff --git a/Source/Euonia.Bus.ActiveMq/Euonia.Bus.ActiveMq.csproj b/Source/Euonia.Bus.ActiveMq/Euonia.Bus.ActiveMq.csproj index 60e8711..ad9ed16 100644 --- a/Source/Euonia.Bus.ActiveMq/Euonia.Bus.ActiveMq.csproj +++ b/Source/Euonia.Bus.ActiveMq/Euonia.Bus.ActiveMq.csproj @@ -1,20 +1,25 @@ - - - + + + disable - + - - + + + + + + - + - + + - + True @@ -22,7 +27,7 @@ Resources.resx - + ResXFileCodeGenerator From 10b06c4ab892ec5a82deccb612bfdd2dd7edd2aa Mon Sep 17 00:00:00 2001 From: damon Date: Wed, 22 Nov 2023 21:24:27 +0800 Subject: [PATCH 05/37] Add ArgumentAssert class. --- .../Euonia.Core/Extensions/Extensions.Type.cs | 13 +++ Source/Euonia.Core/System/ArgumentAssert.cs | 67 +++++++++++++++ Source/Euonia.Core/System/Gen2GcCallback.cs | 85 +++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 Source/Euonia.Core/System/ArgumentAssert.cs create mode 100644 Source/Euonia.Core/System/Gen2GcCallback.cs diff --git a/Source/Euonia.Core/Extensions/Extensions.Type.cs b/Source/Euonia.Core/Extensions/Extensions.Type.cs index eb19490..8344085 100644 --- a/Source/Euonia.Core/Extensions/Extensions.Type.cs +++ b/Source/Euonia.Core/Extensions/Extensions.Type.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.CompilerServices; public static partial class Extensions { @@ -211,4 +212,16 @@ public static bool IsPrimitiveType(this Type type) Check.EnsureNotNull(type, nameof(type)); return type.IsPrimitive || type.IsEnum || type.IsIn(_primitiveTypes); } + + /// + /// Determines whether the specified type is anonymous type. + /// + /// The type to detect. + /// + /// true if the specified type is anonymous type; otherwise, false. + /// + public static bool IsAnonymousType(this Type type) + { + return type.FullName != null && type.HasAttribute() && type.FullName.Contains("AnonymousType"); + } } \ No newline at end of file diff --git a/Source/Euonia.Core/System/ArgumentAssert.cs b/Source/Euonia.Core/System/ArgumentAssert.cs new file mode 100644 index 0000000..816249f --- /dev/null +++ b/Source/Euonia.Core/System/ArgumentAssert.cs @@ -0,0 +1,67 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace System; + +/// +/// Internal polyfill for . +/// +public sealed class ArgumentAssert +{ + /// + /// Throws an if is . + /// + /// The reference type argument to validate as non-. + /// The name of the parameter with which corresponds. + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET5_0_OR_GREATER + public static void ThrowIfNull([NotNull] object argument, [CallerArgumentExpression(nameof(argument))] string paramName = null) +#else + public static void ThrowIfNull([NotNull] object argument, string paramName = null) +#endif + { + if (argument is null) + { + Throw(paramName); + } + } + + /// + /// A specialized version for generic values. + /// + /// The type of values to check. + /// + /// This type is needed because if there had been a generic overload with a generic parameter, all calls + /// would have just been bound by that by the compiler instead of the overload. + /// + public static class For + { + /// + /// Throws an if is . + /// + /// The reference type argument to validate as non-. + /// The name of the parameter with which corresponds. + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#if NET5_0_OR_GREATER + public static void ThrowIfNull([NotNull] T argument, [CallerArgumentExpression(nameof(argument))] string paramName = null) +#else + public static void ThrowIfNull([NotNull] T argument, string paramName = null) +#endif + { + if (argument is null) + { + Throw(paramName); + } + } + } + + /// + /// Throws an . + /// + /// The name of the parameter that failed validation. + [DoesNotReturn] + private static void Throw(string paramName) + { + throw new ArgumentNullException(paramName); + } +} \ No newline at end of file diff --git a/Source/Euonia.Core/System/Gen2GcCallback.cs b/Source/Euonia.Core/System/Gen2GcCallback.cs new file mode 100644 index 0000000..fdae4f1 --- /dev/null +++ b/Source/Euonia.Core/System/Gen2GcCallback.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; + +namespace System; + +/// +/// Schedules a callback roughly every gen 2 GC (you may see a Gen 0 an Gen 1 but only once). +/// Ported from https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Gen2GcCallback.cs. +/// +public sealed class Gen2GcCallback : CriticalFinalizerObject +{ + /// + /// The callback to invoke at each GC. + /// + private readonly Action _callback; + + /// + /// A weak to the target object to pass to . + /// + private GCHandle _handle; + + /// + /// Initializes a new instance of the class. + /// + /// The callback to invoke at each GC. + /// The target object to pass as argument to . + private Gen2GcCallback(Action callback, object target) + { + this._callback = callback; + this._handle = GCHandle.Alloc(target, GCHandleType.Weak); + } + + /// + /// Schedules a callback to be called on each GC until the target is collected. + /// + /// The callback to invoke at each GC. + /// The target object to pass as argument to . + public static void Register(Action callback, object target) + { +#if NETSTANDARD2_0 + if (RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework")) + { + // On .NET Framework using a GC callback causes issues with app domain unloading, + // so the callback is not registered if that runtime is detected and just ignored. + // Users on .NET Framework will have to manually trim the messenger, if they'd like. + return; + } +#endif + + _ = new Gen2GcCallback(callback, target); + } + + /// + /// Finalizes an instance of the class. + /// This finalizer is re-registered with as long as + /// the target object is alive, which means it will be executed again every time a generation 2 + /// collection is triggered (as the instance itself would be moved to + /// that generation after surviving the generation 0 and 1 collections the first time). + /// + ~Gen2GcCallback() + { + // ReSharper disable once ConvertTypeCheckPatternToNullCheck + if (_handle.Target is object target) + { + try + { + _callback(target); + } + catch + { + // Ignore any exception thrown by the callback. + } + + GC.ReRegisterForFinalize(this); + } + else + { + _handle.Free(); + } + } +} \ No newline at end of file From b8b1f6f1ea9e82ae5d791c2838b735438ec71552 Mon Sep 17 00:00:00 2001 From: damon Date: Wed, 22 Nov 2023 21:25:49 +0800 Subject: [PATCH 06/37] Change exception type to NullReferenceException while the value type not found. --- .../RedisValueConverter.cs | 373 +++++++++--------- 1 file changed, 188 insertions(+), 185 deletions(-) diff --git a/Source/Euonia.Caching.Redis/RedisValueConverter.cs b/Source/Euonia.Caching.Redis/RedisValueConverter.cs index 84b1065..6bf3ff2 100644 --- a/Source/Euonia.Caching.Redis/RedisValueConverter.cs +++ b/Source/Euonia.Caching.Redis/RedisValueConverter.cs @@ -6,16 +6,16 @@ namespace Nerosoft.Euonia.Caching.Redis; internal interface IRedisValueConverter { - RedisValue ToRedisValue(T value); + RedisValue ToRedisValue(T value); - T FromRedisValue(RedisValue value, string valueType); + T FromRedisValue(RedisValue value, string valueType); } internal interface IRedisValueConverter { - RedisValue ToRedisValue(T value); + RedisValue ToRedisValue(T value); - T FromRedisValue(RedisValue value, string valueType); + T FromRedisValue(RedisValue value, string valueType); } internal class RedisValueConverter : IRedisValueConverter, @@ -31,205 +31,208 @@ internal class RedisValueConverter : IRedisValueConverter, IRedisValueConverter, IRedisValueConverter { - private static readonly Type _byteArrayType = typeof(byte[]); - private static readonly Type _stringType = typeof(string); - private static readonly Type _intType = typeof(int); - private static readonly Type _uIntType = typeof(uint); - private static readonly Type _shortType = typeof(short); - private static readonly Type _singleType = typeof(float); - private static readonly Type _doubleType = typeof(double); - private static readonly Type _boolType = typeof(bool); - private static readonly Type _longType = typeof(long); - private static readonly Type _uLongType = typeof(ulong); + private static readonly Type _byteArrayType = typeof(byte[]); + private static readonly Type _stringType = typeof(string); + private static readonly Type _intType = typeof(int); + private static readonly Type _uIntType = typeof(uint); + private static readonly Type _shortType = typeof(short); + private static readonly Type _singleType = typeof(float); + private static readonly Type _doubleType = typeof(double); + private static readonly Type _boolType = typeof(bool); + private static readonly Type _longType = typeof(long); + private static readonly Type _uLongType = typeof(ulong); - RedisValue IRedisValueConverter.ToRedisValue(byte[] value) => value; + RedisValue IRedisValueConverter.ToRedisValue(byte[] value) => value; - byte[] IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => value; + byte[] IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => value; - RedisValue IRedisValueConverter.ToRedisValue(string value) => value; + RedisValue IRedisValueConverter.ToRedisValue(string value) => value; - string IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => value; + string IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => value; - RedisValue IRedisValueConverter.ToRedisValue(int value) => value; + RedisValue IRedisValueConverter.ToRedisValue(int value) => value; - int IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => (int)value; + int IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => (int)value; - RedisValue IRedisValueConverter.ToRedisValue(uint value) => value; + RedisValue IRedisValueConverter.ToRedisValue(uint value) => value; - uint IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => (uint)value; + uint IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => (uint)value; - RedisValue IRedisValueConverter.ToRedisValue(short value) => value; + RedisValue IRedisValueConverter.ToRedisValue(short value) => value; - short IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => (short)value; + short IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => (short)value; - RedisValue IRedisValueConverter.ToRedisValue(float value) => (double)value; + RedisValue IRedisValueConverter.ToRedisValue(float value) => (double)value; - float IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => (float)(double)value; + float IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => (float)(double)value; - RedisValue IRedisValueConverter.ToRedisValue(double value) => value; + RedisValue IRedisValueConverter.ToRedisValue(double value) => value; - double IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => (double)value; + double IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => (double)value; - RedisValue IRedisValueConverter.ToRedisValue(bool value) => value; + RedisValue IRedisValueConverter.ToRedisValue(bool value) => value; - bool IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => (bool)value; + bool IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => (bool)value; - RedisValue IRedisValueConverter.ToRedisValue(long value) => value; + RedisValue IRedisValueConverter.ToRedisValue(long value) => value; - long IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => (long)value; + long IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => (long)value; - // ulong can exceed the supported lenght of storing integers (which is signed 64bit integer) - // also, even if we do not exceed long.MaxValue, the SA client stores it as double for no parent reason => cast to long fixes it. - RedisValue IRedisValueConverter.ToRedisValue(ulong value) => value > long.MaxValue ? (RedisValue)value.ToString() : checked((long)value); + // ulong can exceed the supported lenght of storing integers (which is signed 64bit integer) + // also, even if we do not exceed long.MaxValue, the SA client stores it as double for no parent reason => cast to long fixes it. + RedisValue IRedisValueConverter.ToRedisValue(ulong value) => value > long.MaxValue ? (RedisValue)value.ToString() : checked((long)value); - ulong IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => ulong.Parse(value); - - RedisValue IRedisValueConverter.ToRedisValue(object value) - { - var valueType = value.GetType(); - if (valueType == _byteArrayType) - { - var converter = (IRedisValueConverter)this; - return converter.ToRedisValue((byte[])value); - } - - if (valueType == _stringType) - { - var converter = (IRedisValueConverter)this; - return converter.ToRedisValue((string)value); - } - - if (valueType == _intType) - { - var converter = (IRedisValueConverter)this; - return converter.ToRedisValue((int)value); - } - - if (valueType == _uIntType) - { - var converter = (IRedisValueConverter)this; - return converter.ToRedisValue((uint)value); - } - - if (valueType == _shortType) - { - var converter = (IRedisValueConverter)this; - return converter.ToRedisValue((short)value); - } - - if (valueType == _singleType) - { - var converter = (IRedisValueConverter)this; - return converter.ToRedisValue((float)value); - } - - if (valueType == _doubleType) - { - var converter = (IRedisValueConverter)this; - return converter.ToRedisValue((double)value); - } - - if (valueType == _boolType) - { - var converter = (IRedisValueConverter)this; - return converter.ToRedisValue((bool)value); - } - - if (valueType == _longType) - { - var converter = (IRedisValueConverter)this; - return converter.ToRedisValue((long)value); - } - - if (valueType == _uLongType) - { - var converter = (IRedisValueConverter)this; - return converter.ToRedisValue((ulong)value); - } - - { - } - return JsonSerializer.Serialize(value); - } - - object IRedisValueConverter.FromRedisValue(RedisValue value, string type) - { - var valueType = TypeCache.GetType(type); - - if (valueType == _byteArrayType) - { - var converter = (IRedisValueConverter)this; - return converter.FromRedisValue(value, type); - } - - if (valueType == _stringType) - { - var converter = (IRedisValueConverter)this; - return converter.FromRedisValue(value, type); - } - - if (valueType == _intType) - { - var converter = (IRedisValueConverter)this; - return converter.FromRedisValue(value, type); - } - - if (valueType == _uIntType) - { - var converter = (IRedisValueConverter)this; - return converter.FromRedisValue(value, type); - } - - if (valueType == _shortType) - { - var converter = (IRedisValueConverter)this; - return converter.FromRedisValue(value, type); - } - - if (valueType == _singleType) - { - var converter = (IRedisValueConverter)this; - return converter.FromRedisValue(value, type); - } - - if (valueType == _doubleType) - { - var converter = (IRedisValueConverter)this; - return converter.FromRedisValue(value, type); - } - - if (valueType == _boolType) - { - var converter = (IRedisValueConverter)this; - return converter.FromRedisValue(value, type); - } - - if (valueType == _longType) - { - var converter = (IRedisValueConverter)this; - return converter.FromRedisValue(value, type); - } - - if (valueType == _uLongType) - { - var converter = (IRedisValueConverter)this; - return converter.FromRedisValue(value, type); - } - - { - } - return Deserialize(value, type); - } - - public RedisValue ToRedisValue(T value) => JsonSerializer.Serialize(value); - - public T FromRedisValue(RedisValue value, string valueType) => (T)Deserialize(value, valueType); - - private object Deserialize(RedisValue value, string valueType) - { - var type = TypeCache.GetType(valueType); - Check.EnsureNotNull(type, "Type could not be loaded, {0}.", valueType); - - return JsonSerializer.Deserialize(value, type); - } + ulong IRedisValueConverter.FromRedisValue(RedisValue value, string valueType) => ulong.Parse(value); + + RedisValue IRedisValueConverter.ToRedisValue(object value) + { + var valueType = value.GetType(); + if (valueType == _byteArrayType) + { + var converter = (IRedisValueConverter)this; + return converter.ToRedisValue((byte[])value); + } + + if (valueType == _stringType) + { + var converter = (IRedisValueConverter)this; + return converter.ToRedisValue((string)value); + } + + if (valueType == _intType) + { + var converter = (IRedisValueConverter)this; + return converter.ToRedisValue((int)value); + } + + if (valueType == _uIntType) + { + var converter = (IRedisValueConverter)this; + return converter.ToRedisValue((uint)value); + } + + if (valueType == _shortType) + { + var converter = (IRedisValueConverter)this; + return converter.ToRedisValue((short)value); + } + + if (valueType == _singleType) + { + var converter = (IRedisValueConverter)this; + return converter.ToRedisValue((float)value); + } + + if (valueType == _doubleType) + { + var converter = (IRedisValueConverter)this; + return converter.ToRedisValue((double)value); + } + + if (valueType == _boolType) + { + var converter = (IRedisValueConverter)this; + return converter.ToRedisValue((bool)value); + } + + if (valueType == _longType) + { + var converter = (IRedisValueConverter)this; + return converter.ToRedisValue((long)value); + } + + if (valueType == _uLongType) + { + var converter = (IRedisValueConverter)this; + return converter.ToRedisValue((ulong)value); + } + + { + } + return JsonSerializer.Serialize(value); + } + + object IRedisValueConverter.FromRedisValue(RedisValue value, string type) + { + var valueType = TypeCache.GetType(type); + + if (valueType == _byteArrayType) + { + var converter = (IRedisValueConverter)this; + return converter.FromRedisValue(value, type); + } + + if (valueType == _stringType) + { + var converter = (IRedisValueConverter)this; + return converter.FromRedisValue(value, type); + } + + if (valueType == _intType) + { + var converter = (IRedisValueConverter)this; + return converter.FromRedisValue(value, type); + } + + if (valueType == _uIntType) + { + var converter = (IRedisValueConverter)this; + return converter.FromRedisValue(value, type); + } + + if (valueType == _shortType) + { + var converter = (IRedisValueConverter)this; + return converter.FromRedisValue(value, type); + } + + if (valueType == _singleType) + { + var converter = (IRedisValueConverter)this; + return converter.FromRedisValue(value, type); + } + + if (valueType == _doubleType) + { + var converter = (IRedisValueConverter)this; + return converter.FromRedisValue(value, type); + } + + if (valueType == _boolType) + { + var converter = (IRedisValueConverter)this; + return converter.FromRedisValue(value, type); + } + + if (valueType == _longType) + { + var converter = (IRedisValueConverter)this; + return converter.FromRedisValue(value, type); + } + + if (valueType == _uLongType) + { + var converter = (IRedisValueConverter)this; + return converter.FromRedisValue(value, type); + } + + { + } + return Deserialize(value, type); + } + + public RedisValue ToRedisValue(T value) => JsonSerializer.Serialize(value); + + public T FromRedisValue(RedisValue value, string valueType) => (T)Deserialize(value, valueType); + + private static object Deserialize(RedisValue value, string valueType) + { + var type = TypeCache.GetType(valueType); + if (type == null) + { + throw new NullReferenceException($"Type could not be loaded, {valueType}."); + } + + return JsonSerializer.Deserialize(value, type); + } } \ No newline at end of file From 5bf53f16e1e6989ae35881b6cc1dcce1996bd4c0 Mon Sep 17 00:00:00 2001 From: damon Date: Fri, 24 Nov 2023 00:20:56 +0800 Subject: [PATCH 07/37] [WIP]Refactor the service bus. --- .../Behaviors/BearerTokenBehavior.cs | 7 +- .../Behaviors/UserPrincipalBehavior.cs | 7 +- .../Seedwork/PipelineCommand.cs | 3 +- .../Services/BaseApplicationService.cs | 33 +- .../Attributes/ChannelAttribute.cs | 56 + .../Attributes/CommandAttribute.cs | 9 + .../Attributes/EventAttribute.cs | 9 + .../Attributes/QueueAttribute.cs | 27 + .../Attributes/RequestAttribute.cs | 9 + .../Contracts/IBusConfigurator.cs | 63 + .../Contracts/IBusFactory.cs | 13 + .../Euonia.Bus.Abstract/Contracts/ICommand.cs | 8 + .../Contracts/IDispatcher.cs | 43 + .../Euonia.Bus.Abstract/Contracts/IEvent.cs | 8 + .../Euonia.Bus.Abstract/Contracts/IMessage.cs | 8 + .../Euonia.Bus.Abstract/Contracts/IRequest.cs | 8 + .../Contracts/ISubscriber.cs | 36 + .../Euonia.Bus.Abstract.csproj | 8 + .../Events/MessageAcknowledgedEventArgs.cs | 19 + .../Events/MessageDispatchedEventArgs.cs | 19 + .../Events/MessageHandledEventArgs.cs | 26 + .../Events}/MessageProcessType.cs | 0 .../Events/MessageProcessedEventArgs.cs | 40 + .../Events/MessageReceivedEventArgs.cs | 19 + .../Events}/MessageRepliedEventArgs.cs | 0 .../Events}/MessageSubscribedEventArgs.cs | 0 Source/Euonia.Bus.Abstract/IMessageContent.cs | 30 + Source/Euonia.Bus.Abstract/IMessageContext.cs | 37 + .../Euonia.Bus.Abstract/IMessageEnvelope.cs | 35 + Source/Euonia.Bus.Abstract/IRoutedMessage.cs | 22 + Source/Euonia.Bus.Abstract/MessageContext.cs | 153 +++ Source/Euonia.Bus.Abstract/MessageHeaders.cs | 12 + .../MessageMetadata.cs | 2 +- .../Messages/IMessageTypeCache.cs | 8 + .../Persistence/IMessageStore.cs | 19 + Source/Euonia.Bus.Abstract/RoutedMessage.cs | 108 ++ .../Tracking/IMessageTracking.cs | 8 + Source/Euonia.Bus.InMemory/CommandBus.cs | 8 +- .../Euonia.Bus.InMemory.csproj | 23 +- Source/Euonia.Bus.InMemory/EventBus.cs | 26 +- .../Euonia.Bus.InMemory/InMemoryBusFactory.cs | 24 + .../Euonia.Bus.InMemory/InMemoryBusOptions.cs | 16 + .../Euonia.Bus.InMemory/InMemoryDispatcher.cs | 96 ++ .../InMemoryMessageBusModule.cs | 20 - .../Euonia.Bus.InMemory/InMemorySubscriber.cs | 57 + .../Internal/ArrayPoolBufferWriter.cs | 120 ++ .../Internal/ConditionalWeakTable2.cs | 705 ++++++++++ .../Internal/EquatableType.cs | 77 ++ .../Internal/HashHelpers.cs | 118 ++ .../Internal/MessageHandlerDispatcher.cs | 52 + .../Internal/TypeDictionary.Interface.cs | 51 + .../Internal/TypeDictionary.cs | 473 +++++++ Source/Euonia.Bus.InMemory/Internal/Unit.cs | 29 + Source/Euonia.Bus.InMemory/MessageBus.cs | 197 ++- Source/Euonia.Bus.InMemory/MessageQueue.cs | 3 +- .../Messages/AsyncCollectionRequestMessage.cs | 137 ++ .../Messages/AsyncRequestMessage.cs | 92 ++ .../Messages/CollectionRequestMessage.cs | 41 + .../Messages/MessageHandler.cs | 15 + .../Messages/MessagePack.cs | 33 + .../Messages/RequestMessage.cs | 82 ++ .../Messages/ValueChangedMessage.cs | 22 + .../Messenger/IMessenger.cs | 152 +++ .../Messenger/IRecipient.cs | 15 + .../MessengerExtensions.Observables.cs | 192 +++ .../Messenger/MessengerExtensions.cs | 430 ++++++ .../Messenger/MessengerReferenceType.cs | 17 + .../Messenger/StrongReferenceMessenger.cs | 850 ++++++++++++ .../Messenger/WeakReferenceMessenger.cs | 540 ++++++++ .../ServiceCollectionExtensions.cs | 144 +- Source/Euonia.Bus.RabbitMq/CommandBus.cs | 2 +- Source/Euonia.Bus.RabbitMq/CommandConsumer.cs | 6 +- .../Euonia.Bus.RabbitMq.csproj | 26 +- Source/Euonia.Bus.RabbitMq/EventBus.cs | 22 +- Source/Euonia.Bus.RabbitMq/EventConsumer.cs | 4 +- Source/Euonia.Bus.RabbitMq/MessageBus.cs | 10 +- .../Euonia.Bus.RabbitMq/RabbitMqBusFactory.cs | 9 + .../RabbitMqMessageBusModule.cs | 2 +- .../RabbitMqMessageDispatcher.cs | 5 + .../ServiceCollectionExtensions.cs | 249 ++-- .../Attributes/SubscribeAttribute.cs | 27 + .../Behaviors/MessageLoggingBehavior.cs | 28 + Source/Euonia.Bus/BusConfigurator.cs | 240 ++++ .../Euonia.Bus/Commands/CommandAttribute.cs | 25 - Source/Euonia.Bus/Commands/CommandRequest.cs | 28 - Source/Euonia.Bus/Commands/ICommandBus.cs | 14 - Source/Euonia.Bus/Commands/ICommandHandler.cs | 81 -- Source/Euonia.Bus/Commands/ICommandSender.cs | 53 - .../Euonia.Bus/Commands/ICommandSubscriber.cs | 20 - .../Conventions/AttributeMessageConvention.cs | 24 + .../Conventions/DefaultMessageConvention.cs | 32 + .../Conventions/IMessageConvention.cs | 26 + .../Conventions/MessageConvention.cs | 88 ++ .../Conventions/MessageConventionBuilder.cs | 59 + .../OverridableMessageConvention.cs | 46 + .../Converters/BytesMessageDataConverter.cs | 22 + .../Converters/IMessageDataConverter.cs | 16 + .../Converters/JsonMessageDataConverter.cs | 30 + .../Converters/StreamMessageDataConverter.cs | 18 + .../Converters/StringMessageDataConverter.cs | 22 + Source/Euonia.Bus/Core/HandlerBase.cs | 30 + Source/Euonia.Bus/Core/HandlerContext.cs | 212 +++ Source/Euonia.Bus/Core/IBus.cs | 49 + Source/Euonia.Bus/Core/IHandler.cs | 48 + Source/Euonia.Bus/Core/IHandlerContext.cs | 31 + Source/Euonia.Bus/Core/ServiceBus.cs | 82 ++ Source/Euonia.Bus/Euonia.Bus.csproj | 6 +- .../Euonia.Bus/Events/EventNameAttribute.cs | 58 - Source/Euonia.Bus/Events/EventStore.cs | 170 --- .../Events/EventSubscribeAttribute.cs | 22 - Source/Euonia.Bus/Events/IEventBus.cs | 14 - Source/Euonia.Bus/Events/IEventDispatcher.cs | 32 - Source/Euonia.Bus/Events/IEventHandler.cs | 25 - Source/Euonia.Bus/Events/IEventStore.cs | 52 - Source/Euonia.Bus/Events/IEventSubscriber.cs | 20 - Source/Euonia.Bus/MessageBusActiveService.cs | 70 - Source/Euonia.Bus/MessageBusModule.cs | 50 - Source/Euonia.Bus/Messages/IMessageBus.cs | 12 - .../Euonia.Bus/Messages/IMessageDispatcher.cs | 19 - Source/Euonia.Bus/Messages/IMessageHandler.cs | 44 - .../Messages/IMessageHandlerContext.cs | 79 -- .../Euonia.Bus/Messages/IMessageSubscriber.cs | 26 - .../Messages/MessageAcknowledgedEventArgs.cs | 21 - .../Messages/MessageBusException.cs | 95 +- Source/Euonia.Bus/Messages/MessageContext.cs | 127 -- ...onversionDelegate.cs => MessageConvert.cs} | 2 +- .../Messages/MessageDispatchedEventArgs.cs | 21 - .../Messages/MessageHandledEventArgs.cs | 28 - Source/Euonia.Bus/Messages/MessageHandler.cs | 13 + .../Euonia.Bus/Messages/MessageHandlerBase.cs | 76 -- .../Messages/MessageHandlerContext.cs | 343 ----- .../Messages/MessageHandlerOptions.cs | 140 -- .../Messages/MessageProcessedEventArgs.cs | 42 - .../Messages/MessageProcessingException.cs | 47 +- .../Messages/MessageReceivedEventArgs.cs | 21 - .../Messages/MessageSubscription.cs | 78 +- Source/Euonia.Bus/Messages/PipelineMessage.cs | 128 ++ Source/Euonia.Bus/Properties/Resources.resx | 6 + .../Serialization/IMessageSerializer.cs | 16 + .../Serialization/NewtonsoftJsonSerializer.cs | 19 + .../Serialization/SystemTextJsonSerializer.cs | 15 + .../Euonia.Bus/ServiceCollectionExtensions.cs | 290 ++-- Source/Euonia.Domain/Commands/Command.cs | 1172 ++++++++--------- Source/Euonia.Domain/Commands/ICommand.cs | 20 - Source/Euonia.Domain/Commands/NamedCommand.cs | 37 - Source/Euonia.Domain/Euonia.Domain.csproj | 1 - Source/Euonia.Domain/Events/DomainEvent.cs | 95 +- Source/Euonia.Domain/Events/Event.cs | 83 +- Source/Euonia.Domain/Events/IDomainEvent.cs | 26 +- Source/Euonia.Domain/Events/IEvent.cs | 54 +- Source/Euonia.Domain/Events/NamedEvent.cs | 37 - .../Extensions/CommandExtensions.cs | 4 +- Source/Euonia.Domain/Messages/IMessage.cs | 28 - .../Euonia.Domain/Messages/INamedMessage.cs | 17 - Source/Euonia.Domain/Messages/Message.cs | 102 -- 155 files changed, 8002 insertions(+), 3356 deletions(-) create mode 100644 Source/Euonia.Bus.Abstract/Attributes/ChannelAttribute.cs create mode 100644 Source/Euonia.Bus.Abstract/Attributes/CommandAttribute.cs create mode 100644 Source/Euonia.Bus.Abstract/Attributes/EventAttribute.cs create mode 100644 Source/Euonia.Bus.Abstract/Attributes/QueueAttribute.cs create mode 100644 Source/Euonia.Bus.Abstract/Attributes/RequestAttribute.cs create mode 100644 Source/Euonia.Bus.Abstract/Contracts/IBusConfigurator.cs create mode 100644 Source/Euonia.Bus.Abstract/Contracts/IBusFactory.cs create mode 100644 Source/Euonia.Bus.Abstract/Contracts/ICommand.cs create mode 100644 Source/Euonia.Bus.Abstract/Contracts/IDispatcher.cs create mode 100644 Source/Euonia.Bus.Abstract/Contracts/IEvent.cs create mode 100644 Source/Euonia.Bus.Abstract/Contracts/IMessage.cs create mode 100644 Source/Euonia.Bus.Abstract/Contracts/IRequest.cs create mode 100644 Source/Euonia.Bus.Abstract/Contracts/ISubscriber.cs create mode 100644 Source/Euonia.Bus.Abstract/Events/MessageAcknowledgedEventArgs.cs create mode 100644 Source/Euonia.Bus.Abstract/Events/MessageDispatchedEventArgs.cs create mode 100644 Source/Euonia.Bus.Abstract/Events/MessageHandledEventArgs.cs rename Source/{Euonia.Bus/Messages => Euonia.Bus.Abstract/Events}/MessageProcessType.cs (100%) create mode 100644 Source/Euonia.Bus.Abstract/Events/MessageProcessedEventArgs.cs create mode 100644 Source/Euonia.Bus.Abstract/Events/MessageReceivedEventArgs.cs rename Source/{Euonia.Bus/Messages => Euonia.Bus.Abstract/Events}/MessageRepliedEventArgs.cs (100%) rename Source/{Euonia.Bus/Messages => Euonia.Bus.Abstract/Events}/MessageSubscribedEventArgs.cs (100%) create mode 100644 Source/Euonia.Bus.Abstract/IMessageContent.cs create mode 100644 Source/Euonia.Bus.Abstract/IMessageContext.cs create mode 100644 Source/Euonia.Bus.Abstract/IMessageEnvelope.cs create mode 100644 Source/Euonia.Bus.Abstract/IRoutedMessage.cs create mode 100644 Source/Euonia.Bus.Abstract/MessageContext.cs create mode 100644 Source/Euonia.Bus.Abstract/MessageHeaders.cs rename Source/{Euonia.Domain/Messages => Euonia.Bus.Abstract}/MessageMetadata.cs (98%) create mode 100644 Source/Euonia.Bus.Abstract/Messages/IMessageTypeCache.cs create mode 100644 Source/Euonia.Bus.Abstract/Persistence/IMessageStore.cs create mode 100644 Source/Euonia.Bus.Abstract/RoutedMessage.cs create mode 100644 Source/Euonia.Bus.Abstract/Tracking/IMessageTracking.cs create mode 100644 Source/Euonia.Bus.InMemory/InMemoryBusFactory.cs create mode 100644 Source/Euonia.Bus.InMemory/InMemoryBusOptions.cs create mode 100644 Source/Euonia.Bus.InMemory/InMemoryDispatcher.cs delete mode 100644 Source/Euonia.Bus.InMemory/InMemoryMessageBusModule.cs create mode 100644 Source/Euonia.Bus.InMemory/InMemorySubscriber.cs create mode 100644 Source/Euonia.Bus.InMemory/Internal/ArrayPoolBufferWriter.cs create mode 100644 Source/Euonia.Bus.InMemory/Internal/ConditionalWeakTable2.cs create mode 100644 Source/Euonia.Bus.InMemory/Internal/EquatableType.cs create mode 100644 Source/Euonia.Bus.InMemory/Internal/HashHelpers.cs create mode 100644 Source/Euonia.Bus.InMemory/Internal/MessageHandlerDispatcher.cs create mode 100644 Source/Euonia.Bus.InMemory/Internal/TypeDictionary.Interface.cs create mode 100644 Source/Euonia.Bus.InMemory/Internal/TypeDictionary.cs create mode 100644 Source/Euonia.Bus.InMemory/Internal/Unit.cs create mode 100644 Source/Euonia.Bus.InMemory/Messages/AsyncCollectionRequestMessage.cs create mode 100644 Source/Euonia.Bus.InMemory/Messages/AsyncRequestMessage.cs create mode 100644 Source/Euonia.Bus.InMemory/Messages/CollectionRequestMessage.cs create mode 100644 Source/Euonia.Bus.InMemory/Messages/MessageHandler.cs create mode 100644 Source/Euonia.Bus.InMemory/Messages/MessagePack.cs create mode 100644 Source/Euonia.Bus.InMemory/Messages/RequestMessage.cs create mode 100644 Source/Euonia.Bus.InMemory/Messages/ValueChangedMessage.cs create mode 100644 Source/Euonia.Bus.InMemory/Messenger/IMessenger.cs create mode 100644 Source/Euonia.Bus.InMemory/Messenger/IRecipient.cs create mode 100644 Source/Euonia.Bus.InMemory/Messenger/MessengerExtensions.Observables.cs create mode 100644 Source/Euonia.Bus.InMemory/Messenger/MessengerExtensions.cs create mode 100644 Source/Euonia.Bus.InMemory/Messenger/MessengerReferenceType.cs create mode 100644 Source/Euonia.Bus.InMemory/Messenger/StrongReferenceMessenger.cs create mode 100644 Source/Euonia.Bus.InMemory/Messenger/WeakReferenceMessenger.cs create mode 100644 Source/Euonia.Bus.RabbitMq/RabbitMqBusFactory.cs create mode 100644 Source/Euonia.Bus.RabbitMq/RabbitMqMessageDispatcher.cs create mode 100644 Source/Euonia.Bus/Attributes/SubscribeAttribute.cs create mode 100644 Source/Euonia.Bus/Behaviors/MessageLoggingBehavior.cs create mode 100644 Source/Euonia.Bus/BusConfigurator.cs delete mode 100644 Source/Euonia.Bus/Commands/CommandAttribute.cs delete mode 100644 Source/Euonia.Bus/Commands/CommandRequest.cs delete mode 100644 Source/Euonia.Bus/Commands/ICommandBus.cs delete mode 100644 Source/Euonia.Bus/Commands/ICommandHandler.cs delete mode 100644 Source/Euonia.Bus/Commands/ICommandSender.cs delete mode 100644 Source/Euonia.Bus/Commands/ICommandSubscriber.cs create mode 100644 Source/Euonia.Bus/Conventions/AttributeMessageConvention.cs create mode 100644 Source/Euonia.Bus/Conventions/DefaultMessageConvention.cs create mode 100644 Source/Euonia.Bus/Conventions/IMessageConvention.cs create mode 100644 Source/Euonia.Bus/Conventions/MessageConvention.cs create mode 100644 Source/Euonia.Bus/Conventions/MessageConventionBuilder.cs create mode 100644 Source/Euonia.Bus/Conventions/OverridableMessageConvention.cs create mode 100644 Source/Euonia.Bus/Converters/BytesMessageDataConverter.cs create mode 100644 Source/Euonia.Bus/Converters/IMessageDataConverter.cs create mode 100644 Source/Euonia.Bus/Converters/JsonMessageDataConverter.cs create mode 100644 Source/Euonia.Bus/Converters/StreamMessageDataConverter.cs create mode 100644 Source/Euonia.Bus/Converters/StringMessageDataConverter.cs create mode 100644 Source/Euonia.Bus/Core/HandlerBase.cs create mode 100644 Source/Euonia.Bus/Core/HandlerContext.cs create mode 100644 Source/Euonia.Bus/Core/IBus.cs create mode 100644 Source/Euonia.Bus/Core/IHandler.cs create mode 100644 Source/Euonia.Bus/Core/IHandlerContext.cs create mode 100644 Source/Euonia.Bus/Core/ServiceBus.cs delete mode 100644 Source/Euonia.Bus/Events/EventNameAttribute.cs delete mode 100644 Source/Euonia.Bus/Events/EventStore.cs delete mode 100644 Source/Euonia.Bus/Events/EventSubscribeAttribute.cs delete mode 100644 Source/Euonia.Bus/Events/IEventBus.cs delete mode 100644 Source/Euonia.Bus/Events/IEventDispatcher.cs delete mode 100644 Source/Euonia.Bus/Events/IEventHandler.cs delete mode 100644 Source/Euonia.Bus/Events/IEventStore.cs delete mode 100644 Source/Euonia.Bus/Events/IEventSubscriber.cs delete mode 100644 Source/Euonia.Bus/MessageBusActiveService.cs delete mode 100644 Source/Euonia.Bus/MessageBusModule.cs delete mode 100644 Source/Euonia.Bus/Messages/IMessageBus.cs delete mode 100644 Source/Euonia.Bus/Messages/IMessageDispatcher.cs delete mode 100644 Source/Euonia.Bus/Messages/IMessageHandler.cs delete mode 100644 Source/Euonia.Bus/Messages/IMessageHandlerContext.cs delete mode 100644 Source/Euonia.Bus/Messages/IMessageSubscriber.cs delete mode 100644 Source/Euonia.Bus/Messages/MessageAcknowledgedEventArgs.cs delete mode 100644 Source/Euonia.Bus/Messages/MessageContext.cs rename Source/Euonia.Bus/Messages/{MessageConversionDelegate.cs => MessageConvert.cs} (55%) delete mode 100644 Source/Euonia.Bus/Messages/MessageDispatchedEventArgs.cs delete mode 100644 Source/Euonia.Bus/Messages/MessageHandledEventArgs.cs create mode 100644 Source/Euonia.Bus/Messages/MessageHandler.cs delete mode 100644 Source/Euonia.Bus/Messages/MessageHandlerBase.cs delete mode 100644 Source/Euonia.Bus/Messages/MessageHandlerContext.cs delete mode 100644 Source/Euonia.Bus/Messages/MessageHandlerOptions.cs delete mode 100644 Source/Euonia.Bus/Messages/MessageProcessedEventArgs.cs delete mode 100644 Source/Euonia.Bus/Messages/MessageReceivedEventArgs.cs create mode 100644 Source/Euonia.Bus/Messages/PipelineMessage.cs create mode 100644 Source/Euonia.Bus/Serialization/IMessageSerializer.cs create mode 100644 Source/Euonia.Bus/Serialization/NewtonsoftJsonSerializer.cs create mode 100644 Source/Euonia.Bus/Serialization/SystemTextJsonSerializer.cs delete mode 100644 Source/Euonia.Domain/Commands/ICommand.cs delete mode 100644 Source/Euonia.Domain/Commands/NamedCommand.cs delete mode 100644 Source/Euonia.Domain/Events/NamedEvent.cs delete mode 100644 Source/Euonia.Domain/Messages/IMessage.cs delete mode 100644 Source/Euonia.Domain/Messages/INamedMessage.cs delete mode 100644 Source/Euonia.Domain/Messages/Message.cs diff --git a/Source/Euonia.Application/Behaviors/BearerTokenBehavior.cs b/Source/Euonia.Application/Behaviors/BearerTokenBehavior.cs index fdce161..1f15915 100644 --- a/Source/Euonia.Application/Behaviors/BearerTokenBehavior.cs +++ b/Source/Euonia.Application/Behaviors/BearerTokenBehavior.cs @@ -1,4 +1,5 @@ -using Nerosoft.Euonia.Domain; +using Nerosoft.Euonia.Bus; +using Nerosoft.Euonia.Domain; using Nerosoft.Euonia.Pipeline; namespace Nerosoft.Euonia.Application; @@ -6,7 +7,7 @@ namespace Nerosoft.Euonia.Application; /// /// A pipeline behavior that adds the bearer token to the command metadata. /// -public class BearerTokenBehavior : IPipelineBehavior +public class BearerTokenBehavior : IPipelineBehavior, CommandResponse> { private readonly IRequestContextAccessor _contextAccessor; @@ -27,7 +28,7 @@ public BearerTokenBehavior(IRequestContextAccessor contextAccessor) } /// - public async Task HandleAsync(ICommand context, PipelineDelegate next) + public async Task HandleAsync(RoutedMessage context, PipelineDelegate, CommandResponse> next) { if (_contextAccessor?.RequestHeaders.TryGetValue("Authorization", out var values) == true) { diff --git a/Source/Euonia.Application/Behaviors/UserPrincipalBehavior.cs b/Source/Euonia.Application/Behaviors/UserPrincipalBehavior.cs index 1bfe894..9592608 100644 --- a/Source/Euonia.Application/Behaviors/UserPrincipalBehavior.cs +++ b/Source/Euonia.Application/Behaviors/UserPrincipalBehavior.cs @@ -1,4 +1,5 @@ -using Nerosoft.Euonia.Claims; +using Nerosoft.Euonia.Bus; +using Nerosoft.Euonia.Claims; using Nerosoft.Euonia.Domain; using Nerosoft.Euonia.Pipeline; @@ -7,7 +8,7 @@ namespace Nerosoft.Euonia.Application; /// /// A pipeline behavior that adds the user principal to the command metadata. /// -public class UserPrincipalBehavior : IPipelineBehavior +public class UserPrincipalBehavior : IPipelineBehavior, CommandResponse> { private readonly UserPrincipal _user; @@ -28,7 +29,7 @@ public UserPrincipalBehavior(UserPrincipal user) } /// - public async Task HandleAsync(ICommand context, PipelineDelegate next) + public async Task HandleAsync(RoutedMessage context, PipelineDelegate, CommandResponse> next) { if (_user is { IsAuthenticated: true }) { diff --git a/Source/Euonia.Application/Seedwork/PipelineCommand.cs b/Source/Euonia.Application/Seedwork/PipelineCommand.cs index c63a725..39d4191 100644 --- a/Source/Euonia.Application/Seedwork/PipelineCommand.cs +++ b/Source/Euonia.Application/Seedwork/PipelineCommand.cs @@ -1,4 +1,5 @@ -using Nerosoft.Euonia.Domain; +using Nerosoft.Euonia.Bus; +using Nerosoft.Euonia.Domain; using Nerosoft.Euonia.Pipeline; namespace Nerosoft.Euonia.Application; diff --git a/Source/Euonia.Application/Services/BaseApplicationService.cs b/Source/Euonia.Application/Services/BaseApplicationService.cs index 76a4bba..eb89858 100644 --- a/Source/Euonia.Application/Services/BaseApplicationService.cs +++ b/Source/Euonia.Application/Services/BaseApplicationService.cs @@ -16,14 +16,9 @@ public abstract class BaseApplicationService : IApplicationService public virtual ILazyServiceProvider LazyServiceProvider { get; set; } /// - /// Gets the instance. + /// Gets the instance. /// - protected virtual ICommandBus CommandBus => LazyServiceProvider.GetService(); - - /// - /// Gets the instance. - /// - protected virtual IEventBus EventBus => LazyServiceProvider.GetService(); + protected virtual IBus Bus => LazyServiceProvider.GetService(); /// /// Gets the current request user principal. @@ -36,7 +31,7 @@ public abstract class BaseApplicationService : IApplicationService protected virtual TimeSpan CommandTimeout => TimeSpan.FromSeconds(300); /// - /// Send command message of using . + /// Send command message of using . /// /// /// @@ -52,7 +47,7 @@ protected virtual async Task SendCommandAsync(TCommand command, Action Validator.Validate(command); - await CommandBus.SendAsync(command, responseHandler, cancellationToken); + await Bus.SendAsync(command, responseHandler, cancellationToken); } /// @@ -72,7 +67,7 @@ protected virtual async Task SendCommandAsync(TComman Validator.Validate(command); - return await CommandBus.SendAsync(command, cancellationToken); + return await Bus.SendAsync(command, cancellationToken); } /// @@ -93,7 +88,7 @@ protected virtual async Task SendCommandAsync(TCommand comman Validator.Validate(command); - await CommandBus.SendAsync(command, responseHandler, cancellationToken); + await Bus.SendAsync(command, responseHandler, cancellationToken); } /// @@ -114,7 +109,7 @@ protected virtual async Task> SendCommandAsync>(command, cancellationToken); + return await Bus.SendAsync>(command, cancellationToken); } /// @@ -162,23 +157,23 @@ protected virtual async Task> SendCommandAsync - /// Publish application event message using . + /// Publish application event message using . /// /// /// protected async void PublishEvent(TEvent @event) - where TEvent : class, IEvent + where TEvent : class { - if (EventBus == null) + if (Bus == null) { return; } - await EventBus.PublishAsync(@event); + await Bus.PublishAsync(@event); } /// - /// Publish application event message using with specified name. + /// Publish application event message using with specified name. /// /// /// @@ -186,11 +181,11 @@ protected async void PublishEvent(TEvent @event) protected async void PublishEvent(string name, TEvent @event) where TEvent : class { - if (EventBus == null) + if (Bus == null) { return; } - await EventBus.PublishAsync(name, @event); + await Bus.PublishAsync(name, @event); } } \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Attributes/ChannelAttribute.cs b/Source/Euonia.Bus.Abstract/Attributes/ChannelAttribute.cs new file mode 100644 index 0000000..75399df --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Attributes/ChannelAttribute.cs @@ -0,0 +1,56 @@ +using System.Reflection; + +namespace Nerosoft.Euonia.Bus; + +/// +/// Represents the attributed event has a specified name. +/// +[AttributeUsage(AttributeTargets.Class)] +public class ChannelAttribute : Attribute +{ + /// + /// Gets the event name. + /// + public string Name { get; } + + /// + /// Initialize a new instance of . + /// + /// + /// + public ChannelAttribute(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + Name = name; + } + + /// + /// + /// + /// + /// + public static string GetName() + { + return GetName(typeof(TMessage)); + } + + /// + /// + /// + /// + /// + /// + public static string GetName(Type messageType) + { + if (messageType == null) + { + throw new ArgumentNullException(nameof(messageType)); + } + + return messageType.GetCustomAttribute()?.Name ?? messageType.Name; + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Attributes/CommandAttribute.cs b/Source/Euonia.Bus.Abstract/Attributes/CommandAttribute.cs new file mode 100644 index 0000000..23991ce --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Attributes/CommandAttribute.cs @@ -0,0 +1,9 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Represents the class is a command. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class CommandAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Attributes/EventAttribute.cs b/Source/Euonia.Bus.Abstract/Attributes/EventAttribute.cs new file mode 100644 index 0000000..598e901 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Attributes/EventAttribute.cs @@ -0,0 +1,9 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Represents the class is a event. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class EventAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Attributes/QueueAttribute.cs b/Source/Euonia.Bus.Abstract/Attributes/QueueAttribute.cs new file mode 100644 index 0000000..cad4ac8 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Attributes/QueueAttribute.cs @@ -0,0 +1,27 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Represents the decorated message type should be enqueued. +/// +[AttributeUsage(AttributeTargets.Class)] +public class QueueAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// + public QueueAttribute(string name) + { + Name = name; + } + + /// + /// Gets the name of the queue. + /// + public string Name { get; } + + /// + /// Gets or sets the priority of the message. + /// + public int Priority { get; set; } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Attributes/RequestAttribute.cs b/Source/Euonia.Bus.Abstract/Attributes/RequestAttribute.cs new file mode 100644 index 0000000..f75ca4e --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Attributes/RequestAttribute.cs @@ -0,0 +1,9 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Represents the decorator for the request. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class RequestAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Contracts/IBusConfigurator.cs b/Source/Euonia.Bus.Abstract/Contracts/IBusConfigurator.cs new file mode 100644 index 0000000..34a4104 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Contracts/IBusConfigurator.cs @@ -0,0 +1,63 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Nerosoft.Euonia.Bus; + +/// +/// The bus configurator abstract interface. +/// ` +public interface IBusConfigurator +{ + /// + /// Get the service collection. + /// + IServiceCollection Service { get; } + + /// + /// Get the message subscriptions. + /// + /// + IEnumerable GetSubscriptions(); + + /// + /// Set the service bus factory. + /// + /// + /// + IBusConfigurator SerFactory() + where TFactory : class, IBusFactory; + + /// + /// Set the service bus factory. + /// + /// + /// + /// + IBusConfigurator SerFactory(TFactory factory) + where TFactory : class, IBusFactory; + + /// + /// Set the service bus factory. + /// + /// + /// + /// + IBusConfigurator SerFactory(Func factory) + where TFactory : class, IBusFactory; + + /// + /// Set the message store provider. + /// + /// + /// + IBusConfigurator SetMessageStore() + where TStore : class, IMessageStore; + + /// + /// Set the message store provider. + /// + /// + /// + /// + IBusConfigurator SetMessageStore(Func store) + where TStore : class, IMessageStore; +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Contracts/IBusFactory.cs b/Source/Euonia.Bus.Abstract/Contracts/IBusFactory.cs new file mode 100644 index 0000000..bdca570 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Contracts/IBusFactory.cs @@ -0,0 +1,13 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// The bus factory interface +/// +public interface IBusFactory +{ + /// + /// Create a new message dispatcher + /// + /// + IDispatcher CreateDispatcher(); +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Contracts/ICommand.cs b/Source/Euonia.Bus.Abstract/Contracts/ICommand.cs new file mode 100644 index 0000000..8abeff9 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Contracts/ICommand.cs @@ -0,0 +1,8 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +public interface ICommand : IMessage +{ +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Contracts/IDispatcher.cs b/Source/Euonia.Bus.Abstract/Contracts/IDispatcher.cs new file mode 100644 index 0000000..b22dd62 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Contracts/IDispatcher.cs @@ -0,0 +1,43 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +public interface IDispatcher +{ + /// + /// Occurs when [message dispatched]. + /// + event EventHandler Delivered; + + /// + /// Publishes the specified message. + /// + /// + /// + /// + /// + Task PublishAsync(RoutedMessage pack, CancellationToken cancellationToken = default) + where TMessage : class; + + /// + /// Sends the specified message. + /// + /// + /// + /// + /// + Task SendAsync(RoutedMessage pack, CancellationToken cancellationToken = default) + where TMessage : class; + + /// + /// Sends the specified message. + /// + /// + /// + /// + /// + /// + Task SendAsync(RoutedMessage pack, CancellationToken cancellationToken = default) + where TMessage : class; +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Contracts/IEvent.cs b/Source/Euonia.Bus.Abstract/Contracts/IEvent.cs new file mode 100644 index 0000000..172e18c --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Contracts/IEvent.cs @@ -0,0 +1,8 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +public interface IEvent : IMessage +{ +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Contracts/IMessage.cs b/Source/Euonia.Bus.Abstract/Contracts/IMessage.cs new file mode 100644 index 0000000..ee8a00e --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Contracts/IMessage.cs @@ -0,0 +1,8 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// The base contract of message. +/// +public interface IMessage +{ +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Contracts/IRequest.cs b/Source/Euonia.Bus.Abstract/Contracts/IRequest.cs new file mode 100644 index 0000000..4272dca --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Contracts/IRequest.cs @@ -0,0 +1,8 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +public interface IRequest : IMessage +{ +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Contracts/ISubscriber.cs b/Source/Euonia.Bus.Abstract/Contracts/ISubscriber.cs new file mode 100644 index 0000000..d974109 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Contracts/ISubscriber.cs @@ -0,0 +1,36 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Interface ISubscriber +/// Implements the +/// +/// +public interface ISubscriber : IDisposable +{ + /// + /// Occurs when [message received]. + /// + event EventHandler MessageReceived; + + /// + /// Occurs when [message acknowledged]. + /// + event EventHandler MessageAcknowledged; + + /// + /// Occurs when [message subscribed]. + /// + event EventHandler MessageSubscribed; + + /// + /// Gets the subscriber name. + /// + string Name { get; } + + /// + /// Subscribes the specified message type. + /// + /// + /// + void Subscribe(Type messageType, Type handlerType); +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Euonia.Bus.Abstract.csproj b/Source/Euonia.Bus.Abstract/Euonia.Bus.Abstract.csproj index 58ebc95..cbe6f49 100644 --- a/Source/Euonia.Bus.Abstract/Euonia.Bus.Abstract.csproj +++ b/Source/Euonia.Bus.Abstract/Euonia.Bus.Abstract.csproj @@ -6,6 +6,10 @@ Nerosoft.Euonia.Bus disable + + + + @@ -22,4 +26,8 @@ + + + + diff --git a/Source/Euonia.Bus.Abstract/Events/MessageAcknowledgedEventArgs.cs b/Source/Euonia.Bus.Abstract/Events/MessageAcknowledgedEventArgs.cs new file mode 100644 index 0000000..b69d39d --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Events/MessageAcknowledgedEventArgs.cs @@ -0,0 +1,19 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Class MessageAcknowledgedEventArgs. +/// Implements the +/// +/// +public class MessageAcknowledgedEventArgs : MessageProcessedEventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The message context. + public MessageAcknowledgedEventArgs(object message, IMessageContext context) + : base(message, context, MessageProcessType.Receive) + { + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Events/MessageDispatchedEventArgs.cs b/Source/Euonia.Bus.Abstract/Events/MessageDispatchedEventArgs.cs new file mode 100644 index 0000000..569a381 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Events/MessageDispatchedEventArgs.cs @@ -0,0 +1,19 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Class MessageDispatchedEventArgs. +/// Implements the +/// +/// +public class MessageDispatchedEventArgs : MessageProcessedEventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The message context. + public MessageDispatchedEventArgs(object message, IMessageContext context) + : base(message, context, MessageProcessType.Dispatch) + { + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Events/MessageHandledEventArgs.cs b/Source/Euonia.Bus.Abstract/Events/MessageHandledEventArgs.cs new file mode 100644 index 0000000..5cf8de9 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Events/MessageHandledEventArgs.cs @@ -0,0 +1,26 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Occurs when message was handled. +/// +public class MessageHandledEventArgs : EventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// + public MessageHandledEventArgs(object message) + { + Message = message; + } + + /// + /// Gets the handle message. + /// + public object Message { get; } + + /// + /// Gets the handler type. + /// + public Type HandlerType { get; internal set; } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageProcessType.cs b/Source/Euonia.Bus.Abstract/Events/MessageProcessType.cs similarity index 100% rename from Source/Euonia.Bus/Messages/MessageProcessType.cs rename to Source/Euonia.Bus.Abstract/Events/MessageProcessType.cs diff --git a/Source/Euonia.Bus.Abstract/Events/MessageProcessedEventArgs.cs b/Source/Euonia.Bus.Abstract/Events/MessageProcessedEventArgs.cs new file mode 100644 index 0000000..3721a01 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Events/MessageProcessedEventArgs.cs @@ -0,0 +1,40 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Class MessageProcessedEventArgs. +/// Implements the +/// +/// +public class MessageProcessedEventArgs : EventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The message context. + /// Type of the process. + public MessageProcessedEventArgs(object message, IMessageContext context, MessageProcessType processType) + { + Message = message; + Context = context; + ProcessType = processType; + } + + /// + /// Gets the message. + /// + /// The message. + public object Message { get; } + + /// + /// Gets the message id. + /// + /// The message id. + public IMessageContext Context { get; } + + /// + /// Gets the type of the process. + /// + /// The type of the process. + public MessageProcessType ProcessType { get; } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Events/MessageReceivedEventArgs.cs b/Source/Euonia.Bus.Abstract/Events/MessageReceivedEventArgs.cs new file mode 100644 index 0000000..f1a0b2e --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Events/MessageReceivedEventArgs.cs @@ -0,0 +1,19 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Class MessageReceivedEventArgs. +/// Implements the +/// +/// +public class MessageReceivedEventArgs : MessageProcessedEventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The message context. + public MessageReceivedEventArgs(object message, IMessageContext context) + : base(message, context, MessageProcessType.Receive) + { + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageRepliedEventArgs.cs b/Source/Euonia.Bus.Abstract/Events/MessageRepliedEventArgs.cs similarity index 100% rename from Source/Euonia.Bus/Messages/MessageRepliedEventArgs.cs rename to Source/Euonia.Bus.Abstract/Events/MessageRepliedEventArgs.cs diff --git a/Source/Euonia.Bus/Messages/MessageSubscribedEventArgs.cs b/Source/Euonia.Bus.Abstract/Events/MessageSubscribedEventArgs.cs similarity index 100% rename from Source/Euonia.Bus/Messages/MessageSubscribedEventArgs.cs rename to Source/Euonia.Bus.Abstract/Events/MessageSubscribedEventArgs.cs diff --git a/Source/Euonia.Bus.Abstract/IMessageContent.cs b/Source/Euonia.Bus.Abstract/IMessageContent.cs new file mode 100644 index 0000000..5b81296 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/IMessageContent.cs @@ -0,0 +1,30 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +public interface IMessageContent +{ + /// + /// Gets the message content length. + /// + long? Length { get; } + + /// + /// Read the message body as a stream + /// + /// + Stream ReadAsStream(); + + /// + /// Read the message body as a byte array + /// + /// + byte[] ReadAsBytes(); + + /// + /// Read the message body as a string + /// + /// + string ReadAsString(); +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/IMessageContext.cs b/Source/Euonia.Bus.Abstract/IMessageContext.cs new file mode 100644 index 0000000..0545f9e --- /dev/null +++ b/Source/Euonia.Bus.Abstract/IMessageContext.cs @@ -0,0 +1,37 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Interface IMessageContext +/// +public interface IMessageContext : IDisposable +{ + /// + /// Gets or sets the message data. + /// + object Message { get; } + + /// + /// Gets or sets the message unique identifier. + /// + string MessageId { get; set; } + + /// + /// Gets or sets the correlation identifier. + /// + string CorrelationId { get; set; } + + /// + /// Gets or sets the conversation identifier. + /// + string ConversationId { get; set; } + + /// + /// Gets or sets the request trace identifier. + /// + string RequestTraceId { get; set; } + + /// + /// Gets or sets the message request headers. + /// + IReadOnlyDictionary Headers { get; set; } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/IMessageEnvelope.cs b/Source/Euonia.Bus.Abstract/IMessageEnvelope.cs new file mode 100644 index 0000000..acb75e0 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/IMessageEnvelope.cs @@ -0,0 +1,35 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +public interface IMessageEnvelope +{ + /// + /// Gets or sets the identifier of the message. + /// + /// + /// The identifier of the message. + /// + string MessageId { get; } + + /// + /// Gets the correlation identifier. + /// + string CorrelationId { get; } + + /// + /// Gets the conversation identifier. + /// + string ConversationId { get; } + + /// + /// Gets the request trace identifier. + /// + string RequestTraceId { get; } + + /// + /// Gets or sets the message channel. + /// + string Channel { get; set; } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/IRoutedMessage.cs b/Source/Euonia.Bus.Abstract/IRoutedMessage.cs new file mode 100644 index 0000000..68f7ad7 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/IRoutedMessage.cs @@ -0,0 +1,22 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Defines a message pack basic information contract. +/// +public interface IRoutedMessage : IMessageEnvelope +{ + /// + /// Gets the message creation timestamp. + /// + long Timestamp { get; } + + /// + /// Gets a instance that contains the metadata information of the message. + /// + MessageMetadata Metadata { get; } + + /// + /// Gets the data of the message. + /// + object Data { get; } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/MessageContext.cs b/Source/Euonia.Bus.Abstract/MessageContext.cs new file mode 100644 index 0000000..11db180 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/MessageContext.cs @@ -0,0 +1,153 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// The message context. +/// +public sealed class MessageContext : IMessageContext +{ + private readonly WeakEventManager _events = new(); + + private readonly IDictionary _headers = new Dictionary(); + + private bool _disposedValue; + + /// + /// Initializes a new instance of the class. + /// + public MessageContext() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + public MessageContext(object message) + { + Message = message; + } + + /// + /// Initializes a new instance of the class. + /// + /// + public MessageContext(IRoutedMessage pack) + : this(pack.Data) + { + MessageId = pack.MessageId; + CorrelationId = pack.CorrelationId; + ConversationId = pack.ConversationId; + RequestTraceId = pack.RequestTraceId; + } + + /// + /// Invoked while message was handled and replied to dispatcher. + /// + public event EventHandler OnResponse + { + add => _events.AddEventHandler(value); + remove => _events.RemoveEventHandler(value); + } + + /// + /// Invoke while message context disposed. + /// + public event EventHandler Completed + { + add => _events.AddEventHandler(value); + remove => _events.RemoveEventHandler(value); + } + + /// + public object Message { get; } + + /// + public string MessageId { get; set; } + + /// + public string CorrelationId { get; set; } + + /// + public string ConversationId { get; set; } + + /// + public string RequestTraceId { get; set; } + + /// + public IReadOnlyDictionary Headers { get; set; } + + /// + /// Replies message handling result to message dispatcher. + /// + /// The message to reply. + public void Response(object message) + { + _events.HandleEvent(this, new MessageRepliedEventArgs(message), nameof(OnResponse)); + } + + /// + /// Replies message handling result to message dispatcher. + /// + /// The type of the message. + /// The message to reply. + public void Response(TMessage message) + { + Response((object)message); + } + + /// + /// Called after the message has been handled. + /// This operate will raised up the event. + /// + /// + public void Complete(object message) + { + _events.HandleEvent(this, new MessageHandledEventArgs(message), nameof(Completed)); + } + + /// + /// Called after the message has been handled. + /// This operate will raised up the event. + /// + /// + /// + public void Complete(object message, Type handlerType) + { + _events.HandleEvent(this, new MessageHandledEventArgs(message) { HandlerType = handlerType }, nameof(Completed)); + } + + /// + /// Called after the message has been handled. + /// + /// + private void Dispose(bool disposing) + { + if (_disposedValue) + { + return; + } + + if (disposing) + { + Complete(Message); + } + + _events.RemoveEventHandlers(); + _disposedValue = true; + } + + /// + /// Finalizes the current instance of the class. + /// + ~MessageContext() + { + Dispose(disposing: false); + } + + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/MessageHeaders.cs b/Source/Euonia.Bus.Abstract/MessageHeaders.cs new file mode 100644 index 0000000..7ea26b9 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/MessageHeaders.cs @@ -0,0 +1,12 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Defines the message header keys. +/// +public static class MessageHeaders +{ + /// + /// Defines the message correlation identifier header key. + /// + public const string CorrelationId = "CorrelationId"; +} \ No newline at end of file diff --git a/Source/Euonia.Domain/Messages/MessageMetadata.cs b/Source/Euonia.Bus.Abstract/MessageMetadata.cs similarity index 98% rename from Source/Euonia.Domain/Messages/MessageMetadata.cs rename to Source/Euonia.Bus.Abstract/MessageMetadata.cs index 93c5078..4c15031 100644 --- a/Source/Euonia.Domain/Messages/MessageMetadata.cs +++ b/Source/Euonia.Bus.Abstract/MessageMetadata.cs @@ -1,4 +1,4 @@ -namespace Nerosoft.Euonia.Domain; +namespace Nerosoft.Euonia.Bus; /// /// The message meta data. diff --git a/Source/Euonia.Bus.Abstract/Messages/IMessageTypeCache.cs b/Source/Euonia.Bus.Abstract/Messages/IMessageTypeCache.cs new file mode 100644 index 0000000..db8b54b --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Messages/IMessageTypeCache.cs @@ -0,0 +1,8 @@ +using System.Reflection; + +namespace Nerosoft.Euonia.Bus; + +public interface IMessageTypeCache +{ + IEnumerable Properties { get; } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Persistence/IMessageStore.cs b/Source/Euonia.Bus.Abstract/Persistence/IMessageStore.cs new file mode 100644 index 0000000..aed7b42 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Persistence/IMessageStore.cs @@ -0,0 +1,19 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Interface IMessageStore +/// Implements the +/// +/// +public interface IMessageStore : IDisposable +{ + /// + /// Save the specified message to the current message store. + /// + /// The message to be saved. + /// The message context. + /// The cancellation token. + /// + Task SaveAsync(TMessage message, IMessageContext context, CancellationToken cancellationToken = default) + where TMessage : class; +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/RoutedMessage.cs b/Source/Euonia.Bus.Abstract/RoutedMessage.cs new file mode 100644 index 0000000..ff71af3 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/RoutedMessage.cs @@ -0,0 +1,108 @@ +using System.Runtime.Serialization; + +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +[Serializable] +public class RoutedMessage : IRoutedMessage + where TData : class +{ + /// + /// Initializes a new instance of the class. + /// + public RoutedMessage() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The data. + /// + public RoutedMessage(TData data, string channel) + { + Data = data; + Channel = channel; + } + + /// + /// The message type key + /// + private const string MESSAGE_TYPE_KEY = "$nerosoft.euonia:message.type"; + + /// + [DataMember] + public string MessageId { get; set; } = Guid.NewGuid().ToString(); + + /// + [DataMember] + public string CorrelationId { get; } + + /// + [DataMember] + public string ConversationId { get; } + + /// + [DataMember] + public string RequestTraceId { get; } + + /// + [DataMember] + public string Channel { get; set; } + + /// + /// Gets or sets the timestamp that describes when the message occurs. + /// + /// + /// The timestamp that describes when the message occurs. + /// + [DataMember] + public long Timestamp { get; set; } + + /// + /// Gets a instance that contains the metadata information of the message. + /// + [DataMember] + public MessageMetadata Metadata { get; set; } = new(); + + object IRoutedMessage.Data => Data; + + /// + private TData _data; + + /// + /// Gets or sets the payload of the message. + /// + [DataMember] + public TData Data + { + get => _data; + set + { + _data = value; + if (value != null) + { + Metadata[MESSAGE_TYPE_KEY] = value.GetType().AssemblyQualifiedName; + } + } + } + + /// + /// Gets the .NET CLR assembly qualified name of the message. + /// + /// + /// The assembly qualified name of the message. + /// + public string GetTypeName() => Metadata[MESSAGE_TYPE_KEY] as string; + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() => $"{MessageId}:{{GetTypeName()}}"; +} + diff --git a/Source/Euonia.Bus.Abstract/Tracking/IMessageTracking.cs b/Source/Euonia.Bus.Abstract/Tracking/IMessageTracking.cs new file mode 100644 index 0000000..6ae4f7b --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Tracking/IMessageTracking.cs @@ -0,0 +1,8 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Defines message tracking interface. +/// +public interface IMessageTracking +{ +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/CommandBus.cs b/Source/Euonia.Bus.InMemory/CommandBus.cs index 613e4c7..9576381 100644 --- a/Source/Euonia.Bus.InMemory/CommandBus.cs +++ b/Source/Euonia.Bus.InMemory/CommandBus.cs @@ -18,7 +18,7 @@ public class CommandBus : MessageBus, ICommandBus /// /// The message handler context. /// - public CommandBus(IMessageHandlerContext handlerContext, IServiceAccessor accessor) + public CommandBus(IHandlerContext handlerContext, IServiceAccessor accessor) : base(handlerContext, accessor) { MessageReceived += HandleMessageReceivedEvent; @@ -112,7 +112,7 @@ public async Task SendAsync(TCommand command, Cancel } var messageContext = new MessageContext(command); - messageContext.Replied += (_, args) => + messageContext.OnResponse += (_, args) => { taskCompletion.TrySetResult((TResult)args.Result); }; @@ -159,7 +159,7 @@ protected override void Dispose(bool disposing) private async void HandleMessageReceivedEvent(object sender, MessageReceivedEventArgs args) { - OnMessageAcknowledged(new MessageAcknowledgedEventArgs(args.Message, args.MessageContext)); - await HandlerContext.HandleAsync(args.Message, args.MessageContext); + OnMessageAcknowledged(new MessageAcknowledgedEventArgs(args.Message, args.Context)); + await HandlerContext.HandleAsync(args.Message, args.Context); } } \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Euonia.Bus.InMemory.csproj b/Source/Euonia.Bus.InMemory/Euonia.Bus.InMemory.csproj index 9cc9b25..3c50376 100644 --- a/Source/Euonia.Bus.InMemory/Euonia.Bus.InMemory.csproj +++ b/Source/Euonia.Bus.InMemory/Euonia.Bus.InMemory.csproj @@ -1,19 +1,24 @@ - - - + + + disable + true - + - + + + + - + - + + - + True @@ -21,7 +26,7 @@ Resources.resx - + ResXFileCodeGenerator diff --git a/Source/Euonia.Bus.InMemory/EventBus.cs b/Source/Euonia.Bus.InMemory/EventBus.cs index 14e808b..02950c8 100644 --- a/Source/Euonia.Bus.InMemory/EventBus.cs +++ b/Source/Euonia.Bus.InMemory/EventBus.cs @@ -1,18 +1,16 @@ -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus.InMemory; +namespace Nerosoft.Euonia.Bus.InMemory; /// public class EventBus : MessageBus, IEventBus { - private readonly IEventStore _eventStore; + private readonly IMessageStore _messageStore; /// /// /// /// /// - public EventBus(IMessageHandlerContext handlerContext, IServiceAccessor accessor) + public EventBus(IHandlerContext handlerContext, IServiceAccessor accessor) : base(handlerContext, accessor) { MessageReceived += HandleMessageReceivedEvent; @@ -23,20 +21,20 @@ public EventBus(IMessageHandlerContext handlerContext, IServiceAccessor accessor /// /// /// - /// - public EventBus(IMessageHandlerContext handlerContext, IServiceAccessor accessor, IEventStore eventStore) + /// + public EventBus(IHandlerContext handlerContext, IServiceAccessor accessor, IMessageStore messageStore) : base(handlerContext, accessor) { - _eventStore = eventStore; + _messageStore = messageStore; } /// public async Task PublishAsync(TEvent @event, CancellationToken cancellationToken = default) where TEvent : IEvent { - if (_eventStore != null) + if (_messageStore != null) { - await _eventStore.SaveAsync(@event, cancellationToken); + await _messageStore.SaveAsync(@event, cancellationToken); } await Task.Run(() => @@ -58,9 +56,9 @@ public async Task PublishAsync(string name, TEvent @event, CancellationT where TEvent : class { var namedEvent = new NamedEvent(name, @event); - if (_eventStore != null) + if (_messageStore != null) { - await _eventStore.SaveAsync(namedEvent, cancellationToken); + await _messageStore.SaveAsync(namedEvent, cancellationToken); } await Task.Run(() => @@ -78,8 +76,8 @@ protected override void Dispose(bool disposing) private async void HandleMessageReceivedEvent(object sender, MessageReceivedEventArgs args) { - await HandlerContext.HandleAsync(args.Message, args.MessageContext); + await HandlerContext.HandleAsync(args.Message, args.Context); - OnMessageAcknowledged(new MessageAcknowledgedEventArgs(args.Message, args.MessageContext)); + OnMessageAcknowledged(new MessageAcknowledgedEventArgs(args.Message, args.Context)); } } \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/InMemoryBusFactory.cs b/Source/Euonia.Bus.InMemory/InMemoryBusFactory.cs new file mode 100644 index 0000000..dda7adb --- /dev/null +++ b/Source/Euonia.Bus.InMemory/InMemoryBusFactory.cs @@ -0,0 +1,24 @@ +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// The in-memory bus factory. +/// +public class InMemoryBusFactory : IBusFactory +{ + private readonly InMemoryDispatcher _dispatcher; + + /// + /// Initializes a new instance of the class. + /// + /// + public InMemoryBusFactory(InMemoryDispatcher dispatcher) + { + _dispatcher = dispatcher; + } + + /// + public IDispatcher CreateDispatcher() + { + return _dispatcher; + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/InMemoryBusOptions.cs b/Source/Euonia.Bus.InMemory/InMemoryBusOptions.cs new file mode 100644 index 0000000..4b55b0b --- /dev/null +++ b/Source/Euonia.Bus.InMemory/InMemoryBusOptions.cs @@ -0,0 +1,16 @@ +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// +/// +public class InMemoryBusOptions +{ + public bool LazyInitialize { get; set; } = true; + + public int MaxConcurrentCalls { get; set; } = 1; + + /// + /// Gets or sets the messenger reference type. + /// + public MessengerReferenceType MessengerReference { get; set; } = MessengerReferenceType.StrongReference; +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/InMemoryDispatcher.cs b/Source/Euonia.Bus.InMemory/InMemoryDispatcher.cs new file mode 100644 index 0000000..8cda806 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/InMemoryDispatcher.cs @@ -0,0 +1,96 @@ +using Microsoft.Extensions.Options; + +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// +/// +public class InMemoryDispatcher : DisposableObject, IDispatcher +{ + /// + public event EventHandler Delivered; + + private readonly InMemoryBusOptions _options; + + private readonly IMessenger _messenger; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public InMemoryDispatcher(IMessenger messenger, IOptions options) + { + _options = options.Value; + _messenger = messenger; + } + + /// + public async Task PublishAsync(RoutedMessage message, CancellationToken cancellationToken = default) + where TMessage : class + { + var context = new MessageContext(message); + var pack = new MessagePack(message, context) + { + Aborted = cancellationToken + }; + _messenger.Send(pack, message.Channel); + Delivered?.Invoke(this, new MessageDispatchedEventArgs(message.Data, context)); + await Task.CompletedTask; + } + + /// + public async Task SendAsync(RoutedMessage message, CancellationToken cancellationToken = default) + where TMessage : class + { + var context = new MessageContext(message); + var pack = new MessagePack(message, context) + { + Aborted = cancellationToken + }; + + var taskCompletionSource = new TaskCompletionSource(); + + context.Completed += (_, _) => + { + taskCompletionSource.SetResult(); + }; + + _messenger.Send(pack, message.Channel); + Delivered?.Invoke(this, new MessageDispatchedEventArgs(message.Data, context)); + + await taskCompletionSource.Task; + } + + /// + public async Task SendAsync(RoutedMessage message, CancellationToken cancellationToken = default) + where TMessage : class + { + var context = new MessageContext(message); + var pack = new MessagePack(message, context) + { + Aborted = cancellationToken + }; + + var taskCompletionSource = new TaskCompletionSource(); + context.OnResponse += (_, args) => + { + taskCompletionSource.SetResult((TResult)args.Result); + }; + context.Completed += (_, _) => + { + taskCompletionSource.SetResult(default); + }; + + _messenger.Send(pack, message.Channel); + Delivered?.Invoke(this, new MessageDispatchedEventArgs(message.Data, context)); + + return await taskCompletionSource.Task; + } + + /// + protected override void Dispose(bool disposing) + { + _messenger.Cleanup(); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/InMemoryMessageBusModule.cs b/Source/Euonia.Bus.InMemory/InMemoryMessageBusModule.cs deleted file mode 100644 index 1a7a669..0000000 --- a/Source/Euonia.Bus.InMemory/InMemoryMessageBusModule.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Nerosoft.Euonia.Modularity; - -namespace Nerosoft.Euonia.Bus.InMemory; - -/// -/// Import this module to use the in-memory message bus. -/// -[DependsOn(typeof(MessageBusModule))] -public class InMemoryMessageBusModule : ModuleContextBase -{ - /// - public override void ConfigureServices(ServiceConfigurationContext context) - { - context.Services.TryAddSingleton(_ => MessageConverter.Convert); - context.Services.AddInMemoryCommandBus(); - context.Services.AddInMemoryEventBus(); - } -} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/InMemorySubscriber.cs b/Source/Euonia.Bus.InMemory/InMemorySubscriber.cs new file mode 100644 index 0000000..0b0e1b5 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/InMemorySubscriber.cs @@ -0,0 +1,57 @@ +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// +/// +public class InMemorySubscriber : DisposableObject, ISubscriber, IRecipient +{ + /// + /// Occurs when [message received]. + /// + public event EventHandler MessageReceived; + + /// + /// Occurs when [message acknowledged]. + /// + public event EventHandler MessageAcknowledged; + + /// + /// + /// + public event EventHandler MessageSubscribed; + + private readonly string _channel; + + /// + /// Initializes a new instance of the class. + /// + /// + public InMemorySubscriber(string channel) + { + _channel = channel; + } + + /// + public string Name { get; } = nameof(InMemorySubscriber); + + /// + public void Subscribe(Type messageType, Type handlerType) + { + //throw new NotImplementedException(); + } + + #region IDisposable + + /// + protected override void Dispose(bool disposing) + { + } + + #endregion + + /// + public void Receive(MessagePack message) + { + MessageReceived?.Invoke(this, new MessageReceivedEventArgs(message.Message, message.Context)); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Internal/ArrayPoolBufferWriter.cs b/Source/Euonia.Bus.InMemory/Internal/ArrayPoolBufferWriter.cs new file mode 100644 index 0000000..6bf656d --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Internal/ArrayPoolBufferWriter.cs @@ -0,0 +1,120 @@ +using System.Buffers; +using System.Runtime.CompilerServices; + +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// A simple buffer writer implementation using pooled arrays. +/// +/// The type of items to store in the list. +/// +/// This type is a to avoid the object allocation and to +/// enable the pattern-based support. We aren't worried with consumers not +/// using this type correctly since it's private and only accessible within the parent type. +/// +internal ref struct ArrayPoolBufferWriter +{ + /// + /// The default buffer size to use to expand empty arrays. + /// + private const int DEFAULT_INITIAL_BUFFER_SIZE = 128; + + /// + /// The underlying array. + /// + private T[] _array; + + /// + /// The span mapping to . + /// + /// All writes are done through this to avoid covariance checks. + private Span _span; + + /// + /// The starting offset within . + /// + private int _index; + + /// + /// Creates a new instance of the struct. + /// + /// A new instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ArrayPoolBufferWriter Create() + { + ArrayPoolBufferWriter instance; + + instance._span = instance._array = ArrayPool.Shared.Rent(DEFAULT_INITIAL_BUFFER_SIZE); + instance._index = 0; + + return instance; + } + + /// + /// Gets a with the current items. + /// + public ReadOnlySpan Span + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _span[.._index]; + } + + /// + /// Adds a new item to the current collection. + /// + /// The item to add. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(T item) + { + var span = _span; + var index = _index; + + if ((uint)index < (uint)span.Length) + { + span[index] = item; + + _index = index + 1; + } + else + { + ResizeBufferAndAdd(item); + } + } + + /// + /// Resets the underlying array and the stored items. + /// + public void Reset() + { + Array.Clear(_array, 0, _index); + + _index = 0; + } + + /// + /// Resizes when there is no space left for new items, then adds one + /// + /// The item to add. + [MethodImpl(MethodImplOptions.NoInlining)] + private void ResizeBufferAndAdd(T item) + { + var rent = ArrayPool.Shared.Rent(_index << 2); + + Array.Copy(_array, 0, rent, 0, _index); + Array.Clear(_array, 0, _index); + + ArrayPool.Shared.Return(_array); + + _span = _array = rent; + + _span[_index++] = item; + } + + /// + public void Dispose() + { + Array.Clear(_array, 0, _index); + + ArrayPool.Shared.Return(_array); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Internal/ConditionalWeakTable2.cs b/Source/Euonia.Bus.InMemory/Internal/ConditionalWeakTable2.cs new file mode 100644 index 0000000..05efe2d --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Internal/ConditionalWeakTable2.cs @@ -0,0 +1,705 @@ +using System.Diagnostics.CodeAnalysis; +using Nerosoft.Euonia.Bus.InMemory; + +namespace System.Runtime.CompilerServices; + +/// +/// A custom instance that is specifically optimized to be used +/// by . In particular, it offers zero-allocation enumeration of stored items. +/// +/// Tke key of items to store in the table. +/// The values to store in the table. +internal sealed class ConditionalWeakTable2 + where TKey : class + where TValue : class +{ + /// + /// Initial length of the table. Must be a power of two. + /// + private const int INITIAL_CAPACITY = 8; + + /// + /// This lock protects all mutation of data in the table. Readers do not take this lock. + /// + private readonly object _lockObject; + + /// + /// The actual storage for the table; swapped out as the table grows. + /// + private volatile Container _container; + + /// + /// Initializes a new instance of the class. + /// + public ConditionalWeakTable2() + { + _lockObject = new object(); + _container = new Container(this); + } + + /// + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + return _container.TryGetValueWorker(key, out value); + } + + /// + /// Tries to add a new pair to the table. + /// + /// The key to add. + /// The value to associate with key. + public bool TryAdd(TKey key, TValue value) + { + lock (_lockObject) + { + var entryIndex = _container.FindEntry(key, out _); + + if (entryIndex != -1) + { + return false; + } + + CreateEntry(key, value); + + return true; + } + } + + /// + public bool Remove(TKey key) + { + lock (_lockObject) + { + return _container.Remove(key); + } + } + + /// + [UnconditionalSuppressMessage( + "ReflectionAnalysis", + "IL2091", + Justification = "ConditionalWeakTable is only referenced to reuse the callback delegate type, but no value is ever created through reflection.")] + public TValue GetValue(TKey key, ConditionalWeakTable.CreateValueCallback createValueCallback) + { + return TryGetValue(key, out var existingValue) ? existingValue : GetValueLocked(key, createValueCallback); + } + + /// + /// Implements the functionality for under a lock. + /// + /// The input key. + /// The callback to use to create a new item. + /// The new item to store. + [UnconditionalSuppressMessage( + "ReflectionAnalysis", + "IL2091", + Justification = "ConditionalWeakTable is only referenced to reuse the callback delegate type, but no value is ever created through reflection.")] + private TValue GetValueLocked(TKey key, ConditionalWeakTable.CreateValueCallback createValueCallback) + { + // If we got here, the key was not in the table. Invoke the callback + // (outside the lock) to generate the new value for the key. + var newValue = createValueCallback(key); + + lock (_lockObject) + { + // Now that we've taken the lock, must recheck in case we lost a race to add the key + if (_container.TryGetValueWorker(key, out var existingValue)) + { + return existingValue; + } + else + { + // Verified in-lock that we won the race to add the key. Add it now + CreateEntry(key, newValue); + + return newValue; + } + } + } + + public Enumerator GetEnumerator() + { + // This is an optimization specific for this custom table that relies on the way the enumerator is being + // used within the messenger type. Specifically, enumerators are always used in a using block, meaning + // Dispose() is always guaranteed to be executed. Given we cannot remove the internal lock for the table + // as it's needed to ensure consistency in case a container is resurrected (see below), the solution to + // speedup iteration is to avoid taking and releasing a lock repeatedly every single time MoveNext() is + // invoked. This is fine in this specific scenario because we're the only users of the enumerators so + // there's no concern about blocking other threads while enumerating. So here we just preemptively take + // a lock for the entire lifetime of the enumerator, and just release it once once we're done. + Monitor.Enter(_lockObject); + + return new(this); + } + + /// + /// Provides an enumerator for the current instance. + /// + public ref struct Enumerator + { + /// + /// Parent table, set to null when disposed. + /// + private ConditionalWeakTable2 _table; + + /// + /// Last index in the container that should be enumerated. + /// + private readonly int _maxIndexInclusive; + + /// + /// The current index into the container. + /// + private int _currentIndex; + + /// + /// The current key, if available. + /// + private TKey _key; + + /// + /// The current value, if available. + /// + private TValue _value; + + /// + /// Initializes a new instance of the class. + /// + /// The input instance being enumerated. + public Enumerator(ConditionalWeakTable2 table) + { + // Store a reference to the parent table and increase its active enumerator count + _table = table; + + var container = table._container; + + if (container is null || container.FirstFreeEntry == 0) + { + // The max index is the same as the current to prevent enumeration + _maxIndexInclusive = -1; + } + else + { + // Store the max index to be enumerated + _maxIndexInclusive = container.FirstFreeEntry - 1; + } + + _currentIndex = -1; + _key = null; + _value = null; + } + + /// + public void Dispose() + { + // Release the lock + Monitor.Exit(_table._lockObject); + + _table = null!; + + // Ensure we don't keep the last current alive unnecessarily + _key = null; + _value = null; + } + + /// + public bool MoveNext() + { + // From the table, we have to get the current container. This could have changed + // since we grabbed the enumerator, but the index-to-pair mapping should not have + // due to there being at least one active enumerator. If the table (or rather its + // container at the time) has already been finalized, this will be null. + var c = _table._container; + + var currentIndex = _currentIndex; + var maxIndexInclusive = _maxIndexInclusive; + + // We have the container. Find the next entry to return, if there is one. We need to loop as we + // may try to get an entry that's already been removed or collected, in which case we try again. + while (currentIndex < maxIndexInclusive) + { + currentIndex++; + + if (c.TryGetEntry(currentIndex, out _key, out _value)) + { + _currentIndex = currentIndex; + + return true; + } + } + + _currentIndex = currentIndex; + + return false; + } + + /// + /// Gets the current key. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TKey GetKey() + { + return _key!; + } + + /// + /// Gets the current value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue GetValue() + { + return _value!; + } + } + + /// + /// Worker for adding a new key/value pair. Will resize the container if it is full. + /// + /// The key for the new entry. + /// The value for the new entry. + private void CreateEntry(TKey key, TValue value) + { + var container = _container; + + if (!container.HasCapacity) + { + _container = container = container.Resize(); + } + + container.CreateEntryNoResize(key, value); + } + + /// + /// A single entry within a instance. + /// + private struct Entry + { + /// + /// Holds key and value using a weak reference for the key and a strong reference for the + /// value that is traversed only if the key is reachable without going through the value. + /// + public DependentHandle DepHnd; + + /// + /// Cached copy of key's hashcode. + /// + public int HashCode; + + /// + /// Index of next entry, -1 if last. + /// + public int Next; + } + + /// + /// Container holds the actual data for the table. A given instance of Container always has the same capacity. When we need + /// more capacity, we create a new Container, copy the old one into the new one, and discard the old one. This helps enable + /// lock-free reads from the table, as readers never need to deal with motion of entries due to rehashing. + /// + private sealed class Container + { + /// + /// The with which this container is associated. + /// + private readonly ConditionalWeakTable2 _parent; + + /// + /// buckets[hashcode & (buckets.Length - 1)] contains index of the first entry in bucket (-1 if empty). + /// + private int[] _buckets; + + /// + /// The table entries containing the stored dependency handles + /// + private Entry[] _entries; + + /// + /// firstFreeEntry < entries.Length => table has capacity, entries grow from the bottom of the table. + /// + private int _firstFreeEntry; + + /// + /// Flag detects if OOM or other background exception threw us out of the lock. + /// + private bool _invalid; + + /// + /// Set to true when initially finalized + /// + private bool _finalized; + + /// + /// Used to ensure the next allocated container isn't finalized until this one is GC'd. + /// + private volatile object _oldKeepAlive; + + /// + /// Initializes a new instance of the class. + /// + /// The input object associated with the current instance. + internal Container(ConditionalWeakTable2 parent) + { + _buckets = new int[INITIAL_CAPACITY]; + + for (var i = 0; i < _buckets.Length; i++) + { + _buckets[i] = -1; + } + + _entries = new Entry[INITIAL_CAPACITY]; + + // Only store the parent after all of the allocations have happened successfully. + // Otherwise, as part of growing or clearing the container, we could end up allocating + // a new Container that fails (OOMs) part way through construction but that gets finalized + // and ends up clearing out some other container present in the associated CWT. + _parent = parent; + } + + /// + /// Initializes a new instance of the class. + /// + /// The input object associated with the current instance. + /// The array of buckets. + /// The array of entries. + /// The index of the first free entry. + private Container(ConditionalWeakTable2 parent, int[] buckets, Entry[] entries, int firstFreeEntry) + { + _parent = parent; + _buckets = buckets; + _entries = entries; + _firstFreeEntry = firstFreeEntry; + } + + /// + /// Gets the capacity of the current container. + /// + internal bool HasCapacity => _firstFreeEntry < _entries.Length; + + /// + /// Gets the index of the first free entry. + /// + internal int FirstFreeEntry => _firstFreeEntry; + + /// + /// Worker for adding a new key/value pair. Container must NOT be full. + /// + internal void CreateEntryNoResize(TKey key, TValue value) + { + VerifyIntegrity(); + + _invalid = true; + + var hashCode = RuntimeHelpers.GetHashCode(key) & int.MaxValue; + var newEntry = _firstFreeEntry++; + + _entries[newEntry].HashCode = hashCode; + _entries[newEntry].DepHnd = new DependentHandle(key, value); + + var bucket = hashCode & (_buckets.Length - 1); + + _entries[newEntry].Next = _buckets[bucket]; + + // This write must be volatile, as we may be racing with concurrent readers. If they + // see the new entry, they must also see all of the writes earlier in this method. + Volatile.Write(ref _buckets[bucket], newEntry); + + _invalid = false; + } + + /// + /// Worker for finding a key/value pair. Must hold lock. + /// + internal bool TryGetValueWorker(TKey key, [MaybeNullWhen(false)] out TValue value) + { + var entryIndex = FindEntry(key, out var secondary); + + value = Unsafe.As(secondary); + + return entryIndex != -1; + } + + /// + /// Returns -1 if not found (if key expires during FindEntry, this can be treated as "not found."). + /// Must hold lock, or be prepared to retry the search while holding lock. + /// + /// This method requires to be on the stack to be properly tracked. + internal int FindEntry(TKey key, out object value) + { + var hashCode = RuntimeHelpers.GetHashCode(key) & int.MaxValue; + var bucket = hashCode & (_buckets.Length - 1); + + for (var entriesIndex = Volatile.Read(ref _buckets[bucket]); entriesIndex != -1; entriesIndex = _entries[entriesIndex].Next) + { + if (_entries[entriesIndex].HashCode == hashCode) + { + // if (_entries[entriesIndex].depHnd.UnsafeGetTargetAndDependent(out value) == key) + (var oKey, value) = _entries[entriesIndex].DepHnd.TargetAndDependent; + + if (oKey == key) + { + // Ensure we don't get finalized while accessing DependentHandle + GC.KeepAlive(this); + + return entriesIndex; + } + } + } + + // Ensure we don't get finalized while accessing DependentHandle + GC.KeepAlive(this); + + value = null; + + return -1; + } + + /// + /// Gets the entry at the specified entry index. + /// + internal bool TryGetEntry(int index, [NotNullWhen(true)] out TKey key, [MaybeNullWhen(false)] out TValue value) + { + if (index < _entries.Length) + { + // object? oKey = entries[index].depHnd.UnsafeGetTargetAndDependent(out object? oValue); + var (oKey, oValue) = _entries[index].DepHnd.TargetAndDependent; + + // Ensure we don't get finalized while accessing DependentHandle + GC.KeepAlive(this); + + if (oKey != null) + { + key = Unsafe.As(oKey); + value = Unsafe.As(oValue)!; + + return true; + } + } + + key = default; + value = default; + + return false; + } + + /// + /// Removes the specified key from the table, if it exists. + /// + internal bool Remove(TKey key) + { + VerifyIntegrity(); + + var entryIndex = FindEntry(key, out _); + + if (entryIndex != -1) + { + RemoveIndex(entryIndex); + + return true; + } + + return false; + } + + /// + /// Removes a given entry at a specified index. + /// + /// The index of the entry to remove. + private void RemoveIndex(int entryIndex) + { + ref var entry = ref _entries[entryIndex]; + + // We do not free the handle here, as we may be racing with readers who already saw the hash code. + // Instead, we simply overwrite the entry's hash code, so subsequent reads will ignore it. + // The handle will be free'd in Container's finalizer, after the table is resized or discarded. + Volatile.Write(ref entry.HashCode, -1); + + // Also, clear the key to allow GC to collect objects pointed to by the entry + // entry.depHnd.UnsafeSetTargetToNull(); + entry.DepHnd.Target = null; + } + + /// + /// Resize, and scrub expired keys off bucket lists. Must hold . + /// + /// + /// is less than entries.Length on exit, that is, the table has at least one free entry. + /// + internal Container Resize() + { + var hasExpiredEntries = false; + var newSize = _buckets.Length; + + // If any expired or removed keys exist, we won't resize + for (var entriesIndex = 0; entriesIndex < _entries.Length; entriesIndex++) + { + ref var entry = ref _entries[entriesIndex]; + + if (entry.HashCode == -1) + { + // the entry was removed + hasExpiredEntries = true; + + break; + } + + if (entry.DepHnd.IsAllocated && + // entry.depHnd.UnsafeGetTarget() is null) + entry.DepHnd.Target is null) + { + // the entry has expired + hasExpiredEntries = true; + + break; + } + } + + if (!hasExpiredEntries) + { + // Not necessary to check for overflow here, the attempt to allocate new arrays will throw + newSize = _buckets.Length * 2; + } + + return Resize(newSize); + } + + /// + /// Creates a new of a specified size with the current items. + /// + /// The new requested size. + /// The new instance with the requested size. + internal Container Resize(int newSize) + { + // Reallocate both buckets and entries and rebuild the bucket and entries from scratch. + // This serves both to scrub entries with expired keys and to put the new entries in the proper bucket. + var newBuckets = new int[newSize]; + + for (var bucketIndex = 0; bucketIndex < newBuckets.Length; bucketIndex++) + { + newBuckets[bucketIndex] = -1; + } + + var newEntries = new Entry[newSize]; + var newEntriesIndex = 0; + + // There are no active enumerators, which means we want to compact by removing expired/removed entries + for (var entriesIndex = 0; entriesIndex < _entries.Length; entriesIndex++) + { + ref var oldEntry = ref _entries[entriesIndex]; + var hashCode = oldEntry.HashCode; + var depHnd = oldEntry.DepHnd; + + if (hashCode != -1 && depHnd.IsAllocated) + { + // if (depHnd.UnsafeGetTarget() is not null) + if (depHnd.Target is not null) + { + ref var newEntry = ref newEntries[newEntriesIndex]; + + // Entry is used and has not expired. Link it into the appropriate bucket list + newEntry.HashCode = hashCode; + newEntry.DepHnd = depHnd; + + var bucket = hashCode & (newBuckets.Length - 1); + + newEntry.Next = newBuckets[bucket]; + newBuckets[bucket] = newEntriesIndex; + newEntriesIndex++; + } + else + { + // Pretend the item was removed, so that this container's finalizer will clean up this dependent handle + Volatile.Write(ref oldEntry.HashCode, -1); + } + } + } + + // Create the new container. We want to transfer the responsibility of freeing the handles from + // the old container to the new container, and also ensure that the new container isn't finalized + // while the old container may still be in use. As such, we store a reference from the old container + // to the new one, which will keep the new container alive as long as the old one is. + Container newContainer = new(_parent!, newBuckets, newEntries, newEntriesIndex); + + // Once this is set, the old container's finalizer will not free transferred dependent handles + _oldKeepAlive = newContainer; + + // Ensure we don't get finalized while accessing DependentHandles + GC.KeepAlive(this); + + return newContainer; + } + + /// + /// Verifies that the current instance is valid. + /// + /// Thrown if the current instance is invalid. + private void VerifyIntegrity() + { + if (_invalid) + { + static void Throw() => throw new InvalidOperationException("The current collection is in a corrupted state."); + + Throw(); + } + } + + /// + /// Finalizes the current instance. + /// + ~Container() + { + // Skip doing anything if the container is invalid, including if somehow + // the container object was allocated but its associated table never set. + if (_invalid || _parent is null) + { + return; + } + + // It's possible that the ConditionalWeakTable2 could have been resurrected, in which case code could + // be accessing this Container as it's being finalized. We don't support usage after finalization, + // but we also don't want to potentially corrupt state by allowing dependency handles to be used as + // or after they've been freed. To avoid that, if it's at all possible that another thread has a + // reference to this container via the CWT, we remove such a reference and then re-register for + // finalization: the next time around, we can be sure that no references remain to this and we can + // clean up the dependency handles without fear of corruption. + if (!_finalized) + { + _finalized = true; + + lock (_parent._lockObject) + { + if (_parent._container == this) + { + _parent._container = null!; + } + } + + // Next time it's finalized, we'll be sure there are no remaining refs + GC.ReRegisterForFinalize(this); + + return; + } + + var entries = _entries; + + _invalid = true; + _entries = null!; + _buckets = null!; + + if (entries != null) + { + for (var entriesIndex = 0; entriesIndex < entries.Length; entriesIndex++) + { + // We need to free handles in two cases: + // - If this container still owns the dependency handle (meaning ownership hasn't been transferred + // to another container that replaced this one), then it should be freed. + // - If this container had the entry removed, then even if in general ownership was transferred to + // another container, removed entries are not, therefore this container must free them. + if (_oldKeepAlive is null || entries[entriesIndex].HashCode == -1) + { + entries[entriesIndex].DepHnd.Dispose(); + } + } + } + } + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Internal/EquatableType.cs b/Source/Euonia.Bus.InMemory/Internal/EquatableType.cs new file mode 100644 index 0000000..da2e276 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Internal/EquatableType.cs @@ -0,0 +1,77 @@ +using System.Runtime.CompilerServices; + +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// A simple type representing an immutable pair of types. +/// +/// +/// This type replaces a simple as it's faster in its +/// and methods, and because +/// unlike a value tuple it exposes its fields as immutable. Additionally, the +/// and fields provide additional clarity reading +/// the code compared to and . +/// +internal readonly struct EquatableType : IEquatable +{ + /// + /// The type of registered message. + /// + public readonly Type Message; + + /// + /// The type of registration token. + /// + public readonly Type Token; + + /// + /// Initializes a new instance of the struct. + /// + /// The type of registered message. + /// The type of registration token. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public EquatableType(Type message, Type token) + { + Message = message; + Token = token; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(EquatableType other) + { + // We can't just use reference equality, as that's technically not guaranteed + // to work and might fail in very rare cases (eg. with type forwarding between + // different assemblies). Instead, we can use the == operator to compare for + // equality, which still avoids the callvirt overhead of calling Type.Equals, + // and is also implemented as a JIT intrinsic on runtimes such as .NET Core. + return + Message == other.Message && + Token == other.Token; + } + + /// + public override bool Equals(object obj) + { + return obj is EquatableType other && Equals(other); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() + { + // To combine the two hashes, we can simply use the fast djb2 hash algorithm. Unfortunately we + // can't really skip the callvirt here (eg. by using RuntimeHelpers.GetHashCode like in other + // cases), as there are some niche cases mentioned above that might break when doing so. + // However since this method is not generally used in a hot path (eg. the message broadcasting + // only invokes this a handful of times when initially retrieving the target mapping), this + // doesn't actually make a noticeable difference despite the minor overhead of the virtual call. + int hash = Message.GetHashCode(); + + hash = (hash << 5) + hash; + + hash += Token.GetHashCode(); + + return hash; + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Internal/HashHelpers.cs b/Source/Euonia.Bus.InMemory/Internal/HashHelpers.cs new file mode 100644 index 0000000..12cc5c7 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Internal/HashHelpers.cs @@ -0,0 +1,118 @@ +using System.Runtime.CompilerServices; + +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// +/// +internal static class HashHelpers +{ + /// + /// Maximum prime smaller than the maximum array length. + /// + private const int MAX_PRIME_ARRAY_LENGTH = 0x7FFFFFC3; + + /// + /// An arbitrary prime factor used in . + /// + private const int HASH_PRIME = 101; + + /// + /// Table of prime numbers to use as hash table sizes. + /// + private static readonly int[] _primes = + { + 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, + 1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, + 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437, + 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, + 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369 + }; + + /// + /// Checks whether a value is a prime. + /// + /// The value to check. + /// Whether or not is a prime. + private static bool IsPrime(int candidate) + { + if ((candidate & 1) != 0) + { + int limit = (int)Math.Sqrt(candidate); + + for (int divisor = 3; divisor <= limit; divisor += 2) + { + if ((candidate % divisor) == 0) + { + return false; + } + } + + return true; + } + + return candidate == 2; + } + + /// + /// Gets the smallest prime bigger than a specified value. + /// + /// The target minimum value. + /// The new prime that was found. + public static int GetPrime(int min) + { + foreach (var prime in _primes) + { + if (prime >= min) + { + return prime; + } + } + + for (var i = min | 1; i < int.MaxValue; i += 2) + { + if (IsPrime(i) && ((i - 1) % HASH_PRIME != 0)) + { + return i; + } + } + + return min; + } + + /// + /// Returns size of hashtable to grow to. + /// + /// The previous table size. + /// The expanded table size. + public static int ExpandPrime(int oldSize) + { + int newSize = 2 * oldSize; + + if ((uint)newSize > MAX_PRIME_ARRAY_LENGTH && MAX_PRIME_ARRAY_LENGTH > oldSize) + { + return MAX_PRIME_ARRAY_LENGTH; + } + + return GetPrime(newSize); + } + + /// + /// Returns approximate reciprocal of the divisor: ceil(2**64 / divisor). + /// + /// This should only be used on 64-bit. + public static ulong GetFastModMultiplier(uint divisor) + { + return ulong.MaxValue / divisor + 1; + } + + /// + /// Performs a mod operation using the multiplier pre-computed with . + /// + /// This should only be used on 64-bit. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint FastMod(uint value, uint divisor, ulong multiplier) + { + return (uint)(((((multiplier * value) >> 32) + 1) * divisor) >> 32); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Internal/MessageHandlerDispatcher.cs b/Source/Euonia.Bus.InMemory/Internal/MessageHandlerDispatcher.cs new file mode 100644 index 0000000..aed0698 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Internal/MessageHandlerDispatcher.cs @@ -0,0 +1,52 @@ +using System.Runtime.CompilerServices; + +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// A dispatcher type that invokes a given callback. +/// +/// +/// This type is used to avoid type aliasing with when the generic +/// arguments are not known. Additionally, this is an abstract class and not an interface so that when +/// is called, virtual dispatch will be used instead of interface +/// stub dispatch, which is much slower and with more indirections. +/// +internal abstract class MessageHandlerDispatcher +{ + /// + /// Invokes the current callback on a target recipient, with a specified message. + /// + /// The target recipient for the message. + /// The message being broadcast. + public abstract void Invoke(object recipient, object message); + + /// + /// A generic version of . + /// + /// The type of recipient for the message. + /// The type of message to receive. + public sealed class For : MessageHandlerDispatcher + where TRecipient : class + where TMessage : class + { + /// + /// The underlying callback to invoke. + /// + private readonly MessageHandler _handler; + + /// + /// Initializes a new instance of the class. + /// + /// The input instance. + public For(MessageHandler handler) + { + _handler = handler; + } + + /// + public override void Invoke(object recipient, object message) + { + _handler(Unsafe.As(recipient), Unsafe.As(message)); + } + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Internal/TypeDictionary.Interface.cs b/Source/Euonia.Bus.InMemory/Internal/TypeDictionary.Interface.cs new file mode 100644 index 0000000..b4dd9bd --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Internal/TypeDictionary.Interface.cs @@ -0,0 +1,51 @@ +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// A base interface masking instances and exposing non-generic functionalities. +/// +internal interface TypeDictionary +{ + /// + /// Gets the count of entries in the dictionary. + /// + int Count { get; } + + /// + /// Clears the current dictionary. + /// + void Clear(); +} + +/// +/// An interface providing key type contravariant access to a instance. +/// +/// The contravariant type of keys in the dictionary. +internal interface TypeDictionary : TypeDictionary + where TKey : IEquatable +{ + /// + /// Tries to remove a value with a specified key, if present. + /// + /// The key of the value to remove. + /// Whether or not the key was present. + bool TryRemove(TKey key); +} + +/// +/// An interface providing key type contravariant and value type covariant access +/// to a instance. +/// +/// The contravariant type of keys in the dictionary. +/// The covariant type of values in the dictionary. +internal interface ITypeDictionary : TypeDictionary + where TKey : IEquatable + where TValue : class +{ + /// + /// Gets the value with the specified key. + /// + /// The key to look for. + /// The returned value. + /// Thrown if the key wasn't present. + TValue this[TKey key] { get; } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Internal/TypeDictionary.cs b/Source/Euonia.Bus.InMemory/Internal/TypeDictionary.cs new file mode 100644 index 0000000..eff66f5 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Internal/TypeDictionary.cs @@ -0,0 +1,473 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Nerosoft.Euonia.Bus.InMemory; + +#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type + +/// +/// A specialized implementation to be used with messenger types. +/// +/// The type of keys in the dictionary. +/// The type of values in the dictionary. +[DebuggerDisplay("Count = {Count}")] +internal class TypeDictionary : ITypeDictionary + where TKey : IEquatable + where TValue : class +{ + /// + /// The index indicating the start of a free linked list. + /// + private const int START_OF_FREE_LIST = -3; + + /// + /// The array of 1-based indices for the items stored in . + /// + private int[] _buckets; + + /// + /// The array of currently stored key-value pairs (ie. the lists for each hash group). + /// + private Entry[] _entries; + + /// + /// A coefficient used to speed up retrieving the target bucket when doing lookups. + /// + private ulong _fastModMultiplier; + + /// + /// The current number of items stored in the map. + /// + private int _count; + + /// + /// The 1-based index for the start of the free list within . + /// + private int _freeList; + + /// + /// The total number of empty items. + /// + private int _freeCount; + + /// + /// Initializes a new instance of the class. + /// + public TypeDictionary() + { + Initialize(0); + } + + /// + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _count - _freeCount; + } + + /// + public TValue this[TKey key] + { + get + { + ref var value = ref FindValue(key); + + if (!Unsafe.IsNullRef(ref value)) + { + return value; + } + + ThrowArgumentExceptionForKeyNotFound(key); + + return default!; + } + } + + /// + public void Clear() + { + var count = _count; + + if (count > 0) + { +#if NETSTANDARD2_0_OR_GREATER + Array.Clear(_buckets!, 0, _buckets!.Length); +#else + Array.Clear(_buckets!); +#endif + + _count = 0; + _freeList = -1; + _freeCount = 0; + + Array.Clear(_entries!, 0, count); + } + } + + /// + /// Checks whether or not the dictionary contains a pair with a specified key. + /// + /// The key to look for. + /// Whether or not the key was present in the dictionary. + public bool ContainsKey(TKey key) + { + return !Unsafe.IsNullRef(ref FindValue(key)); + } + + /// + /// Gets the value if present for the specified key. + /// + /// The key to look for. + /// The value found, otherwise . + /// Whether or not the key was present. + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + ref var valRef = ref FindValue(key); + + if (!Unsafe.IsNullRef(ref valRef)) + { + value = valRef; + return true; + } + + value = default; + + return false; + } + + /// + public bool TryRemove(TKey key) + { + var hashCode = (uint)key.GetHashCode(); + ref var bucket = ref GetBucket(hashCode); + var entries = _entries; + var last = -1; + var i = bucket - 1; + + while (i >= 0) + { + ref var entry = ref entries[i]; + + if (entry.HashCode == hashCode && entry.Key.Equals(key)) + { + if (last < 0) + { + bucket = entry.Next + 1; + } + else + { + entries[last].Next = entry.Next; + } + + entry.Next = START_OF_FREE_LIST - _freeList; + + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + entry.Key = default!; + } + + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + entry.Value = default!; + } + + _freeList = i; + _freeCount++; + + return true; + } + + last = i; + i = entry.Next; + } + + return false; + } + + /// + /// Gets the value for the specified key, or, if the key is not present, + /// adds an entry and returns the value by ref. This makes it possible to + /// add or update a value in a single look up operation. + /// + /// Key to look for. + /// Reference to the new or existing value. + public ref TValue GetOrAddValueRef(TKey key) + { + var entries = _entries; + var hashCode = (uint)key.GetHashCode(); + ref var bucket = ref GetBucket(hashCode); + var i = bucket - 1; + + while (true) + { + if ((uint)i >= (uint)entries.Length) + { + break; + } + + if (entries[i].HashCode == hashCode && entries[i].Key.Equals(key)) + { + return ref entries[i].Value!; + } + + i = entries[i].Next; + } + + int index; + + if (_freeCount > 0) + { + index = _freeList; + + _freeList = START_OF_FREE_LIST - entries[_freeList].Next; + _freeCount--; + } + else + { + var count = _count; + + if (count == entries.Length) + { + Resize(); + bucket = ref GetBucket(hashCode); + } + + index = count; + + _count = count + 1; + + entries = _entries; + } + + ref var entry = ref entries![index]; + + entry.HashCode = hashCode; + entry.Next = bucket - 1; + entry.Key = key; + entry.Value = default!; + bucket = index + 1; + + return ref entry.Value!; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator GetEnumerator() => new(this); + + /// + /// Enumerator for . + /// + public ref struct Enumerator + { + /// + /// The entries being enumerated. + /// + private readonly Entry[] _entries; + + /// + /// The current enumeration index. + /// + private int _index; + + /// + /// The current dictionary count. + /// + private readonly int _count; + + /// + /// Creates a new instance. + /// + /// The input dictionary to enumerate. + internal Enumerator(TypeDictionary dictionary) + { + _entries = dictionary._entries; + _index = 0; + _count = dictionary._count; + } + + /// + public bool MoveNext() + { + while ((uint)_index < (uint)_count) + { + // We need to preemptively increment the current index so that we still correctly keep track + // of the current position in the dictionary even if the users doesn't access any of the + // available properties in the enumerator. As this is a possibility, we can't rely on one of + // them to increment the index before MoveNext is invoked again. We ditch the standard enumerator + // API surface here to expose the Key/Value properties directly and minimize the memory copies. + // For the same reason, we also removed the KeyValuePair field here, and instead + // rely on the properties lazily accessing the target instances directly from the current entry + // pointed at by the index property (adjusted backwards to account for the increment here). + if (_entries![_index++].Next >= -1) + { + return true; + } + } + + _index = _count + 1; + + return false; + } + + /// + /// Gets the current key. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TKey GetKey() + { + return _entries[_index - 1].Key; + } + + /// + /// Gets the current value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue GetValue() + { + return _entries[_index - 1].Value!; + } + } + + /// + /// Gets the value for the specified key, or. + /// + /// Key to look for. + /// Reference to the existing value. + private unsafe ref TValue FindValue(TKey key) + { + ref var entry = ref *(Entry*)null; + + var hashCode = (uint)key.GetHashCode(); + var i = GetBucket(hashCode); + var entries = _entries; + + i--; + do + { + if ((uint)i >= (uint)entries.Length) + { + goto ReturnNotFound; + } + + entry = ref entries[i]; + + if (entry.HashCode == hashCode && entry.Key.Equals(key)) + { + goto ReturnFound; + } + + i = entry.Next; + } + while (true); + + ReturnFound: + ref var value = ref entry.Value!; + + Return: + return ref value; + + ReturnNotFound: + + value = ref *(TValue*)null; + + goto Return; + } + + /// + /// Initializes the current instance. + /// + /// The target capacity. + /// + [MemberNotNull(nameof(_buckets), nameof(_entries))] + private void Initialize(int capacity) + { + var size = HashHelpers.GetPrime(capacity); + var buckets = new int[size]; + var entries = new Entry[size]; + + _freeList = -1; + _fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)size); + _buckets = buckets; + _entries = entries; + } + + /// + /// Resizes the current dictionary to reduce the number of collisions + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void Resize() + { + var newSize = HashHelpers.ExpandPrime(_count); + var entries = new Entry[newSize]; + var count = _count; + + Array.Copy(_entries, entries, count); + + _buckets = new int[newSize]; + _fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)newSize); + + for (var i = 0; i < count; i++) + { + if (entries[i].Next >= -1) + { + ref var bucket = ref GetBucket(entries[i].HashCode); + + entries[i].Next = bucket - 1; + bucket = i + 1; + } + } + + _entries = entries; + } + + /// + /// Gets a reference to a target bucket from an input hashcode. + /// + /// The input hashcode. + /// A reference to the target bucket. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ref int GetBucket(uint hashCode) + { + var buckets = _buckets!; + + return ref buckets[HashHelpers.FastMod(hashCode, (uint)buckets.Length, _fastModMultiplier)]; + } + + /// + /// A type representing a map entry, ie. a node in a given list. + /// + private struct Entry + { + /// + /// The cached hashcode for ; + /// + public uint HashCode; + + /// + /// 0-based index of next entry in chain: -1 means end of chain + /// also encodes whether this entry this.itself_ is part of the free list by changing sign and subtracting 3, + /// so -2 means end of free list, -3 means index 0 but on free list, -4 means index 1 but on free list, etc. + /// + public int Next; + + /// + /// The key for the value in the current node. + /// + public TKey Key; + + /// + /// The value in the current node, if present. + /// + public TValue Value; + } + + /// + /// Throws an when trying to load an element with a missing key. + /// + private static void ThrowArgumentExceptionForKeyNotFound(TKey key) + { + throw new ArgumentException($"The target key {key} was not present in the dictionary"); + } +} +#pragma warning restore CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Internal/Unit.cs b/Source/Euonia.Bus.InMemory/Internal/Unit.cs new file mode 100644 index 0000000..0920a05 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Internal/Unit.cs @@ -0,0 +1,29 @@ +using System.Runtime.CompilerServices; + +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// An empty type representing a generic token with no specific value. +/// +internal readonly struct Unit : IEquatable +{ + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Unit other) + { + return true; + } + + /// + public override bool Equals(object obj) + { + return obj is Unit; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() + { + return 0; + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/MessageBus.cs b/Source/Euonia.Bus.InMemory/MessageBus.cs index c1ce67e..5fb3083 100644 --- a/Source/Euonia.Bus.InMemory/MessageBus.cs +++ b/Source/Euonia.Bus.InMemory/MessageBus.cs @@ -3,119 +3,118 @@ /// /// Class MessageBus. /// Implements the -/// Implements the +/// Implements the /// /// -/// -public abstract class MessageBus : DisposableObject, IMessageBus +public abstract class MessageBus : DisposableObject { - /// - /// Occurs when [message subscribed]. - /// - public event EventHandler MessageSubscribed; + /// + /// Occurs when [message subscribed]. + /// + public event EventHandler MessageSubscribed; - /// - /// Occurs when [message dispatched]. - /// - public event EventHandler MessageDispatched; + /// + /// Occurs when [message dispatched]. + /// + public event EventHandler Dispatched; - /// - /// Occurs when [message received]. - /// - public event EventHandler MessageReceived; + /// + /// Occurs when [message received]. + /// + public event EventHandler MessageReceived; - /// - /// Occurs when [message acknowledged]. - /// - public event EventHandler MessageAcknowledged; + /// + /// Occurs when [message acknowledged]. + /// + public event EventHandler MessageAcknowledged; - /// - /// Initializes a new instance of the class. - /// - /// The message handler context. - /// - protected MessageBus(IMessageHandlerContext handlerContext, IServiceAccessor accessor) - { - HandlerContext = handlerContext; - ServiceAccessor = accessor; - } + /// + /// Initializes a new instance of the class. + /// + /// The message handler context. + /// + protected MessageBus(IHandlerContext handlerContext, IServiceAccessor accessor) + { + HandlerContext = handlerContext; + ServiceAccessor = accessor; + } - /// - /// Gets the service accessor. - /// - protected IServiceAccessor ServiceAccessor { get; } + /// + /// Gets the service accessor. + /// + protected IServiceAccessor ServiceAccessor { get; } - /// - /// Gets the message handler context. - /// - /// The message handler context. - protected IMessageHandlerContext HandlerContext { get; } + /// + /// Gets the message handler context. + /// + /// The message handler context. + protected IHandlerContext HandlerContext { get; } - /// - /// Handles the event. - /// - /// The instance containing the event data. - protected virtual void OnMessageSubscribed(MessageSubscribedEventArgs args) - { - var queue = new MessageQueue(); - queue.MessagePushed += (sender, e) => - { - var message = (sender as MessageQueue)?.Dequeue(); - if (message == null) - { - return; - } + /// + /// Handles the event. + /// + /// The instance containing the event data. + protected virtual void OnMessageSubscribed(MessageSubscribedEventArgs args) + { + var queue = new MessageQueue(); + queue.MessagePushed += (sender, e) => + { + var message = (sender as MessageQueue)?.Dequeue(); + if (message == null) + { + return; + } - OnMessageReceived(new MessageReceivedEventArgs(message, e.MessageContext)); - }; - MessageQueue.AddQueue(args.MessageType, queue); - MessageSubscribed?.Invoke(this, args); - } + OnMessageReceived(new MessageReceivedEventArgs(message, e.Context)); + }; + MessageQueue.AddQueue(args.MessageType, queue); + MessageSubscribed?.Invoke(this, args); + } - /// - /// Handles the event. - /// - /// The instance containing the event data. - protected virtual void OnMessageDispatched(MessageDispatchedEventArgs args) - { - MessageDispatched?.Invoke(this, args); - } + /// + /// Handles the event. + /// + /// The instance containing the event data. + protected virtual void OnMessageDispatched(MessageDispatchedEventArgs args) + { + Dispatched?.Invoke(this, args); + } - /// - /// Handles the event. - /// - /// The instance containing the event data. - protected virtual void OnMessageReceived(MessageReceivedEventArgs args) - { - MessageReceived?.Invoke(this, args); - } + /// + /// Handles the event. + /// + /// The instance containing the event data. + protected virtual void OnMessageReceived(MessageReceivedEventArgs args) + { + MessageReceived?.Invoke(this, args); + } - /// - /// Handles the event. - /// - /// The instance containing the event data. - protected virtual void OnMessageAcknowledged(MessageAcknowledgedEventArgs args) - { - MessageAcknowledged?.Invoke(this, args); - } + /// + /// Handles the event. + /// + /// The instance containing the event data. + protected virtual void OnMessageAcknowledged(MessageAcknowledgedEventArgs args) + { + MessageAcknowledged?.Invoke(this, args); + } - /// - /// Subscribes the specified message type. - /// - /// - /// - public virtual void Subscribe(Type messageType, Type handlerType) - { - HandlerContext.Register(messageType, handlerType); - } + /// + /// Subscribes the specified message type. + /// + /// + /// + public virtual void Subscribe(Type messageType, Type handlerType) + { + HandlerContext.Register(messageType, handlerType); + } - /// - /// Subscribes the specified message name. - /// - /// - /// - public virtual void Subscribe(string messageName, Type handlerType) - { - HandlerContext.Register(messageName, handlerType); - } + /// + /// Subscribes the specified message name. + /// + /// + /// + public virtual void Subscribe(string messageName, Type handlerType) + { + HandlerContext.Register(messageName, handlerType); + } } \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/MessageQueue.cs b/Source/Euonia.Bus.InMemory/MessageQueue.cs index c6810d5..0557411 100644 --- a/Source/Euonia.Bus.InMemory/MessageQueue.cs +++ b/Source/Euonia.Bus.InMemory/MessageQueue.cs @@ -1,6 +1,5 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; -using Nerosoft.Euonia.Domain; namespace Nerosoft.Euonia.Bus.InMemory; @@ -16,7 +15,7 @@ internal sealed class MessageQueue public event EventHandler MessagePushed; - internal void Enqueue(IMessage message, MessageContext messageContext, MessageProcessType processType) + internal void Enqueue(IMessage message, IMessageContext messageContext, MessageProcessType processType) { _queue.Enqueue(message); diff --git a/Source/Euonia.Bus.InMemory/Messages/AsyncCollectionRequestMessage.cs b/Source/Euonia.Bus.InMemory/Messages/AsyncCollectionRequestMessage.cs new file mode 100644 index 0000000..822b907 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Messages/AsyncCollectionRequestMessage.cs @@ -0,0 +1,137 @@ +using System.ComponentModel; + +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// A for request messages that can receive multiple replies, which can either be used directly or through derived classes. +/// +/// The type of request to make. +public class AsyncCollectionRequestMessage : IAsyncEnumerable +{ + /// + /// The collection of received replies. We accept both instance, representing already running + /// operations that can be executed in parallel, or instances, which can be used so that multiple + /// asynchronous operations are only started sequentially from and do not overlap in time. + /// + private readonly List<(Task, Func>)> _responses = new(); + + /// + /// The instance used to link the token passed to + /// and the one passed to all subscribers to the message. + /// + private readonly CancellationTokenSource _cancellationTokenSource = new(); + + /// + /// Gets the instance that will be linked to the + /// one used to asynchronously enumerate the received responses. This can be used to cancel asynchronous + /// replies that are still being processed, if no new items are needed from this request message. + /// Consider the following example, where we define a message to retrieve the currently opened documents: + /// + /// public class OpenDocumentsRequestMessage : AsyncCollectionRequestMessage<XmlDocument> { } + /// + /// We can then request and enumerate the results like so: + /// + /// await foreach (var document in Messenger.Default.Send<OpenDocumentsRequestMessage>()) + /// { + /// // Process each document here... + /// } + /// + /// If we also want to control the cancellation of the token passed to each subscriber to the message, + /// we can do so by passing a token we control to the returned message before starting the enumeration + /// (). + /// The previous snippet with this additional change looks as follows: + /// + /// await foreach (var document in Messenger.Default.Send<OpenDocumentsRequestMessage>().WithCancellation(cts.Token)) + /// { + /// // Process each document here... + /// } + /// + /// When no more new items are needed (or for any other reason depending on the situation), the token + /// passed to the enumerator can be canceled (by calling ), + /// and that will also notify the remaining tasks in the request message. The token exposed by the message + /// itself will automatically be linked and canceled with the one passed to the enumerator. + /// + public CancellationToken CancellationToken => _cancellationTokenSource.Token; + + /// + /// Replies to the current request message. + /// + /// The response to use to reply to the request message. + public void Reply(T response) + { + Reply(Task.FromResult(response)); + } + + /// + /// Replies to the current request message. + /// + /// The response to use to reply to the request message. + /// Thrown if is . + public void Reply(Task response) + { + ArgumentAssert.ThrowIfNull(response); + + _responses.Add((response, null)); + } + + /// + /// Replies to the current request message. + /// + /// The response to use to reply to the request message. + /// Thrown if is . + public void Reply(Func> response) + { + ArgumentAssert.ThrowIfNull(response); + + _responses.Add((null, response)); + } + + /// + /// Gets the collection of received response items. + /// + /// A value to stop the operation. + /// The collection of received response items. + public async Task> GetResponsesAsync(CancellationToken cancellationToken = default) + { + if (cancellationToken.CanBeCanceled) + { + _ = cancellationToken.Register(_cancellationTokenSource.Cancel); + } + + List results = new(_responses.Count); + + await foreach (T response in this.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + results.Add(response); + } + + return results; + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + if (cancellationToken.CanBeCanceled) + { + _ = cancellationToken.Register(_cancellationTokenSource.Cancel); + } + + foreach (var (task, func) in _responses) + { + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + if (task is not null) + { + yield return await task.ConfigureAwait(false); + } + else + { + yield return await func!(cancellationToken).ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Messages/AsyncRequestMessage.cs b/Source/Euonia.Bus.InMemory/Messages/AsyncRequestMessage.cs new file mode 100644 index 0000000..1da4fb7 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Messages/AsyncRequestMessage.cs @@ -0,0 +1,92 @@ +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// A for async request messages, which can either be used directly or through derived classes. +/// +/// The type of request to make. +public class AsyncRequestMessage +{ + private Task _response; + + /// + /// Gets the message response. + /// + /// Thrown when is . + public Task Response + { + get + { + if (!HasReceivedResponse) + { + ThrowInvalidOperationExceptionForNoResponseReceived(); + } + + return _response!; + } + } + + /// + /// Gets a value indicating whether a response has already been assigned to this instance. + /// + public bool HasReceivedResponse { get; private set; } + + /// + /// Replies to the current request message. + /// + /// The response to use to reply to the request message. + /// Thrown if has already been set. + public void Reply(T response) + { + Reply(Task.FromResult(response)); + } + + /// + /// Replies to the current request message. + /// + /// The response to use to reply to the request message. + /// Thrown if is . + /// Thrown if has already been set. + public void Reply(Task response) + { + ArgumentAssert.ThrowIfNull(response); + + if (HasReceivedResponse) + { + ThrowInvalidOperationExceptionForDuplicateReply(); + } + + HasReceivedResponse = true; + + _response = response; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [EditorBrowsable(EditorBrowsableState.Never)] + public TaskAwaiter GetAwaiter() + { + return Response.GetAwaiter(); + } + + /// + /// Throws an when a response is not available. + /// + [DoesNotReturn] + private static void ThrowInvalidOperationExceptionForNoResponseReceived() + { + throw new InvalidOperationException("No response was received for the given request message."); + } + + /// + /// Throws an when or are called twice. + /// + [DoesNotReturn] + private static void ThrowInvalidOperationExceptionForDuplicateReply() + { + throw new InvalidOperationException("A response has already been issued for the current message."); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Messages/CollectionRequestMessage.cs b/Source/Euonia.Bus.InMemory/Messages/CollectionRequestMessage.cs new file mode 100644 index 0000000..b96476d --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Messages/CollectionRequestMessage.cs @@ -0,0 +1,41 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// A for request messages that can receive multiple replies, which can either be used directly or through derived classes. +/// +/// The type of request to make. +public class CollectionRequestMessage : IEnumerable +{ + private readonly List _responses = new(); + + /// + /// Gets the message responses. + /// + public IReadOnlyCollection Responses => _responses; + + /// + /// Replies to the current request message. + /// + /// The response to use to reply to the request message. + public void Reply(T response) + { + _responses.Add(response); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerator GetEnumerator() + { + return _responses.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Messages/MessageHandler.cs b/Source/Euonia.Bus.InMemory/Messages/MessageHandler.cs new file mode 100644 index 0000000..e6d783e --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Messages/MessageHandler.cs @@ -0,0 +1,15 @@ +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// A used to represent actions to invoke when a message is received. +/// The recipient is given as an input argument to allow message registrations to avoid creating +/// closures: if an instance method on a recipient needs to be invoked it is possible to just +/// cast the recipient to the right type and then access the local method from that instance. +/// +/// The type of recipient for the message. +/// The type of message to receive. +/// The recipient that is receiving the message. +/// The message being received. +public delegate void MessageHandler(TRecipient recipient, TMessage message) + where TRecipient : class + where TMessage : class; \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Messages/MessagePack.cs b/Source/Euonia.Bus.InMemory/Messages/MessagePack.cs new file mode 100644 index 0000000..d4ba15a --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Messages/MessagePack.cs @@ -0,0 +1,33 @@ +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// Defines a message pack to transport. +/// +public sealed class MessagePack +{ + /// + /// + /// + /// + /// + public MessagePack(IRoutedMessage message, IMessageContext context) + { + Message = message; + Context = context; + } + + /// + /// Get the message. + /// + public IRoutedMessage Message { get; } + + /// + /// Get the message context. + /// + public IMessageContext Context { get; } + + /// + /// Gets or sets the cancellation token. + /// + public CancellationToken Aborted { get; set; } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Messages/RequestMessage.cs b/Source/Euonia.Bus.InMemory/Messages/RequestMessage.cs new file mode 100644 index 0000000..16d9549 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Messages/RequestMessage.cs @@ -0,0 +1,82 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// A for request messages, which can either be used directly or through derived classes. +/// +/// The type of request to make. +public class RequestMessage +{ + private T _response; + + /// + /// Gets the message response. + /// + /// Thrown when is . + public T Response + { + get + { + if (!HasReceivedResponse) + { + ThrowInvalidOperationExceptionForNoResponseReceived(); + } + + return _response; + } + } + + /// + /// Gets a value indicating whether a response has already been assigned to this instance. + /// + public bool HasReceivedResponse { get; private set; } + + /// + /// Replies to the current request message. + /// + /// The response to use to reply to the request message. + /// Thrown if has already been set. + public void Reply(T response) + { + if (HasReceivedResponse) + { + ThrowInvalidOperationExceptionForDuplicateReply(); + } + + HasReceivedResponse = true; + + _response = response; + } + + /// + /// Implicitly gets the response from a given instance. + /// + /// The input instance. + /// Thrown if is . + /// Thrown when is . + public static implicit operator T(RequestMessage message) + { + ArgumentAssert.ThrowIfNull(message); + + return message.Response; + } + + /// + /// Throws an when a response is not available. + /// + [DoesNotReturn] + private static void ThrowInvalidOperationExceptionForNoResponseReceived() + { + throw new InvalidOperationException("No response was received for the given request message."); + } + + /// + /// Throws an when is called twice. + /// + [DoesNotReturn] + private static void ThrowInvalidOperationExceptionForDuplicateReply() + { + throw new InvalidOperationException("A response has already been issued for the current message."); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Messages/ValueChangedMessage.cs b/Source/Euonia.Bus.InMemory/Messages/ValueChangedMessage.cs new file mode 100644 index 0000000..c34f93d --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Messages/ValueChangedMessage.cs @@ -0,0 +1,22 @@ +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// A base message that signals whenever a specific value has changed. +/// +/// The type of value that has changed. +public class ValueChangedMessage +{ + /// + /// Initializes a new instance of the class. + /// + /// The value that has changed. + public ValueChangedMessage(T value) + { + Value = value; + } + + /// + /// Gets the value that has changed. + /// + public T Value { get; } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Messenger/IMessenger.cs b/Source/Euonia.Bus.InMemory/Messenger/IMessenger.cs new file mode 100644 index 0000000..14b5888 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Messenger/IMessenger.cs @@ -0,0 +1,152 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// An interface for a type providing the ability to exchange messages between different objects. +/// This can be useful to decouple different modules of an application without having to keep strong +/// references to types being referenced. It is also possible to send messages to specific channels, uniquely +/// identified by a token, and to have different messengers in different sections of an applications. +/// In order to use the functionalities, first define a message type, like so: +/// +/// public sealed class LoginCompletedMessage { } +/// +/// Then, register your a recipient for this message: +/// +/// Messenger.Default.Register<MyRecipientType, LoginCompletedMessage>(this, (r, m) => +/// { +/// // Handle the message here... +/// }); +/// +/// The message handler here is a lambda expression taking two parameters: the recipient and the message. +/// This is done to avoid the allocations for the closures that would've been generated if the expression +/// had captured the current instance. The recipient type parameter is used so that the recipient can be +/// directly accessed within the handler without the need to manually perform type casts. This allows the +/// code to be less verbose and more reliable, as all the checks are done just at build time. If the handler +/// is defined within the same type as the recipient, it is also possible to directly access private members. +/// This allows the message handler to be a static method, which enables the C# compiler to perform a number +/// of additional memory optimizations (such as caching the delegate, avoiding unnecessary memory allocations). +/// Finally, send a message when needed, like so: +/// +/// Messenger.Default.Send<LoginCompletedMessage>(); +/// +/// Additionally, the method group syntax can also be used to specify the message handler +/// to invoke when receiving a message, if a method with the right signature is available +/// in the current scope. This is helpful to keep the registration and handling logic separate. +/// Following up from the previous example, consider a class having this method: +/// +/// private static void Receive(MyRecipientType recipient, LoginCompletedMessage message) +/// { +/// // Handle the message there +/// } +/// +/// The registration can then be performed in a single line like so: +/// +/// Messenger.Default.Register(this, Receive); +/// +/// The C# compiler will automatically convert that expression to a instance +/// compatible with . +/// This will also work if multiple overloads of that method are available, each handling a different +/// message type: the C# compiler will automatically pick the right one for the current message type. +/// It is also possible to register message handlers explicitly using the interface. +/// To do so, the recipient just needs to implement the interface and then call the +/// extension, which will automatically register +/// all the handlers that are declared by the recipient type. Registration for individual handlers is supported as well. +/// +public interface IMessenger +{ + /// + /// Checks whether or not a given recipient has already been registered for a message. + /// + /// The type of message to check for the given recipient. + /// The type of token to check the channel for. + /// The target recipient to check the registration for. + /// The token used to identify the target channel to check. + /// Whether or not has already been registered for the specified message. + /// Thrown if or are . + bool IsRegistered(object recipient, TToken token) + where TMessage : class + where TToken : IEquatable; + + /// + /// Registers a recipient for a given type of message. + /// + /// The type of recipient for the message. + /// The type of message to receive. + /// The type of token to use to pick the messages to receive. + /// The recipient that will receive the messages. + /// A token used to determine the receiving channel to use. + /// The to invoke when a message is received. + /// Thrown if , or are . + /// Thrown when trying to register the same message twice. + void Register(TRecipient recipient, TToken token, MessageHandler handler) + where TRecipient : class + where TMessage : class + where TToken : IEquatable; + + /// + /// Unregisters a recipient from all registered messages. + /// + /// The recipient to unregister. + /// + /// This method will unregister the target recipient across all channels. + /// Use this method as an easy way to lose all references to a target recipient. + /// If the recipient has no registered handler, this method does nothing. + /// + /// Thrown if is . + void UnregisterAll(object recipient); + + /// + /// Unregisters a recipient from all messages on a specific channel. + /// + /// The type of token to identify what channel to unregister from. + /// The recipient to unregister. + /// The token to use to identify which handlers to unregister. + /// If the recipient has no registered handler, this method does nothing. + /// Thrown if or are . + void UnregisterAll(object recipient, TToken token) + where TToken : IEquatable; + + /// + /// Unregisters a recipient from messages of a given type. + /// + /// The type of message to stop receiving. + /// The type of token to identify what channel to unregister from. + /// The recipient to unregister. + /// The token to use to identify which handlers to unregister. + /// If the recipient has no registered handler, this method does nothing. + /// Thrown if or are . + void Unregister(object recipient, TToken token) + where TMessage : class + where TToken : IEquatable; + + /// + /// Sends a message of the specified type to all registered recipients. + /// + /// The type of message to send. + /// The type of token to identify what channel to use to send the message. + /// The message to send. + /// The token indicating what channel to use. + /// The message that was sent (ie. ). + /// Thrown if or are . + TMessage Send(TMessage message, TToken token) + where TMessage : class + where TToken : IEquatable; + + /// + /// Performs a cleanup on the current messenger. + /// Invoking this method does not unregister any of the currently registered + /// recipient, and it can be used to perform cleanup operations such as + /// trimming the internal data structures of a messenger implementation. + /// + void Cleanup(); + + /// + /// Resets the instance and unregisters all the existing recipients. + /// + void Reset(); +} diff --git a/Source/Euonia.Bus.InMemory/Messenger/IRecipient.cs b/Source/Euonia.Bus.InMemory/Messenger/IRecipient.cs new file mode 100644 index 0000000..7983b3e --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Messenger/IRecipient.cs @@ -0,0 +1,15 @@ +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// An interface for a recipient that declares a registration for a specific message type. +/// +/// The type of message to receive. +public interface IRecipient + where TMessage : class +{ + /// + /// Receives a given message instance. + /// + /// The message being received. + void Receive(TMessage message); +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Messenger/MessengerExtensions.Observables.cs b/Source/Euonia.Bus.InMemory/Messenger/MessengerExtensions.Observables.cs new file mode 100644 index 0000000..8aa0db8 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Messenger/MessengerExtensions.Observables.cs @@ -0,0 +1,192 @@ +namespace Nerosoft.Euonia.Bus.InMemory; + +partial class MessengerExtensions +{ + /// + /// Creates an instance that can be used to be notified whenever a message of a given type is broadcast by a messenger. + /// + /// The type of message to use to receive notification for through the resulting instance. + /// The instance to use to register the recipient. + /// An instance to receive notifications for messages being broadcast. + /// Thrown if is . + public static IObservable CreateObservable(this IMessenger messenger) + where TMessage : class + { + ArgumentAssert.ThrowIfNull(messenger); + + return new Observable(messenger); + } + + /// + /// Creates an instance that can be used to be notified whenever a message of a given type is broadcast by a messenger. + /// + /// The type of message to use to receive notification for through the resulting instance. + /// The type of token to identify what channel to use to receive messages. + /// The instance to use to register the recipient. + /// A token used to determine the receiving channel to use. + /// An instance to receive notifications for messages being broadcast. + /// Thrown if or are . + public static IObservable CreateObservable(this IMessenger messenger, TToken token) + where TMessage : class + where TToken : IEquatable + { + ArgumentAssert.ThrowIfNull(messenger); + ArgumentAssert.For.ThrowIfNull(token); + + return new Observable(messenger, token); + } + + /// + /// An implementations for a given message type. + /// + /// The type of messages to listen to. + private sealed class Observable : IObservable + where TMessage : class + { + /// + /// The instance to use to register the recipient. + /// + private readonly IMessenger _messenger; + + /// + /// Creates a new instance with the given parameters. + /// + /// The instance to use to register the recipient. + public Observable(IMessenger messenger) + { + _messenger = messenger; + } + + /// + public IDisposable Subscribe(IObserver observer) + { + return new Recipient(_messenger, observer); + } + + /// + /// An implementation for . + /// + private sealed class Recipient : IRecipient, IDisposable + { + /// + /// The instance to use to register the recipient. + /// + private readonly IMessenger _messenger; + + /// + /// The target instance currently in use. + /// + private readonly IObserver _observer; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The instance to use to register the recipient. + /// The instance to use to create the recipient for. + public Recipient(IMessenger messenger, IObserver observer) + { + _messenger = messenger; + _observer = observer; + + messenger.Register(this); + } + + /// + public void Receive(TMessage message) + { + _observer.OnNext(message); + } + + /// + public void Dispose() + { + _messenger.Unregister(this); + } + } + } + + /// + /// An implementations for a given pair of message and token types. + /// + /// The type of messages to listen to. + /// The type of token to identify what channel to use to receive messages. + private sealed class Observable : IObservable + where TMessage : class + where TToken : IEquatable + { + /// + /// The instance to use to register the recipient. + /// + private readonly IMessenger _messenger; + + /// + /// The token used to determine the receiving channel to use. + /// + private readonly TToken _token; + + /// + /// Creates a new instance with the given parameters. + /// + /// The instance to use to register the recipient. + /// A token used to determine the receiving channel to use. + public Observable(IMessenger messenger, TToken token) + { + _messenger = messenger; + _token = token; + } + + /// + public IDisposable Subscribe(IObserver observer) + { + return new Recipient(_messenger, observer, _token); + } + + /// + /// An implementation for . + /// + private sealed class Recipient : IRecipient, IDisposable + { + /// + /// The instance to use to register the recipient. + /// + private readonly IMessenger _messenger; + + /// + /// The target instance currently in use. + /// + private readonly IObserver _observer; + + /// + /// The token used to determine the receiving channel to use. + /// + private readonly TToken _token; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The instance to use to register the recipient. + /// The instance to use to create the recipient for. + /// A token used to determine the receiving channel to use. + public Recipient(IMessenger messenger, IObserver observer, TToken token) + { + _messenger = messenger; + _observer = observer; + _token = token; + + messenger.Register(this, token); + } + + /// + public void Receive(TMessage message) + { + _observer.OnNext(message); + } + + /// + public void Dispose() + { + _messenger.Unregister(this, _token); + } + } + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Messenger/MessengerExtensions.cs b/Source/Euonia.Bus.InMemory/Messenger/MessengerExtensions.cs new file mode 100644 index 0000000..9049b23 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Messenger/MessengerExtensions.cs @@ -0,0 +1,430 @@ +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// Extensions for the type. +/// +public static partial class MessengerExtensions +{ + /// + /// A class that acts as a container to load the instance linked to + /// the method. + /// This class is needed to avoid forcing the initialization code in the static constructor to run as soon as + /// the type is referenced, even if that is done just to use methods + /// that do not actually require this instance to be available. + /// We're effectively using this type to leverage the lazy loading of static constructors done by the runtime. + /// + private static class MethodInfos + { + /// + /// The instance associated with . + /// + public static readonly MethodInfo RegisterIRecipient = new Action, Unit>(Register).Method.GetGenericMethodDefinition(); + } + + /// + /// A non-generic version of . + /// + private static class DiscoveredRecipients + { + /// + /// The instance used to track the preloaded registration action for each recipient. + /// + public static readonly ConditionalWeakTable> RegistrationMethods = new(); + } + + /// + /// A class that acts as a static container to associate a instance to each + /// type in use. This is done because we can only use a single type as key, but we need to track + /// associations of each recipient type also across different communication channels, each identified by a token. + /// Since the token is actually a compile-time parameter, we can use a wrapping class to let the runtime handle a different + /// instance for each generic type instantiation. This lets us only worry about the recipient type being inspected. + /// + /// The token indicating what channel to use. + private static class DiscoveredRecipients + where TToken : IEquatable + { + /// + /// The instance used to track the preloaded registration action for each recipient. + /// + public static readonly ConditionalWeakTable> RegistrationMethods = new(); + } + + /// + /// Checks whether or not a given recipient has already been registered for a message. + /// + /// The type of message to check for the given recipient. + /// The instance to use to check the registration. + /// The target recipient to check the registration for. + /// Whether or not has already been registered for the specified message. + /// This method will use the default channel to check for the requested registration. + /// Thrown if or are . + public static bool IsRegistered(this IMessenger messenger, object recipient) + where TMessage : class + { + ArgumentAssert.ThrowIfNull(messenger); + ArgumentAssert.ThrowIfNull(recipient); + + return messenger.IsRegistered(recipient, default); + } + + /// + /// Registers all declared message handlers for a given recipient, using the default channel. + /// + /// The instance to use to register the recipient. + /// The recipient that will receive the messages. + /// See notes for for more info. + /// Thrown if or are . + public static void RegisterAll(this IMessenger messenger, object recipient) + { + ArgumentAssert.ThrowIfNull(messenger); + ArgumentAssert.ThrowIfNull(recipient); + + // We use this method as a callback for the conditional weak table, which will handle + // thread-safety for us. This first callback will try to find a generated method for the + // target recipient type, and just invoke it to get the delegate to cache and use later. + static Action LoadRegistrationMethodsForType(Type recipientType) + { + if (recipientType.Assembly.GetType("CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions") is Type extensionsType && + extensionsType.GetMethod("CreateAllMessagesRegistrator", new[] { recipientType }) is MethodInfo methodInfo) + { + return (Action)methodInfo.Invoke(null, new object[] { null })!; + } + + return null; + } + + // Try to get the cached delegate, if the generator has run correctly + Action registrationAction = DiscoveredRecipients.RegistrationMethods.GetValue( + recipient.GetType(), + static (t) => LoadRegistrationMethodsForType(t)); + + if (registrationAction is not null) + { + registrationAction(messenger, recipient); + } + else + { + messenger.RegisterAll(recipient, default(Unit)); + } + } + + /// + /// Registers all declared message handlers for a given recipient. + /// + /// The type of token to identify what channel to use to receive messages. + /// The instance to use to register the recipient. + /// The recipient that will receive the messages. + /// The token indicating what channel to use. + /// + /// This method will register all messages corresponding to the interfaces + /// being implemented by . If none are present, this method will do nothing. + /// Note that unlike all other extensions, this method will use reflection to find the handlers to register. + /// Once the registration is complete though, the performance will be exactly the same as with handlers + /// registered directly through any of the other generic extensions for the interface. + /// + /// Thrown if , or are . + public static void RegisterAll(this IMessenger messenger, object recipient, TToken token) + where TToken : IEquatable + { + ArgumentAssert.ThrowIfNull(messenger); + ArgumentAssert.ThrowIfNull(recipient); + ArgumentAssert.For.ThrowIfNull(token); + + // We use this method as a callback for the conditional weak table, which will handle + // thread-safety for us. This first callback will try to find a generated method for the + // target recipient type, and just invoke it to get the delegate to cache and use later. + // In this case we also need to create a generic instantiation of the target method first. + static Action LoadRegistrationMethodsForType(Type recipientType) + { + if (recipientType.Assembly.GetType("CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions") is Type extensionsType && + extensionsType.GetMethod("CreateAllMessagesRegistratorWithToken", new[] { recipientType }) is MethodInfo methodInfo) + { + MethodInfo genericMethodInfo = methodInfo.MakeGenericMethod(typeof(TToken)); + + return (Action)genericMethodInfo.Invoke(null, new object[] { null })!; + } + + return LoadRegistrationMethodsForTypeFallback(recipientType); + } + + // Fallback method when a generated method is not found. + // This method is only invoked once per recipient type and token type, so we're not + // worried about making it super efficient, and we can use the LINQ code for clarity. + // The LINQ codegen bloat is not really important for the same reason. + static Action LoadRegistrationMethodsForTypeFallback(Type recipientType) + { + // Get the collection of validation methods + MethodInfo[] registrationMethods = ( + from interfaceType in recipientType.GetInterfaces() + where interfaceType.IsGenericType && + interfaceType.GetGenericTypeDefinition() == typeof(IRecipient<>) + let messageType = interfaceType.GenericTypeArguments[0] + select MethodInfos.RegisterIRecipient.MakeGenericMethod(messageType, typeof(TToken))).ToArray(); + + // Short path if there are no message handlers to register + if (registrationMethods.Length == 0) + { + return static (_, _, _) => + { + }; + } + + // Input parameters (IMessenger instance, non-generic recipient, token) + ParameterExpression arg0 = Expression.Parameter(typeof(IMessenger)); + ParameterExpression arg1 = Expression.Parameter(typeof(object)); + ParameterExpression arg2 = Expression.Parameter(typeof(TToken)); + + // Declare a local resulting from the (RecipientType)recipient cast + UnaryExpression inst1 = Expression.Convert(arg1, recipientType); + + // We want a single compiled LINQ expression that executes the registration for all + // the declared message types in the input type. To do so, we create a block with the + // unrolled invocations for the individual message registration (for each IRecipient). + // The code below will generate the following block expression: + // =============================================================================== + // { + // var inst1 = (RecipientType)arg1; + // IMessengerExtensions.Register(arg0, inst1, arg2); + // IMessengerExtensions.Register(arg0, inst1, arg2); + // ... + // IMessengerExtensions.Register(arg0, inst1, arg2); + // } + // =============================================================================== + // We also add an explicit object conversion to cast the input recipient type to + // the actual specific type, so that the exposed message handlers are accessible. + BlockExpression body = Expression.Block( + from registrationMethod in registrationMethods + select Expression.Call(registrationMethod, new Expression[] + { + arg0, + inst1, + arg2 + })); + + return Expression.Lambda>(body, arg0, arg1, arg2).Compile(); + } + + // Get or compute the registration method for the current recipient type. + // As in CommunityToolkit.Diagnostics.TypeExtensions.ToTypeString, we use a lambda + // expression instead of a method group expression to leverage the statically initialized + // delegate and avoid repeated allocations for each invocation of this method. + // For more info on this, see the related issue at https://github.com/dotnet/roslyn/issues/5835. + Action registrationAction = DiscoveredRecipients.RegistrationMethods.GetValue( + recipient.GetType(), + static (t) => LoadRegistrationMethodsForType(t)); + + // Invoke the cached delegate to actually execute the message registration + registrationAction(messenger, recipient, token); + } + + /// + /// Registers a recipient for a given type of message. + /// + /// The type of message to receive. + /// The instance to use to register the recipient. + /// The recipient that will receive the messages. + /// Thrown when trying to register the same message twice. + /// This method will use the default channel to perform the requested registration. + /// Thrown if or are . + public static void Register(this IMessenger messenger, IRecipient recipient) + where TMessage : class + { + ArgumentAssert.ThrowIfNull(messenger); + ArgumentAssert.ThrowIfNull(recipient); + + if (messenger is WeakReferenceMessenger weakReferenceMessenger) + { + weakReferenceMessenger.Register(recipient, default); + } + else if (messenger is StrongReferenceMessenger strongReferenceMessenger) + { + strongReferenceMessenger.Register(recipient, default); + } + else + { + messenger.Register, TMessage, Unit>(recipient, default, static (r, m) => r.Receive(m)); + } + } + + /// + /// Registers a recipient for a given type of message. + /// + /// The type of message to receive. + /// The type of token to identify what channel to use to receive messages. + /// The instance to use to register the recipient. + /// The recipient that will receive the messages. + /// The token indicating what channel to use. + /// Thrown when trying to register the same message twice. + /// This method will use the default channel to perform the requested registration. + /// Thrown if , or are . + public static void Register(this IMessenger messenger, IRecipient recipient, TToken token) + where TMessage : class + where TToken : IEquatable + { + ArgumentAssert.ThrowIfNull(messenger); + ArgumentAssert.ThrowIfNull(recipient); + ArgumentAssert.For.ThrowIfNull(token); + + if (messenger is WeakReferenceMessenger weakReferenceMessenger) + { + weakReferenceMessenger.Register(recipient, token); + } + else if (messenger is StrongReferenceMessenger strongReferenceMessenger) + { + strongReferenceMessenger.Register(recipient, token); + } + else + { + messenger.Register, TMessage, TToken>(recipient, token, static (r, m) => r.Receive(m)); + } + } + + /// + /// Registers a recipient for a given type of message. + /// + /// The type of message to receive. + /// The instance to use to register the recipient. + /// The recipient that will receive the messages. + /// The to invoke when a message is received. + /// Thrown when trying to register the same message twice. + /// This method will use the default channel to perform the requested registration. + /// Thrown if , or are . + public static void Register(this IMessenger messenger, object recipient, MessageHandler handler) + where TMessage : class + { + ArgumentAssert.ThrowIfNull(messenger); + ArgumentAssert.ThrowIfNull(recipient); + ArgumentAssert.ThrowIfNull(handler); + + messenger.Register(recipient, default(Unit), handler); + } + + /// + /// Registers a recipient for a given type of message. + /// + /// The type of recipient for the message. + /// The type of message to receive. + /// The instance to use to register the recipient. + /// The recipient that will receive the messages. + /// The to invoke when a message is received. + /// Thrown when trying to register the same message twice. + /// This method will use the default channel to perform the requested registration. + /// Thrown if , or are . + public static void Register(this IMessenger messenger, TRecipient recipient, MessageHandler handler) + where TRecipient : class + where TMessage : class + { + ArgumentAssert.ThrowIfNull(messenger); + ArgumentAssert.ThrowIfNull(recipient); + ArgumentAssert.ThrowIfNull(handler); + + messenger.Register(recipient, default(Unit), handler); + } + + /// + /// Registers a recipient for a given type of message. + /// + /// The type of message to receive. + /// The type of token to use to pick the messages to receive. + /// The instance to use to register the recipient. + /// The recipient that will receive the messages. + /// A token used to determine the receiving channel to use. + /// The to invoke when a message is received. + /// Thrown when trying to register the same message twice. + /// Thrown if , or are . + public static void Register(this IMessenger messenger, object recipient, TToken token, MessageHandler handler) + where TMessage : class + where TToken : IEquatable + { + ArgumentAssert.ThrowIfNull(messenger); + ArgumentAssert.ThrowIfNull(recipient); + ArgumentAssert.For.ThrowIfNull(token); + ArgumentAssert.ThrowIfNull(handler); + + messenger.Register(recipient, token, handler); + } + + /// + /// Unregisters a recipient from messages of a given type. + /// + /// The type of message to stop receiving. + /// The instance to use to unregister the recipient. + /// The recipient to unregister. + /// + /// This method will unregister the target recipient only from the default channel. + /// If the recipient has no registered handler, this method does nothing. + /// + /// Thrown if or are . + public static void Unregister(this IMessenger messenger, object recipient) + where TMessage : class + { + ArgumentAssert.ThrowIfNull(messenger); + ArgumentAssert.ThrowIfNull(recipient); + + messenger.Unregister(recipient, default); + } + + /// + /// Sends a message of the specified type to all registered recipients. + /// + /// The type of message to send. + /// The instance to use to send the message. + /// The message that has been sent. + /// + /// This method is a shorthand for when the + /// message type exposes a parameterless constructor: it will automatically create + /// a new instance and send that to its recipients. + /// + /// Thrown if is . + public static TMessage Send(this IMessenger messenger) + where TMessage : class, new() + { + ArgumentAssert.ThrowIfNull(messenger); + + return messenger.Send(new TMessage(), default(Unit)); + } + + /// + /// Sends a message of the specified type to all registered recipients. + /// + /// The type of message to send. + /// The instance to use to send the message. + /// The message to send. + /// The message that was sent (ie. ). + /// Thrown if or are . + public static TMessage Send(this IMessenger messenger, TMessage message) + where TMessage : class + { + ArgumentAssert.ThrowIfNull(messenger); + ArgumentAssert.ThrowIfNull(message); + + return messenger.Send(message, default(Unit)); + } + + /// + /// Sends a message of the specified type to all registered recipients. + /// + /// The type of message to send. + /// The type of token to identify what channel to use to send the message. + /// The instance to use to send the message. + /// The token indicating what channel to use. + /// The message that has been sen. + /// + /// This method will automatically create a new instance + /// just like , and then send it to the right recipients. + /// + /// Thrown if or are . + public static TMessage Send(this IMessenger messenger, TToken token) + where TMessage : class, new() + where TToken : IEquatable + { + ArgumentAssert.ThrowIfNull(messenger); + ArgumentAssert.For.ThrowIfNull(token); + + return messenger.Send(new TMessage(), token); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Messenger/MessengerReferenceType.cs b/Source/Euonia.Bus.InMemory/Messenger/MessengerReferenceType.cs new file mode 100644 index 0000000..90101b9 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Messenger/MessengerReferenceType.cs @@ -0,0 +1,17 @@ +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// +/// +public enum MessengerReferenceType +{ + /// + /// + /// + StrongReference, + + /// + /// + /// + WeakReference +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Messenger/StrongReferenceMessenger.cs b/Source/Euonia.Bus.InMemory/Messenger/StrongReferenceMessenger.cs new file mode 100644 index 0000000..b340dc7 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Messenger/StrongReferenceMessenger.cs @@ -0,0 +1,850 @@ +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// A class providing a reference implementation for the interface. +/// +/// +/// This implementation uses strong references to track the registered +/// recipients, so it is necessary to manually unregister them when they're no longer needed. +/// +public sealed class StrongReferenceMessenger : IMessenger +{ + // This messenger uses the following logic to link stored instances together: + // -------------------------------------------------------------------------------------------------------- + // TypeDictionary> recipientsMap; + // | \________________[*]ITypeDictionary> + // | \_______________[*]ITypeDictionary / + // | \_________/_________/___ / + // |\ _(recipients registrations)_/ / \ / + // | \__________________ / _____(channel registrations)_____/______\____/ + // | \ / / __________________________/ \ + // | / / / \ + // | TypeDictionary mapping = Mapping________________\ + // | __________________/ / | / \ + // |/ / | / \ + // TypeDictionary> mapping = Mapping____________\ + // / / / / + // ___(EquatableType.TToken)____/ / / / + // /________________(EquatableType.TMessage)_______/_______/__/ + // / ________________________________/ + // / / + // TypeDictionary typesMap; + // -------------------------------------------------------------------------------------------------------- + // Each combination of results in a concrete Mapping type (if TToken is Unit) or Mapping type, + // which holds the references from registered recipients to handlers. Mapping is used when the default channel is being + // requested, as in that case there will only ever be up to a handler per recipient, per message type. In that case, + // each recipient will only track the message dispatcher (stored as an object?, see notes below), instead of a dictionary + // mapping each TToken value to the corresponding dispatcher for that recipient. When a custom channel is used, the + // dispatchers are stored in a dictionary, so that each recipient can have up to one registered handler + // for a given token, for each message type. Note that the registered dispatchers are only stored as object references, as + // they can either be null or a MessageHandlerDispatcher.For instance. + // + // The first case happens if the handler was registered through an IRecipient instance, while the second one is + // used to wrap input MessageHandler instances. The MessageHandlerDispatcher.For + // instances will just be cast to MessageHandlerDispatcher when invoking it. This allows users to retain type information on + // each registered recipient, instead of having to manually cast each recipient to the right type within the handler + // (additionally, using double dispatch here avoids the need to alias delegate types). The type conversion is guaranteed to be + // respected due to how the messenger type itself works - as registered handlers are always invoked on their respective recipients. + // + // Each mapping is stored in the types map, which associates each pair of concrete types to its mapping instance. Mapping instances + // are exposed as IMapping items, as each will be a closed type over a different combination of TMessage and TToken generic type + // parameters (or just of TMessage, for the default channel). Each existing recipient is also stored in the main recipients map, + // along with a set of all the existing (dictionaries of) handlers for that recipient (for all message types and token types, if any). + // + // A recipient is stored in the main map as long as it has at least one registered handler in any of the existing mappings for every + // message/token type combination. The shared map is used to access the set of all registered handlers for a given recipient, without + // having to know in advance the type of message or token being used for the registration, and without having to use reflection. This + // is the same approach used in the types map, as we expose saved items as IMapping values too. + // + // Note that each mapping stored in the associated set for each recipient also indirectly implements either ITypeDictionary + // or ITypeDictionary, with any token type currently in use by that recipient (or none, if using the default channel). This allows + // to retrieve the type-closed mappings of registered handlers with a given token type, for any message type, for every receiver, again + // without having to use reflection. This shared map is used to unregister messages from a given recipients either unconditionally, by + // message type, by token, or for a specific pair of message type and token value. + + /// + /// The collection of currently registered recipients, with a link to their linked message receivers. + /// + /// + /// This collection is used to allow reflection-free access to all the existing + /// registered recipients from and other methods in this type, + /// so that all the existing handlers can be removed without having to dynamically create + /// the generic types for the containers of the various dictionaries mapping the handlers. + /// + private readonly TypeDictionary> _recipientsMap = new(); + + /// + /// The and instance for types combination. + /// + /// + /// The values are just of type as we don't know the type parameters in advance. + /// Each method relies on to get the type-safe instance of the + /// or class for each pair of generic arguments in use. + /// + private readonly TypeDictionary _typesMap = new(); + + /// + /// Gets the default instance. + /// + public static StrongReferenceMessenger Default { get; } = new(); + + /// + public bool IsRegistered(object recipient, TToken token) + where TMessage : class + where TToken : IEquatable + { + ArgumentAssert.ThrowIfNull(recipient); + ArgumentAssert.For.ThrowIfNull(token); + + lock (_recipientsMap) + { + if (typeof(TToken) == typeof(Unit)) + { + if (!TryGetMapping(out var mapping)) + { + return false; + } + + Recipient key = new(recipient); + + return mapping.ContainsKey(key); + } + else + { + if (!TryGetMapping(out var mapping)) + { + return false; + } + + Recipient key = new(recipient); + + return + mapping.TryGetValue(key, out var handlers) && + handlers.ContainsKey(token); + } + } + } + + /// + public void Register(TRecipient recipient, TToken token, MessageHandler handler) + where TRecipient : class + where TMessage : class + where TToken : IEquatable + { + ArgumentAssert.ThrowIfNull(recipient); + ArgumentAssert.For.ThrowIfNull(token); + ArgumentAssert.ThrowIfNull(handler); + + Register(recipient, token, new MessageHandlerDispatcher.For(handler)); + } + + /// + internal void Register(IRecipient recipient, TToken token) + where TMessage : class + where TToken : IEquatable + { + Register(recipient, token, null); + } + + /// + /// Registers a recipient for a given type of message. + /// + /// The type of message to receive. + /// The type of token to use to pick the messages to receive. + /// The recipient that will receive the messages. + /// A token used to determine the receiving channel to use. + /// The input instance to register, or null. + /// Thrown when trying to register the same message twice. + private void Register(object recipient, TToken token, MessageHandlerDispatcher dispatcher) + where TMessage : class + where TToken : IEquatable + { + lock (_recipientsMap) + { + Recipient key = new(recipient); + IMapping mapping; + + // Fast path for unit tokens + if (typeof(TToken) == typeof(Unit)) + { + // Get the registration list for this recipient + var underlyingMapping = GetOrAddMapping(); + ref var registeredHandler = ref underlyingMapping.GetOrAddValueRef(key); + + if (registeredHandler is not null) + { + ThrowInvalidOperationExceptionForDuplicateRegistration(); + } + + // Store the input handler + registeredHandler = dispatcher; + + mapping = underlyingMapping; + } + else + { + // Get the registration list for this recipient + var underlyingMapping = GetOrAddMapping(); + ref var map = ref underlyingMapping.GetOrAddValueRef(key); + + map ??= new TypeDictionary(); + + // Add the new registration entry + ref var registeredHandler = ref map.GetOrAddValueRef(token); + + if (registeredHandler is not null) + { + ThrowInvalidOperationExceptionForDuplicateRegistration(); + } + + registeredHandler = dispatcher; + mapping = underlyingMapping; + } + + // Make sure this registration map is tracked for the current recipient + ref var set = ref _recipientsMap.GetOrAddValueRef(key); + + set ??= new HashSet(); + + _ = set.Add(mapping); + } + } + + /// + public void UnregisterAll(object recipient) + { + ArgumentAssert.ThrowIfNull(recipient); + + lock (_recipientsMap) + { + // If the recipient has no registered messages at all, ignore + Recipient key = new(recipient); + + if (!_recipientsMap.TryGetValue(key, out var set)) + { + return; + } + + // Removes all the lists of registered handlers for the recipient + foreach (var mapping in set) + { + if (mapping.TryRemove(key) && + mapping.Count == 0) + { + // Maps here are really of type Mapping<,> and with unknown type arguments. + // If after removing the current recipient a given map becomes empty, it means + // that there are no registered recipients at all for a given pair of message + // and token types. In that case, we also remove the map from the types map. + // The reason for keeping a key in each mapping is that removing items from a + // dictionary (a hashed collection) only costs O(1) in the best case, while + // if we had tried to iterate the whole dictionary every time we would have + // paid an O(n) minimum cost for each single remove operation. + _ = _typesMap.TryRemove(mapping.TypeArguments); + } + } + + // Remove the associated set in the recipients map + _ = _recipientsMap.TryRemove(key); + } + } + + /// + public void UnregisterAll(object recipient, TToken token) + where TToken : IEquatable + { + ArgumentAssert.ThrowIfNull(recipient); + ArgumentAssert.For.ThrowIfNull(token); + + // This method is never called with the unit type, so this path is not implemented. This + // exception should not ever be thrown, it's here just to double check for regressions in + // case a bug was introduced that caused this path to somehow be invoked with the Unit type. + // This type is internal, so consumers of the library would never be able to pass it here, + // and there are (and shouldn't be) any APIs publicly exposed from the library that would + // cause this path to be taken either. When using the default channel, only UnregisterAll(object) + // is supported, which would just unregister all recipients regardless of the selected channel. + if (typeof(TToken) == typeof(Unit)) + { + throw new NotImplementedException(); + } + + var lockTaken = false; + object[] maps = null; + var i = 0; + + // We use an explicit try/finally block here instead of the lock syntax so that we can use a single + // one both to release the lock and to clear the rented buffer and return it to the pool. The reason + // why we're declaring the buffer here and clearing and returning it in this outer finally block is + // that doing so doesn't require the lock to be kept, and releasing it before performing this last + // step reduces the total time spent while the lock is acquired, which in turn reduces the lock + // contention in multi-threaded scenarios where this method is invoked concurrently. + try + { + Monitor.Enter(_recipientsMap, ref lockTaken); + + // Get the shared set of mappings for the recipient, if present + Recipient key = new(recipient); + + if (!_recipientsMap.TryGetValue(key, out var set)) + { + return; + } + + // Copy the candidate mappings for the target recipient to a local array, as we can't modify the + // contents of the set while iterating it. The rented buffer is oversized and will also include + // mappings for handlers of messages that are registered through a different token. Note that + // we're using just an object array to minimize the number of total rented buffers, that would + // just remain in the shared pool unused, other than when they are rented here. Instead, we're + // using a type that would possibly also be used by the users of the library, which increases + // the opportunities to reuse existing buffers for both. When we need to reference an item + // stored in the buffer with the type we know it will have, we use Unsafe.As to avoid the + // expensive type check in the cast, since we already know the assignment will be valid. + maps = ArrayPool.Shared.Rent(set.Count); + + foreach (var item in set) + { + // Select all mappings using the same token type + if (item is ITypeDictionary> mapping) + { + maps[i++] = mapping; + } + } + + // Iterate through all the local maps. These are all the currently + // existing maps of handlers for messages of any given type, with a token + // of the current type, for the target recipient. We heavily rely on + // interfaces here to be able to iterate through all the available mappings + // without having to know the concrete type in advance, and without having + // to deal with reflection: we can just check if the type of the closed interface + // matches with the token type currently in use, and operate on those instances. + foreach (var obj in maps.AsSpan(0, i)) + { + var handlersMap = Unsafe.As>>(obj); + + // We don't need whether or not the map contains the recipient, as the + // sequence of maps has already been copied from the set containing all + // the mappings for the target recipients: it is guaranteed to be here. + var holder = handlersMap[key]; + + // Try to remove the registered handler for the input token, + // for the current message type (unknown from here). + if (holder.TryRemove(token) && + holder.Count == 0) + { + // If the map is empty, remove the recipient entirely from its container + _ = handlersMap.TryRemove(key); + + var mapping = Unsafe.As(handlersMap); + + // This recipient has no registrations left for this combination of token + // and message type, so this mapping can be removed from its associated set. + _ = set.Remove(mapping); + + // If the resulting set is empty, then this means that there are no more handlers + // left for this recipient for any message or token type, so the recipient can also + // be removed from the map of all existing recipients with at least one handler. + if (set.Count == 0) + { + _ = _recipientsMap.TryRemove(key); + } + + // If no handlers are left at all for any recipient, across all message types and token + // types, remove the set of mappings entirely for the current recipient, and remove the + // strong reference to it as well. This is the same situation that would've been achieved + // by just calling UnregisterAll(recipient). + if (handlersMap.Count == 0) + { + _ = _typesMap.TryRemove(mapping.TypeArguments); + } + } + } + } + finally + { + // Release the lock, if we did acquire it + if (lockTaken) + { + Monitor.Exit(_recipientsMap); + } + + // If we got to renting the array of maps, return it to the shared pool. + // Remove references to avoid leaks coming from the shared memory pool. + // We manually create a span and clear it as a small optimization, as + // arrays rented from the pool can be larger than the requested size. + if (maps is not null) + { + maps.AsSpan(0, i).Clear(); + + ArrayPool.Shared.Return(maps); + } + } + } + + /// + public void Unregister(object recipient, TToken token) + where TMessage : class + where TToken : IEquatable + { + ArgumentAssert.ThrowIfNull(recipient); + ArgumentAssert.For.ThrowIfNull(token); + + lock (_recipientsMap) + { + if (typeof(TToken) == typeof(Unit)) + { + // Get the registration list, if available + if (!TryGetMapping(out var mapping)) + { + return; + } + + Recipient key = new(recipient); + + // Remove the handler (there can only be one for the unit type) + if (!mapping.TryRemove(key)) + { + return; + } + + // Remove the map entirely from this container, and remove the link to the map itself to + // the current mapping between existing registered recipients (or entire recipients too). + // This is the same as below, except for the unit type there can only be one handler, so + // removing it already implies the target recipient has no remaining handlers left. + _ = mapping.TryRemove(key); + + // If there are no handlers left at all for this type combination, drop it + if (mapping.Count == 0) + { + _ = _typesMap.TryRemove(mapping.TypeArguments); + } + + var set = _recipientsMap[key]; + + // The current mapping no longer has any handlers left for this recipient. + // Remove it and then also remove the recipient if this was the last handler. + // Again, this is the same as below, except with the assumption of the unit type. + _ = set.Remove(mapping); + + if (set.Count == 0) + { + _ = _recipientsMap.TryRemove(key); + } + } + else + { + // Get the registration list, if available + if (!TryGetMapping(out var mapping)) + { + return; + } + + Recipient key = new(recipient); + + if (!mapping.TryGetValue(key, out var dictionary)) + { + return; + } + + // Remove the target handler + if (dictionary.TryRemove(token) && + dictionary.Count == 0) + { + // If the map is empty, it means that the current recipient has no remaining + // registered handlers for the current combination, regardless, + // of the specific token value (ie. the channel used to receive messages of that type). + // We can remove the map entirely from this container, and remove the link to the map itself + // to the current mapping between existing registered recipients (or entire recipients too). + _ = mapping.TryRemove(key); + + // If there are no handlers left at all for this type combination, drop it + if (mapping.Count == 0) + { + _ = _typesMap.TryRemove(mapping.TypeArguments); + } + + var set = _recipientsMap[key]; + + // The current mapping no longer has any handlers left for this recipient + _ = set.Remove(mapping); + + // If the current recipients has no handlers left at all, remove it + if (set.Count == 0) + { + _ = _recipientsMap.TryRemove(key); + } + } + } + } + } + + /// + public TMessage Send(TMessage message, TToken token) + where TMessage : class + where TToken : IEquatable + { + ArgumentAssert.ThrowIfNull(message); + ArgumentAssert.For.ThrowIfNull(token); + + object[] rentedArray; + Span pairs; + var i = 0; + + lock (_recipientsMap) + { + if (typeof(TToken) == typeof(Unit)) + { + // Check whether there are any registered recipients + if (!TryGetMapping(out var mapping)) + { + goto End; + } + + // Check the number of remaining handlers, see below + var totalHandlersCount = mapping.Count; + + if (totalHandlersCount == 0) + { + goto End; + } + + pairs = rentedArray = ArrayPool.Shared.Rent(2 * totalHandlersCount); + + // Same logic as below, except here we're only traversing one handler per recipient + var mappingEnumerator = mapping.GetEnumerator(); + + while (mappingEnumerator.MoveNext()) + { + pairs[2 * i] = mappingEnumerator.GetValue(); + pairs[(2 * i) + 1] = mappingEnumerator.GetKey().Target; + i++; + } + } + else + { + // Check whether there are any registered recipients + if (!TryGetMapping(out var mapping)) + { + goto End; + } + + // We need to make a local copy of the currently registered handlers, since users might + // try to unregister (or register) new handlers from inside one of the currently existing + // handlers. We can use memory pooling to reuse arrays, to minimize the average memory + // usage. In practice, we usually just need to pay the small overhead of copying the items. + // The current mapping contains all the currently registered recipients and handlers for + // the combination in use. In the worst case scenario, all recipients + // will have a registered handler with a token matching the input one, meaning that we could + // have at worst a number of pending handlers to invoke equal to the total number of recipient + // in the mapping. This relies on the fact that tokens are unique, and that there is only + // one handler associated with a given token. We can use this upper bound as the requested + // size for each array rented from the pool, which guarantees that we'll have enough space. + var totalHandlersCount = mapping.Count; + + if (totalHandlersCount == 0) + { + goto End; + } + + // Rent the array and also assign it to a span, which will be used to access values. + // We're doing this to avoid the array covariance checks slowdown in the loops below. + pairs = rentedArray = ArrayPool.Shared.Rent(2 * totalHandlersCount); + + // Copy the handlers to the local collection. + // The array is oversized at this point, since it also includes + // handlers for different tokens. We can reuse the same variable + // to count the number of matching handlers to invoke later on. + // This will be the array slice with valid handler in the rented buffer. + var mappingEnumerator = mapping.GetEnumerator(); + + // Explicit enumerator usage here as we're using a custom one + // that doesn't expose the single standard Current property. + while (mappingEnumerator.MoveNext()) + { + // Pick the target handler, if the token is a match for the recipient + if (mappingEnumerator.GetValue().TryGetValue(token, out var handler)) + { + // This span access should always guaranteed to be valid due to the size of the + // array being set according to the current total number of registered handlers, + // which will always be greater or equal than the ones matching the previous test. + // We're still using a checked span accesses here though to make sure an out of + // bounds write can never happen even if an error was present in the logic above. + pairs[2 * i] = handler; + pairs[(2 * i) + 1] = mappingEnumerator.GetKey().Target; + i++; + } + } + } + } + + try + { + // The core broadcasting logic is the same as the weak reference messenger one + WeakReferenceMessenger.SendAll(pairs, i, message); + } + finally + { + // As before, we also need to clear it first to avoid having potentially long + // lasting memory leaks due to leftover references being stored in the pool. + Array.Clear(rentedArray, 0, 2 * i); + + ArrayPool.Shared.Return(rentedArray); + } + + End: + return message; + } + + /// + void IMessenger.Cleanup() + { + // The current implementation doesn't require any kind of cleanup operation, as + // all the internal data structures are already kept in sync whenever a recipient + // is added or removed. This method is implemented through an explicit interface + // implementation so that developers using this type directly will not see it in + // the API surface (as it wouldn't be useful anyway, since it's a no-op here). + } + + /// + public void Reset() + { + lock (_recipientsMap) + { + _recipientsMap.Clear(); + _typesMap.Clear(); + } + } + + /// + /// Tries to get the instance of currently + /// registered recipients for the input type. + /// + /// The type of message to send. + /// The resulting instance, if found. + /// Whether or not the required instance was found. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryGetMapping([NotNullWhen(true)] out Mapping mapping) + where TMessage : class + { + EquatableType key = new(typeof(TMessage), typeof(Unit)); + + if (_typesMap.TryGetValue(key, out var target)) + { + // This method and the ones below are the only ones handling values in the types map, + // and here we are sure that the object reference we have points to an instance of the + // right type. Using an unsafe cast skips two conditional branches and is faster. + mapping = Unsafe.As(target); + + return true; + } + + mapping = null; + + return false; + } + + /// + /// Tries to get the instance of currently registered recipients + /// for the combination of types and . + /// + /// The type of message to send. + /// The type of token to identify what channel to use to send the message. + /// The resulting instance, if found. + /// Whether or not the required instance was found. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryGetMapping([NotNullWhen(true)] out Mapping mapping) + where TMessage : class + where TToken : IEquatable + { + EquatableType key = new(typeof(TMessage), typeof(TToken)); + + if (_typesMap.TryGetValue(key, out var target)) + { + mapping = Unsafe.As>(target); + + return true; + } + + mapping = null; + + return false; + } + + /// + /// Gets the instance of currently + /// registered recipients for the input type. + /// + /// The type of message to send. + /// A instance with the requested type arguments. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Mapping GetOrAddMapping() + where TMessage : class + { + EquatableType key = new(typeof(TMessage), typeof(Unit)); + ref var target = ref _typesMap.GetOrAddValueRef(key); + + target ??= Mapping.Create(); + + return Unsafe.As(target); + } + + /// + /// Gets the instance of currently registered recipients + /// for the combination of types and . + /// + /// The type of message to send. + /// The type of token to identify what channel to use to send the message. + /// A instance with the requested type arguments. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Mapping GetOrAddMapping() + where TMessage : class + where TToken : IEquatable + { + EquatableType key = new(typeof(TMessage), typeof(TToken)); + ref var target = ref _typesMap.GetOrAddValueRef(key); + + target ??= Mapping.Create(); + + return Unsafe.As>(target); + } + + /// + /// A mapping type representing a link to recipients and their view of handlers per communication channel. + /// + /// + /// This type is a specialization of for tokens. + /// + private sealed class Mapping : TypeDictionary, IMapping + { + /// + /// Initializes a new instance of the class. + /// + /// The message type being used. + private Mapping(Type messageType) + { + TypeArguments = new EquatableType(messageType, typeof(Unit)); + } + + /// + /// Creates a new instance of the class. + /// + /// The type of message to receive. + /// A new instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Mapping Create() + where TMessage : class + { + return new(typeof(TMessage)); + } + + /// + public EquatableType TypeArguments { get; } + } + + /// + /// A mapping type representing a link to recipients and their view of handlers per communication channel. + /// + /// The type of token to use to pick the messages to receive. + /// + /// This type is defined for simplicity and as a workaround for the lack of support for using type aliases + /// over open generic types in C# (using type aliases can only be used for concrete, closed types). + /// + private sealed class Mapping : TypeDictionary>, IMapping + where TToken : IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The message type being used. + private Mapping(Type messageType) + { + TypeArguments = new EquatableType(messageType, typeof(TToken)); + } + + /// + /// Creates a new instance of the class. + /// + /// The type of message to receive. + /// A new instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Mapping Create() + where TMessage : class + { + return new(typeof(TMessage)); + } + + /// + public EquatableType TypeArguments { get; } + } + + /// + /// An interface for the and types which allows to retrieve + /// the type arguments from a given generic instance without having any prior knowledge about those arguments. + /// + private interface IMapping : TypeDictionary + { + /// + /// Gets the instance representing the current type arguments. + /// + EquatableType TypeArguments { get; } + } + + /// + /// A simple type representing a recipient. + /// + /// + /// This type is used to enable fast indexing in each mapping dictionary, + /// since it acts as an external override for the and + /// methods for arbitrary objects, removing both + /// the virtual call and preventing instances overriding those methods in this context. + /// Using this type guarantees that all the equality operations are always only done + /// based on reference equality for each registered recipient, regardless of its type. + /// + private readonly struct Recipient : IEquatable + { + /// + /// The registered recipient. + /// + public readonly object Target; + + /// + /// Initializes a new instance of the struct. + /// + /// The target recipient instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Recipient(object target) + { + Target = target; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(Recipient other) + { + return ReferenceEquals(Target, other.Target); + } + + /// + public override bool Equals(object obj) + { + return obj is Recipient other && Equals(other); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override int GetHashCode() + { + return RuntimeHelpers.GetHashCode(Target); + } + } + + /// + /// Throws an when trying to add a duplicate handler. + /// + private static void ThrowInvalidOperationExceptionForDuplicateRegistration() + { + throw new InvalidOperationException("The target recipient has already subscribed to the target message."); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Messenger/WeakReferenceMessenger.cs b/Source/Euonia.Bus.InMemory/Messenger/WeakReferenceMessenger.cs new file mode 100644 index 0000000..6245173 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/Messenger/WeakReferenceMessenger.cs @@ -0,0 +1,540 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// A class providing a reference implementation for the interface. +/// +/// +/// +/// This implementation uses weak references to track the registered +/// recipients, so it is not necessary to manually unregister them when they're no longer needed. +/// +/// +/// The type will automatically perform internal trimming when +/// full GC collections are invoked, so calling manually is not necessary to +/// ensure that on average the internal data structures are as trimmed and compact as possible. +/// +/// +public sealed class WeakReferenceMessenger : IMessenger +{ + // This messenger uses the following logic to link stored instances together: + // -------------------------------------------------------------------------------------------------------- + // TypeDictionary mapping + // / / / + // ___(EquatableType.TToken)___/ / / ___(if EquatableType.TToken is Unit) + // /_________(EquatableType.TMessage)______________/ / / + // / _________________/___MessageHandlerDispatcher? + // / / \ + // TypeDictionary> recipientsMap; \___(null if using IRecipient) + // -------------------------------------------------------------------------------------------------------- + // Just like in the strong reference variant, each pair of message and token types is used as a key in the + // recipients map. In this case, the values in the dictionary are ConditionalWeakTable2<,> instances, that + // link each registered recipient to a map of currently registered handlers, through dependent handles. This + // ensures that handlers will remain alive as long as their associated recipient is also alive (so there is no + // need for users to manually indicate whether a given handler should be kept alive in case it creates a closure). + // The value in each conditional table can either be TypeDictionary or object. The + // first case is used when any token type other than the default Unit type is used, as in this case there could be + // multiple handlers for each recipient that need to be tracked separately. In order to invoke all the handlers from + // a context where their type parameters is not known, handlers are stored as MessageHandlerDispatcher instances. There + // are two possible cases here: either a given instance is of type MessageHandlerDispatcher.For, + // or null. The first is the default case: whenever a subscription is done with a MessageHandler, + // that delegate is wrapped in an instance of this class so that it can keep track internally of the generic context in + // use, so that it can be retrieved when the callback is executed. If the subscription is done directly on a recipient + // that implements IRecipient + /// The map of currently registered recipients for all message types. + /// + private readonly TypeDictionary> _recipientsMap = new(); + + /// + /// Initializes a new instance of the class. + /// + public WeakReferenceMessenger() + { + // Proxy function for the GC callback. This needs to be static and to take the target instance as + // an input parameter in order to avoid rooting it from the Gen2GcCallback object invoking it. + static void Gen2GcCallbackProxy(object target) + { + ((WeakReferenceMessenger)target).CleanupWithNonBlockingLock(); + } + + // Register an automatic GC callback to trigger a non-blocking cleanup. This will ensure that the + // current messenger instance is trimmed and without leftover recipient maps that are no longer used. + // This is necessary (as in, some form of cleanup, either explicit or automatic like in this case) + // because the ConditionalWeakTable instances will just remove key-value pairs on their + // own as soon as a key (ie. a recipient) is collected, causing their own keys (ie. the EquatableType instances + // mapping to each conditional table for a pair of message and token types) to potentially remain in the + // root mapping structure but without any remaining recipients actually registered there, which just + // adds unnecessary overhead when trying to enumerate recipients during broadcasting operations later on. + Gen2GcCallback.Register(Gen2GcCallbackProxy, this); + } + + /// + /// Gets the default instance. + /// + public static WeakReferenceMessenger Default { get; } = new(); + + /// + public bool IsRegistered(object recipient, TToken token) + where TMessage : class + where TToken : IEquatable + { + ArgumentAssert.ThrowIfNull(recipient); + ArgumentAssert.For.ThrowIfNull(token); + + lock (_recipientsMap) + { + EquatableType equatableType = new(typeof(TMessage), typeof(TToken)); + + // Get the conditional table associated with the target recipient, for the current pair + // of token and message types. If it exists, check if there is a matching token. + if (!_recipientsMap.TryGetValue(equatableType, out var table)) + { + return false; + } + + // Special case for unit tokens + if (typeof(TToken) == typeof(Unit)) + { + return table.TryGetValue(recipient, out _); + } + + // Custom token type, so each recipient has an associated map + return + table.TryGetValue(recipient, out var mapping) && + Unsafe.As>(mapping!).ContainsKey(token); + } + } + + /// + public void Register(TRecipient recipient, TToken token, MessageHandler handler) + where TRecipient : class + where TMessage : class + where TToken : IEquatable + { + ArgumentAssert.ThrowIfNull(recipient); + ArgumentAssert.For.ThrowIfNull(token); + ArgumentAssert.ThrowIfNull(handler); + + Register(recipient, token, new MessageHandlerDispatcher.For(handler)); + } + + /// + /// Registers a recipient for a given type of message. + /// + /// The type of message to receive. + /// The type of token to use to pick the messages to receive. + /// The recipient that will receive the messages. + /// A token used to determine the receiving channel to use. + /// Thrown when trying to register the same message twice. + /// + /// This method is a variation of + /// that is specialized for recipients implementing . See more comments at the top of this type, as well as + /// within and in the types. + /// + internal void Register(IRecipient recipient, TToken token) + where TMessage : class + where TToken : IEquatable + { + Register(recipient, token, null); + } + + /// + /// Registers a recipient for a given type of message. + /// + /// The type of message to receive. + /// The type of token to use to pick the messages to receive. + /// The recipient that will receive the messages. + /// A token used to determine the receiving channel to use. + /// The input instance to register, or null. + /// Thrown when trying to register the same message twice. + private void Register(object recipient, TToken token, MessageHandlerDispatcher dispatcher) + where TMessage : class + where TToken : IEquatable + { + lock (_recipientsMap) + { + EquatableType equatableType = new(typeof(TMessage), typeof(TToken)); + + // Get the conditional table for the pair of type arguments, or create it if it doesn't exist + ref var mapping = ref _recipientsMap.GetOrAddValueRef(equatableType); + + mapping ??= new ConditionalWeakTable2(); + + // Fast path for unit tokens + if (typeof(TToken) == typeof(Unit)) + { + if (!mapping.TryAdd(recipient, dispatcher)) + { + ThrowInvalidOperationExceptionForDuplicateRegistration(); + } + } + else + { + // Get or create the handlers dictionary for the target recipient + var map = Unsafe.As>(mapping.GetValue(recipient, static _ => new TypeDictionary())!); + + // Add the new registration entry + ref var registeredHandler = ref map.GetOrAddValueRef(token); + + if (registeredHandler is not null) + { + ThrowInvalidOperationExceptionForDuplicateRegistration(); + } + + // Store the input handler + registeredHandler = dispatcher; + } + } + } + + /// + public void UnregisterAll(object recipient) + { + ArgumentAssert.ThrowIfNull(recipient); + + lock (_recipientsMap) + { + var enumerator = _recipientsMap.GetEnumerator(); + + // Traverse all the existing conditional tables and remove all the ones + // with the target recipient as key. We don't perform a cleanup here, + // as that is responsibility of a separate method defined below. + while (enumerator.MoveNext()) + { + _ = enumerator.GetValue().Remove(recipient); + } + } + } + + /// + public void UnregisterAll(object recipient, TToken token) + where TToken : IEquatable + { + ArgumentAssert.ThrowIfNull(recipient); + ArgumentAssert.For.ThrowIfNull(token); + + // This method is never called with the unit type. See more details in + // the comments in the corresponding method in StrongReferenceMessenger. + if (typeof(TToken) == typeof(Unit)) + { + throw new NotImplementedException(); + } + + lock (_recipientsMap) + { + var enumerator = _recipientsMap.GetEnumerator(); + + // Same as above, with the difference being that this time we only go through + // the conditional tables having a matching token type as key, and that we + // only try to remove handlers with a matching token, if any. + while (enumerator.MoveNext()) + { + if (enumerator.GetKey().Token == typeof(TToken)) + { + if (enumerator.GetValue().TryGetValue(recipient, out var mapping)) + { + _ = Unsafe.As>(mapping!).TryRemove(token); + } + } + } + } + } + + /// + public void Unregister(object recipient, TToken token) + where TMessage : class + where TToken : IEquatable + { + ArgumentAssert.ThrowIfNull(recipient); + ArgumentAssert.For.ThrowIfNull(token); + + lock (_recipientsMap) + { + EquatableType equatableType = new(typeof(TMessage), typeof(TToken)); + + // Get the target mapping table for the combination of message and token types, + // and remove the handler with a matching token (the entire map), if present. + if (_recipientsMap.TryGetValue(equatableType, out var value)) + { + if (typeof(TToken) == typeof(Unit)) + { + _ = value.Remove(recipient); + } + else if (value.TryGetValue(recipient, out var mapping)) + { + _ = Unsafe.As>(mapping!).TryRemove(token); + } + } + } + } + + /// + public TMessage Send(TMessage message, TToken token) + where TMessage : class + where TToken : IEquatable + { + ArgumentAssert.ThrowIfNull(message); + ArgumentAssert.For.ThrowIfNull(token); + + ArrayPoolBufferWriter bufferWriter; + var i = 0; + + lock (_recipientsMap) + { + EquatableType equatableType = new(typeof(TMessage), typeof(TToken)); + + // Try to get the target table + if (!_recipientsMap.TryGetValue(equatableType, out var table)) + { + return message; + } + + bufferWriter = ArrayPoolBufferWriter.Create(); + + // We need a local, temporary copy of all the pending recipients and handlers to + // invoke, to avoid issues with handlers unregistering from messages while we're + // holding the lock. To do this, we can just traverse the conditional table in use + // to enumerate all the existing recipients for the token and message types pair + // corresponding to the generic arguments for this invocation, and then track the + // handlers with a matching token, and their corresponding recipients. + using var enumerator = table.GetEnumerator(); + + while (enumerator.MoveNext()) + { + if (typeof(TToken) == typeof(Unit)) + { + bufferWriter.Add(enumerator.GetValue()); + bufferWriter.Add(enumerator.GetKey()); + i++; + } + else + { + var map = Unsafe.As>(enumerator.GetValue()!); + + if (map.TryGetValue(token, out var handler)) + { + bufferWriter.Add(handler); + bufferWriter.Add(enumerator.GetKey()); + i++; + } + } + } + } + + try + { + SendAll(bufferWriter.Span, i, message); + } + finally + { + bufferWriter.Dispose(); + } + + return message; + } + + /// + /// Implements the broadcasting logic for . + /// + /// + /// + /// + /// + /// + /// This method is not a local function to avoid triggering multiple compilations due to TToken + /// potentially being a value type, which results in specialized code due to reified generics. This is + /// necessary to work around a Roslyn limitation that causes unnecessary type parameters in local + /// functions not to be discarded in the synthesized methods. Additionally, keeping this loop outside + /// of the EH block (the block) can help result in slightly better codegen. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void SendAll(ReadOnlySpan pairs, int i, TMessage message) + where TMessage : class + { + // This Slice calls executes bounds checks for the loop below, in case i was somehow wrong. + // The rest of the implementation relies on bounds checks removal and loop strength reduction + // done manually (which results in a 20% speedup during broadcast), since the JIT is not able + // to recognize this pattern. Skipping checks below is a provably safe optimization: the slice + // has exactly 2 * i elements (due to this slicing), and each loop iteration processes a pair. + // The loops ends when the initial reference reaches the end, and that's incremented by 2 at + // the end of each iteration. The target being a span, obviously means the length is constant. + var slice = pairs.Slice(0, 2 * i); + + ref var sliceStart = ref MemoryMarshal.GetReference(slice); + ref var sliceEnd = ref Unsafe.Add(ref sliceStart, slice.Length); + + while (Unsafe.IsAddressLessThan(ref sliceStart, ref sliceEnd)) + { + var handler = sliceStart; + var recipient = Unsafe.Add(ref sliceStart, 1)!; + + // Here we need to distinguish the two possible cases: either the recipient was registered + // through the IRecipient interface, or with a custom handler. In the first case, + // the handler stored in the messenger is just null, so we can check that and branch to a + // fast path that just invokes IRecipient directly on the recipient. Otherwise, + // we will use the standard double dispatch approach. This check is particularly convenient + // as we only need to check for null to determine what registration type was used, without + // having to store any additional info in the messenger. This will produce code as follows, + // with the advantage of also being compact and not having to use any additional registers: + // ============================= + // L0000: test rcx, rcx + // L0003: jne short L0040 + // ============================= + // Which is extremely fast. The reason for this conditional check in the first place is that + // we're doing manual (null based) guarded devirtualization: if the handler is the marker + // type and not an actual handler then we know that the recipient implements + // IRecipient, so we can just cast to it and invoke it directly. This avoids + // having to store the proxy callback when registering, and also skips an indirection + // (invoking the delegate that then invokes the actual method). Additional note: this + // pattern ensures that both casts below do not actually alias incompatible reference + // types (as in, they would both succeed if they were safe casts), which lets the code + // not rely on undefined behavior to run correctly (ie. we're not aliasing delegates). + if (handler is null) + { + Unsafe.As>(recipient).Receive(message); + } + else + { + Unsafe.As(handler).Invoke(recipient, message); + } + + sliceStart = ref Unsafe.Add(ref sliceStart, 2); + } + } + + /// + public void Cleanup() + { + lock (_recipientsMap) + { + CleanupWithoutLock(); + } + } + + /// + public void Reset() + { + lock (_recipientsMap) + { + _recipientsMap.Clear(); + } + } + + /// + /// Executes a cleanup without locking the current instance. This method has to be + /// invoked when a lock on has already been acquired. + /// + private void CleanupWithNonBlockingLock() + { + object lockObject = _recipientsMap; + var lockTaken = false; + + try + { + Monitor.TryEnter(lockObject, ref lockTaken); + + if (lockTaken) + { + CleanupWithoutLock(); + } + } + finally + { + if (lockTaken) + { + Monitor.Exit(lockObject); + } + } + } + + /// + /// Executes a cleanup without locking the current instance. This method has to be + /// invoked when a lock on has already been acquired. + /// + private void CleanupWithoutLock() + { + using var type = ArrayPoolBufferWriter.Create(); + using var emptyRecipients = ArrayPoolBufferWriter.Create(); + + var type2Enumerator = _recipientsMap.GetEnumerator(); + + // First, we go through all the currently registered pairs of token and message types. + // These represents all the combinations of generic arguments with at least one registered + // handler, with the exception of those with recipients that have already been collected. + while (type2Enumerator.MoveNext()) + { + emptyRecipients.Reset(); + + var hasAtLeastOneHandler = false; + + if (type2Enumerator.GetKey().Token == typeof(Unit)) + { + // When the token type is unit, there can be no registered recipients with no handlers, + // as when the single handler is unsubscribed the recipient is also removed immediately. + // Therefore, we need to check that there exists at least one recipient for the message. + using var recipientsEnumerator = type2Enumerator.GetValue().GetEnumerator(); + + while (recipientsEnumerator.MoveNext()) + { + hasAtLeastOneHandler = true; + + break; + } + } + else + { + // Go through the currently alive recipients to look for those with no handlers left. We track + // the ones we find to remove them outside of the loop (can't modify during enumeration). + using (var recipientsEnumerator = type2Enumerator.GetValue().GetEnumerator()) + { + while (recipientsEnumerator.MoveNext()) + { + if (Unsafe.As(recipientsEnumerator.GetValue()!).Count == 0) + { + emptyRecipients.Add(recipientsEnumerator.GetKey()); + } + else + { + hasAtLeastOneHandler = true; + } + } + } + + // Remove the handler maps for recipients that are still alive but with no handlers + foreach (var recipient in emptyRecipients.Span) + { + _ = type2Enumerator.GetValue().Remove(recipient); + } + } + + // Track the type combinations with no recipients or handlers left + if (!hasAtLeastOneHandler) + { + type.Add(type2Enumerator.GetKey()); + } + } + + // Remove all the mappings with no handlers left + foreach (var key in type.Span) + { + _ = _recipientsMap.TryRemove(key); + } + } + + /// + /// Throws an when trying to add a duplicate handler. + /// + private static void ThrowInvalidOperationExceptionForDuplicateRegistration() + { + throw new InvalidOperationException("The target recipient has already subscribed to the target message."); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/ServiceCollectionExtensions.cs b/Source/Euonia.Bus.InMemory/ServiceCollectionExtensions.cs index bc15b1a..16023bf 100644 --- a/Source/Euonia.Bus.InMemory/ServiceCollectionExtensions.cs +++ b/Source/Euonia.Bus.InMemory/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using Nerosoft.Euonia.Bus; using Nerosoft.Euonia.Bus.InMemory; @@ -9,61 +10,96 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class ServiceCollectionExtensions { - /// - /// Adds the in-memory command bus to the service collection. - /// - /// - /// - public static IServiceCollection AddInMemoryCommandBus(this IServiceCollection services) - { - return services.AddSingleton(provider => - { - var bus = ActivatorUtilities.GetServiceOrCreateInstance(provider); - var options = provider.GetService>()?.Value; - if (options != null) - { - foreach (var subscription in options.Subscription) - { - bus.Subscribe(subscription.MessageType, subscription.HandlerType); - } - } + /// + /// Adds the in-memory command bus to the service collection. + /// + /// + /// + public static IServiceCollection AddInMemoryCommandBus(this IServiceCollection services) + { + return services.AddSingleton(provider => + { + var bus = ActivatorUtilities.GetServiceOrCreateInstance(provider); + var options = provider.GetService>()?.Value; + if (options != null) + { + foreach (var subscription in options.Registration) + { + bus.Subscribe(subscription.MessageType, subscription.HandlerType); + } + } - { - } - return bus; - }); - } + { + } + return bus; + }); + } - /// - /// Adds the in-memory event bus to the service collection. - /// - /// - /// - public static IServiceCollection AddInMemoryEventBus(this IServiceCollection services) - { - return services.AddSingleton(provider => - { - var bus = ActivatorUtilities.GetServiceOrCreateInstance(provider); - var options = provider.GetService>()?.Value; - if (options != null) - { - foreach (var subscription in options.Subscription) - { - if (subscription.MessageType != null) - { - bus.Subscribe(subscription.MessageType, subscription.HandlerType); - } - else - { - bus.Subscribe(subscription.MessageName, subscription.HandlerType); - } - } - } + /// + /// Adds the in-memory event bus to the service collection. + /// + /// + /// + public static IServiceCollection AddInMemoryEventBus(this IServiceCollection services) + { + return services.AddSingleton(provider => + { + var bus = ActivatorUtilities.GetServiceOrCreateInstance(provider); + var options = provider.GetService>()?.Value; + if (options != null) + { + foreach (var subscription in options.Registration) + { + if (subscription.MessageType != null) + { + bus.Subscribe(subscription.MessageType, subscription.HandlerType); + } + else + { + bus.Subscribe(subscription.MessageName, subscription.HandlerType); + } + } + } - { - } + { + } - return bus; - }); - } + return bus; + }); + } + + /// + /// Adds the in-memory message bus to the service collection. + /// + /// + /// + /// + /// + public static void UseInMemory(this IBusConfigurator configurator, Action configuration) + { + configurator.Service.Configure(configuration); + configurator.Service.TryAddSingleton(provider => + { + var options = provider.GetService>()?.Value; + if (options == null) + { + throw new InvalidOperationException("The in-memory message dispatcher options is not configured."); + } + + IMessenger messenger = options.MessengerReference switch + { + MessengerReferenceType.StrongReference => StrongReferenceMessenger.Default, + MessengerReferenceType.WeakReference => WeakReferenceMessenger.Default, + _ => throw new ArgumentOutOfRangeException(nameof(options.MessengerReference), options.MessengerReference, null) + }; + foreach (var subscription in configurator.GetSubscriptions()) + { + messenger.Register(new InMemorySubscriber(subscription), subscription); + } + + return messenger; + }); + configurator.Service.TryAddSingleton(); + configurator.SerFactory(); + } } \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/CommandBus.cs b/Source/Euonia.Bus.RabbitMq/CommandBus.cs index 21482b3..89e8ec0 100644 --- a/Source/Euonia.Bus.RabbitMq/CommandBus.cs +++ b/Source/Euonia.Bus.RabbitMq/CommandBus.cs @@ -25,7 +25,7 @@ public class CommandBus : MessageBus, ICommandBus /// /// /// - public CommandBus(IMessageHandlerContext handlerContext, IOptionsMonitor monitor, IServiceAccessor accessor, ILoggerFactory logger) + public CommandBus(IHandlerContext handlerContext, IOptionsMonitor monitor, IServiceAccessor accessor, ILoggerFactory logger) : base(handlerContext, monitor, accessor) { _logger = logger.CreateLogger(); diff --git a/Source/Euonia.Bus.RabbitMq/CommandConsumer.cs b/Source/Euonia.Bus.RabbitMq/CommandConsumer.cs index 954232e..0f3a45d 100644 --- a/Source/Euonia.Bus.RabbitMq/CommandConsumer.cs +++ b/Source/Euonia.Bus.RabbitMq/CommandConsumer.cs @@ -39,7 +39,7 @@ public class CommandConsumer : CommandConsumer private readonly IModel _channel; private readonly IConnection _connection; private readonly EventingBasicConsumer _consumer; - private readonly IMessageHandlerContext _handlerContext; + private readonly IHandlerContext _handlerContext; /// /// Initializes a new instance of the class. @@ -47,7 +47,7 @@ public class CommandConsumer : CommandConsumer /// /// /// - public CommandConsumer(IConnectionFactory factory, RabbitMqMessageBusOptions options, IMessageHandlerContext handlerContext) + public CommandConsumer(IConnectionFactory factory, RabbitMqMessageBusOptions options, IHandlerContext handlerContext) { _handlerContext = handlerContext; _connection = factory.CreateConnection(); @@ -78,7 +78,7 @@ private async void HandleMessageReceived(object _, BasicDeliverEventArgs args) OnMessageReceived(new MessageReceivedEventArgs(message, messageContext)); var taskCompletion = new TaskCompletionSource(); - messageContext.Replied += (_, a) => + messageContext.OnResponse += (_, a) => { taskCompletion.TrySetResult(a.Result); }; diff --git a/Source/Euonia.Bus.RabbitMq/Euonia.Bus.RabbitMq.csproj b/Source/Euonia.Bus.RabbitMq/Euonia.Bus.RabbitMq.csproj index 0e5baca..c27ce22 100644 --- a/Source/Euonia.Bus.RabbitMq/Euonia.Bus.RabbitMq.csproj +++ b/Source/Euonia.Bus.RabbitMq/Euonia.Bus.RabbitMq.csproj @@ -1,21 +1,25 @@ - - - + + + disable - + - - - + + + + + + - + - + + - + True @@ -23,7 +27,7 @@ Resources.resx - + ResXFileCodeGenerator diff --git a/Source/Euonia.Bus.RabbitMq/EventBus.cs b/Source/Euonia.Bus.RabbitMq/EventBus.cs index ba2c900..64f76c5 100644 --- a/Source/Euonia.Bus.RabbitMq/EventBus.cs +++ b/Source/Euonia.Bus.RabbitMq/EventBus.cs @@ -13,7 +13,7 @@ namespace Nerosoft.Euonia.Bus.RabbitMq; public class EventBus : MessageBus, IEventBus { private readonly ConnectionFactory _factory; - private readonly IEventStore _eventStore; + private readonly IMessageStore _messageStore; private readonly IConnection _connection; private readonly IModel _channel; private readonly ILogger _logger; @@ -29,7 +29,7 @@ public class EventBus : MessageBus, IEventBus /// /// /// - public EventBus(IMessageHandlerContext handlerContext, IOptionsMonitor monitor, IServiceAccessor accessor, ILoggerFactory logger) + public EventBus(IHandlerContext handlerContext, IOptionsMonitor monitor, IServiceAccessor accessor, ILoggerFactory logger) : base(handlerContext, monitor, accessor) { _logger = logger.CreateLogger(); @@ -59,12 +59,12 @@ public EventBus(IMessageHandlerContext handlerContext, IOptionsMonitor /// /// - /// + /// /// - public EventBus(IMessageHandlerContext handlerContext, IOptionsMonitor monitor, IServiceAccessor accessor, IEventStore eventStore, ILoggerFactory logger) + public EventBus(IHandlerContext handlerContext, IOptionsMonitor monitor, IServiceAccessor accessor, IMessageStore messageStore, ILoggerFactory logger) : this(handlerContext, monitor, accessor, logger) { - _eventStore = eventStore; + _messageStore = messageStore; } private void HandleMessageSubscribed(object sender, MessageSubscribedEventArgs args) @@ -87,9 +87,9 @@ private void HandleMessageSubscribed(object sender, MessageSubscribedEventArgs a public async Task PublishAsync(TEvent @event, CancellationToken cancellationToken = default) where TEvent : IEvent { - if (_eventStore != null) + if (_messageStore != null) { - await _eventStore.SaveAsync(@event, cancellationToken); + await _messageStore.SaveAsync(@event, cancellationToken); } var messageContext = new MessageContext(); @@ -101,13 +101,13 @@ await Task.Run(() => var messageBody = Serialize(@event); var props = _channel.CreateBasicProperties(); props.Headers ??= new Dictionary(); - if (@event.HasAttribute(out EventNameAttribute attribute, false)) + if (@event.HasAttribute(out ChannelAttribute attribute, false)) { props.Headers[Constants.MessageHeaderEventAttr] = attribute.Name; //Encoding.UTF8.GetBytes(attribute.Name); } else { - props.Headers[Constants.MessageHeaderEventType] = @event.Metadata[Message.MessageTypeKey]; //Encoding.UTF8.GetBytes((@event.Metadata[MessageBase.MESSAGE_TYPE_KEY] as string)!); + props.Headers[Constants.MessageHeaderEventType] = @event.Metadata[RoutedMessage<>.MessageTypeKey]; //Encoding.UTF8.GetBytes((@event.Metadata[MessageBase.MESSAGE_TYPE_KEY] as string)!); } Policy.Handle() @@ -135,9 +135,9 @@ public async Task PublishAsync(string name, TEvent @event, CancellationT where TEvent : class { var namedEvent = new NamedEvent(name, @event); - if (_eventStore != null) + if (_messageStore != null) { - await _eventStore.SaveAsync(namedEvent, cancellationToken); + await _messageStore.SaveAsync(namedEvent, cancellationToken); } var messageContext = new MessageContext(); diff --git a/Source/Euonia.Bus.RabbitMq/EventConsumer.cs b/Source/Euonia.Bus.RabbitMq/EventConsumer.cs index 923aad3..bb59257 100644 --- a/Source/Euonia.Bus.RabbitMq/EventConsumer.cs +++ b/Source/Euonia.Bus.RabbitMq/EventConsumer.cs @@ -14,9 +14,9 @@ public class EventConsumer : DisposableObject private readonly EventingBasicConsumer _consumer; private readonly string _messageName; private readonly RabbitMqMessageBusOptions _options; - private readonly IMessageHandlerContext _handlerContext; + private readonly IHandlerContext _handlerContext; - internal EventConsumer(IConnectionFactory factory, RabbitMqMessageBusOptions options, IMessageHandlerContext handlerContext, string messageName) + internal EventConsumer(IConnectionFactory factory, RabbitMqMessageBusOptions options, IHandlerContext handlerContext, string messageName) { _options = options; _handlerContext = handlerContext; diff --git a/Source/Euonia.Bus.RabbitMq/MessageBus.cs b/Source/Euonia.Bus.RabbitMq/MessageBus.cs index 99b435e..e4231c4 100644 --- a/Source/Euonia.Bus.RabbitMq/MessageBus.cs +++ b/Source/Euonia.Bus.RabbitMq/MessageBus.cs @@ -6,7 +6,7 @@ namespace Nerosoft.Euonia.Bus.RabbitMq; /// /// /// -public abstract class MessageBus : DisposableObject, IMessageBus +public abstract class MessageBus : DisposableObject, IBus { private static readonly JsonSerializerSettings _serializerSettings = new() { @@ -19,7 +19,7 @@ public abstract class MessageBus : DisposableObject, IMessageBus public event EventHandler MessageSubscribed; /// - public event EventHandler MessageDispatched; + public event EventHandler Dispatched; /// public event EventHandler MessageReceived; @@ -33,7 +33,7 @@ public abstract class MessageBus : DisposableObject, IMessageBus /// /// /// - internal MessageBus(IMessageHandlerContext handlerContext, IOptionsMonitor monitor, IServiceAccessor accessor) + internal MessageBus(IHandlerContext handlerContext, IOptionsMonitor monitor, IServiceAccessor accessor) { HandlerContext = handlerContext; ServiceAccessor = accessor; @@ -57,7 +57,7 @@ internal MessageBus(IMessageHandlerContext handlerContext, IOptionsMonitor /// Gets the message handler context. /// - protected IMessageHandlerContext HandlerContext { get; } + protected IHandlerContext HandlerContext { get; } /// /// Gets the message bus options. @@ -79,7 +79,7 @@ protected virtual void OnMessageSubscribed(MessageSubscribedEventArgs args) /// protected virtual void OnMessageDispatched(MessageDispatchedEventArgs args) { - MessageDispatched?.Invoke(this, args); + Dispatched?.Invoke(this, args); } /// diff --git a/Source/Euonia.Bus.RabbitMq/RabbitMqBusFactory.cs b/Source/Euonia.Bus.RabbitMq/RabbitMqBusFactory.cs new file mode 100644 index 0000000..2cf0e31 --- /dev/null +++ b/Source/Euonia.Bus.RabbitMq/RabbitMqBusFactory.cs @@ -0,0 +1,9 @@ +namespace Nerosoft.Euonia.Bus.RabbitMq; + +public class RabbitMqBusFactory : IBusFactory +{ + public IDispatcher CreateDispatcher() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/RabbitMqMessageBusModule.cs b/Source/Euonia.Bus.RabbitMq/RabbitMqMessageBusModule.cs index de01a70..2e5002d 100644 --- a/Source/Euonia.Bus.RabbitMq/RabbitMqMessageBusModule.cs +++ b/Source/Euonia.Bus.RabbitMq/RabbitMqMessageBusModule.cs @@ -13,7 +13,7 @@ public class RabbitMqMessageBusModule : ModuleContextBase /// public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.TryAddSingleton(_ => MessageConverter.Convert); + context.Services.TryAddSingleton(_ => MessageConverter.Convert); context.Services.AddRabbitMqCommandBus(); context.Services.AddRabbitMqEventBus(); } diff --git a/Source/Euonia.Bus.RabbitMq/RabbitMqMessageDispatcher.cs b/Source/Euonia.Bus.RabbitMq/RabbitMqMessageDispatcher.cs new file mode 100644 index 0000000..0314acd --- /dev/null +++ b/Source/Euonia.Bus.RabbitMq/RabbitMqMessageDispatcher.cs @@ -0,0 +1,5 @@ +namespace Nerosoft.Euonia.Bus.RabbitMq; + +public class RabbitMqMessageDispatcher : IDispatcher +{ +} \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/ServiceCollectionExtensions.cs b/Source/Euonia.Bus.RabbitMq/ServiceCollectionExtensions.cs index 2188235..23cf206 100644 --- a/Source/Euonia.Bus.RabbitMq/ServiceCollectionExtensions.cs +++ b/Source/Euonia.Bus.RabbitMq/ServiceCollectionExtensions.cs @@ -10,136 +10,141 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class ServiceCollectionExtensions { - /// - /// Add message bus. - /// - /// - /// - /// - /// - public static void AddRabbitMqMessageBus(this IServiceCollection services, IConfiguration configuration, string optionKey, Action action = null) - { - services.Configure(configuration.GetSection(optionKey)); - if (action == null) - { - services.AddSingleton(); - } - else - { - services.AddSingleton(provider => - { - var commandBus = ActivatorUtilities.GetServiceOrCreateInstance(provider); - action(commandBus); - return commandBus; - }); - } + /// + /// Add message bus. + /// + /// + /// + /// + /// + public static void AddRabbitMqMessageBus(this IServiceCollection services, IConfiguration configuration, string optionKey, Action action = null) + { + services.Configure(configuration.GetSection(optionKey)); + if (action == null) + { + services.AddSingleton(); + } + else + { + services.AddSingleton(provider => + { + var commandBus = ActivatorUtilities.GetServiceOrCreateInstance(provider); + action(commandBus); + return commandBus; + }); + } - services.AddSingleton(); - } + services.AddSingleton(); + } - /// - /// Add message bus. - /// - /// - /// - /// - public static void AddRabbitMqMessageBus(this IServiceCollection services, Action configureOptions, Action action = null) - { - services.Configure(configureOptions); - if (action == null) - { - services.AddSingleton(); - } - else - { - services.AddSingleton(provider => - { - var commandBus = ActivatorUtilities.GetServiceOrCreateInstance(provider); - action(commandBus); - return commandBus; - }); - } - } + /// + /// Add message bus. + /// + /// + /// + /// + public static void AddRabbitMqMessageBus(this IServiceCollection services, Action configureOptions, Action action = null) + { + services.Configure(configureOptions); + if (action == null) + { + services.AddSingleton(); + } + else + { + services.AddSingleton(provider => + { + var commandBus = ActivatorUtilities.GetServiceOrCreateInstance(provider); + action(commandBus); + return commandBus; + }); + } + } - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddRabbitMqConfiguration(this IServiceCollection services, Action configureOptions) - { - return services.Configure(configureOptions); - } + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddRabbitMqConfiguration(this IServiceCollection services, Action configureOptions) + { + return services.Configure(configureOptions); + } - /// - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddRabbitMqConfiguration(this IServiceCollection services, IConfiguration configuration, string key) - { - return services.Configure(configuration.GetSection(key)); - } + /// + /// + /// + /// + /// + /// + /// + public static IServiceCollection AddRabbitMqConfiguration(this IServiceCollection services, IConfiguration configuration, string key) + { + return services.Configure(configuration.GetSection(key)); + } - /// - /// - /// - /// - /// - public static IServiceCollection AddRabbitMqCommandBus(this IServiceCollection services) - { - return services.AddSingleton(provider => - { - var bus = ActivatorUtilities.GetServiceOrCreateInstance(provider); - var options = provider.GetService>()?.Value; - if (options != null) - { - foreach (var subscription in options.Subscription) - { - bus.Subscribe(subscription.MessageType, subscription.HandlerType); - } - } + /// + /// + /// + /// + /// + public static IServiceCollection AddRabbitMqCommandBus(this IServiceCollection services) + { + return services.AddSingleton(provider => + { + var bus = ActivatorUtilities.GetServiceOrCreateInstance(provider); + var options = provider.GetService>()?.Value; + if (options != null) + { + foreach (var subscription in options.Registration) + { + bus.Subscribe(subscription.MessageType, subscription.HandlerType); + } + } - { - } + { + } - return bus; - }); - } + return bus; + }); + } - /// - /// - /// - /// - /// - public static IServiceCollection AddRabbitMqEventBus(this IServiceCollection services) - { - return services.AddSingleton(provider => - { - var bus = ActivatorUtilities.GetServiceOrCreateInstance(provider); - var options = provider.GetService>()?.Value; - if (options != null) - { - foreach (var subscription in options.Subscription) - { - if (subscription.MessageType != null) - { - bus.Subscribe(subscription.MessageType, subscription.HandlerType); - } - else - { - bus.Subscribe(subscription.MessageName, subscription.HandlerType); - } - } - } + /// + /// + /// + /// + /// + public static IServiceCollection AddRabbitMqEventBus(this IServiceCollection services) + { + return services.AddSingleton(provider => + { + var bus = ActivatorUtilities.GetServiceOrCreateInstance(provider); + var options = provider.GetService>()?.Value; + if (options != null) + { + foreach (var subscription in options.Registration) + { + if (subscription.MessageType != null) + { + bus.Subscribe(subscription.MessageType, subscription.HandlerType); + } + else + { + bus.Subscribe(subscription.MessageName, subscription.HandlerType); + } + } + } - { - } + { + } - return bus; - }); - } + return bus; + }); + } + + public static void UseRabbitMq(this BusConfigurator configurator, Action configureOptions) + { + configurator.SerFactory(); + } } \ No newline at end of file diff --git a/Source/Euonia.Bus/Attributes/SubscribeAttribute.cs b/Source/Euonia.Bus/Attributes/SubscribeAttribute.cs new file mode 100644 index 0000000..12e5b09 --- /dev/null +++ b/Source/Euonia.Bus/Attributes/SubscribeAttribute.cs @@ -0,0 +1,27 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Represents the attributed method would handle an message. +/// +[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] +public class SubscribeAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// + public SubscribeAttribute(string name) + { + Name = name; + } + + /// + /// Gets the name of the message. + /// + public string Name { get; } + + /// + /// Gets or sets the message group name. + /// + public string Group { get; set; } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Behaviors/MessageLoggingBehavior.cs b/Source/Euonia.Bus/Behaviors/MessageLoggingBehavior.cs new file mode 100644 index 0000000..e33570d --- /dev/null +++ b/Source/Euonia.Bus/Behaviors/MessageLoggingBehavior.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Logging; +using Nerosoft.Euonia.Pipeline; + +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +public sealed class MessageLoggingBehavior : IPipelineBehavior +{ + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The logger service factory. + public MessageLoggingBehavior(ILoggerFactory logger) + { + _logger = logger.CreateLogger(); + } + + /// + public async Task HandleAsync(IRoutedMessage context, PipelineDelegate next) + { + _logger.LogInformation("Message {Id} - {FullName}: {Context}", context.MessageId, context.GetType().FullName, context); + await next(context); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/BusConfigurator.cs b/Source/Euonia.Bus/BusConfigurator.cs new file mode 100644 index 0000000..b10dcff --- /dev/null +++ b/Source/Euonia.Bus/BusConfigurator.cs @@ -0,0 +1,240 @@ +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +// ReSharper disable MemberCanBePrivate.Global + +namespace Nerosoft.Euonia.Bus; + +/// +/// The message bus configurator. +/// +public class BusConfigurator : IBusConfigurator +{ + private const BindingFlags BINDING_FLAGS = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; + + private MessageConventionBuilder ConventionBuilder { get; } = new(); + + /// + /// The message handler types. + /// + internal List Registrations { get; } = new(); + + /// + /// Initialize a new instance of + /// + /// + public BusConfigurator(IServiceCollection service) + { + Service = service; + } + + /// + public IServiceCollection Service { get; } + + /// + public IEnumerable GetSubscriptions() + { + return Registrations.Select(t => t.Name); + } + + /// + /// + /// + /// + /// + public IBusConfigurator SerFactory() + where TFactory : class, IBusFactory + { + Service.AddSingleton(); + return this; + } + + /// + /// + /// + /// + /// + /// + public IBusConfigurator SerFactory(TFactory factory) + where TFactory : class, IBusFactory + { + Service.AddSingleton(factory); + return this; + } + + /// + /// + /// + /// + /// + /// + public IBusConfigurator SerFactory(Func factory) + where TFactory : class, IBusFactory + { + Service.TryAddSingleton(factory); + return this; + } + + /// + /// Set the message serializer. + /// + /// + /// + public BusConfigurator SetSerializer() + where TSerializer : class, IMessageSerializer + { + Service.TryAddSingleton(); + return this; + } + + /// + /// Set the message serializer. + /// + /// + /// + /// + public BusConfigurator SetSerializer(TSerializer serializer) + where TSerializer : class, IMessageSerializer + { + Service.TryAddSingleton(serializer); + return this; + } + + /// + /// Set the message store provider. + /// + /// + /// + public IBusConfigurator SetMessageStore() + where TStore : class, IMessageStore + { + Service.TryAddTransient(); + return this; + } + + /// + /// Set the message store provider. + /// + /// + /// + /// + public IBusConfigurator SetMessageStore(Func store) + where TStore : class, IMessageStore + { + Service.TryAddTransient(store); + return this; + } + + /// + /// Register the message handlers. + /// + /// + /// + public BusConfigurator RegisterHandlers(Assembly assembly) + { + return RegisterHandlers(() => assembly.DefinedTypes); + } + + /// + /// Register the message handlers. + /// + /// + /// + public BusConfigurator RegisterHandlers(Func> handlerTypesFactory) + { + return RegisterHandlers(handlerTypesFactory()); + } + + /// + /// Register the message handlers. + /// + /// + /// + public BusConfigurator RegisterHandlers(IEnumerable handlerTypes) + { + foreach (var handlerType in handlerTypes) + { + if (!handlerType.IsClass || handlerType.IsInterface || handlerType.IsAbstract) + { + continue; + } + + if (handlerType.IsImplementsGeneric(typeof(IHandler<>))) + { + var interfaces = handlerType.GetInterfaces().Where(t => t.IsImplementsGeneric(typeof(IHandler<>))); + + foreach (var @interface in interfaces) + { + var messageType = @interface.GetGenericArguments().FirstOrDefault(); + + if (messageType == null) + { + continue; + } + + Registrations.Add(new MessageSubscription(messageType, handlerType, @interface.GetRuntimeMethod(nameof(IHandler.HandleAsync), new[] { messageType, typeof(MessageContext), typeof(CancellationToken) }))); + } + + Service.TryAddScoped(typeof(IHandler<>), handlerType); + } + else + { + var methods = handlerType.GetMethods(BINDING_FLAGS).Where(method => method.GetCustomAttributes().Any()); + + if (!methods.Any()) + { + continue; + } + + foreach (var method in methods) + { + var parameters = method.GetParameters(); + + if (!parameters.Any(t => t.ParameterType != typeof(CancellationToken) && t.ParameterType != typeof(MessageContext))) + { + throw new InvalidOperationException("Invalid handler method"); + } + + var firstParameter = parameters[0]; + + if (firstParameter.ParameterType.IsPrimitiveType()) + { + throw new InvalidOperationException("The first parameter of handler method must be message type"); + } + + switch (parameters.Length) + { + case 2 when parameters[1].ParameterType != typeof(MessageContext) || parameters[1].ParameterType != typeof(CancellationToken): + throw new InvalidOperationException("The second parameter of handler method must be MessageContext or CancellationToken if the method contains 2 parameters"); + case 3 when parameters[1].ParameterType != typeof(MessageContext) && parameters[2].ParameterType != typeof(CancellationToken): + throw new InvalidOperationException("The second and third parameter of handler method must be MessageContext and CancellationToken if the method contains 3 parameters"); + } + + var attributes = method.GetCustomAttributes(); + + foreach (var attribute in attributes) + { + Registrations.Add(new MessageSubscription(attribute.Name, handlerType, method)); + } + } + + Service.TryAddScoped(handlerType); + } + } + + return this; + } + + /// + /// Set the message convention. + /// + /// + /// + public BusConfigurator SetConventions(Action configure) + { + configure?.Invoke(ConventionBuilder); + Service.TryAddSingleton(ConventionBuilder.Convention); + return this; + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Commands/CommandAttribute.cs b/Source/Euonia.Bus/Commands/CommandAttribute.cs deleted file mode 100644 index cbcf515..0000000 --- a/Source/Euonia.Bus/Commands/CommandAttribute.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace Nerosoft.Euonia.Bus; - -/// -/// Represents the attributed class is a command. -/// -[AttributeUsage(AttributeTargets.Class, Inherited = false)] -public abstract class CommandAttribute : Attribute -{ -} - -/// -/// Represents the attributed class is a distributed command. -/// -[AttributeUsage(AttributeTargets.Class, Inherited = false)] -public class DistributedCommandAttribute : CommandAttribute -{ -} - -/// -/// Represents the attributed class is an integrated command. -/// -[AttributeUsage(AttributeTargets.Class, Inherited = false)] -public class IntegratedCommandAttribute : CommandAttribute -{ -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Commands/CommandRequest.cs b/Source/Euonia.Bus/Commands/CommandRequest.cs deleted file mode 100644 index b71b111..0000000 --- a/Source/Euonia.Bus/Commands/CommandRequest.cs +++ /dev/null @@ -1,28 +0,0 @@ -using MediatR; -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// -/// -/// -/// -public class CommandRequest : IRequest - where TCommand : ICommand -{ - /// - /// - /// - /// - /// - public CommandRequest(TCommand command, bool waitResponse) - { - Command = command; - WaitResponse = waitResponse; - } - - internal TCommand Command { get; } - - internal bool WaitResponse { get; } -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Commands/ICommandBus.cs b/Source/Euonia.Bus/Commands/ICommandBus.cs deleted file mode 100644 index 6940f6c..0000000 --- a/Source/Euonia.Bus/Commands/ICommandBus.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Nerosoft.Euonia.Bus; - -/// -/// Specifies contract for command bus. -/// Implements the -/// Implements the -/// Implements the -/// -/// -/// -/// -public interface ICommandBus : IMessageBus, ICommandSender, ICommandSubscriber -{ -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Commands/ICommandHandler.cs b/Source/Euonia.Bus/Commands/ICommandHandler.cs deleted file mode 100644 index 4943c09..0000000 --- a/Source/Euonia.Bus/Commands/ICommandHandler.cs +++ /dev/null @@ -1,81 +0,0 @@ -using MediatR; -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// Specifies contract for command handler. -/// Implements the -/// -/// -public interface ICommandHandler : IMessageHandler -{ -} - -/// -/// Specifies contract for command handler. -/// Implements the -/// Implements the -/// -/// The type of command. -/// -/// -public interface ICommandHandler : IMessageHandler, ICommandHandler, IRequestHandler, object> - where TCommand : ICommand -{ - async Task IRequestHandler, object>.Handle(CommandRequest request, CancellationToken cancellationToken) - { - var messageContext = new MessageContext(request.Command); - - var taskCompletion = new TaskCompletionSource(); - - if (request.WaitResponse) - { - messageContext.Replied += (_, args) => - { - taskCompletion.TrySetResult(args.Result); - }; - } - - messageContext.Completed += (_, _) => - { - taskCompletion.TrySetResult(null); - }; - - using (messageContext) - { - await HandleAsync(request.Command, messageContext, cancellationToken); - } - - return await taskCompletion.Task; - } -} - -/// -/// -/// -/// -/// -public interface ICommandHandler : IMessageHandler, ICommandHandler, IRequestHandler, TResult> - where TCommand : ICommand -{ - async Task IRequestHandler, TResult>.Handle(CommandRequest request, CancellationToken cancellationToken) - { - var taskCompletion = new TaskCompletionSource(); - - if (cancellationToken != default) - { - cancellationToken.Register(() => taskCompletion.TrySetCanceled(), false); - } - - var messageContext = new MessageContext(); - messageContext.Replied += (_, args) => - { - var result = (TResult)args.Result; - taskCompletion.TrySetResult(result); - }; - - await HandleAsync(request.Command, messageContext, cancellationToken); - return await taskCompletion.Task; - } -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Commands/ICommandSender.cs b/Source/Euonia.Bus/Commands/ICommandSender.cs deleted file mode 100644 index 2b82646..0000000 --- a/Source/Euonia.Bus/Commands/ICommandSender.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// Specifies contract for command sender. -/// Implements the -/// -/// -public interface ICommandSender : IMessageDispatcher -{ - /// - /// Asynchronously send a command request to a single handler - /// - /// The type of command. - /// The command to be sent. - /// The cancellation token. - /// Task. - Task SendAsync(TCommand command, CancellationToken cancellationToken = default) - where TCommand : ICommand; - - /// - /// Asynchronously send a command request to a single handler - /// - /// The type of send result. - /// The type of command. - /// The command to be sent. - /// The cancellation token. - /// Task<TResult>. - Task SendAsync(TCommand command, CancellationToken cancellationToken = default) - where TCommand : ICommand; - - /// - /// Asynchronously send a command request to a single handler - /// - /// The type of send result. - /// The type of command. - /// The command to be sent. - /// The action to execute after command has been sent. - /// The cancellation token. - /// Task. - Task SendAsync(TCommand command, Action callback, CancellationToken cancellationToken = default) - where TCommand : ICommand; - - /// - /// Asynchronously send a command request to a single handler - /// - /// Response type - /// Request object - /// Optional cancellation token - /// A task that represents the send operation. The task result contains the handler response - Task SendAsync(ICommand command, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Commands/ICommandSubscriber.cs b/Source/Euonia.Bus/Commands/ICommandSubscriber.cs deleted file mode 100644 index d9babaf..0000000 --- a/Source/Euonia.Bus/Commands/ICommandSubscriber.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// Specifies contract for command subscriber. -/// Implements the -/// -/// -public interface ICommandSubscriber : IMessageSubscriber -{ - /// - /// Subscribe a command. - /// - /// The type of command. - /// The type of command handler. - void Subscribe() - where TCommand : ICommand - where THandler : ICommandHandler; -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Conventions/AttributeMessageConvention.cs b/Source/Euonia.Bus/Conventions/AttributeMessageConvention.cs new file mode 100644 index 0000000..d2bff74 --- /dev/null +++ b/Source/Euonia.Bus/Conventions/AttributeMessageConvention.cs @@ -0,0 +1,24 @@ +using System.Reflection; + +namespace Nerosoft.Euonia.Bus; + +/// +/// Evaluate whether a type is a message, command, or event by attribute decorated on the type. +/// +public class AttributeMessageConvention : IMessageConvention +{ + /// + public string Name { get; } = "Attribute decoration message convention"; + + /// + public bool IsCommandType(Type type) + { + return type.GetCustomAttribute(false) != null; + } + + /// + public bool IsEventType(Type type) + { + return type.GetCustomAttribute(false) != null; + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Conventions/DefaultMessageConvention.cs b/Source/Euonia.Bus/Conventions/DefaultMessageConvention.cs new file mode 100644 index 0000000..06e3db9 --- /dev/null +++ b/Source/Euonia.Bus/Conventions/DefaultMessageConvention.cs @@ -0,0 +1,32 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// The default message convention. +/// +public class DefaultMessageConvention : IMessageConvention +{ + /// + public string Name => "Default Message Convention"; + + /// + public bool IsCommandType(Type type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type), "Type cannot be null."); + } + + return type.IsAssignableTo(typeof(ICommand)) && type != typeof(ICommand); + } + + /// + public bool IsEventType(Type type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type), "Type cannot be null."); + } + + return type.IsAssignableTo(typeof(IEvent)) && type != typeof(IEvent); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Conventions/IMessageConvention.cs b/Source/Euonia.Bus/Conventions/IMessageConvention.cs new file mode 100644 index 0000000..7a948af --- /dev/null +++ b/Source/Euonia.Bus/Conventions/IMessageConvention.cs @@ -0,0 +1,26 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// A set of conventions for determining if a class represents a message, command, or event. +/// +public interface IMessageConvention +{ + /// + /// The name of the convention. Used for diagnostic purposes. + /// + string Name { get; } + + /// + /// Determine if a type is a command type. + /// + /// The type to check.. + /// true if represents a command. + bool IsCommandType(Type type); + + /// + /// Determine if a type is an event type. + /// + /// The type to check.. + /// true if represents an event. + bool IsEventType(Type type); +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Conventions/MessageConvention.cs b/Source/Euonia.Bus/Conventions/MessageConvention.cs new file mode 100644 index 0000000..f21f7a8 --- /dev/null +++ b/Source/Euonia.Bus/Conventions/MessageConvention.cs @@ -0,0 +1,88 @@ +using System.Collections.Concurrent; + +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +public class MessageConvention +{ + private readonly OverridableMessageConvention _defaultConvention = new(new DefaultMessageConvention()); + private readonly List _conventions = new(); + private readonly ConventionCache _commandConventionCache = new(); + private readonly ConventionCache _eventConventionCache = new(); + + /// + /// Determines whether the specified type is a command. + /// + /// + /// + /// + public bool IsCommandType(Type type) + { + ArgumentAssert.ThrowIfNull(type); + + return _commandConventionCache.Apply(type, handle => + { + var t = Type.GetTypeFromHandle(handle); + return _conventions.Any(x => x.IsCommandType(t)); + }); + } + + /// + /// Determines whether the specified type is an event. + /// + /// + /// + /// + public bool IsEventType(Type type) + { + ArgumentAssert.ThrowIfNull(type); + + return _eventConventionCache.Apply(type, handle => + { + var t = Type.GetTypeFromHandle(handle); + return _conventions.Any(x => x.IsEventType(t)); + }); + } + + internal void DefineCommandTypeConvention(Func convention) + { + _defaultConvention.DefineCommandType(convention); + } + + internal void DefineEventTypeConvention(Func convention) + { + _defaultConvention.DefineEventType(convention); + } + + internal void Add(params IMessageConvention[] conventions) + { + if (conventions == null || conventions.Length == 0) + { + throw new ArgumentException("At least one convention must be provided.", nameof(conventions)); + } + + _conventions.AddRange(conventions); + } + + /// + /// Gets the registered conventions. + /// + internal string[] RegisteredConventions => _conventions.Select(x => x.Name).ToArray(); + + private class ConventionCache + { + public bool Apply(Type type, Func convention) + { + return _cache.GetOrAdd(type.TypeHandle, convention); + } + + public void Reset() + { + _cache.Clear(); + } + + private readonly ConcurrentDictionary _cache = new(); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Conventions/MessageConventionBuilder.cs b/Source/Euonia.Bus/Conventions/MessageConventionBuilder.cs new file mode 100644 index 0000000..591c2fb --- /dev/null +++ b/Source/Euonia.Bus/Conventions/MessageConventionBuilder.cs @@ -0,0 +1,59 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Build customer message convention instead of default message convention. +/// +public class MessageConventionBuilder +{ + internal MessageConvention Convention { get; } = new(); + + /// + /// + /// + /// + /// + public MessageConventionBuilder EvaluateCommand(Func convention) + { + ArgumentAssert.ThrowIfNull(convention); + Convention.DefineCommandTypeConvention(convention); + return this; + } + + /// + /// + /// + /// + /// + public MessageConventionBuilder EvaluateEvent(Func convention) + { + ArgumentAssert.ThrowIfNull(convention); + Convention.DefineEventTypeConvention(convention); + return this; + } + + /// + /// Adds a message convention that will be used to evaluate whether a type is a message, command, or event. + /// + /// The message convention instance. + /// The message convention type. + /// + public MessageConventionBuilder Add(TConvention convention) + where TConvention : class, IMessageConvention + { + ArgumentAssert.ThrowIfNull(convention); + Convention.Add(convention); + return this; + } + + /// + /// Adds a message convention that will be used to evaluate whether a type is a message, command, or event. + /// + /// The message convention type. + /// + public MessageConventionBuilder Add() + where TConvention : class, IMessageConvention, new() + { + Convention.Add(new TConvention()); + return this; + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Conventions/OverridableMessageConvention.cs b/Source/Euonia.Bus/Conventions/OverridableMessageConvention.cs new file mode 100644 index 0000000..f636c32 --- /dev/null +++ b/Source/Euonia.Bus/Conventions/OverridableMessageConvention.cs @@ -0,0 +1,46 @@ +namespace Nerosoft.Euonia.Bus; + +internal class OverridableMessageConvention : IMessageConvention +{ + private readonly IMessageConvention _innerConvention; + private Func _isCommandType, _isEventType; + + public OverridableMessageConvention(IMessageConvention innerConvention) + { + _innerConvention = innerConvention; + } + + public string Name => $"Override with {_innerConvention.Name}"; + + bool IMessageConvention.IsCommandType(Type type) + { + return IsCommandType(type); + } + + bool IMessageConvention.IsEventType(Type type) + { + return IsEventType(type); + } + + public Func IsCommandType + { + get => _isCommandType ?? _innerConvention.IsCommandType; + set => _isCommandType = value; + } + + public Func IsEventType + { + get => _isEventType ?? _innerConvention.IsEventType; + set => _isEventType = value; + } + + public void DefineCommandType(Func convention) + { + _isCommandType = convention; + } + + public void DefineEventType(Func convention) + { + _isEventType = convention; + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Converters/BytesMessageDataConverter.cs b/Source/Euonia.Bus/Converters/BytesMessageDataConverter.cs new file mode 100644 index 0000000..7afb59c --- /dev/null +++ b/Source/Euonia.Bus/Converters/BytesMessageDataConverter.cs @@ -0,0 +1,22 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +public class BytesMessageDataConverter : IMessageDataConverter +{ + /// + /// + /// + /// + /// + /// + public async Task Convert(Stream stream, CancellationToken cancellationToken) + { + using var ms = new MemoryStream(); + + await stream.CopyToAsync(ms, 4096, cancellationToken).ConfigureAwait(false); + + return ms.ToArray(); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Converters/IMessageDataConverter.cs b/Source/Euonia.Bus/Converters/IMessageDataConverter.cs new file mode 100644 index 0000000..810d218 --- /dev/null +++ b/Source/Euonia.Bus/Converters/IMessageDataConverter.cs @@ -0,0 +1,16 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +/// +public interface IMessageDataConverter +{ + /// + /// + /// + /// + /// + /// + Task Convert(Stream stream, CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Converters/JsonMessageDataConverter.cs b/Source/Euonia.Bus/Converters/JsonMessageDataConverter.cs new file mode 100644 index 0000000..934b832 --- /dev/null +++ b/Source/Euonia.Bus/Converters/JsonMessageDataConverter.cs @@ -0,0 +1,30 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +/// +public class JsonMessageDataConverter : IMessageDataConverter +{ + private readonly IMessageSerializer _serializer; + + /// + /// + /// + /// + public JsonMessageDataConverter(IMessageSerializer serializer) + { + _serializer = serializer; + } + + /// + /// + /// + /// + /// + /// + public Task Convert(Stream stream, CancellationToken cancellationToken) + { + return _serializer.DeserializeAsync(stream, cancellationToken); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Converters/StreamMessageDataConverter.cs b/Source/Euonia.Bus/Converters/StreamMessageDataConverter.cs new file mode 100644 index 0000000..2a90d29 --- /dev/null +++ b/Source/Euonia.Bus/Converters/StreamMessageDataConverter.cs @@ -0,0 +1,18 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +public class StreamMessageDataConverter : IMessageDataConverter +{ + /// + /// + /// + /// + /// + /// + public Task Convert(Stream stream, CancellationToken cancellationToken) + { + return Task.FromResult(stream); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Converters/StringMessageDataConverter.cs b/Source/Euonia.Bus/Converters/StringMessageDataConverter.cs new file mode 100644 index 0000000..ee4b6d0 --- /dev/null +++ b/Source/Euonia.Bus/Converters/StringMessageDataConverter.cs @@ -0,0 +1,22 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +public class StringMessageDataConverter : IMessageDataConverter +{ + /// + /// + /// + /// + /// + /// + public async Task Convert(Stream stream, CancellationToken cancellationToken) + { + using var ms = new MemoryStream(); + + await stream.CopyToAsync(ms, 4096, cancellationToken).ConfigureAwait(false); + + return Encoding.UTF8.GetString(ms.ToArray()); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Core/HandlerBase.cs b/Source/Euonia.Bus/Core/HandlerBase.cs new file mode 100644 index 0000000..706e08c --- /dev/null +++ b/Source/Euonia.Bus/Core/HandlerBase.cs @@ -0,0 +1,30 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// The abstract implement of . +/// Implements the +/// +/// The type of the message to be handled. +/// +public abstract class HandlerBase : IHandler + where TMessage : class +{ + /// + /// Determines whether this instance can handle the specified message type. + /// + /// Type of the message. + /// true if this instance can handle the specified message type; otherwise, false. + public virtual bool CanHandle(Type messageType) + { + return typeof(TMessage) == messageType; + } + + /// + /// Handles the asynchronous. + /// + /// The message. + /// The message context. + /// The cancellation token. + /// + public abstract Task HandleAsync(TMessage message, MessageContext messageContext, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Core/HandlerContext.cs b/Source/Euonia.Bus/Core/HandlerContext.cs new file mode 100644 index 0000000..7560e97 --- /dev/null +++ b/Source/Euonia.Bus/Core/HandlerContext.cs @@ -0,0 +1,212 @@ +using System.Collections.Concurrent; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Nerosoft.Euonia.Bus; + +/// +/// Default message handler context using Microsoft dependency injection. +/// +public class HandlerContext : IHandlerContext +{ + /// + /// + /// + public event EventHandler MessageSubscribed; + + private readonly ConcurrentDictionary> _handlerContainer = new(); + private static readonly ConcurrentDictionary _messageTypeMapping = new(); + private readonly IServiceProvider _provider; + private readonly MessageConvert _convert; + private readonly ILogger _logger; + + /// + /// Initialize a new instance of + /// + /// + /// + public HandlerContext(IServiceProvider provider, MessageConvert convert) + { + _provider = provider; + _convert = convert; + _logger = provider.GetService()?.CreateLogger(); + } + + #region Handler register + + internal virtual void Register() + where TMessage : class + where THandler : IHandler + { + Register(typeof(TMessage), typeof(THandler), typeof(THandler).GetMethod(nameof(IHandler.HandleAsync), BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)); + } + + internal virtual void Register(Type messageType, Type handlerType, MethodInfo method) + { + var messageName = messageType.FullName; + + _messageTypeMapping.GetOrAdd(messageName, messageType); + ConcurrentDictionarySafeRegister(messageName, (handlerType, method), _handlerContainer); + MessageSubscribed?.Invoke(this, new MessageSubscribedEventArgs(messageType, handlerType)); + } + + internal void Register(string messageName, Type handlerType, MethodInfo method) + { + ConcurrentDictionarySafeRegister(messageName, (handlerType, method), _handlerContainer); + MessageSubscribed?.Invoke(this, new MessageSubscribedEventArgs(messageName, method.DeclaringType)); + } + + #endregion + + #region Handle message + + /// + public virtual async Task HandleAsync(object message, MessageContext context, CancellationToken cancellationToken = default) + { + if (message == null) + { + return; + } + + var name = message.GetType().FullName; + await HandleAsync(name, message, context, cancellationToken); + } + + /// + public virtual async Task HandleAsync(string name, object message, MessageContext context, CancellationToken cancellationToken = default) + { + if (message == null) + { + return; + } + + var tasks = new List(); + using var scope = _provider.GetRequiredService().CreateScope(); + + if (!_handlerContainer.TryGetValue(name, out var handlerTypes)) + { + throw new InvalidOperationException("No handler registered for message"); + } + + foreach (var (handlerType, handleMethod) in handlerTypes) + { + var handler = ActivatorUtilities.GetServiceOrCreateInstance(scope.ServiceProvider, handlerType); + + var parameters = GetMethodArguments(handleMethod, context, context, cancellationToken); + if (parameters == null) + { + _logger.LogWarning("Method '{Name}' parameter number not matches", handleMethod.Name); + } + else + { + tasks.Add(Invoke(handleMethod, handler, parameters)); + } + } + + if (tasks.Count == 0) + { + return; + } + + await Task.WhenAll(tasks).ContinueWith(_ => + { + _logger?.LogInformation("Message {Id} was completed handled", context.MessageId); + }, cancellationToken); + } + + #endregion + + #region Supports + + private void ConcurrentDictionarySafeRegister(TKey key, TValue value, ConcurrentDictionary> registry) + { + lock (_handlerContainer) + { + if (registry.TryGetValue(key, out var handlers)) + { + if (handlers != null) + { + if (!handlers.Contains(value)) + { + registry[key].Add(value); + } + } + else + { + registry[key] = new List { value }; + } + } + else + { + registry.TryAdd(key, new List { value }); + } + } + } + + private object[] GetMethodArguments(MethodBase method, object message, MessageContext context, CancellationToken cancellationToken) + { + var parameterInfos = method.GetParameters(); + var parameters = new object[parameterInfos.Length]; + switch (parameterInfos.Length) + { + case 0: + break; + case 1: + { + var parameterType = parameterInfos[0].ParameterType; + + if (parameterType == typeof(MessageContext)) + { + parameters[0] = context; + } + else if (parameterType == typeof(CancellationToken)) + { + parameters[0] = cancellationToken; + } + else + { + parameters[0] = _convert(message, parameterType); + } + } + break; + case 2: + case 3: + { + for (var index = 0; index < parameterInfos.Length; index++) + { + if (parameterInfos[index].ParameterType == typeof(MessageContext)) + { + parameters[index] = context; + } + + if (parameterInfos[index].ParameterType == typeof(CancellationToken)) + { + parameters[index] = cancellationToken; + } + } + + parameters[0] ??= _convert(message, parameterInfos[0].ParameterType); + } + break; + default: + return null; + } + + return parameters; + } + + private static Task Invoke(MethodInfo method, object handler, params object[] parameters) + { + if (method.ReturnType.IsAssignableTo(typeof(IAsyncResult))) + { + return (Task)method.Invoke(handler, parameters); + } + else + { + return Task.Run(() => method.Invoke(handler, parameters)); + } + } + + #endregion +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Core/IBus.cs b/Source/Euonia.Bus/Core/IBus.cs new file mode 100644 index 0000000..3d3f6b5 --- /dev/null +++ b/Source/Euonia.Bus/Core/IBus.cs @@ -0,0 +1,49 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Interface IBus +/// +public interface IBus +{ + /// + /// Publishes the specified message. + /// + /// + /// + /// + /// + Task PublishAsync(TMessage message, CancellationToken cancellationToken = default) + where TMessage : class; + + /// + /// Publishes the specified message. + /// + /// + /// + /// + /// + /// + Task PublishAsync(string name, TMessage message, CancellationToken cancellationToken = default) + where TMessage : class; + + /// + /// Sends the specified message. + /// + /// + /// + /// + /// + Task SendAsync(TMessage message, CancellationToken cancellationToken = default) + where TMessage : class; + + /// + /// Sends the specified message. + /// + /// + /// + /// + /// + /// + Task SendAsync(TMessage message, CancellationToken cancellationToken = default) + where TMessage : class; +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Core/IHandler.cs b/Source/Euonia.Bus/Core/IHandler.cs new file mode 100644 index 0000000..c43ba0e --- /dev/null +++ b/Source/Euonia.Bus/Core/IHandler.cs @@ -0,0 +1,48 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Contract of message handler. +/// +public interface IHandler +{ + /// + /// Determines whether the current message handler can handle the message with the specified message type. + /// + /// Type of the message to be checked. + /// true if the current message handler can handle the message with the specified message type; otherwise, false. + bool CanHandle(Type messageType); + + /// + /// Handle message. + /// + /// + /// + /// + /// + Task HandleAsync(object message, MessageContext messageContext, CancellationToken cancellationToken = default); +} + +/// +/// Contract of message handler. +/// +/// The type of the t message. +public interface IHandler : IHandler + where TMessage : class +{ + async Task IHandler.HandleAsync(object message, MessageContext messageContext, CancellationToken cancellationToken) + { + if (message is TMessage knownMessage) + { + await HandleAsync(knownMessage, messageContext, cancellationToken); + } + } + + /// + /// Handle message. + /// + /// The message. + /// The message context. + /// The cancellation token. + /// Task<System.Boolean>. + Task HandleAsync(TMessage message, MessageContext messageContext, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Core/IHandlerContext.cs b/Source/Euonia.Bus/Core/IHandlerContext.cs new file mode 100644 index 0000000..61707a1 --- /dev/null +++ b/Source/Euonia.Bus/Core/IHandlerContext.cs @@ -0,0 +1,31 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Specifies contract of message handler context. +/// +public interface IHandlerContext +{ + /// + /// Occurs when message subscribed. + /// + event EventHandler MessageSubscribed; + + /// + /// Handle message asynchronously. + /// + /// The message to be handled. + /// The message context. + /// The cancellation token. + /// Task. + Task HandleAsync(object message, MessageContext context, CancellationToken cancellationToken = default); + + /// + /// Handle message asynchronously. + /// + /// + /// + /// + /// + /// + Task HandleAsync(string name, object message, MessageContext context, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Core/ServiceBus.cs b/Source/Euonia.Bus/Core/ServiceBus.cs new file mode 100644 index 0000000..9f7f728 --- /dev/null +++ b/Source/Euonia.Bus/Core/ServiceBus.cs @@ -0,0 +1,82 @@ +using System.Reflection; + +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +public sealed class ServiceBus : IBus +{ + private readonly IDispatcher _dispatcher; + private readonly MessageConvention _convention; + + /// + /// Initialize a new instance of + /// + /// + /// + public ServiceBus(IBusFactory factory, MessageConvention convention) + { + _convention = convention; + _dispatcher = factory.CreateDispatcher(); + } + + /// + public async Task PublishAsync(TMessage message, CancellationToken cancellationToken = default) + where TMessage : class + { + if (!_convention.IsEventType(message.GetType())) + { + throw new InvalidOperationException("The message type is not an event type."); + } + + var pack = new RoutedMessage(message, typeof(void).FullName); + await _dispatcher.PublishAsync(pack, cancellationToken); + } + + /// + public async Task PublishAsync(string name, TMessage message, CancellationToken cancellationToken = default) + where TMessage : class + { + if (!_convention.IsEventType(message.GetType())) + { + throw new InvalidOperationException("The message type is not an event type."); + } + + var pack = new RoutedMessage(message, name); + await _dispatcher.PublishAsync(pack, cancellationToken); + } + + /// + public async Task SendAsync(TMessage message, CancellationToken cancellationToken = default) + where TMessage : class + { + if (!_convention.IsCommandType(message.GetType())) + { + throw new InvalidOperationException("The message type is not a command type."); + } + + var pack = new RoutedMessage(message, typeof(void).FullName); + await _dispatcher.SendAsync(pack, cancellationToken); + } + + /// + public async Task SendAsync(TMessage message, CancellationToken cancellationToken = default) + where TMessage : class + { + if (!_convention.IsCommandType(message.GetType())) + { + throw new InvalidOperationException("The message type is not a command type."); + } + + var pack = new RoutedMessage(message, GetChannelName()); + await _dispatcher.SendAsync(pack, cancellationToken); + return default; + } + + private static string GetChannelName() + { + var attribute = typeof(TMessage).GetCustomAttribute(); + return attribute?.Name ?? typeof(TMessage).Name; + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Euonia.Bus.csproj b/Source/Euonia.Bus/Euonia.Bus.csproj index 5d429d2..f760623 100644 --- a/Source/Euonia.Bus/Euonia.Bus.csproj +++ b/Source/Euonia.Bus/Euonia.Bus.csproj @@ -7,13 +7,15 @@ - + + - + + diff --git a/Source/Euonia.Bus/Events/EventNameAttribute.cs b/Source/Euonia.Bus/Events/EventNameAttribute.cs deleted file mode 100644 index 141383b..0000000 --- a/Source/Euonia.Bus/Events/EventNameAttribute.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Reflection; -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// Represents the attributed event has a specified name. -/// -[AttributeUsage(AttributeTargets.Class)] -public class EventNameAttribute : Attribute -{ - /// - /// Gets the event name. - /// - public string Name { get; } - - /// - /// Initialize a new instance of . - /// - /// - /// - public EventNameAttribute(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - Name = name; - } - - /// - /// - /// - /// - /// - public static string GetName() - where TEvent : IEvent - { - return GetName(typeof(TEvent)); - } - - /// - /// - /// - /// - /// - /// - public static string GetName(Type eventType) - { - if (eventType == null) - { - throw new ArgumentNullException(nameof(eventType)); - } - - return eventType.GetCustomAttribute()?.Name ?? eventType.Name; - } -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Events/EventStore.cs b/Source/Euonia.Bus/Events/EventStore.cs deleted file mode 100644 index 54cab28..0000000 --- a/Source/Euonia.Bus/Events/EventStore.cs +++ /dev/null @@ -1,170 +0,0 @@ -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// The abstract implement of . -/// Implements the -/// -/// -public abstract class EventStore : IEventStore -{ - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - /// - public abstract void Dispose(); - - /// - /// Loads the events from event store, by using the specified originator CLR type, originator identifier and the sequence values. - /// - /// The type of the originator key. - /// Type of the originator CLR type. - /// The originator identifier. - /// The minimum event sequence value (inclusive). - /// The maximum event sequence value (inclusive). - /// The events. - /// - public IEnumerable Load(string originatorType, TKey originatorId, long sequenceMin = Constants.MinimalSequence, long sequenceMax = Constants.MaximumSequence) - where TKey : IEquatable - { - var aggregates = LoadAggregates(originatorType, originatorId, sequenceMin, sequenceMax); - foreach (var aggregate in aggregates) - { - if (aggregate.EventPayload is IEvent @event) - { - yield return @event; - } - } - } - - /// - /// load as an asynchronous operation. - /// - /// The type of the originator key. - /// Type of the originator CLR type. - /// The originator identifier. - /// The minimum event sequence value (inclusive). - /// The maximum event sequence value (inclusive). - /// The cancellation token. - /// The events. - /// - public async Task> LoadAsync(string originatorType, TKey originatorId, long sequenceMin = Constants.MinimalSequence, long sequenceMax = Constants.MaximumSequence, CancellationToken cancellationToken = default) - where TKey : IEquatable - { - var aggregates = await LoadAggregatesAsync(originatorType, originatorId, sequenceMin, sequenceMax, cancellationToken); - var events = new List(); - foreach (var aggregate in aggregates) - { - if (aggregate.EventPayload is IEvent @event) - { - events.Add(@event); - } - } - - return events; - } - - /// - /// Saves the specified event to the current event store. - /// - /// The event to be saved. - public void Save(IEvent @event) - { - var aggregate = @event.GetEventAggregate(); - SaveAggregate(aggregate); - } - - /// - /// Saves the specified events to the current event store. - /// - /// The events to be saved. - public void Save(IEnumerable events) - { - var aggregates = new List(); - aggregates.AddRange(events.Select(e => e.GetEventAggregate())); - SaveAggregates(aggregates); - } - - /// - /// save as an asynchronous operation. - /// - /// The event to be saved. - /// The cancellation token. - /// Task. - /// - public async Task SaveAsync(IEvent @event, CancellationToken cancellationToken = default) - { - var aggregate = @event.GetEventAggregate(); - await SaveAggregateAsync(aggregate, cancellationToken); - } - - /// - /// save as an asynchronous operation. - /// - /// The events to be saved. - /// The cancellation token. - /// Task. - /// - public async Task SaveAsync(IEnumerable events, CancellationToken cancellationToken = default) - { - var aggregates = new List(); - aggregates.AddRange(events.Select(e => e.GetEventAggregate())); - await SaveAggregatesAsync(aggregates, cancellationToken); - } - - /// - /// Loads the aggregates. - /// - /// The type of the t key. - /// Type of the originator color. - /// The originator identifier. - /// The sequence minimum. - /// The sequence maximum. - /// IEnumerable<EventAggregate>. - protected abstract IEnumerable LoadAggregates(string originatorType, TKey originatorId, long sequenceMin, long sequenceMax) - where TKey : IEquatable; - - /// - /// load aggregates as an asynchronous operation. - /// - /// The type of the t key. - /// Type of the originator color. - /// The originator identifier. - /// The sequence minimum. - /// The sequence maximum. - /// The cancellation token. - /// Task<IEnumerable<EventAggregate>>. - protected virtual async Task> LoadAggregatesAsync(string originatorClrType, TKey originatorId, long sequenceMin, long sequenceMax, CancellationToken cancellationToken = default) - where TKey : IEquatable => await Task.Run(() => LoadAggregates(originatorClrType, originatorId, sequenceMin, sequenceMax), cancellationToken); - - /// - /// ave event aggregate. - /// - /// The event aggregate to be saved. - protected abstract void SaveAggregate(EventAggregate aggregate); - - /// - /// Save multiple event aggregates. - /// - /// The event aggregates to be saved. - protected abstract void SaveAggregates(IEnumerable aggregates); - - /// - /// Save event aggregate asynchronously. - /// - /// The event aggregate to be saved. - /// The cancellation token. - /// Task. - protected virtual async Task SaveAggregateAsync(EventAggregate aggregate, CancellationToken cancellationToken = default) - => await Task.Run(() => SaveAggregate(aggregate), cancellationToken); - - /// - /// Save multiple event aggregates asynchronously. - /// - /// The event aggregates to be saved. - /// The cancellation token. - /// Task. - protected virtual async Task SaveAggregatesAsync(IEnumerable aggregates, CancellationToken cancellationToken = default) - => await Task.Run(() => SaveAggregates(aggregates), cancellationToken); -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Events/EventSubscribeAttribute.cs b/Source/Euonia.Bus/Events/EventSubscribeAttribute.cs deleted file mode 100644 index a3dfc1b..0000000 --- a/Source/Euonia.Bus/Events/EventSubscribeAttribute.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Nerosoft.Euonia.Bus; - -/// -/// Represents the attributed method would handle an event. -/// -[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] -public class EventSubscribeAttribute : Attribute -{ - /// - /// Initializes a new instance of the class. - /// - /// - public EventSubscribeAttribute(string name) - { - Name = name; - } - - /// - /// Gets the name of the event. - /// - public string Name { get; } -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Events/IEventBus.cs b/Source/Euonia.Bus/Events/IEventBus.cs deleted file mode 100644 index 1ae0ae0..0000000 --- a/Source/Euonia.Bus/Events/IEventBus.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Nerosoft.Euonia.Bus; - -/// -/// Specifies contract for event bus. -/// Implements the -/// Implements the -/// Implements the -/// -/// -/// -/// -public interface IEventBus : IMessageBus, IEventDispatcher, IEventSubscriber -{ -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Events/IEventDispatcher.cs b/Source/Euonia.Bus/Events/IEventDispatcher.cs deleted file mode 100644 index 6a82f10..0000000 --- a/Source/Euonia.Bus/Events/IEventDispatcher.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// Interface IEventDispatcher -/// Implements the -/// -/// -public interface IEventDispatcher : IMessageDispatcher -{ - /// - /// Publish event to event bus asynchronously. - /// - /// The type of the t event. - /// The event. - /// The cancellation token. - /// Task. - Task PublishAsync(TEvent @event, CancellationToken cancellationToken = default) - where TEvent : IEvent; - - /// - /// Publish event with given name. - /// - /// - /// - /// - /// - /// - Task PublishAsync(string name, TEvent @event, CancellationToken cancellationToken = default) - where TEvent : class; -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Events/IEventHandler.cs b/Source/Euonia.Bus/Events/IEventHandler.cs deleted file mode 100644 index 4356a83..0000000 --- a/Source/Euonia.Bus/Events/IEventHandler.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// Interface IEventHandler -/// Implements the -/// -/// -public interface IEventHandler : IMessageHandler -{ -} - -/// -/// Interface IEventHandler -/// Implements the -/// Implements the -/// -/// The type of the t event. -/// -/// -public interface IEventHandler : IEventHandler, IMessageHandler - where TEvent : IEvent -{ -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Events/IEventStore.cs b/Source/Euonia.Bus/Events/IEventStore.cs deleted file mode 100644 index 5fabde8..0000000 --- a/Source/Euonia.Bus/Events/IEventStore.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// Interface IEventStore -/// Implements the -/// -/// -public interface IEventStore : IDisposable -{ - /// - /// Save the specified event to the current event store. - /// - /// The event to be saved. - /// The cancellation token. - /// Task. - Task SaveAsync(IEvent @event, CancellationToken cancellationToken = default); - - /// - /// Saves the specified events to the current event store asynchronously. - /// - /// The events to be saved. - /// The cancellation token. - /// Task. - Task SaveAsync(IEnumerable events, CancellationToken cancellationToken = default); - - /// - /// Loads the events from event store, by using the specified originator CLR type, originator identifier and the sequence values. - /// - /// The type of the originator key. - /// Type of the originator CLR type. - /// The originator identifier. - /// The minimum event sequence value (inclusive). - /// The maximum event sequence value (inclusive). - /// The events. - IEnumerable Load(string originatorType, TKey originatorId, long sequenceMin = Constants.MinimalSequence, long sequenceMax = Constants.MaximumSequence) - where TKey : IEquatable; - - /// - /// Loads the events from event store, by using the specified originator CLR type, originator identifier and the sequence values asynchronously. - /// - /// The type of the originator key. - /// Type of the originator CLR type. - /// The originator identifier. - /// The minimum event sequence value (inclusive). - /// The maximum event sequence value (inclusive). - /// The cancellation token. - /// The events. - Task> LoadAsync(string originatorType, TKey originatorId, long sequenceMin = Constants.MinimalSequence, long sequenceMax = Constants.MaximumSequence, CancellationToken cancellationToken = default) - where TKey : IEquatable; -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Events/IEventSubscriber.cs b/Source/Euonia.Bus/Events/IEventSubscriber.cs deleted file mode 100644 index 2c88994..0000000 --- a/Source/Euonia.Bus/Events/IEventSubscriber.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// Interface IEventSubscriber -/// Implements the -/// -/// -public interface IEventSubscriber : IMessageSubscriber -{ - /// - /// Subscribes this instance. - /// - /// The type of the t event. - /// The type of the t handler. - void Subscribe() - where TEvent : IEvent - where THandler : IEventHandler; -} \ No newline at end of file diff --git a/Source/Euonia.Bus/MessageBusActiveService.cs b/Source/Euonia.Bus/MessageBusActiveService.cs deleted file mode 100644 index 569a85e..0000000 --- a/Source/Euonia.Bus/MessageBusActiveService.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace Nerosoft.Euonia.Bus; - -/// -public class MessageBusActiveService : BackgroundService -{ - private readonly ILogger _logger; - private readonly IServiceProvider _provider; - - /// - public MessageBusActiveService(IServiceProvider provider, ILoggerFactory logger) - { - _provider = provider; - _logger = logger.CreateLogger(); - } - - /// - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - _logger.LogDebug("MessageBusActiveService.ExecuteAsync Called"); - - ActiveCommandBus(); - ActiveEventBus(); - - await Task.CompletedTask; - } - - private void ActiveCommandBus() - { - try - { - var bus = _provider.GetService(); - if (bus != null) - { - bus.MessageReceived += (sender, args) => - { - _logger.LogInformation("Received command: {Id}, {MessageType}. Sender: {Sender}", args.Message.Id, args.Message.GetTypeName(), sender); - }; - } - } - catch (Exception exception) - { - _logger.LogError(exception, "Failed to resolve command bus. {Message}", exception.Message); - throw; - } - } - - private void ActiveEventBus() - { - try - { - var bus = _provider.GetService(); - if (bus != null) - { - bus.MessageReceived += (sender, args) => - { - _logger.LogInformation("Received event: {Id}, {MessageType}. Sender: {Sender}", args.Message?.Id, args.Message?.GetTypeName(), sender); - }; - } - } - catch (Exception exception) - { - _logger.LogError(exception, "Failed to resolve event bus. {Message}", exception.Message); - throw; - } - } -} \ No newline at end of file diff --git a/Source/Euonia.Bus/MessageBusModule.cs b/Source/Euonia.Bus/MessageBusModule.cs deleted file mode 100644 index f17ad67..0000000 --- a/Source/Euonia.Bus/MessageBusModule.cs +++ /dev/null @@ -1,50 +0,0 @@ -using MediatR; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Nerosoft.Euonia.Modularity; - -namespace Nerosoft.Euonia.Bus; - -/// -/// -/// -public class MessageBusModule : ModuleContextBase -{ - /// - public override void ConfigureServices(ServiceConfigurationContext context) - { - { - //context.Services.TryAddScoped(provider => provider.GetService); - context.Services.TryAddScoped(); - - var descriptor = new ServiceDescriptor(typeof(IMessageHandlerContext), provider => - { - var @delegate = provider.GetService(); - var handlerContext = new MessageHandlerContext(provider, @delegate); - - // var options = provider.GetService>()?.Value; - // if (options != null) - // { - // handlerContext.MessageSubscribed += (sender, args) => - // { - // options.MessageSubscribed?.Invoke(sender, args); - // }; - // foreach (var (message, handler) in options.Subscription) - // { - // handlerContext.Register(message, handler); - // } - // } - - { - } - return handlerContext; - }, ServiceLifetime.Singleton); - context.Services.Replace(descriptor); - } - - if (context.Services.All(t => t.ImplementationType != typeof(MessageBusActiveService))) - { - context.Services.AddHostedService(); - } - } -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/IMessageBus.cs b/Source/Euonia.Bus/Messages/IMessageBus.cs deleted file mode 100644 index d346caf..0000000 --- a/Source/Euonia.Bus/Messages/IMessageBus.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Nerosoft.Euonia.Bus; - -/// -/// Interface IMessageBus -/// Implements the -/// Implements the -/// -/// -/// -public interface IMessageBus : IMessageDispatcher, IMessageSubscriber -{ -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/IMessageDispatcher.cs b/Source/Euonia.Bus/Messages/IMessageDispatcher.cs deleted file mode 100644 index af6a67b..0000000 --- a/Source/Euonia.Bus/Messages/IMessageDispatcher.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Nerosoft.Euonia.Bus; - -/// -/// Interface IMessageDispatcher -/// Implements the -/// -/// -public interface IMessageDispatcher : IDisposable -{ - /// - /// Occurs when [message subscribed]. - /// - event EventHandler MessageSubscribed; - - /// - /// Occurs when [message dispatched]. - /// - event EventHandler MessageDispatched; -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/IMessageHandler.cs b/Source/Euonia.Bus/Messages/IMessageHandler.cs deleted file mode 100644 index 78da74e..0000000 --- a/Source/Euonia.Bus/Messages/IMessageHandler.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// Contract of message handler. -/// -public interface IMessageHandler -{ - /// - /// Determines whether the current message handler can handle the message with the specified message type. - /// - /// Type of the message to be checked. - /// true if the current message handler can handle the message with the specified message type; otherwise, false. - bool CanHandle(Type messageType); - - /// - /// Handle message. - /// - /// The message. - /// The message context. - /// The cancellation token. - /// Task<System.Boolean>. - Task HandleAsync(IMessage message, MessageContext messageContext, CancellationToken cancellationToken = default); -} - -/// -/// Contract of message handler. -/// Implements the -/// -/// The type of the t message. -/// -public interface IMessageHandler : IMessageHandler - where TMessage : IMessage -{ - /// - /// Handle message. - /// - /// The message. - /// The message context. - /// The cancellation token. - /// Task<System.Boolean>. - Task HandleAsync(TMessage message, MessageContext messageContext, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/IMessageHandlerContext.cs b/Source/Euonia.Bus/Messages/IMessageHandlerContext.cs deleted file mode 100644 index d30a6cf..0000000 --- a/Source/Euonia.Bus/Messages/IMessageHandlerContext.cs +++ /dev/null @@ -1,79 +0,0 @@ -using MediatR; -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// Specifies contract of message handler context. -/// -public interface IMessageHandlerContext -{ - /// - /// Occurs when message subscribed. - /// - event EventHandler MessageSubscribed; - - /// - /// Registers handler of for . - /// - /// The type of the message. - /// The type of the handler. - void Register() - where TMessage : IMessage - where THandler : IMessageHandler; - - /// - /// Registers handler for message of type - /// - /// Type of the message. - /// Type of the handler. - void Register(Type messageType, Type handlerType); - - /// - /// Register handler for named message. - /// - /// - /// - void Register(string messageName, Type handlerType); - - /// - /// Determine whether the message would be handled by a handler of type . - /// - /// The type of the message. - /// The type of the handler. - /// true if [is handler registered]; otherwise, false. - bool IsHandlerRegistered() - where TMessage : IMessage - where THandler : IMessageHandler; - - /// - /// Determine whether the message would be handled by a handler of type . - /// - /// The type of the message. - /// The type of the handler. - /// true if [is handler registered] [the specified message type]; otherwise, false. - bool IsHandlerRegistered(Type messageType, Type handlerType); - - /// - /// Determine whether the message would be handled by a handle of type . - /// - /// The message name. - /// The type of the handler. - /// true if [is handler registered] [the specified message type]; otherwise, false. - bool IsHandlerRegistered(string messageName, Type handlerType); - - /// - /// Handle message asynchronously. - /// - /// The message to be handled. - /// The message context. - /// The cancellation token. - /// Task. - Task HandleAsync(IMessage message, MessageContext context, CancellationToken cancellationToken = default); - - /// - /// Gets the mediator instance. - /// - /// - IMediator GetMediator(); -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/IMessageSubscriber.cs b/Source/Euonia.Bus/Messages/IMessageSubscriber.cs deleted file mode 100644 index 54e339e..0000000 --- a/Source/Euonia.Bus/Messages/IMessageSubscriber.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Nerosoft.Euonia.Bus; - -/// -/// Interface IMessageSubscriber -/// Implements the -/// -/// -public interface IMessageSubscriber : IDisposable -{ - /// - /// Occurs when [message received]. - /// - event EventHandler MessageReceived; - - /// - /// Occurs when [message acknowledged]. - /// - event EventHandler MessageAcknowledged; - - /// - /// Subscribes the specified message type. - /// - /// - /// - void Subscribe(Type eventType, Type handlerType); -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageAcknowledgedEventArgs.cs b/Source/Euonia.Bus/Messages/MessageAcknowledgedEventArgs.cs deleted file mode 100644 index f162ebd..0000000 --- a/Source/Euonia.Bus/Messages/MessageAcknowledgedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// Class MessageAcknowledgedEventArgs. -/// Implements the -/// -/// -public class MessageAcknowledgedEventArgs : MessageProcessedEventArgs -{ - /// - /// Initializes a new instance of the class. - /// - /// The message. - /// The message context. - public MessageAcknowledgedEventArgs(IMessage message, MessageContext messageContext) - : base(message, messageContext, MessageProcessType.Receive) - { - } -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageBusException.cs b/Source/Euonia.Bus/Messages/MessageBusException.cs index f506d27..523de40 100644 --- a/Source/Euonia.Bus/Messages/MessageBusException.cs +++ b/Source/Euonia.Bus/Messages/MessageBusException.cs @@ -1,5 +1,4 @@ using System.Runtime.Serialization; -using Nerosoft.Euonia.Domain; namespace Nerosoft.Euonia.Bus; @@ -11,57 +10,57 @@ namespace Nerosoft.Euonia.Bus; [Serializable] public class MessageBusException : Exception { - private IMessage _message; + private object _message; - /// - /// Initializes a new instance of the class. - /// - /// Type of the message. - public MessageBusException(IMessage messageContext) - { - _message = messageContext; - } + /// + /// Initializes a new instance of the class. + /// + /// Type of the message. + public MessageBusException(object messageContext) + { + _message = messageContext; + } - /// - /// Initializes a new instance of the class. - /// - /// Type of the message. - /// The message. - public MessageBusException(IMessage messageContext, string message) - : base(message) - { - _message = messageContext; - } + /// + /// Initializes a new instance of the class. + /// + /// Type of the message. + /// The message. + public MessageBusException(object messageContext, string message) + : base(message) + { + _message = messageContext; + } - /// - /// Initializes a new instance of the class. - /// - /// Type of the message. - /// The message. - /// The inner exception. - public MessageBusException(IMessage messageContext, string message, Exception innerException) - : base(message, innerException) - { - _message = messageContext; - } + /// + /// Initializes a new instance of the class. + /// + /// Type of the message. + /// The message. + /// The inner exception. + public MessageBusException(object messageContext, string message, Exception innerException) + : base(message, innerException) + { + _message = messageContext; + } - /// - /// The type of the handled message. - /// - /// The type of the message. - public virtual IMessage MessageContext => _message; + /// + /// The type of the handled message. + /// + /// The type of the message. + public virtual object MessageContext => _message; - /// - public MessageBusException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - _message = info.GetValue(nameof(MessageContext), typeof(IMessage)) as IMessage; - } + /// + public MessageBusException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + _message = info.GetValue(nameof(MessageContext), MessageContext.GetType()); + } - /// - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - info.AddValue(nameof(MessageContext), _message, MessageContext.GetType()); - } + /// + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue(nameof(MessageContext), _message, MessageContext.GetType()); + } } \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageContext.cs b/Source/Euonia.Bus/Messages/MessageContext.cs deleted file mode 100644 index 96ff1c0..0000000 --- a/Source/Euonia.Bus/Messages/MessageContext.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// The message context. -/// -public class MessageContext : IDisposable -{ - private readonly WeakEventManager _events = new(); - - private bool _disposedValue; - - /// - /// Initializes a new instance of the class. - /// - public MessageContext() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - public MessageContext(IMessage message) - { - Message = message; - } - - /// - /// Invoked while message was handled and replied to dispatcher. - /// - public event EventHandler Replied - { - add => _events.AddEventHandler(value); - remove => _events.RemoveEventHandler(value); - } - - /// - /// Invoke while message context disposed. - /// - public event EventHandler Completed - { - add => _events.AddEventHandler(value); - remove => _events.RemoveEventHandler(value); - } - - /// - /// Gets or sets the message. - /// - public IMessage Message { get; set; } - - /// - /// Replies message handling result to message dispatcher. - /// - /// The message to reply. - public void Reply(object message) - { - _events.HandleEvent(this, new MessageRepliedEventArgs(message), nameof(Replied)); - } - - /// - /// Replies message handling result to message dispatcher. - /// - /// The type of the message. - /// The message to reply. - public void Reply(TMessage message) - { - Reply((object)message); - } - - /// - /// Called after the message has been handled. - /// This operate will raised up the event. - /// - /// - public void Complete(IMessage message) - { - _events.HandleEvent(this, new MessageHandledEventArgs(message), nameof(Completed)); - } - - /// - /// Called after the message has been handled. - /// This operate will raised up the event. - /// - /// - /// - public void Complete(IMessage message, Type handlerType) - { - _events.HandleEvent(this, new MessageHandledEventArgs(message) { HandlerType = handlerType }, nameof(Completed)); - } - - /// - /// Called after the message has been handled. - /// - /// - protected virtual void Dispose(bool disposing) - { - if (_disposedValue) - { - return; - } - - if (disposing) - { - Complete(Message); - } - - _events.RemoveEventHandlers(); - _disposedValue = true; - } - - /// - /// Finalizes the current instance of the class. - /// - ~MessageContext() - { - Dispose(disposing: false); - } - - /// - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); - } -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageConversionDelegate.cs b/Source/Euonia.Bus/Messages/MessageConvert.cs similarity index 55% rename from Source/Euonia.Bus/Messages/MessageConversionDelegate.cs rename to Source/Euonia.Bus/Messages/MessageConvert.cs index 27a88fe..074f9a1 100644 --- a/Source/Euonia.Bus/Messages/MessageConversionDelegate.cs +++ b/Source/Euonia.Bus/Messages/MessageConvert.cs @@ -3,4 +3,4 @@ /// /// The message conversion delegate. /// -public delegate object MessageConversionDelegate(object source, Type targetType); \ No newline at end of file +public delegate object MessageConvert(object source, Type targetType); \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageDispatchedEventArgs.cs b/Source/Euonia.Bus/Messages/MessageDispatchedEventArgs.cs deleted file mode 100644 index c71800a..0000000 --- a/Source/Euonia.Bus/Messages/MessageDispatchedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// Class MessageDispatchedEventArgs. -/// Implements the -/// -/// -public class MessageDispatchedEventArgs : MessageProcessedEventArgs -{ - /// - /// Initializes a new instance of the class. - /// - /// The message. - /// The message context. - public MessageDispatchedEventArgs(IMessage message, MessageContext messageContext) - : base(message, messageContext, MessageProcessType.Dispatch) - { - } -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageHandledEventArgs.cs b/Source/Euonia.Bus/Messages/MessageHandledEventArgs.cs deleted file mode 100644 index dd028bb..0000000 --- a/Source/Euonia.Bus/Messages/MessageHandledEventArgs.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// Occurs when message was handled. -/// -public class MessageHandledEventArgs : EventArgs -{ - /// - /// Initializes a new instance of the class. - /// - /// - public MessageHandledEventArgs(IMessage message) - { - Message = message; - } - - /// - /// Gets the handle message. - /// - public IMessage Message { get; } - - /// - /// Gets the handler type. - /// - public Type HandlerType { get; internal set; } -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageHandler.cs b/Source/Euonia.Bus/Messages/MessageHandler.cs new file mode 100644 index 0000000..8e453c5 --- /dev/null +++ b/Source/Euonia.Bus/Messages/MessageHandler.cs @@ -0,0 +1,13 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +/// +public delegate Task MessageHandler(TMessage message, MessageContext context, CancellationToken cancellationToken = default) + where TMessage : class; + +/// +/// +/// +public delegate Task MessageHandler(object message, MessageContext context, CancellationToken cancellationToken = default); \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageHandlerBase.cs b/Source/Euonia.Bus/Messages/MessageHandlerBase.cs deleted file mode 100644 index 47c8f55..0000000 --- a/Source/Euonia.Bus/Messages/MessageHandlerBase.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// The abstract implement of . -/// Implements the -/// -/// -public abstract class MessageHandlerBase : IMessageHandler -{ - /// - /// Determines whether the current message handler can handle the message with the specified message type. - /// - /// Type of the message to be checked. - /// true if the current message handler can handle the message with the specified message type; otherwise, false. - public abstract bool CanHandle(Type messageType); - - /// - /// Handle message. - /// - /// The message. - /// The message context. - /// The cancellation token. - /// Task<System.Boolean>. - public abstract Task HandleAsync(IMessage message, MessageContext messageContext, CancellationToken cancellationToken = default); -} - -/// -/// The abstract implement of . -/// Implements the -/// Implements the -/// -/// The type of the message to be handled. -/// -/// -public abstract class MessageHandlerBase : MessageHandlerBase, IMessageHandler - where TMessage : IMessage -{ - /// - /// Determines whether this instance can handle the specified message type. - /// - /// Type of the message. - /// true if this instance can handle the specified message type; otherwise, false. - public override bool CanHandle(Type messageType) - { - return typeof(TMessage) == messageType; - } - - /// - /// handle as an asynchronous operation. - /// - /// The message. - /// The message context. - /// The cancellation token. - /// - /// Task<System.Boolean>. - public sealed override Task HandleAsync(IMessage message, MessageContext messageContext, CancellationToken cancellationToken = default) - { - return message switch - { - null => throw new ArgumentNullException(nameof(message)), - TMessage typedMessage => HandleAsync(typedMessage, messageContext, cancellationToken), - _ => Task.CompletedTask - }; - } - - /// - /// Handles the asynchronous. - /// - /// The message. - /// The message context. - /// The cancellation token. - /// Task<System.Boolean>. - public abstract Task HandleAsync(TMessage message, MessageContext messageContext, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageHandlerContext.cs b/Source/Euonia.Bus/Messages/MessageHandlerContext.cs deleted file mode 100644 index 2023011..0000000 --- a/Source/Euonia.Bus/Messages/MessageHandlerContext.cs +++ /dev/null @@ -1,343 +0,0 @@ -using System.Collections.Concurrent; -using System.Reflection; -using MediatR; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// Default message handler context using Microsoft dependency injection. -/// -public class MessageHandlerContext : IMessageHandlerContext -{ - /// - /// - /// - public event EventHandler MessageSubscribed; - - private readonly ConcurrentDictionary> _handlerContainer = new(); - private static readonly ConcurrentDictionary _messageTypeMapping = new(); - private readonly IServiceProvider _provider; - private readonly MessageConversionDelegate _conversion; - private readonly ILogger _logger; - - /// - /// Initialize a new instance of - /// - /// - /// - public MessageHandlerContext(IServiceProvider provider, MessageConversionDelegate conversion) - { - _provider = provider; - _conversion = conversion; - _logger = provider.GetService()?.CreateLogger(); - } - - /// - public virtual IMediator GetMediator() - { - return _provider.GetService(); - } - - #region Handler register - - /// - public virtual void Register() - where TMessage : IMessage - where THandler : IMessageHandler - { - if (IsHandlerRegistered()) - { - return; - } - - Register(typeof(TMessage), typeof(THandler)); - } - - /// - public virtual void Register(Type messageType, Type handlerType) - { - if (IsHandlerRegistered(messageType, handlerType)) - { - return; - } - - var messageName = messageType.FullName; - - _messageTypeMapping.GetOrAdd(messageName, messageType); - ConcurrentDictionarySafeRegister(messageName, handlerType, _handlerContainer); - MessageSubscribed?.Invoke(this, new MessageSubscribedEventArgs(messageType, handlerType)); - } - - /// - public void Register(string messageName, Type handlerType) - { - if (IsHandlerRegistered(messageName, handlerType)) - { - return; - } - - ConcurrentDictionarySafeRegister(messageName, handlerType, _handlerContainer); - MessageSubscribed?.Invoke(this, new MessageSubscribedEventArgs(messageName, handlerType)); - } - - /// - public virtual bool IsHandlerRegistered() - where TMessage : IMessage - where THandler : IMessageHandler - { - return IsHandlerRegistered(typeof(TMessage), typeof(THandler)); - } - - /// - public virtual bool IsHandlerRegistered(Type messageType, Type handlerType) - { - return IsHandlerRegistered(messageType.FullName!, handlerType); - } - - /// - public virtual bool IsHandlerRegistered(string messageName, Type handlerType) - { - if (_handlerContainer.TryGetValue(messageName, out var handlers)) - { - return handlers != null && handlers.Contains(handlerType); - } - - return false; - } - - #endregion - - #region Handle message - - /// - public virtual async Task HandleAsync(IMessage message, MessageContext context, CancellationToken cancellationToken = default) - { - if (message == null) - { - return; - } - - var tasks = new List(); - using var scope = _provider.GetRequiredService().CreateScope(); - - if (message is INamedMessage namedMessage) - { - if (!_handlerContainer.TryGetValue(namedMessage.Name, out var handlerTypes)) - { - return; - } - - foreach (var handlerType in handlerTypes) - { - if (handlerType.IsSubclassOf(typeof(IMessageHandler))) - { - if (!_messageTypeMapping.TryGetValue(namedMessage.Name, out var messageType) || !messageType.IsAssignableTo(typeof(IMessage))) - { - continue; - } - - var messageToHandle = (IMessage)_conversion(namedMessage.Data, messageType); - - var handler = (IMessageHandler)ActivatorUtilities.GetServiceOrCreateInstance(scope.ServiceProvider, handlerType); - - tasks.Add(handler.HandleAsync(messageToHandle, context, cancellationToken)); - } - else - { - var methods = handlerType.GetRuntimeMethods().Where(method => method.GetCustomAttributes().Any(t => t.Name.Equals(namedMessage.Name))); - - if (!methods.Any()) - { - continue; - } - - var handler = ActivatorUtilities.GetServiceOrCreateInstance(scope.ServiceProvider, handlerType); - foreach (var method in methods) - { - var parameters = GetMethodArguments(method, context, context, cancellationToken); - if (parameters == null) - { - _logger.LogWarning("Method '{Name}' parameter number not matches", method.Name); - } - else - { - tasks.Add(Invoke(method, handler, parameters)); - } - } - } - } - } - else - { - var handlers = GetOrCreateHandlers(message, scope.ServiceProvider); - - if (handlers == null || !handlers.Any()) - { - return; - } - - var messageType = message.GetType(); - - foreach (var handler in handlers) - { - if (!handler.CanHandle(messageType)) - { - continue; - } - - _logger.LogInformation("Message {Id}({MessageType}) will be handled by {HandlerType}", message.Id, messageType.FullName, handler); - - tasks.Add(handler.HandleAsync(message, context, cancellationToken)); - } - } - - if (tasks.Count == 0) - { - return; - } - - await Task.WhenAll(tasks).ContinueWith(_ => - { - _logger?.LogInformation("Message {Id} was completed handled", message.Id); - }, cancellationToken); - } - - #endregion - - #region Supports - - private IEnumerable GetOrCreateHandlers(IMessage message, IServiceProvider provider) - { - var messageType = message.GetType(); - - if (_handlerContainer.TryGetValue(messageType.FullName!, out var handlerTypes)) - { - if (handlerTypes == null || handlerTypes.Count < 1) - { - yield break; - } - - // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator - foreach (var handlerType in handlerTypes) - { - var handler = (IMessageHandler)ActivatorUtilities.GetServiceOrCreateInstance(provider, handlerType); - yield return handler; - } - } - else - { - var required = message is Command; - var interfaceType = message switch - { - Event _ => typeof(IEventHandler<>).MakeGenericType(messageType), - Command _ => typeof(ICommandHandler<>).MakeGenericType(messageType), - _ => throw new InvalidOperationException() - }; - - var handlers = provider.GetServices(interfaceType); - if (required && (handlers == null || !handlers.Any())) - { - throw new InvalidOperationException($"No handler was found for {messageType.FullName}"); - } - - foreach (var handler in handlers) - { - yield return (IMessageHandler)handler; - } - } - } - - private void ConcurrentDictionarySafeRegister(TKey key, TValue value, ConcurrentDictionary> registry) - { - lock (_handlerContainer) - { - if (registry.TryGetValue(key, out var handlers)) - { - if (handlers != null) - { - if (!handlers.Contains(value)) - { - registry[key].Add(value); - } - } - else - { - registry[key] = new List { value }; - } - } - else - { - registry.TryAdd(key, new List { value }); - } - } - } - - private object[] GetMethodArguments(MethodInfo method, object message, MessageContext context, CancellationToken cancellationToken) - { - var parameterInfos = method.GetParameters(); - var parameters = new object[parameterInfos.Length]; - switch (parameterInfos.Length) - { - case 0: - break; - case 1: - { - var parameterType = parameterInfos[0].ParameterType; - - if (parameterType == typeof(MessageContext)) - { - parameters[0] = context; - } - else if (parameterType == typeof(CancellationToken)) - { - parameters[0] = cancellationToken; - } - else - { - parameters[0] = _conversion(message, parameterType); - } - } - break; - case 2: - case 3: - { - for (var index = 0; index < parameterInfos.Length; index++) - { - if (parameterInfos[index].ParameterType == typeof(MessageContext)) - { - parameters[index] = context; - } - - if (parameterInfos[index].ParameterType == typeof(CancellationToken)) - { - parameters[index] = cancellationToken; - } - } - - parameters[0] ??= _conversion(message, parameterInfos[0].ParameterType); - } - break; - default: - return null; - } - - return parameters; - } - - private static Task Invoke(MethodInfo method, object handler, params object[] parameters) - { - if (method.ReturnType.IsAssignableTo(typeof(IAsyncResult))) - { - return (Task)method.Invoke(handler, parameters); - } - else - { - return Task.Run(() => method.Invoke(handler, parameters)); - } - } - - #endregion -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageHandlerOptions.cs b/Source/Euonia.Bus/Messages/MessageHandlerOptions.cs deleted file mode 100644 index c29fd27..0000000 --- a/Source/Euonia.Bus/Messages/MessageHandlerOptions.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System.Reflection; -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// The message handler options. -/// -public class MessageHandlerOptions -{ - private const BindingFlags BINDING_FLAGS = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; - - /// - /// The message handler types. - /// - public List Subscription { get; } = new(); - - /// - /// Subscribes the specified message type. - /// - /// - /// - public void Subscribe() - where TMessage : IMessage - where THandler : IMessageHandler - { - Subscribe(typeof(TMessage), typeof(THandler)); - } - - /// - /// Subscribes the specified message type. - /// - /// - /// - public void Subscribe(Type messageType, Type handlerType) - { - if (!messageType.IsAssignableTo()) - { - throw new InvalidOperationException($"The message must inherits type of {typeof(IEvent).FullName} or {typeof(ICommand).FullName}"); - } - - if (!handlerType.IsAssignableTo(typeof(IMessageHandler<>).MakeGenericType(messageType))) - { - throw new InvalidOperationException($"The message handler type must implements ICommandHandler<{messageType.Name}> or IEventHandler<{messageType.Name}>"); - } - - Subscription.Add(new MessageSubscription(messageType, handlerType)); - } - - /// - /// Subscribes the specified message type. - /// - /// - /// - public void Subscribe(Type messageType, IEnumerable handlerTypes) - { - foreach (var handlerType in handlerTypes) - { - Subscribe(messageType, handlerType); - } - } - - /// - /// Subscribes the specified message. - /// - /// - /// - public void Subscribe(string messageName, Type handlerType) - { - Subscription.Add(new MessageSubscription(messageName, handlerType)); - } - - /// - /// - /// - /// - /// - public void Subscribe(Type handlerType) - { - if (!handlerType.IsAssignableTo(typeof(IMessageHandler))) - { - throw new InvalidOperationException($"The message handler type must implements ICommandHandler<> or IEventHandler<>"); - } - - var messageTypes = from interfaceType in handlerType.GetInterfaces() - where interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IMessageHandler<>) - select interfaceType.GetGenericArguments()[0]; - - messageTypes = messageTypes.Distinct(); - - foreach (var messageType in messageTypes) - { - if (!messageType.IsAssignableTo(typeof(IMessage))) - { - continue; - } - - Subscribe(messageType, handlerType); - } - } - - /// - /// Register all handlers in assembly. - /// - /// The assembly. - public void Subscribe(Assembly assembly) - { - var handlerTypes = from type in assembly.DefinedTypes - where type.IsClass && !type.IsAbstract // && type.IsAssignableTo(typeof(IMessageHandler)) - select type; - - foreach (var handlerType in handlerTypes) - { - if (handlerType.IsAssignableTo(typeof(IMessageHandler))) - { - var messageTypes = from interfaceType in handlerType.ImplementedInterfaces - where interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IMessageHandler<>) - select interfaceType.GetGenericArguments()[0]; - - messageTypes = messageTypes.Distinct(); - - foreach (var messageType in messageTypes) - { - Subscribe(messageType, handlerType); - } - } - else - { - var methods = handlerType.GetMethods(BINDING_FLAGS).Where(method => method.GetCustomAttributes().Any()); - - var attributes = methods.SelectMany(t => t.GetCustomAttributes()); - - foreach (var attribute in attributes) - { - Subscribe(attribute.Name, handlerType); - } - } - } - } -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageProcessedEventArgs.cs b/Source/Euonia.Bus/Messages/MessageProcessedEventArgs.cs deleted file mode 100644 index 0bb0a5b..0000000 --- a/Source/Euonia.Bus/Messages/MessageProcessedEventArgs.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// Class MessageProcessedEventArgs. -/// Implements the -/// -/// -public class MessageProcessedEventArgs : EventArgs -{ - /// - /// Initializes a new instance of the class. - /// - /// The message. - /// The message context. - /// Type of the process. - public MessageProcessedEventArgs(IMessage message, MessageContext messageContext, MessageProcessType processType) - { - Message = message; - MessageContext = messageContext; - ProcessType = processType; - } - - /// - /// Gets the message. - /// - /// The message. - public IMessage Message { get; } - - /// - /// Gets the message context. - /// - /// The message context. - public MessageContext MessageContext { get; } - - /// - /// Gets the type of the process. - /// - /// The type of the process. - public MessageProcessType ProcessType { get; } -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageProcessingException.cs b/Source/Euonia.Bus/Messages/MessageProcessingException.cs index ad700ae..ebb10d0 100644 --- a/Source/Euonia.Bus/Messages/MessageProcessingException.cs +++ b/Source/Euonia.Bus/Messages/MessageProcessingException.cs @@ -5,31 +5,32 @@ /// Implements the /// /// +[Serializable] public class MessageProcessingException : Exception { - /// - /// Initializes a new instance of the class. - /// - public MessageProcessingException() - { - } + /// + /// Initializes a new instance of the class. + /// + public MessageProcessingException() + { + } - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public MessageProcessingException(string message) - : base(message) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public MessageProcessingException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. - public MessageProcessingException(string message, Exception innerException) - : base(message, innerException) - { - } + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. + public MessageProcessingException(string message, Exception innerException) + : base(message, innerException) + { + } } \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageReceivedEventArgs.cs b/Source/Euonia.Bus/Messages/MessageReceivedEventArgs.cs deleted file mode 100644 index 0859ef3..0000000 --- a/Source/Euonia.Bus/Messages/MessageReceivedEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus; - -/// -/// Class MessageReceivedEventArgs. -/// Implements the -/// -/// -public class MessageReceivedEventArgs : MessageProcessedEventArgs -{ - /// - /// Initializes a new instance of the class. - /// - /// The message. - /// The message context. - public MessageReceivedEventArgs(IMessage message, MessageContext messageContext) - : base(message, messageContext, MessageProcessType.Receive) - { - } -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageSubscription.cs b/Source/Euonia.Bus/Messages/MessageSubscription.cs index 0e2a1d0..213b6fc 100644 --- a/Source/Euonia.Bus/Messages/MessageSubscription.cs +++ b/Source/Euonia.Bus/Messages/MessageSubscription.cs @@ -1,44 +1,54 @@ -namespace Nerosoft.Euonia.Bus; +using System.Reflection; + +namespace Nerosoft.Euonia.Bus; /// /// The message subscription. /// -public class MessageSubscription +internal class MessageSubscription { - /// - /// Initializes a new instance of the class. - /// - /// - /// - public MessageSubscription(Type messageType, Type handlerType) - : this(messageType.FullName, handlerType) - { - MessageType = messageType; - } + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + public MessageSubscription(Type type, Type handlerType, MethodInfo handleMethod) + : this(type.FullName, handlerType, handleMethod) + { + Type = type; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + public MessageSubscription(string name, Type handlerType, MethodInfo handleMethod) + { + Name = name; + HandlerType = handlerType; + HandleMethod = handleMethod; + } - /// - /// Initializes a new instance of the class. - /// - /// - /// - public MessageSubscription(string messageName, Type handlerType) - { - MessageName = messageName; - HandlerType = handlerType; - } + /// + /// Gets or sets the message name. + /// + public string Name { get; set; } - /// - /// Gets or sets the message name. - /// - public string MessageName { get; set; } + /// + /// Gets or sets the message type. + /// + public Type Type { get; set; } - /// - /// Gets or sets the message type. - /// - public Type MessageType { get; set; } + /// + /// Gets or sets the message handler type. + /// + public Type HandlerType { get; set; } - /// - /// Gets or sets the message handler type. - /// - public Type HandlerType { get; set; } + /// + /// Gets or sets the message handler method. + /// + public MethodInfo HandleMethod { get; set; } } \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/PipelineMessage.cs b/Source/Euonia.Bus/Messages/PipelineMessage.cs new file mode 100644 index 0000000..7f9cbb5 --- /dev/null +++ b/Source/Euonia.Bus/Messages/PipelineMessage.cs @@ -0,0 +1,128 @@ +using Nerosoft.Euonia.Pipeline; + +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +/// +/// +public class PipelineMessage + where TMessage : class +{ + /// + /// + /// + /// + public PipelineMessage(TMessage command) + { + Command = command; + } + + /// + /// + /// + /// + /// + public PipelineMessage(TMessage command, IPipeline pipeline) + { + Command = command; + Pipeline = pipeline; + } + + /// + /// + /// + public TMessage Command { get; } + + /// + /// + /// + public IPipeline Pipeline { get; private set; } + + /// + /// + /// + /// + /// + /// + public PipelineMessage Use(Type type, params object[] args) + { + Pipeline = Pipeline.Use(type, args); + return this; + } + + /// + /// + /// + /// + /// + public PipelineMessage Use() + where TBehavior : IPipelineBehavior + { + Pipeline = Pipeline.Use(); + return this; + } +} + +/// +/// +/// +/// +public class PipelineMessage + where TMessage : class +{ + /// + /// + /// + /// + public PipelineMessage(TMessage command) + { + Command = command; + } + + /// + /// + /// + /// + /// + public PipelineMessage(TMessage command, IPipeline pipeline) + { + Command = command; + Pipeline = pipeline; + } + + /// + /// + /// + public TMessage Command { get; } + + /// + /// + /// + public IPipeline Pipeline { get; private set; } + + /// + /// + /// + /// + /// + /// + public PipelineMessage Use(Type type, params object[] args) + { + Pipeline = Pipeline.Use(type, args); + return this; + } + + /// + /// + /// + /// + /// + public PipelineMessage Use() + where TBehavior : IPipelineBehavior + { + Pipeline = Pipeline.Use(); + return this; + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Properties/Resources.resx b/Source/Euonia.Bus/Properties/Resources.resx index 4fdb1b6..443e4fa 100644 --- a/Source/Euonia.Bus/Properties/Resources.resx +++ b/Source/Euonia.Bus/Properties/Resources.resx @@ -98,4 +98,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The type cannot be null. + + + At least one convention must be provided. + \ No newline at end of file diff --git a/Source/Euonia.Bus/Serialization/IMessageSerializer.cs b/Source/Euonia.Bus/Serialization/IMessageSerializer.cs new file mode 100644 index 0000000..4d0d934 --- /dev/null +++ b/Source/Euonia.Bus/Serialization/IMessageSerializer.cs @@ -0,0 +1,16 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +public interface IMessageSerializer +{ + /// + /// Asynchronously reads a JSON value from the provided and converts it to an instance of a type specified by a generic parameter. + /// + /// The JSON data to parse. + /// + /// + /// + Task DeserializeAsync(Stream stream, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Serialization/NewtonsoftJsonSerializer.cs b/Source/Euonia.Bus/Serialization/NewtonsoftJsonSerializer.cs new file mode 100644 index 0000000..839b543 --- /dev/null +++ b/Source/Euonia.Bus/Serialization/NewtonsoftJsonSerializer.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Nerosoft.Euonia.Bus; + +/// +/// Serializer for message using Newtonsoft.Json. +/// +public class NewtonsoftJsonSerializer : IMessageSerializer +{ + public Task DeserializeAsync(Stream stream, CancellationToken cancellationToken = default) + { + using var reader = new StreamReader(stream, Encoding.UTF8, false, 1024, true); + using var jsonReader = new JsonTextReader(reader); + + var value = JsonSerializer.Create().Deserialize(jsonReader); + + return Task.FromResult(value); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Serialization/SystemTextJsonSerializer.cs b/Source/Euonia.Bus/Serialization/SystemTextJsonSerializer.cs new file mode 100644 index 0000000..c75a4b7 --- /dev/null +++ b/Source/Euonia.Bus/Serialization/SystemTextJsonSerializer.cs @@ -0,0 +1,15 @@ +using System.Text.Json; + +namespace Nerosoft.Euonia.Bus; + +/// +/// Serializer for message using System.Text.Json. +/// +public class SystemTextJsonSerializer : IMessageSerializer +{ + /// + public async Task DeserializeAsync(Stream stream, CancellationToken cancellationToken = default) + { + return await JsonSerializer.DeserializeAsync(stream, cancellationToken: cancellationToken); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/ServiceCollectionExtensions.cs b/Source/Euonia.Bus/ServiceCollectionExtensions.cs index 0ab9f3a..4d87bb5 100644 --- a/Source/Euonia.Bus/ServiceCollectionExtensions.cs +++ b/Source/Euonia.Bus/ServiceCollectionExtensions.cs @@ -1,8 +1,5 @@ -using System.Reflection; -using MediatR; -using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.DependencyInjection.Extensions; using Nerosoft.Euonia.Bus; -using Nerosoft.Euonia.Domain; namespace Microsoft.Extensions.DependencyInjection; @@ -11,169 +8,124 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class ServiceCollectionExtensions { - private static readonly string[] _handlerTypes = - { - typeof(INotificationHandler<>).FullName, - typeof(IRequestHandler<,>).FullName - }; - - /// - /// Add message handler. - /// - /// - /// - public static void AddMessageHandler(this IServiceCollection services, Action callback = null) - { - services.AddSingleton(provider => - { - var @delegate = provider.GetService(); - var context = new MessageHandlerContext(provider, @delegate); - context.MessageSubscribed += (sender, args) => - { - callback?.Invoke(sender, args); - }; - return context; - }); - } - - /// - /// Add message handler. - /// - /// - /// - /// - public static void AddMessageHandler(this IServiceCollection services, Assembly assembly, Action callback = null) - { - services.AddMessageHandler(() => - { - var handlerTypes = assembly.GetTypes().Where(t => t.GetInterface(nameof(IMessageHandler)) != null && t.IsClass && !t.IsAbstract).ToList(); - return handlerTypes; - }, callback); - } - - /// - /// Add message handler. - /// - /// - /// - /// - public static void AddMessageHandler(this IServiceCollection services, Func> handlerTypesFactory, Action callback = null) - { - var handlerTypes = handlerTypesFactory?.Invoke(); - - services.AddMessageHandler(handlerTypes, callback); - } - - /// - /// Add message handler. - /// - /// - /// - /// - public static void AddMessageHandler(this IServiceCollection services, IEnumerable handlerTypes, Action callback = null) - { - services.AddMessageHandler(callback); - - if (handlerTypes == null) - { - return; - } - - if (!handlerTypes.Any()) - { - return; - } - - foreach (var handlerType in handlerTypes) - { - if (!handlerType.IsClass) - { - continue; - } - - if (handlerType.IsAbstract) - { - continue; - } - - if (handlerType.GetInterface(nameof(IMessageHandler)) == null) - { - continue; - } - - var inheritedTypes = handlerType.GetInterfaces().Where(t => t.IsGenericType); - - foreach (var inheritedType in inheritedTypes) - { - if (inheritedType.Name.Contains(nameof(IMessageHandler))) - { - continue; - } - - if (inheritedType.GenericTypeArguments.Length == 0) - { - continue; - } - - var messageType = inheritedType.GenericTypeArguments[0]; - if (messageType.IsSubclassOf(typeof(Command))) - { - var interfaceType = typeof(ICommandHandler<>).MakeGenericType(messageType); - services.TryAddScoped(interfaceType, handlerType); - services.TryAddScoped(handlerType); - } - else if (messageType.IsSubclassOf(typeof(Event))) - { - var interfaceType = typeof(IEventHandler<>).MakeGenericType(messageType); - - services.AddScoped(interfaceType, handlerType); - services.AddScoped(handlerType); - } - } - } - } - - /// - /// - /// - /// - /// - public static void AddMediatorHandler(this IServiceCollection services, params Assembly[] assemblies) - { - var types = assemblies.SelectMany(t => t.GetTypes()); - services.AddMediatorHandler(types.ToArray()); - } - - /// - /// - /// - /// - /// - public static void AddMediatorHandler(this IServiceCollection services, params Type[] types) - { - foreach (var type in types) - { - if (type.IsAbstract || !type.IsClass) - { - continue; - } - - var interfaces = type.FindInterfaces(HandlerInterfaceFilter, null); - if (interfaces.Length == 0) - { - continue; - } - - foreach (var @interface in interfaces) - { - services.AddTransient(@interface, type); - } - } - } - - private static bool HandlerInterfaceFilter(Type type, object criteria) - { - var typeName = $"{type.Namespace}.{type.Name}"; - return _handlerTypes.Contains(typeName); - } + /// + /// Add message handler. + /// + /// + /// + internal static void AddMessageHandler(this IServiceCollection services, Action callback = null) + { + services.AddSingleton(provider => + { + var @delegate = provider.GetService(); + var context = new HandlerContext(provider, @delegate); + context.MessageSubscribed += (sender, args) => + { + callback?.Invoke(sender, args); + }; + return context; + }); + } + + /// + /// Add message handler. + /// + /// + /// + /// + internal static void AddMessageHandler(this IServiceCollection services, Func> handlerTypesFactory, Action callback = null) + { + var handlerTypes = handlerTypesFactory?.Invoke(); + + services.AddMessageHandler(handlerTypes, callback); + } + + /// + /// Add message handler. + /// + /// + /// + /// + internal static void AddMessageHandler(this IServiceCollection services, IEnumerable handlerTypes, Action callback = null) + { + services.AddMessageHandler(callback); + + if (handlerTypes == null) + { + return; + } + + if (!handlerTypes.Any()) + { + return; + } + + foreach (var handlerType in handlerTypes) + { + if (!handlerType.IsClass) + { + continue; + } + + if (handlerType.IsAbstract) + { + continue; + } + + if (handlerType.GetInterface(nameof(IHandler)) == null) + { + continue; + } + + var inheritedTypes = handlerType.GetInterfaces().Where(t => t.IsGenericType); + + foreach (var inheritedType in inheritedTypes) + { + if (inheritedType.Name.Contains(nameof(IHandler))) + { + continue; + } + + if (inheritedType.GenericTypeArguments.Length == 0) + { + continue; + } + + services.TryAddScoped(inheritedType, handlerType); + services.TryAddScoped(handlerType); + } + } + } + + /// + /// Register message bus. + /// + /// + /// + public static void AddServiceBus(this IServiceCollection services, Action config) + { + var configurator = Singleton.Get(() => new BusConfigurator(services)); + + config?.Invoke(configurator); + + services.AddSingleton(provider => + { + var @delegate = provider.GetService(); + var context = new HandlerContext(provider, @delegate); + foreach (var subscription in configurator.Registrations) + { + if (subscription.Type != null) + { + context.Register(subscription.Type, subscription.HandlerType, subscription.HandleMethod); + } + else + { + context.Register(subscription.Name, subscription.HandlerType, subscription.HandleMethod); + } + } + + return context; + }); + services.AddSingleton(); + } } \ No newline at end of file diff --git a/Source/Euonia.Domain/Commands/Command.cs b/Source/Euonia.Domain/Commands/Command.cs index 0c73631..2caddc3 100644 --- a/Source/Euonia.Domain/Commands/Command.cs +++ b/Source/Euonia.Domain/Commands/Command.cs @@ -6,635 +6,633 @@ namespace Nerosoft.Euonia.Domain; /// /// The abstract implement of /// -/// [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] -public abstract class Command : Message, ICommand +public abstract class Command { - /// - /// Gets the extended properties of command. - /// - public IDictionary Properties { get; set; } = new Dictionary(); - - /// - /// Gets or sets the command property with specified name. - /// - /// - public object this[string name] - { - get => Properties.TryGetValue(name, out var value) ? value : default; - set => Properties[name] = value; - } - - /// - /// Gets value of property . - /// - /// - /// - /// - public virtual T GetProperty(string name) - { - return (T)this[name]; - } + /// + /// Gets the extended properties of command. + /// + public IDictionary Properties { get; set; } = new Dictionary(); + + /// + /// Gets or sets the command property with specified name. + /// + /// + public object this[string name] + { + get => Properties.TryGetValue(name, out var value) ? value : default; + set => Properties[name] = value; + } + + /// + /// Gets value of property . + /// + /// + /// + /// + public virtual T GetProperty(string name) + { + return (T)this[name]; + } } /// [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public abstract class Command : Command { - /// - protected Command() - { - } - - /// - protected Command(Tuple data) - { - Data = data; - } - - /// - protected Command(T1 item1) - : this(Tuple.Create(item1)) - { - } - - /// - /// Gets or sets the command data. - /// - public Tuple Data { get; set; } - - /// - /// Gets the item value at specified index. - /// - /// - /// - /// - public object this[int index] - { - get - { - return index switch - { - 0 => Data, - 1 => Data.Item1, - _ => throw new IndexOutOfRangeException() - }; - } - } - - /// - /// Gets the item value at specified index. - /// - /// - /// - /// - public virtual T GetItem(int index) - { - var value = this[index]; - if (value is T t) - { - return t; - } - - return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(value); - } - - /// - /// Gets the value of the first item. - /// - public T1 Item1 => Data.Item1; + /// + protected Command() + { + } + + /// + protected Command(Tuple data) + { + Data = data; + } + + /// + protected Command(T1 item1) + : this(Tuple.Create(item1)) + { + } + + /// + /// Gets or sets the command data. + /// + public Tuple Data { get; set; } + + /// + /// Gets the item value at specified index. + /// + /// + /// + /// + public object this[int index] + { + get + { + return index switch + { + 0 => Data, + 1 => Data.Item1, + _ => throw new IndexOutOfRangeException() + }; + } + } + + /// + /// Gets the item value at specified index. + /// + /// + /// + /// + public virtual T GetItem(int index) + { + var value = this[index]; + if (value is T t) + { + return t; + } + + return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(value); + } + + /// + /// Gets the value of the first item. + /// + public T1 Item1 => Data.Item1; } /// [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public abstract class Command : Command { - /// - protected Command() - { - } - - /// - protected Command(Tuple data) - { - Data = data; - } - - /// - protected Command(T1 item1, T2 item2) - : this(Tuple.Create(item1, item2)) - { - } - - /// - /// Gets or sets the command data. - /// - public Tuple Data { get; set; } - - /// - /// Gets the item value at specified index. - /// - /// - /// - /// - public object this[int index] - { - get - { - return index switch - { - 0 => Data, - 1 => Data.Item1, - 2 => Data.Item2, - _ => throw new IndexOutOfRangeException() - }; - } - } - - /// - /// Gets the item value at specified index. - /// - /// - /// - /// - public virtual T GetItem(int index) - { - var value = this[index]; - if (value is T t) - { - return t; - } - - return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(value); - } - - /// - /// Gets the value of the first item. - /// - public T1 Item1 => Data.Item1; - - /// - /// Gets the value of the second item. - /// - public T2 Item2 => Data.Item2; + /// + protected Command() + { + } + + /// + protected Command(Tuple data) + { + Data = data; + } + + /// + protected Command(T1 item1, T2 item2) + : this(Tuple.Create(item1, item2)) + { + } + + /// + /// Gets or sets the command data. + /// + public Tuple Data { get; set; } + + /// + /// Gets the item value at specified index. + /// + /// + /// + /// + public object this[int index] + { + get + { + return index switch + { + 0 => Data, + 1 => Data.Item1, + 2 => Data.Item2, + _ => throw new IndexOutOfRangeException() + }; + } + } + + /// + /// Gets the item value at specified index. + /// + /// + /// + /// + public virtual T GetItem(int index) + { + var value = this[index]; + if (value is T t) + { + return t; + } + + return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(value); + } + + /// + /// Gets the value of the first item. + /// + public T1 Item1 => Data.Item1; + + /// + /// Gets the value of the second item. + /// + public T2 Item2 => Data.Item2; } /// [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public abstract class Command : Command { - /// - protected Command() - { - } - - /// - protected Command(Tuple data) - { - Data = data; - } - - /// - protected Command(T1 item1, T2 item2, T3 item3) - : this(Tuple.Create(item1, item2, item3)) - { - } - - /// - /// Gets or sets the command data. - /// - public Tuple Data { get; set; } - - /// - /// Gets the item value at specified index. - /// - /// - /// - /// - public object this[int index] - { - get - { - return index switch - { - 0 => Data, - 1 => Data.Item1, - 2 => Data.Item2, - 3 => Data.Item3, - _ => throw new IndexOutOfRangeException() - }; - } - } - - /// - /// Gets the item value at specified index. - /// - /// - /// - /// - public T GetItem(int index) - { - var value = this[index]; - if (value is T t) - { - return t; - } - - return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(value); - } - - /// - /// Gets the value of the first item. - /// - public T1 Item1 => Data.Item1; - - /// - /// Gets the value of the second item. - /// - public T2 Item2 => Data.Item2; - - /// - /// Gets the value of the third item. - /// - public T3 Item3 => Data.Item3; + /// + protected Command() + { + } + + /// + protected Command(Tuple data) + { + Data = data; + } + + /// + protected Command(T1 item1, T2 item2, T3 item3) + : this(Tuple.Create(item1, item2, item3)) + { + } + + /// + /// Gets or sets the command data. + /// + public Tuple Data { get; set; } + + /// + /// Gets the item value at specified index. + /// + /// + /// + /// + public object this[int index] + { + get + { + return index switch + { + 0 => Data, + 1 => Data.Item1, + 2 => Data.Item2, + 3 => Data.Item3, + _ => throw new IndexOutOfRangeException() + }; + } + } + + /// + /// Gets the item value at specified index. + /// + /// + /// + /// + public T GetItem(int index) + { + var value = this[index]; + if (value is T t) + { + return t; + } + + return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(value); + } + + /// + /// Gets the value of the first item. + /// + public T1 Item1 => Data.Item1; + + /// + /// Gets the value of the second item. + /// + public T2 Item2 => Data.Item2; + + /// + /// Gets the value of the third item. + /// + public T3 Item3 => Data.Item3; } /// [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public abstract class Command : Command { - /// - protected Command() - { - } - - /// - protected Command(Tuple data) - { - Data = data; - } - - /// - protected Command(T1 item1, T2 item2, T3 item3, T4 item4) - : this(Tuple.Create(item1, item2, item3, item4)) - { - } - - /// - /// Gets or sets the command data. - /// - public Tuple Data { get; set; } - - /// - /// Gets the item value at specified index. - /// - /// - /// - /// - public object this[int index] - { - get - { - return index switch - { - 0 => Data, - 1 => Data.Item1, - 2 => Data.Item2, - 3 => Data.Item3, - 4 => Data.Item4, - _ => throw new IndexOutOfRangeException() - }; - } - } - - /// - /// Gets the item value at specified index. - /// - /// - /// - /// - public virtual T GetItem(int index) - { - var value = this[index]; - if (value is T t) - { - return t; - } - - return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(value); - } - - /// - /// Gets the value of the first item. - /// - public T1 Item1 => Data.Item1; - - /// - /// Gets the value of the first item. - /// - public T2 Item2 => Data.Item2; - - /// - /// Gets the value of the third item. - /// - public T3 Item3 => Data.Item3; - - /// - /// Gets the value of the fourth item. - /// - public T4 Item4 => Data.Item4; + /// + protected Command() + { + } + + /// + protected Command(Tuple data) + { + Data = data; + } + + /// + protected Command(T1 item1, T2 item2, T3 item3, T4 item4) + : this(Tuple.Create(item1, item2, item3, item4)) + { + } + + /// + /// Gets or sets the command data. + /// + public Tuple Data { get; set; } + + /// + /// Gets the item value at specified index. + /// + /// + /// + /// + public object this[int index] + { + get + { + return index switch + { + 0 => Data, + 1 => Data.Item1, + 2 => Data.Item2, + 3 => Data.Item3, + 4 => Data.Item4, + _ => throw new IndexOutOfRangeException() + }; + } + } + + /// + /// Gets the item value at specified index. + /// + /// + /// + /// + public virtual T GetItem(int index) + { + var value = this[index]; + if (value is T t) + { + return t; + } + + return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(value); + } + + /// + /// Gets the value of the first item. + /// + public T1 Item1 => Data.Item1; + + /// + /// Gets the value of the first item. + /// + public T2 Item2 => Data.Item2; + + /// + /// Gets the value of the third item. + /// + public T3 Item3 => Data.Item3; + + /// + /// Gets the value of the fourth item. + /// + public T4 Item4 => Data.Item4; } /// [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public abstract class Command : Command { - /// - protected Command() - { - } - - /// - protected Command(Tuple data) - { - Data = data; - } - - /// - protected Command(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5) - : this(Tuple.Create(item1, item2, item3, item4, item5)) - { - } - - /// - /// Gets or sets the command data. - /// - public Tuple Data { get; set; } - - /// - /// Gets the item value at specified index. - /// - /// - /// - /// - public object this[int index] - { - get - { - return index switch - { - 0 => Data, - 1 => Data.Item1, - 2 => Data.Item2, - 3 => Data.Item3, - 4 => Data.Item4, - 5 => Data.Item5, - _ => throw new IndexOutOfRangeException() - }; - } - } - - /// - /// Gets the item value at specified index. - /// - /// - /// - /// - public virtual T GetItem(int index) - { - var value = this[index]; - if (value is T t) - { - return t; - } - - return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(value); - } - - /// - /// Gets the value of the first item. - /// - public T1 Item1 => Data.Item1; - - /// - /// Gets the value of the first item. - /// - public T2 Item2 => Data.Item2; - - /// - /// Gets the value of the third item. - /// - public T3 Item3 => Data.Item3; - - /// - /// Gets the value of the fourth item. - /// - public T4 Item4 => Data.Item4; - - /// - /// Gets the value of the fifth item. - /// - public T5 Item5 => Data.Item5; + /// + protected Command() + { + } + + /// + protected Command(Tuple data) + { + Data = data; + } + + /// + protected Command(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5) + : this(Tuple.Create(item1, item2, item3, item4, item5)) + { + } + + /// + /// Gets or sets the command data. + /// + public Tuple Data { get; set; } + + /// + /// Gets the item value at specified index. + /// + /// + /// + /// + public object this[int index] + { + get + { + return index switch + { + 0 => Data, + 1 => Data.Item1, + 2 => Data.Item2, + 3 => Data.Item3, + 4 => Data.Item4, + 5 => Data.Item5, + _ => throw new IndexOutOfRangeException() + }; + } + } + + /// + /// Gets the item value at specified index. + /// + /// + /// + /// + public virtual T GetItem(int index) + { + var value = this[index]; + if (value is T t) + { + return t; + } + + return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(value); + } + + /// + /// Gets the value of the first item. + /// + public T1 Item1 => Data.Item1; + + /// + /// Gets the value of the first item. + /// + public T2 Item2 => Data.Item2; + + /// + /// Gets the value of the third item. + /// + public T3 Item3 => Data.Item3; + + /// + /// Gets the value of the fourth item. + /// + public T4 Item4 => Data.Item4; + + /// + /// Gets the value of the fifth item. + /// + public T5 Item5 => Data.Item5; } /// [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public abstract class Command : Command { - /// - protected Command() - { - } - - /// - protected Command(Tuple data) - { - Data = data; - } - - /// - protected Command(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5, T6 item6) - : this(Tuple.Create(item1, item2, item3, item4, item5, item6)) - { - } - - - /// - /// Gets or sets the command data. - /// - public Tuple Data { get; set; } - - /// - /// Gets the item value at specified index. - /// - /// - /// - public object this[int index] - { - get - { - return index switch - { - 0 => Data, - 1 => Data.Item1, - 2 => Data.Item2, - 3 => Data.Item3, - 4 => Data.Item4, - 5 => Data.Item5, - 6 => Data.Item6, - _ => throw new IndexOutOfRangeException() - }; - } - } - - /// - /// Gets the item value at specified index. - /// - /// - /// - /// - public virtual T GetItem(int index) - { - var value = this[index]; - if (value is T t) - { - return t; - } - - return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(value); - } - - /// - /// Gets the value of the first item. - /// - public T1 Item1 => Data.Item1; - - /// - /// Gets the value of the first item. - /// - public T2 Item2 => Data.Item2; - - /// - /// Gets the value of the third item. - /// - public T3 Item3 => Data.Item3; - - /// - /// Gets the value of the fourth item. - /// - public T4 Item4 => Data.Item4; - - /// - /// Gets the value of the fifth item. - /// - public T5 Item5 => Data.Item5; - - /// - /// Gets the value of the sixth item. - /// - public T6 Item6 => Data.Item6; + /// + protected Command() + { + } + + /// + protected Command(Tuple data) + { + Data = data; + } + + /// + protected Command(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5, T6 item6) + : this(Tuple.Create(item1, item2, item3, item4, item5, item6)) + { + } + + /// + /// Gets or sets the command data. + /// + public Tuple Data { get; set; } + + /// + /// Gets the item value at specified index. + /// + /// + /// + public object this[int index] + { + get + { + return index switch + { + 0 => Data, + 1 => Data.Item1, + 2 => Data.Item2, + 3 => Data.Item3, + 4 => Data.Item4, + 5 => Data.Item5, + 6 => Data.Item6, + _ => throw new IndexOutOfRangeException() + }; + } + } + + /// + /// Gets the item value at specified index. + /// + /// + /// + /// + public virtual T GetItem(int index) + { + var value = this[index]; + if (value is T t) + { + return t; + } + + return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(value); + } + + /// + /// Gets the value of the first item. + /// + public T1 Item1 => Data.Item1; + + /// + /// Gets the value of the first item. + /// + public T2 Item2 => Data.Item2; + + /// + /// Gets the value of the third item. + /// + public T3 Item3 => Data.Item3; + + /// + /// Gets the value of the fourth item. + /// + public T4 Item4 => Data.Item4; + + /// + /// Gets the value of the fifth item. + /// + public T5 Item5 => Data.Item5; + + /// + /// Gets the value of the sixth item. + /// + public T6 Item6 => Data.Item6; } /// [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public abstract class Command : Command { - /// - protected Command() - { - } - - /// - protected Command(Tuple data) - { - Data = data; - } - - /// - protected Command(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5, T6 item6, T7 item7) - : this(Tuple.Create(item1, item2, item3, item4, item5, item6, item7)) - { - } - - /// - /// Gets or sets the data. - /// - public Tuple Data { get; set; } - - /// - /// Gets the item value at specified index. - /// - /// - /// - /// - public object this[int index] - { - get - { - return index switch - { - 0 => Data, - 1 => Data.Item1, - 2 => Data.Item2, - 3 => Data.Item3, - 4 => Data.Item4, - 5 => Data.Item5, - 6 => Data.Item6, - 7 => Data.Item7, - _ => throw new IndexOutOfRangeException() - }; - } - } - - /// - /// Gets the item value at specified index. - /// - /// - /// - /// - public virtual T GetItem(int index) - { - var value = this[index]; - if (value is T t) - { - return t; - } - - return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(value); - } - - /// - /// Gets the value of the first item. - /// - public T1 Item1 => Data.Item1; - - /// - /// Gets the value of the first item. - /// - public T2 Item2 => Data.Item2; - - /// - /// Gets the value of the third item. - /// - public T3 Item3 => Data.Item3; - - /// - /// Gets the value of the fourth item. - /// - public T4 Item4 => Data.Item4; - - /// - /// Gets the value of the fifth item. - /// - public T5 Item5 => Data.Item5; - - /// - /// Gets the value of the sixth item. - /// - public T6 Item6 => Data.Item6; - - /// - /// Get the value of the seventh item. - /// - public T7 Item7 => Data.Item7; + /// + protected Command() + { + } + + /// + protected Command(Tuple data) + { + Data = data; + } + + /// + protected Command(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5, T6 item6, T7 item7) + : this(Tuple.Create(item1, item2, item3, item4, item5, item6, item7)) + { + } + + /// + /// Gets or sets the data. + /// + public Tuple Data { get; set; } + + /// + /// Gets the item value at specified index. + /// + /// + /// + /// + public object this[int index] + { + get + { + return index switch + { + 0 => Data, + 1 => Data.Item1, + 2 => Data.Item2, + 3 => Data.Item3, + 4 => Data.Item4, + 5 => Data.Item5, + 6 => Data.Item6, + 7 => Data.Item7, + _ => throw new IndexOutOfRangeException() + }; + } + } + + /// + /// Gets the item value at specified index. + /// + /// + /// + /// + public virtual T GetItem(int index) + { + var value = this[index]; + if (value is T t) + { + return t; + } + + return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(value); + } + + /// + /// Gets the value of the first item. + /// + public T1 Item1 => Data.Item1; + + /// + /// Gets the value of the first item. + /// + public T2 Item2 => Data.Item2; + + /// + /// Gets the value of the third item. + /// + public T3 Item3 => Data.Item3; + + /// + /// Gets the value of the fourth item. + /// + public T4 Item4 => Data.Item4; + + /// + /// Gets the value of the fifth item. + /// + public T5 Item5 => Data.Item5; + + /// + /// Gets the value of the sixth item. + /// + public T6 Item6 => Data.Item6; + + /// + /// Get the value of the seventh item. + /// + public T7 Item7 => Data.Item7; } \ No newline at end of file diff --git a/Source/Euonia.Domain/Commands/ICommand.cs b/Source/Euonia.Domain/Commands/ICommand.cs deleted file mode 100644 index 3b7f8c4..0000000 --- a/Source/Euonia.Domain/Commands/ICommand.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediatR; - -namespace Nerosoft.Euonia.Domain; - -/// -/// The contract interface of command. -/// -/// -public interface ICommand : IMessage -{ -} - -/// -/// The contract interface of command. -/// -/// -/// -public interface ICommand : IRequest, ICommand -{ -} \ No newline at end of file diff --git a/Source/Euonia.Domain/Commands/NamedCommand.cs b/Source/Euonia.Domain/Commands/NamedCommand.cs deleted file mode 100644 index c18b924..0000000 --- a/Source/Euonia.Domain/Commands/NamedCommand.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Nerosoft.Euonia.Domain; - -/// -/// Defines a command with a name. -/// -public class NamedCommand : Command, INamedMessage -{ - /// - /// Initialize a new instance of . - /// - /// - public NamedCommand(string name) - { - Name = name; - } - - /// - /// Initialize a new instance of . - /// - /// The event name. - /// The event data. - public NamedCommand(string name, object data) - : this(name) - { - Data = data; - } - - /// - /// Gets the event name. - /// - public string Name { get; } - - /// - /// Gets the event data. - /// - public object Data { get; } -} diff --git a/Source/Euonia.Domain/Euonia.Domain.csproj b/Source/Euonia.Domain/Euonia.Domain.csproj index e81ccc3..616b754 100644 --- a/Source/Euonia.Domain/Euonia.Domain.csproj +++ b/Source/Euonia.Domain/Euonia.Domain.csproj @@ -8,7 +8,6 @@ - diff --git a/Source/Euonia.Domain/Events/DomainEvent.cs b/Source/Euonia.Domain/Events/DomainEvent.cs index 2aac09b..16f99eb 100644 --- a/Source/Euonia.Domain/Events/DomainEvent.cs +++ b/Source/Euonia.Domain/Events/DomainEvent.cs @@ -9,56 +9,55 @@ /// public abstract class DomainEvent : Event, IDomainEvent { - /// - /// Gets or sets the sequence of the current event. - /// - public long Sequence { get; set; } = DateTime.UtcNow.Ticks; + /// + /// Gets or sets the sequence of the current event. + /// + public long Sequence { get; set; } = DateTime.UtcNow.Ticks; - /// - /// Attaches the current event to the specified event. - /// - /// - /// - public void Attach(IAggregateRoot aggregate) - where TKey : IEquatable - { - Metadata[Constants.EventIntentMetadataKey] = aggregate.GetType().AssemblyQualifiedName; - Metadata[Constants.EventOriginatorId] = aggregate.Id; - Metadata[Constants.EventOriginTypeKey] = aggregate.GetType().FullName; - AggregatePayload = aggregate; - } + /// + /// Attaches the current event to the specified event. + /// + /// + /// + public void Attach(IAggregateRoot aggregate) + where TKey : IEquatable + { + OriginatorId = aggregate.Id.ToString(); + OriginatorType = aggregate.GetType().AssemblyQualifiedName; + AggregatePayload = aggregate; + } - /// - /// - /// - /// - public override EventAggregate GetEventAggregate() - { - var aggregate = base.GetEventAggregate(); - aggregate.EventSequence = Sequence; - aggregate.EventPayload = this; - return aggregate; - } + /// + /// + /// + /// + public override EventAggregate GetEventAggregate() + { + var aggregate = base.GetEventAggregate(); + aggregate.EventSequence = Sequence; + aggregate.EventPayload = this; + return aggregate; + } - /// - /// Gets or sets the aggregate payload. - /// - /// The aggregate payload. - public virtual object AggregatePayload { get; set; } + /// + /// Gets or sets the aggregate payload. + /// + /// The aggregate payload. + public virtual object AggregatePayload { get; set; } - /// - /// Gets attached aggregate root object. - /// - /// - /// - public virtual TAggregate GetAggregate() - where TAggregate : IAggregateRoot - { - return AggregatePayload switch - { - null => default, - TAggregate aggregate => aggregate, - _ => default, - }; - } + /// + /// Gets attached aggregate root object. + /// + /// + /// + public virtual TAggregate GetAggregate() + where TAggregate : IAggregateRoot + { + return AggregatePayload switch + { + null => default, + TAggregate aggregate => aggregate, + _ => default, + }; + } } \ No newline at end of file diff --git a/Source/Euonia.Domain/Events/Event.cs b/Source/Euonia.Domain/Events/Event.cs index cd76a55..3cce9c7 100644 --- a/Source/Euonia.Domain/Events/Event.cs +++ b/Source/Euonia.Domain/Events/Event.cs @@ -3,51 +3,50 @@ /// /// The abstract class implements . /// -public abstract class Event : Message, IEvent +public abstract class Event : IEvent { - /// - /// Initializes a new instance of the class. - /// - protected Event() - { - var type = GetType(); - Metadata[Constants.EventIntentMetadataKey] = type.Name; - } + /// + /// Initializes a new instance of the class. + /// + protected Event() + { + var type = GetType(); + EventIntent = type.Name; + } - /// - /// Gets the intent of the event. - /// - /// The intent of the event. - public virtual string GetEventIntent() => Metadata[Constants.EventIntentMetadataKey]?.ToString(); + /// + /// Gets the intent of the event. + /// + /// The intent of the event. + public virtual string EventIntent { get; set; } - /// - /// Gets the .NET CLR type of the originator of the event. - /// - /// The .NET CLR type of the originator of the event. - public virtual string GetOriginatorType() => Metadata[Constants.EventOriginTypeKey]?.ToString(); + /// + /// Gets the .NET CLR type of the originator of the event. + /// + /// The .NET CLR type of the originator of the event. + public virtual string OriginatorType { get; set; } - /// - /// Gets the originator identifier. - /// - /// The originator identifier. - public virtual string GetOriginatorId() => Metadata[Constants.EventOriginatorId]?.ToString(); + /// + /// Gets the originator identifier. + /// + /// The originator identifier. + public virtual string OriginatorId { get; set; } - /// - /// Gets the event aggregate. - /// - /// EventAggregate. - public virtual EventAggregate GetEventAggregate() - { - return new EventAggregate - { - Id = Guid.NewGuid(), - TypeName = GetTypeName(), - EventId = Id, - EventIntent = GetEventIntent(), - Timestamp = DateTime.UtcNow, - OriginatorId = GetOriginatorId(), - OriginatorType = GetOriginatorType(), - EventPayload = this - }; - } + /// + /// Gets the event aggregate. + /// + /// EventAggregate. + public virtual EventAggregate GetEventAggregate() + { + return new EventAggregate + { + Id = Guid.NewGuid(), + TypeName = GetType().AssemblyQualifiedName, + EventIntent = EventIntent, + Timestamp = DateTime.UtcNow, + OriginatorId = OriginatorId, + OriginatorType = OriginatorType, + EventPayload = this + }; + } } \ No newline at end of file diff --git a/Source/Euonia.Domain/Events/IDomainEvent.cs b/Source/Euonia.Domain/Events/IDomainEvent.cs index 8d056da..0a8c1e0 100644 --- a/Source/Euonia.Domain/Events/IDomainEvent.cs +++ b/Source/Euonia.Domain/Events/IDomainEvent.cs @@ -1,23 +1,21 @@ -using MediatR; - -namespace Nerosoft.Euonia.Domain; +namespace Nerosoft.Euonia.Domain; /// /// Interface IDomainEvent /// Implements the /// /// -public interface IDomainEvent : IEvent, INotification +public interface IDomainEvent : IEvent { - /// - /// Gets or sets the sequence of the current event. - /// - long Sequence { get; set; } + /// + /// Gets or sets the sequence of the current event. + /// + long Sequence { get; set; } - /// - /// Attaches the current event to the specified event. - /// - /// - /// - void Attach(IAggregateRoot aggregate) where TKey : IEquatable; + /// + /// Attaches the current event to the specified event. + /// + /// + /// + void Attach(IAggregateRoot aggregate) where TKey : IEquatable; } \ No newline at end of file diff --git a/Source/Euonia.Domain/Events/IEvent.cs b/Source/Euonia.Domain/Events/IEvent.cs index 64ee7e7..65eccbd 100644 --- a/Source/Euonia.Domain/Events/IEvent.cs +++ b/Source/Euonia.Domain/Events/IEvent.cs @@ -3,35 +3,35 @@ /// /// The event interface. /// -public interface IEvent : IMessage +public interface IEvent { - /// - /// Gets the intent of the event. - /// - /// - /// The intent of the event. - /// - string GetEventIntent(); + /// + /// Gets the intent of the event. + /// + /// + /// The intent of the event. + /// + string EventIntent { get; set; } - /// - /// Gets the .NET CLR type of the originator of the event. - /// - /// - /// The .NET CLR type of the originator of the event. - /// - string GetOriginatorType(); + /// + /// Gets the .NET CLR type of the originator of the event. + /// + /// + /// The .NET CLR type of the originator of the event. + /// + string OriginatorType { get; set; } - /// - /// Gets the originator identifier. - /// - /// - /// The originator identifier. - /// - string GetOriginatorId(); + /// + /// Gets the originator identifier. + /// + /// + /// The originator identifier. + /// + string OriginatorId { get; set; } - /// - /// - /// - /// - EventAggregate GetEventAggregate(); + /// + /// + /// + /// + EventAggregate GetEventAggregate(); } \ No newline at end of file diff --git a/Source/Euonia.Domain/Events/NamedEvent.cs b/Source/Euonia.Domain/Events/NamedEvent.cs deleted file mode 100644 index f8f3b4a..0000000 --- a/Source/Euonia.Domain/Events/NamedEvent.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Nerosoft.Euonia.Domain; - -/// -/// Defines an event with a name. -/// -public class NamedEvent : Event, INamedMessage -{ - /// - /// Initialize a new instance of . - /// - /// - public NamedEvent(string name) - { - Name = name; - } - - /// - /// Initialize a new instance of . - /// - /// The event name. - /// The event data. - public NamedEvent(string name, object data) - : this(name) - { - Data = data; - } - - /// - /// Gets the event name. - /// - public string Name { get; } - - /// - /// Gets the event data. - /// - public object Data { get; } -} \ No newline at end of file diff --git a/Source/Euonia.Domain/Extensions/CommandExtensions.cs b/Source/Euonia.Domain/Extensions/CommandExtensions.cs index 68301a2..a4857bf 100644 --- a/Source/Euonia.Domain/Extensions/CommandExtensions.cs +++ b/Source/Euonia.Domain/Extensions/CommandExtensions.cs @@ -6,6 +6,7 @@ namespace Nerosoft.Euonia.Domain; /// /// The extensions for . /// +/* public static class CommandExtensions { private const string HEADER_USER_ID = "$nerosoft:user.id"; @@ -196,4 +197,5 @@ public static void SetTenant(this ICommand command, string tenant) { command.Metadata?.Set(HEADER_USER_TENANT, tenant); } -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/Source/Euonia.Domain/Messages/IMessage.cs b/Source/Euonia.Domain/Messages/IMessage.cs deleted file mode 100644 index 8c68d87..0000000 --- a/Source/Euonia.Domain/Messages/IMessage.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Nerosoft.Euonia.Domain; - -/// -/// The base contract of message. -/// -public interface IMessage -{ - /// - /// Gets or sets the message identifier. - /// - Guid Id { get; } - - /// - /// Gets or sets the timestamp. - /// - DateTime Timestamp { get; } - - /// - /// Gets the message meta data. - /// - MessageMetadata Metadata { get; } - - /// - /// Gets the assembly qualified name of the current message. - /// - /// The type of current message. - string GetTypeName(); -} \ No newline at end of file diff --git a/Source/Euonia.Domain/Messages/INamedMessage.cs b/Source/Euonia.Domain/Messages/INamedMessage.cs deleted file mode 100644 index e5ab5a9..0000000 --- a/Source/Euonia.Domain/Messages/INamedMessage.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Nerosoft.Euonia.Domain; - -/// -/// Represents a named message. -/// -public interface INamedMessage : IMessage -{ - /// - /// Gets the message name. - /// - string Name { get; } - - /// - /// Gets the message data. - /// - object Data { get; } -} \ No newline at end of file diff --git a/Source/Euonia.Domain/Messages/Message.cs b/Source/Euonia.Domain/Messages/Message.cs deleted file mode 100644 index 6251d8f..0000000 --- a/Source/Euonia.Domain/Messages/Message.cs +++ /dev/null @@ -1,102 +0,0 @@ -namespace Nerosoft.Euonia.Domain; - -/// -/// A abstract implement of . -/// -public abstract class Message : IMessage -{ - /// - /// The message type key - /// - public const string MessageTypeKey = "$nerosoft.euonia:message.type"; - - /// - /// Initializes a new instance of . - /// - protected Message() - { - Id = Guid.NewGuid(); - Timestamp = DateTime.UtcNow; - Metadata[MessageTypeKey] = GetType().AssemblyQualifiedName; - } - - /// - /// Gets or sets the identifier of the message. - /// - /// - /// The identifier of the message. - /// - public Guid Id { get; } - - /// - /// Gets or sets the timestamp that describes when the message occurs. - /// - /// - /// The timestamp that describes when the message occurs. - /// - public DateTime Timestamp { get; } - - /// - /// Gets a instance that contains the metadata information of the message. - /// - public MessageMetadata Metadata { get; } = new(); - - /// - /// Gets the .NET CLR assembly qualified name of the message. - /// - /// - /// The assembly qualified name of the message. - /// - public string GetTypeName() => Metadata[MessageTypeKey] as string; - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() => Id.ToString(); - - /// - /// Determines whether the specified is equals to this instance. - /// - /// - /// The to compare with this instance. - /// - /// - /// true if the specified is equals to this instance; otherwise false. - /// - public override bool Equals(object obj) - { - if (obj == null) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - return obj is Message message && message.Id == Id; - } - - /// - /// Get hash code of this instance. - /// - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public override int GetHashCode() - { - return GetHashCode(Id.GetHashCode(), Timestamp.GetHashCode()); - } - - private static int GetHashCode(params int[] hashCodesForProperties) - { - unchecked - { - return hashCodesForProperties.Aggregate(23, (current, code) => current * 29 + code); - } - } -} \ No newline at end of file From 2b62e5a1fc5916c0f9242c4760c545ee6772336e Mon Sep 17 00:00:00 2001 From: damon Date: Sat, 25 Nov 2023 16:50:54 +0800 Subject: [PATCH 08/37] Move IMessageSerializer.cs to abstract. --- .../Euonia.Bus.Abstract/IMessageSerializer.cs | 50 +++++++++++++++++ .../Serialization/IMessageSerializer.cs | 16 ------ .../Serialization/NewtonsoftJsonSerializer.cs | 53 +++++++++++++++++++ .../Serialization/SystemTextJsonSerializer.cs | 40 ++++++++++++++ 4 files changed, 143 insertions(+), 16 deletions(-) create mode 100644 Source/Euonia.Bus.Abstract/IMessageSerializer.cs delete mode 100644 Source/Euonia.Bus/Serialization/IMessageSerializer.cs diff --git a/Source/Euonia.Bus.Abstract/IMessageSerializer.cs b/Source/Euonia.Bus.Abstract/IMessageSerializer.cs new file mode 100644 index 0000000..47c8cb2 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/IMessageSerializer.cs @@ -0,0 +1,50 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +public interface IMessageSerializer +{ + /// + /// + /// + /// + /// + /// + /// + Task SerializeAsync(T message, CancellationToken cancellationToken = default); + + /// + /// Asynchronously reads a JSON value from the provided and converts it to an instance of a type specified by a generic parameter. + /// + /// The JSON data to parse. + /// + /// + /// + Task DeserializeAsync(Stream stream, CancellationToken cancellationToken = default); + + /// + /// Asynchronously reads a JSON value from the provided array and converts it to an instance of a type specified by a generic parameter. + /// + /// + /// + /// + /// + Task DeserializeAsync(byte[] bytes, CancellationToken cancellationToken = default); + + /// + /// Deserializes the specified bytes. + /// + /// + /// + /// + T Deserialize(byte[] bytes); + + T Deserialize(string json); + + T Deserialize(Stream stream); + + string Serialize(T obj); + + byte[] SerializeToBytes(T obj); +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Serialization/IMessageSerializer.cs b/Source/Euonia.Bus/Serialization/IMessageSerializer.cs deleted file mode 100644 index 4d0d934..0000000 --- a/Source/Euonia.Bus/Serialization/IMessageSerializer.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Nerosoft.Euonia.Bus; - -/// -/// -/// -public interface IMessageSerializer -{ - /// - /// Asynchronously reads a JSON value from the provided and converts it to an instance of a type specified by a generic parameter. - /// - /// The JSON data to parse. - /// - /// - /// - Task DeserializeAsync(Stream stream, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Serialization/NewtonsoftJsonSerializer.cs b/Source/Euonia.Bus/Serialization/NewtonsoftJsonSerializer.cs index 839b543..83096db 100644 --- a/Source/Euonia.Bus/Serialization/NewtonsoftJsonSerializer.cs +++ b/Source/Euonia.Bus/Serialization/NewtonsoftJsonSerializer.cs @@ -7,6 +7,22 @@ namespace Nerosoft.Euonia.Bus; /// public class NewtonsoftJsonSerializer : IMessageSerializer { + /// + public async Task SerializeAsync(T message, CancellationToken cancellationToken = default) + { + await using var stream = new MemoryStream(); + await using var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true); + using var jsonWriter = new JsonTextWriter(writer); + + JsonSerializer.Create().Serialize(jsonWriter, message); + + await jsonWriter.FlushAsync(cancellationToken); + await writer.FlushAsync(); + + return stream.ToArray(); + } + + /// public Task DeserializeAsync(Stream stream, CancellationToken cancellationToken = default) { using var reader = new StreamReader(stream, Encoding.UTF8, false, 1024, true); @@ -16,4 +32,41 @@ public Task DeserializeAsync(Stream stream, CancellationToken cancellation return Task.FromResult(value); } + + /// + public async Task DeserializeAsync(byte[] bytes, CancellationToken cancellationToken = default) + { + await using var stream = new MemoryStream(bytes); + return await DeserializeAsync(stream, cancellationToken); + } + + public T Deserialize(byte[] bytes) + { + return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(bytes)); + } + + public T Deserialize(string json) + { + return JsonConvert.DeserializeObject(json); + } + + public T Deserialize(Stream stream) + { + using var reader = new StreamReader(stream, Encoding.UTF8, false, 1024, true); + using var jsonReader = new JsonTextReader(reader); + + var value = JsonSerializer.Create().Deserialize(jsonReader); + + return value; + } + + public string Serialize(T obj) + { + return JsonConvert.SerializeObject(obj); + } + + public byte[] SerializeToBytes(T obj) + { + return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(obj)); + } } \ No newline at end of file diff --git a/Source/Euonia.Bus/Serialization/SystemTextJsonSerializer.cs b/Source/Euonia.Bus/Serialization/SystemTextJsonSerializer.cs index c75a4b7..a039c4d 100644 --- a/Source/Euonia.Bus/Serialization/SystemTextJsonSerializer.cs +++ b/Source/Euonia.Bus/Serialization/SystemTextJsonSerializer.cs @@ -7,9 +7,49 @@ namespace Nerosoft.Euonia.Bus; /// public class SystemTextJsonSerializer : IMessageSerializer { + /// + public async Task SerializeAsync(T message, CancellationToken cancellationToken = default) + { + await using var stream = new MemoryStream(); + await JsonSerializer.SerializeAsync(stream, message, cancellationToken: cancellationToken); + return stream.ToArray(); + } + /// public async Task DeserializeAsync(Stream stream, CancellationToken cancellationToken = default) { return await JsonSerializer.DeserializeAsync(stream, cancellationToken: cancellationToken); } + + /// + public async Task DeserializeAsync(byte[] bytes, CancellationToken cancellationToken = default) + { + await using var stream = new MemoryStream(bytes); + return await DeserializeAsync(stream, cancellationToken); + } + + public T Deserialize(byte[] bytes) + { + return JsonSerializer.Deserialize(Encoding.UTF8.GetString(bytes)); + } + + public T Deserialize(string json) + { + return JsonSerializer.Deserialize(json); + } + + public T Deserialize(Stream stream) + { + return JsonSerializer.Deserialize(stream); + } + + public string Serialize(T obj) + { + return JsonSerializer.Serialize(obj); + } + + public byte[] SerializeToBytes(T obj) + { + return Encoding.UTF8.GetBytes(Serialize(obj)); + } } \ No newline at end of file From e27b9167ab35712d4d26675fe246d41502d6e86f Mon Sep 17 00:00:00 2001 From: damon Date: Sat, 25 Nov 2023 16:52:06 +0800 Subject: [PATCH 09/37] [WIP] Refactoring --- .../Attributes/ChannelAttribute.cs | 30 +---- .../Contracts/IDispatcher.cs | 14 +-- .../Contracts/ISubscriber.cs | 12 -- .../IHandlerContext.cs | 0 Source/Euonia.Bus.Abstract/RoutedMessage.cs | 114 +++++++++++------- Source/Euonia.Bus/Core/ExtendableOptions.cs | 34 ++++++ Source/Euonia.Bus/Core/IBus.cs | 40 +++++- Source/Euonia.Bus/Core/PublishOptions.cs | 8 ++ Source/Euonia.Bus/Core/SendOptions.cs | 8 ++ Source/Euonia.Bus/Core/ServiceBus.cs | 42 ++----- .../Messages/MessageChannelCache.cs | 28 +++++ .../Messages/MessageHandlerCache.cs | 5 + 12 files changed, 209 insertions(+), 126 deletions(-) rename Source/{Euonia.Bus/Core => Euonia.Bus.Abstract}/IHandlerContext.cs (100%) create mode 100644 Source/Euonia.Bus/Core/ExtendableOptions.cs create mode 100644 Source/Euonia.Bus/Core/PublishOptions.cs create mode 100644 Source/Euonia.Bus/Core/SendOptions.cs create mode 100644 Source/Euonia.Bus/Messages/MessageChannelCache.cs create mode 100644 Source/Euonia.Bus/Messages/MessageHandlerCache.cs diff --git a/Source/Euonia.Bus.Abstract/Attributes/ChannelAttribute.cs b/Source/Euonia.Bus.Abstract/Attributes/ChannelAttribute.cs index 75399df..81b7dcb 100644 --- a/Source/Euonia.Bus.Abstract/Attributes/ChannelAttribute.cs +++ b/Source/Euonia.Bus.Abstract/Attributes/ChannelAttribute.cs @@ -1,6 +1,4 @@ -using System.Reflection; - -namespace Nerosoft.Euonia.Bus; +namespace Nerosoft.Euonia.Bus; /// /// Represents the attributed event has a specified name. @@ -27,30 +25,4 @@ public ChannelAttribute(string name) Name = name; } - - /// - /// - /// - /// - /// - public static string GetName() - { - return GetName(typeof(TMessage)); - } - - /// - /// - /// - /// - /// - /// - public static string GetName(Type messageType) - { - if (messageType == null) - { - throw new ArgumentNullException(nameof(messageType)); - } - - return messageType.GetCustomAttribute()?.Name ?? messageType.Name; - } } \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Contracts/IDispatcher.cs b/Source/Euonia.Bus.Abstract/Contracts/IDispatcher.cs index b22dd62..db25365 100644 --- a/Source/Euonia.Bus.Abstract/Contracts/IDispatcher.cs +++ b/Source/Euonia.Bus.Abstract/Contracts/IDispatcher.cs @@ -13,31 +13,31 @@ public interface IDispatcher /// /// Publishes the specified message. /// - /// + /// /// /// /// - Task PublishAsync(RoutedMessage pack, CancellationToken cancellationToken = default) + Task PublishAsync(RoutedMessage message, CancellationToken cancellationToken = default) where TMessage : class; /// /// Sends the specified message. /// - /// + /// /// /// /// - Task SendAsync(RoutedMessage pack, CancellationToken cancellationToken = default) + Task SendAsync(RoutedMessage message, CancellationToken cancellationToken = default) where TMessage : class; /// /// Sends the specified message. /// - /// + /// /// /// - /// + /// /// - Task SendAsync(RoutedMessage pack, CancellationToken cancellationToken = default) + Task SendAsync(RoutedMessage message, CancellationToken cancellationToken = default) where TMessage : class; } \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Contracts/ISubscriber.cs b/Source/Euonia.Bus.Abstract/Contracts/ISubscriber.cs index d974109..cf333f9 100644 --- a/Source/Euonia.Bus.Abstract/Contracts/ISubscriber.cs +++ b/Source/Euonia.Bus.Abstract/Contracts/ISubscriber.cs @@ -17,20 +17,8 @@ public interface ISubscriber : IDisposable /// event EventHandler MessageAcknowledged; - /// - /// Occurs when [message subscribed]. - /// - event EventHandler MessageSubscribed; - /// /// Gets the subscriber name. /// string Name { get; } - - /// - /// Subscribes the specified message type. - /// - /// - /// - void Subscribe(Type messageType, Type handlerType); } \ No newline at end of file diff --git a/Source/Euonia.Bus/Core/IHandlerContext.cs b/Source/Euonia.Bus.Abstract/IHandlerContext.cs similarity index 100% rename from Source/Euonia.Bus/Core/IHandlerContext.cs rename to Source/Euonia.Bus.Abstract/IHandlerContext.cs diff --git a/Source/Euonia.Bus.Abstract/RoutedMessage.cs b/Source/Euonia.Bus.Abstract/RoutedMessage.cs index ff71af3..4adea4a 100644 --- a/Source/Euonia.Bus.Abstract/RoutedMessage.cs +++ b/Source/Euonia.Bus.Abstract/RoutedMessage.cs @@ -3,54 +3,52 @@ namespace Nerosoft.Euonia.Bus; /// -/// +/// The abstract routed message. /// [Serializable] -public class RoutedMessage : IRoutedMessage - where TData : class +public abstract class RoutedMessage { /// /// Initializes a new instance of the class. /// - public RoutedMessage() + protected RoutedMessage() { } /// - /// Initializes a new instance of the class. + /// The message type key /// - /// The data. - /// - public RoutedMessage(TData data, string channel) - { - Data = data; - Channel = channel; - } + protected const string MessageTypeKey = "$nerosoft.euonia:message.type"; /// - /// The message type key + /// Gets or sets the message identifier. /// - private const string MESSAGE_TYPE_KEY = "$nerosoft.euonia:message.type"; - - /// [DataMember] - public string MessageId { get; set; } = Guid.NewGuid().ToString(); + public virtual string MessageId { get; set; } = Guid.NewGuid().ToString(); - /// + /// + /// Gets or sets the correlation identifier. + /// [DataMember] - public string CorrelationId { get; } + public virtual string CorrelationId { get; } - /// + /// + /// Gets or sets the conversation identifier. + /// [DataMember] - public string ConversationId { get; } + public virtual string ConversationId { get; } - /// + /// + /// Gets or sets the request trace identifier. + /// [DataMember] - public string RequestTraceId { get; } + public virtual string RequestTraceId { get; } - /// + /// + /// Gets or sets the channel that the message send to. + /// [DataMember] - public string Channel { get; set; } + public virtual string Channel { get; set; } /// /// Gets or sets the timestamp that describes when the message occurs. @@ -59,13 +57,48 @@ public RoutedMessage(TData data, string channel) /// The timestamp that describes when the message occurs. /// [DataMember] - public long Timestamp { get; set; } + public virtual long Timestamp { get; set; } = DateTimeOffset.Now.ToUnixTimeMilliseconds(); /// /// Gets a instance that contains the metadata information of the message. /// [DataMember] - public MessageMetadata Metadata { get; set; } = new(); + public virtual MessageMetadata Metadata { get; set; } = new(); + + /// + /// Gets the .NET CLR assembly qualified name of the message. + /// + /// + /// The assembly qualified name of the message. + /// + public virtual string GetTypeName() => Metadata[MessageTypeKey] as string; + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() => $"{MessageId}:{{GetTypeName()}}"; +} + +/// +/// +/// +[Serializable] +public class RoutedMessage : RoutedMessage, IRoutedMessage + where TData : class +{ + /// + /// Initializes a new instance of the class. + /// + /// The data. + /// + public RoutedMessage(TData data, string channel) + { + Data = data; + Channel = channel; + } object IRoutedMessage.Data => Data; @@ -84,25 +117,18 @@ public TData Data _data = value; if (value != null) { - Metadata[MESSAGE_TYPE_KEY] = value.GetType().AssemblyQualifiedName; + Metadata[MessageTypeKey] = value.GetType().AssemblyQualifiedName; } } } - - /// - /// Gets the .NET CLR assembly qualified name of the message. - /// - /// - /// The assembly qualified name of the message. - /// - public string GetTypeName() => Metadata[MESSAGE_TYPE_KEY] as string; - - /// - /// Returns a that represents this instance. - /// - /// - /// A that represents this instance. - /// - public override string ToString() => $"{MessageId}:{{GetTypeName()}}"; } +[Serializable] +public class RoutedMessage : RoutedMessage + where TData : class +{ + public RoutedMessage(TData data, string channel) + : base(data, channel) + { + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Core/ExtendableOptions.cs b/Source/Euonia.Bus/Core/ExtendableOptions.cs new file mode 100644 index 0000000..619cf73 --- /dev/null +++ b/Source/Euonia.Bus/Core/ExtendableOptions.cs @@ -0,0 +1,34 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +public abstract class ExtendableOptions +{ + /// + /// Gets or sets the user defined message id. + /// + /// + /// The origin message id will be replaced by the user defined message id. + /// + public virtual string MessageId { get; set; } + + /// + /// Gets or sets the special channel. + /// + public virtual string Channel { get; set; } + + /// + /// Gets or sets the queue name. + /// + /// + /// The queue name is used to identify the queue to which the message will be sent. + /// The message will be enqueued to the queue if the queue name set. + /// + public virtual string Queue { get; set; } + + /// + /// Gets or sets the queue priority. + /// + public virtual int Priority { get; set; } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Core/IBus.cs b/Source/Euonia.Bus/Core/IBus.cs index 3d3f6b5..fde6311 100644 --- a/Source/Euonia.Bus/Core/IBus.cs +++ b/Source/Euonia.Bus/Core/IBus.cs @@ -13,19 +13,30 @@ public interface IBus /// /// Task PublishAsync(TMessage message, CancellationToken cancellationToken = default) - where TMessage : class; + where TMessage : class => PublishAsync(message, new PublishOptions(), cancellationToken); /// /// Publishes the specified message. /// - /// /// + /// /// /// /// - Task PublishAsync(string name, TMessage message, CancellationToken cancellationToken = default) + Task PublishAsync(TMessage message, PublishOptions options, CancellationToken cancellationToken = default) where TMessage : class; + /// + /// Publishes the specified message. + /// + /// + /// + /// + /// + /// + Task PublishAsync(string channel, TMessage message, CancellationToken cancellationToken = default) + where TMessage : class => PublishAsync(message, new PublishOptions { Channel = channel }, cancellationToken); + /// /// Sends the specified message. /// @@ -34,6 +45,17 @@ Task PublishAsync(string name, TMessage message, CancellationToken can /// /// Task SendAsync(TMessage message, CancellationToken cancellationToken = default) + where TMessage : class => SendAsync(message, new SendOptions(), cancellationToken); + + /// + /// Sends the specified message. + /// + /// + /// + /// + /// + /// + Task SendAsync(TMessage message, SendOptions options, CancellationToken cancellationToken = default) where TMessage : class; /// @@ -45,5 +67,17 @@ Task SendAsync(TMessage message, CancellationToken cancellationToken = /// /// Task SendAsync(TMessage message, CancellationToken cancellationToken = default) + where TMessage : class => SendAsync(message, new SendOptions(), cancellationToken); + + /// + /// Sends the specified message. + /// + /// + /// + /// + /// + /// + /// + Task SendAsync(TMessage message, SendOptions options, CancellationToken cancellationToken = default) where TMessage : class; } \ No newline at end of file diff --git a/Source/Euonia.Bus/Core/PublishOptions.cs b/Source/Euonia.Bus/Core/PublishOptions.cs new file mode 100644 index 0000000..e6c491e --- /dev/null +++ b/Source/Euonia.Bus/Core/PublishOptions.cs @@ -0,0 +1,8 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// The publish options. +/// +public class PublishOptions : ExtendableOptions +{ +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Core/SendOptions.cs b/Source/Euonia.Bus/Core/SendOptions.cs new file mode 100644 index 0000000..8e2f8d6 --- /dev/null +++ b/Source/Euonia.Bus/Core/SendOptions.cs @@ -0,0 +1,8 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +public class SendOptions : ExtendableOptions +{ +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Core/ServiceBus.cs b/Source/Euonia.Bus/Core/ServiceBus.cs index 9f7f728..f6250e0 100644 --- a/Source/Euonia.Bus/Core/ServiceBus.cs +++ b/Source/Euonia.Bus/Core/ServiceBus.cs @@ -1,6 +1,4 @@ -using System.Reflection; - -namespace Nerosoft.Euonia.Bus; +namespace Nerosoft.Euonia.Bus; /// /// @@ -22,7 +20,7 @@ public ServiceBus(IBusFactory factory, MessageConvention convention) } /// - public async Task PublishAsync(TMessage message, CancellationToken cancellationToken = default) + public async Task PublishAsync(TMessage message, PublishOptions options, CancellationToken cancellationToken = default) where TMessage : class { if (!_convention.IsEventType(message.GetType())) @@ -30,25 +28,13 @@ public async Task PublishAsync(TMessage message, CancellationToken can throw new InvalidOperationException("The message type is not an event type."); } - var pack = new RoutedMessage(message, typeof(void).FullName); + var channelName = options?.Channel ?? MessageChannelCache.Default.GetOrAdd(); + var pack = new RoutedMessage(message, channelName); await _dispatcher.PublishAsync(pack, cancellationToken); } /// - public async Task PublishAsync(string name, TMessage message, CancellationToken cancellationToken = default) - where TMessage : class - { - if (!_convention.IsEventType(message.GetType())) - { - throw new InvalidOperationException("The message type is not an event type."); - } - - var pack = new RoutedMessage(message, name); - await _dispatcher.PublishAsync(pack, cancellationToken); - } - - /// - public async Task SendAsync(TMessage message, CancellationToken cancellationToken = default) + public async Task SendAsync(TMessage message, SendOptions options, CancellationToken cancellationToken = default) where TMessage : class { if (!_convention.IsCommandType(message.GetType())) @@ -56,12 +42,12 @@ public async Task SendAsync(TMessage message, CancellationToken cancel throw new InvalidOperationException("The message type is not a command type."); } - var pack = new RoutedMessage(message, typeof(void).FullName); - await _dispatcher.SendAsync(pack, cancellationToken); + var channelName = options?.Channel ?? MessageChannelCache.Default.GetOrAdd(); + await _dispatcher.SendAsync(new RoutedMessage(message, channelName), cancellationToken); } /// - public async Task SendAsync(TMessage message, CancellationToken cancellationToken = default) + public async Task SendAsync(TMessage message, SendOptions options, CancellationToken cancellationToken = default) where TMessage : class { if (!_convention.IsCommandType(message.GetType())) @@ -69,14 +55,8 @@ public async Task SendAsync(TMessage message, Cancel throw new InvalidOperationException("The message type is not a command type."); } - var pack = new RoutedMessage(message, GetChannelName()); - await _dispatcher.SendAsync(pack, cancellationToken); - return default; - } - - private static string GetChannelName() - { - var attribute = typeof(TMessage).GetCustomAttribute(); - return attribute?.Name ?? typeof(TMessage).Name; + var channelName = options?.Channel ?? MessageChannelCache.Default.GetOrAdd(); + var pack = new RoutedMessage(message, channelName); + return await _dispatcher.SendAsync(pack, cancellationToken); } } \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageChannelCache.cs b/Source/Euonia.Bus/Messages/MessageChannelCache.cs new file mode 100644 index 0000000..a23c27d --- /dev/null +++ b/Source/Euonia.Bus/Messages/MessageChannelCache.cs @@ -0,0 +1,28 @@ +using System.Collections.Concurrent; +using System.Reflection; + +namespace Nerosoft.Euonia.Bus; + +internal class MessageChannelCache +{ + private static readonly Lazy _instance = new(() => new MessageChannelCache()); + + private readonly ConcurrentDictionary _channels = new(); + + public static MessageChannelCache Default => _instance.Value; + + public string GetOrAdd() + where TMessage : class + { + return GetOrAdd(typeof(TMessage)); + } + + public string GetOrAdd(Type messageType) + { + return _channels.GetOrAdd(messageType, _ => + { + var channelAttribute = messageType.GetCustomAttribute(); + return channelAttribute != null ? channelAttribute.Name : messageType.FullName; + }); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageHandlerCache.cs b/Source/Euonia.Bus/Messages/MessageHandlerCache.cs new file mode 100644 index 0000000..f060d7e --- /dev/null +++ b/Source/Euonia.Bus/Messages/MessageHandlerCache.cs @@ -0,0 +1,5 @@ +namespace Nerosoft.Euonia.Bus; + +internal class MessageHandlerCache +{ +} \ No newline at end of file From d8802c58b6a0ec2d37317b6291017577cdb3de84 Mon Sep 17 00:00:00 2001 From: damon Date: Sat, 25 Nov 2023 16:53:55 +0800 Subject: [PATCH 10/37] Remove unused files. --- .../BusConfiguratorExtensions.cs | 60 +++++++ Source/Euonia.Bus.InMemory/CommandBus.cs | 165 ------------------ .../Euonia.Bus.InMemory.csproj | 1 + Source/Euonia.Bus.InMemory/EventBus.cs | 83 --------- .../Euonia.Bus.InMemory/InMemoryBusOptions.cs | 16 +- .../Euonia.Bus.InMemory/InMemoryDispatcher.cs | 27 ++- .../Euonia.Bus.InMemory/InMemorySubscriber.cs | 25 +-- Source/Euonia.Bus.InMemory/MessageBus.cs | 120 ------------- .../Messages/MessagePack.cs | 4 +- .../Messenger/IRecipient.cs | 4 +- .../MessengerExtensions.Observables.cs | 8 +- .../ServiceCollectionExtensions.cs | 105 ----------- 12 files changed, 111 insertions(+), 507 deletions(-) create mode 100644 Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs delete mode 100644 Source/Euonia.Bus.InMemory/CommandBus.cs delete mode 100644 Source/Euonia.Bus.InMemory/EventBus.cs delete mode 100644 Source/Euonia.Bus.InMemory/MessageBus.cs delete mode 100644 Source/Euonia.Bus.InMemory/ServiceCollectionExtensions.cs diff --git a/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs b/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs new file mode 100644 index 0000000..1a060e0 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs @@ -0,0 +1,60 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Nerosoft.Euonia.Bus.InMemory; + +namespace Nerosoft.Euonia.Bus; + +/// +/// Message bus extensions for . +/// +public static class BusConfiguratorExtensions +{ + /// + /// Adds the in-memory message bus to the service collection. + /// + /// + /// + /// + /// + public static void UseInMemory(this IBusConfigurator configurator, Action configuration) + { + configurator.Service.Configure(configuration); + configurator.Service.TryAddSingleton(provider => + { + var options = provider.GetService>()?.Value; + if (options == null) + { + throw new InvalidOperationException("The in-memory message dispatcher options is not configured."); + } + + IMessenger messenger = options.MessengerReference switch + { + MessengerReferenceType.StrongReference => StrongReferenceMessenger.Default, + MessengerReferenceType.WeakReference => WeakReferenceMessenger.Default, + _ => throw new ArgumentOutOfRangeException(nameof(options.MessengerReference), options.MessengerReference, null) + }; + + if (options.MultipleSubscriberInstance) + { + foreach (var subscription in configurator.GetSubscriptions()) + { + var subscriber = ActivatorUtilities.GetServiceOrCreateInstance(provider); + messenger.Register(subscriber, subscription); + } + } + else + { + var subscriber = ActivatorUtilities.GetServiceOrCreateInstance(provider); + foreach (var subscription in configurator.GetSubscriptions()) + { + messenger.Register(subscriber, subscription); + } + } + + return messenger; + }); + configurator.Service.TryAddSingleton(); + configurator.SerFactory(); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/CommandBus.cs b/Source/Euonia.Bus.InMemory/CommandBus.cs deleted file mode 100644 index 9576381..0000000 --- a/Source/Euonia.Bus.InMemory/CommandBus.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System.Reflection; -using MediatR; -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus.InMemory; - -/// -/// Class CommandBus. -/// Implements the -/// Implements the -/// -/// -/// -public class CommandBus : MessageBus, ICommandBus -{ - /// - /// Initializes a new instance of the class. - /// - /// The message handler context. - /// - public CommandBus(IHandlerContext handlerContext, IServiceAccessor accessor) - : base(handlerContext, accessor) - { - MessageReceived += HandleMessageReceivedEvent; - - HandlerContext.MessageSubscribed += HandleMessageSubscribedEvent; - } - - /// - /// - /// - private IMediator Mediator => ServiceAccessor.GetService(); - - private void HandleMessageSubscribedEvent(object sender, MessageSubscribedEventArgs args) - { - OnMessageSubscribed(args); - } - - /// - /// Sends the asynchronous. - /// - /// The type of the t command. - /// The command. - /// The cancellation token. - /// Task. - public async Task SendAsync(TCommand command, CancellationToken cancellationToken = default) - where TCommand : ICommand - { - if (typeof(TCommand).GetCustomAttribute() != null) - { - var context = new MessageContext(command); - using (context) - { - await SendCommandAsync(command, context, cancellationToken); - } - } - else - { - var request = new CommandRequest(command, false); - await Mediator.Send(request, cancellationToken); - } - } - - /// - /// Subscribes this instance. - /// - /// The type of the t command. - /// The type of the t handler. - public void Subscribe() - where TCommand : ICommand - where THandler : ICommandHandler - { - HandlerContext.Register(); - } - - /// - /// Sends the command. - /// - /// The message. - /// The message context. - /// - private async Task SendCommandAsync(IMessage message, MessageContext messageContext, CancellationToken cancellationToken = default) - { - await Task.Run(() => - { - MessageQueue.GetQueue(message.GetType()).Enqueue(message, messageContext, MessageProcessType.Send); - OnMessageDispatched(new MessageDispatchedEventArgs(message, messageContext)); - }, cancellationToken); - } - - /// - /// send as an asynchronous operation. - /// - /// The type of the t result. - /// The type of the t command. - /// The command. - /// The cancellation token. - /// Task<TResult>. - public async Task SendAsync(TCommand command, CancellationToken cancellationToken = default) - where TCommand : ICommand - { - TResult result; - - if (typeof(TCommand).GetCustomAttribute() != null) - { - // See https://stackoverflow.com/questions/18760252/timeout-an-async-method-implemented-with-taskcompletionsource - var taskCompletion = new TaskCompletionSource(); - - if (cancellationToken != default) - { - cancellationToken.Register(() => taskCompletion.TrySetCanceled(), false); - } - - var messageContext = new MessageContext(command); - messageContext.OnResponse += (_, args) => - { - taskCompletion.TrySetResult((TResult)args.Result); - }; - - messageContext.Completed += (_, _) => - { - taskCompletion.TrySetResult(default); - }; - - await SendCommandAsync(command, messageContext, cancellationToken); - - result = await taskCompletion.Task; - } - else - { - var request = new CommandRequest(command, true); - System.Diagnostics.Debug.WriteLine(Mediator.GetHashCode()); - result = await Mediator.Send(request, cancellationToken).ContinueWith(task => (TResult)task.Result, cancellationToken); - } - - return result; - } - - /// - public async Task SendAsync(TCommand command, Action callback, CancellationToken cancellationToken = default) - where TCommand : ICommand - { - var result = await SendAsync(command, cancellationToken); - callback.Invoke(result); - } - - /// - public async Task SendAsync(ICommand command, CancellationToken cancellationToken = default) - { - var requestType = typeof(CommandRequest<,>).MakeGenericType(command.GetType(), typeof(object)); - var request = Activator.CreateInstance(requestType, command); - return await Mediator.Send(request!, cancellationToken).ContinueWith(task => (TResult)task.Result, cancellationToken); - } - - /// - protected override void Dispose(bool disposing) - { - } - - private async void HandleMessageReceivedEvent(object sender, MessageReceivedEventArgs args) - { - OnMessageAcknowledged(new MessageAcknowledgedEventArgs(args.Message, args.Context)); - await HandlerContext.HandleAsync(args.Message, args.Context); - } -} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Euonia.Bus.InMemory.csproj b/Source/Euonia.Bus.InMemory/Euonia.Bus.InMemory.csproj index 3c50376..c1a67a9 100644 --- a/Source/Euonia.Bus.InMemory/Euonia.Bus.InMemory.csproj +++ b/Source/Euonia.Bus.InMemory/Euonia.Bus.InMemory.csproj @@ -17,6 +17,7 @@ + diff --git a/Source/Euonia.Bus.InMemory/EventBus.cs b/Source/Euonia.Bus.InMemory/EventBus.cs deleted file mode 100644 index 02950c8..0000000 --- a/Source/Euonia.Bus.InMemory/EventBus.cs +++ /dev/null @@ -1,83 +0,0 @@ -namespace Nerosoft.Euonia.Bus.InMemory; - -/// -public class EventBus : MessageBus, IEventBus -{ - private readonly IMessageStore _messageStore; - - /// - /// - /// - /// - /// - public EventBus(IHandlerContext handlerContext, IServiceAccessor accessor) - : base(handlerContext, accessor) - { - MessageReceived += HandleMessageReceivedEvent; - } - - /// - /// - /// - /// - /// - /// - public EventBus(IHandlerContext handlerContext, IServiceAccessor accessor, IMessageStore messageStore) - : base(handlerContext, accessor) - { - _messageStore = messageStore; - } - - /// - public async Task PublishAsync(TEvent @event, CancellationToken cancellationToken = default) - where TEvent : IEvent - { - if (_messageStore != null) - { - await _messageStore.SaveAsync(@event, cancellationToken); - } - - await Task.Run(() => - { - MessageQueue.GetQueue().Enqueue(@event, new MessageContext(), MessageProcessType.Dispatch); - }, cancellationToken); - - OnMessageDispatched(new MessageDispatchedEventArgs(@event, new MessageContext())); - } - - /// - public void Subscribe() where TEvent : IEvent where THandler : IEventHandler - { - HandlerContext.Register(); - } - - /// - public async Task PublishAsync(string name, TEvent @event, CancellationToken cancellationToken = default) - where TEvent : class - { - var namedEvent = new NamedEvent(name, @event); - if (_messageStore != null) - { - await _messageStore.SaveAsync(namedEvent, cancellationToken); - } - - await Task.Run(() => - { - MessageQueue.GetQueue().Enqueue(namedEvent, new MessageContext(), MessageProcessType.Dispatch); - }, cancellationToken); - - OnMessageDispatched(new MessageDispatchedEventArgs(namedEvent, new MessageContext())); - } - - /// - protected override void Dispose(bool disposing) - { - } - - private async void HandleMessageReceivedEvent(object sender, MessageReceivedEventArgs args) - { - await HandlerContext.HandleAsync(args.Message, args.Context); - - OnMessageAcknowledged(new MessageAcknowledgedEventArgs(args.Message, args.Context)); - } -} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/InMemoryBusOptions.cs b/Source/Euonia.Bus.InMemory/InMemoryBusOptions.cs index 4b55b0b..3c4dd5e 100644 --- a/Source/Euonia.Bus.InMemory/InMemoryBusOptions.cs +++ b/Source/Euonia.Bus.InMemory/InMemoryBusOptions.cs @@ -5,12 +5,26 @@ /// public class InMemoryBusOptions { + /// + /// + /// public bool LazyInitialize { get; set; } = true; + /// + /// Gets or sets the maximum concurrent calls. + /// public int MaxConcurrentCalls { get; set; } = 1; + /// + /// Gets or sets a value indicating whether the subscriber should create for each message channel. + /// + /// + /// true if the subscriber should create for each message channel; otherwise, false. default is false. + /// + public bool MultipleSubscriberInstance { get; set; } + /// /// Gets or sets the messenger reference type. /// - public MessengerReferenceType MessengerReference { get; set; } = MessengerReferenceType.StrongReference; + public MessengerReferenceType MessengerReference { get; init; } = MessengerReferenceType.StrongReference; } \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/InMemoryDispatcher.cs b/Source/Euonia.Bus.InMemory/InMemoryDispatcher.cs index 8cda806..7a552b6 100644 --- a/Source/Euonia.Bus.InMemory/InMemoryDispatcher.cs +++ b/Source/Euonia.Bus.InMemory/InMemoryDispatcher.cs @@ -49,21 +49,26 @@ public async Task SendAsync(RoutedMessage message, Cancellat Aborted = cancellationToken }; - var taskCompletionSource = new TaskCompletionSource(); + var taskCompletion = new TaskCompletionSource(); + + if (cancellationToken != default) + { + cancellationToken.Register(() => taskCompletion.SetCanceled(cancellationToken)); + } context.Completed += (_, _) => { - taskCompletionSource.SetResult(); + taskCompletion.SetResult(); }; _messenger.Send(pack, message.Channel); Delivered?.Invoke(this, new MessageDispatchedEventArgs(message.Data, context)); - await taskCompletionSource.Task; + await taskCompletion.Task; } /// - public async Task SendAsync(RoutedMessage message, CancellationToken cancellationToken = default) + public async Task SendAsync(RoutedMessage message, CancellationToken cancellationToken = default) where TMessage : class { var context = new MessageContext(message); @@ -71,21 +76,27 @@ public async Task SendAsync(RoutedMessage { Aborted = cancellationToken }; + + // See https://stackoverflow.com/questions/18760252/timeout-an-async-method-implemented-with-taskcompletionsource + var taskCompletion = new TaskCompletionSource(); + if (cancellationToken != default) + { + cancellationToken.Register(() => taskCompletion.TrySetCanceled(), false); + } - var taskCompletionSource = new TaskCompletionSource(); context.OnResponse += (_, args) => { - taskCompletionSource.SetResult((TResult)args.Result); + taskCompletion.SetResult((TResponse)args.Result); }; context.Completed += (_, _) => { - taskCompletionSource.SetResult(default); + taskCompletion.TrySetResult(default); }; _messenger.Send(pack, message.Channel); Delivered?.Invoke(this, new MessageDispatchedEventArgs(message.Data, context)); - return await taskCompletionSource.Task; + return await taskCompletion.Task; } /// diff --git a/Source/Euonia.Bus.InMemory/InMemorySubscriber.cs b/Source/Euonia.Bus.InMemory/InMemorySubscriber.cs index 0b0e1b5..697af30 100644 --- a/Source/Euonia.Bus.InMemory/InMemorySubscriber.cs +++ b/Source/Euonia.Bus.InMemory/InMemorySubscriber.cs @@ -15,31 +15,20 @@ public class InMemorySubscriber : DisposableObject, ISubscriber, IRecipient public event EventHandler MessageAcknowledged; - /// - /// - /// - public event EventHandler MessageSubscribed; - - private readonly string _channel; + private readonly IHandlerContext _handler; /// /// Initializes a new instance of the class. /// - /// - public InMemorySubscriber(string channel) + /// + public InMemorySubscriber(IHandlerContext handler) { - _channel = channel; + _handler = handler; } /// public string Name { get; } = nameof(InMemorySubscriber); - /// - public void Subscribe(Type messageType, Type handlerType) - { - //throw new NotImplementedException(); - } - #region IDisposable /// @@ -50,8 +39,10 @@ protected override void Dispose(bool disposing) #endregion /// - public void Receive(MessagePack message) + public async void Receive(MessagePack pack) { - MessageReceived?.Invoke(this, new MessageReceivedEventArgs(message.Message, message.Context)); + MessageReceived?.Invoke(this, new MessageReceivedEventArgs(pack.Message, pack.Context)); + await _handler.HandleAsync(pack.Message.Data, pack.Context, pack.Aborted); + MessageAcknowledged?.Invoke(this, new MessageAcknowledgedEventArgs(pack.Message, pack.Context)); } } \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/MessageBus.cs b/Source/Euonia.Bus.InMemory/MessageBus.cs deleted file mode 100644 index 5fb3083..0000000 --- a/Source/Euonia.Bus.InMemory/MessageBus.cs +++ /dev/null @@ -1,120 +0,0 @@ -namespace Nerosoft.Euonia.Bus.InMemory; - -/// -/// Class MessageBus. -/// Implements the -/// Implements the -/// -/// -public abstract class MessageBus : DisposableObject -{ - /// - /// Occurs when [message subscribed]. - /// - public event EventHandler MessageSubscribed; - - /// - /// Occurs when [message dispatched]. - /// - public event EventHandler Dispatched; - - /// - /// Occurs when [message received]. - /// - public event EventHandler MessageReceived; - - /// - /// Occurs when [message acknowledged]. - /// - public event EventHandler MessageAcknowledged; - - /// - /// Initializes a new instance of the class. - /// - /// The message handler context. - /// - protected MessageBus(IHandlerContext handlerContext, IServiceAccessor accessor) - { - HandlerContext = handlerContext; - ServiceAccessor = accessor; - } - - /// - /// Gets the service accessor. - /// - protected IServiceAccessor ServiceAccessor { get; } - - /// - /// Gets the message handler context. - /// - /// The message handler context. - protected IHandlerContext HandlerContext { get; } - - /// - /// Handles the event. - /// - /// The instance containing the event data. - protected virtual void OnMessageSubscribed(MessageSubscribedEventArgs args) - { - var queue = new MessageQueue(); - queue.MessagePushed += (sender, e) => - { - var message = (sender as MessageQueue)?.Dequeue(); - if (message == null) - { - return; - } - - OnMessageReceived(new MessageReceivedEventArgs(message, e.Context)); - }; - MessageQueue.AddQueue(args.MessageType, queue); - MessageSubscribed?.Invoke(this, args); - } - - /// - /// Handles the event. - /// - /// The instance containing the event data. - protected virtual void OnMessageDispatched(MessageDispatchedEventArgs args) - { - Dispatched?.Invoke(this, args); - } - - /// - /// Handles the event. - /// - /// The instance containing the event data. - protected virtual void OnMessageReceived(MessageReceivedEventArgs args) - { - MessageReceived?.Invoke(this, args); - } - - /// - /// Handles the event. - /// - /// The instance containing the event data. - protected virtual void OnMessageAcknowledged(MessageAcknowledgedEventArgs args) - { - MessageAcknowledged?.Invoke(this, args); - } - - /// - /// Subscribes the specified message type. - /// - /// - /// - public virtual void Subscribe(Type messageType, Type handlerType) - { - HandlerContext.Register(messageType, handlerType); - } - - /// - /// Subscribes the specified message name. - /// - /// - /// - public virtual void Subscribe(string messageName, Type handlerType) - { - HandlerContext.Register(messageName, handlerType); - } -} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Messages/MessagePack.cs b/Source/Euonia.Bus.InMemory/Messages/MessagePack.cs index d4ba15a..9680974 100644 --- a/Source/Euonia.Bus.InMemory/Messages/MessagePack.cs +++ b/Source/Euonia.Bus.InMemory/Messages/MessagePack.cs @@ -10,7 +10,7 @@ public sealed class MessagePack /// /// /// - public MessagePack(IRoutedMessage message, IMessageContext context) + public MessagePack(IRoutedMessage message, MessageContext context) { Message = message; Context = context; @@ -24,7 +24,7 @@ public MessagePack(IRoutedMessage message, IMessageContext context) /// /// Get the message context. /// - public IMessageContext Context { get; } + public MessageContext Context { get; } /// /// Gets or sets the cancellation token. diff --git a/Source/Euonia.Bus.InMemory/Messenger/IRecipient.cs b/Source/Euonia.Bus.InMemory/Messenger/IRecipient.cs index 7983b3e..7c4e24e 100644 --- a/Source/Euonia.Bus.InMemory/Messenger/IRecipient.cs +++ b/Source/Euonia.Bus.InMemory/Messenger/IRecipient.cs @@ -10,6 +10,6 @@ public interface IRecipient /// /// Receives a given message instance. /// - /// The message being received. - void Receive(TMessage message); + /// The message being received. + void Receive(TMessage pack); } \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Messenger/MessengerExtensions.Observables.cs b/Source/Euonia.Bus.InMemory/Messenger/MessengerExtensions.Observables.cs index 8aa0db8..200d1d5 100644 --- a/Source/Euonia.Bus.InMemory/Messenger/MessengerExtensions.Observables.cs +++ b/Source/Euonia.Bus.InMemory/Messenger/MessengerExtensions.Observables.cs @@ -92,9 +92,9 @@ public Recipient(IMessenger messenger, IObserver observer) } /// - public void Receive(TMessage message) + public void Receive(TMessage pack) { - _observer.OnNext(message); + _observer.OnNext(pack); } /// @@ -177,9 +177,9 @@ public Recipient(IMessenger messenger, IObserver observer, TToken toke } /// - public void Receive(TMessage message) + public void Receive(TMessage pack) { - _observer.OnNext(message); + _observer.OnNext(pack); } /// diff --git a/Source/Euonia.Bus.InMemory/ServiceCollectionExtensions.cs b/Source/Euonia.Bus.InMemory/ServiceCollectionExtensions.cs deleted file mode 100644 index 16023bf..0000000 --- a/Source/Euonia.Bus.InMemory/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,105 +0,0 @@ -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; -using Nerosoft.Euonia.Bus; -using Nerosoft.Euonia.Bus.InMemory; - -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// Message bus extensions for . -/// -public static class ServiceCollectionExtensions -{ - /// - /// Adds the in-memory command bus to the service collection. - /// - /// - /// - public static IServiceCollection AddInMemoryCommandBus(this IServiceCollection services) - { - return services.AddSingleton(provider => - { - var bus = ActivatorUtilities.GetServiceOrCreateInstance(provider); - var options = provider.GetService>()?.Value; - if (options != null) - { - foreach (var subscription in options.Registration) - { - bus.Subscribe(subscription.MessageType, subscription.HandlerType); - } - } - - { - } - return bus; - }); - } - - /// - /// Adds the in-memory event bus to the service collection. - /// - /// - /// - public static IServiceCollection AddInMemoryEventBus(this IServiceCollection services) - { - return services.AddSingleton(provider => - { - var bus = ActivatorUtilities.GetServiceOrCreateInstance(provider); - var options = provider.GetService>()?.Value; - if (options != null) - { - foreach (var subscription in options.Registration) - { - if (subscription.MessageType != null) - { - bus.Subscribe(subscription.MessageType, subscription.HandlerType); - } - else - { - bus.Subscribe(subscription.MessageName, subscription.HandlerType); - } - } - } - - { - } - - return bus; - }); - } - - /// - /// Adds the in-memory message bus to the service collection. - /// - /// - /// - /// - /// - public static void UseInMemory(this IBusConfigurator configurator, Action configuration) - { - configurator.Service.Configure(configuration); - configurator.Service.TryAddSingleton(provider => - { - var options = provider.GetService>()?.Value; - if (options == null) - { - throw new InvalidOperationException("The in-memory message dispatcher options is not configured."); - } - - IMessenger messenger = options.MessengerReference switch - { - MessengerReferenceType.StrongReference => StrongReferenceMessenger.Default, - MessengerReferenceType.WeakReference => WeakReferenceMessenger.Default, - _ => throw new ArgumentOutOfRangeException(nameof(options.MessengerReference), options.MessengerReference, null) - }; - foreach (var subscription in configurator.GetSubscriptions()) - { - messenger.Register(new InMemorySubscriber(subscription), subscription); - } - - return messenger; - }); - configurator.Service.TryAddSingleton(); - configurator.SerFactory(); - } -} \ No newline at end of file From b337f828d960e64367ef2691da8b21d24075b69b Mon Sep 17 00:00:00 2001 From: damon Date: Sat, 25 Nov 2023 20:36:28 +0800 Subject: [PATCH 11/37] Split ISubscriber into IEventSubscriber & ICommandExecutor. --- .../Euonia.Bus.Abstract/Consumer/ICommandExecutor.cs | 8 ++++++++ .../Euonia.Bus.Abstract/Consumer/IEventSubscriber.cs | 11 +++++++++++ .../ISubscriber.cs => Consumer/IRecipient.cs} | 6 ++---- .../Euonia.Bus.InMemory/BusConfiguratorExtensions.cs | 4 ++-- .../{InMemorySubscriber.cs => InMemoryRecipient.cs} | 8 ++++---- 5 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 Source/Euonia.Bus.Abstract/Consumer/ICommandExecutor.cs create mode 100644 Source/Euonia.Bus.Abstract/Consumer/IEventSubscriber.cs rename Source/Euonia.Bus.Abstract/{Contracts/ISubscriber.cs => Consumer/IRecipient.cs} (74%) rename Source/Euonia.Bus.InMemory/{InMemorySubscriber.cs => InMemoryRecipient.cs} (76%) diff --git a/Source/Euonia.Bus.Abstract/Consumer/ICommandExecutor.cs b/Source/Euonia.Bus.Abstract/Consumer/ICommandExecutor.cs new file mode 100644 index 0000000..7321ccb --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Consumer/ICommandExecutor.cs @@ -0,0 +1,8 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +public interface ICommandExecutor : IRecipient +{ +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Consumer/IEventSubscriber.cs b/Source/Euonia.Bus.Abstract/Consumer/IEventSubscriber.cs new file mode 100644 index 0000000..6464195 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Consumer/IEventSubscriber.cs @@ -0,0 +1,11 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Interface IEventSubscriber +/// Implements the +/// +/// +public interface IEventSubscriber : IRecipient +{ + +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Contracts/ISubscriber.cs b/Source/Euonia.Bus.Abstract/Consumer/IRecipient.cs similarity index 74% rename from Source/Euonia.Bus.Abstract/Contracts/ISubscriber.cs rename to Source/Euonia.Bus.Abstract/Consumer/IRecipient.cs index cf333f9..6fe5157 100644 --- a/Source/Euonia.Bus.Abstract/Contracts/ISubscriber.cs +++ b/Source/Euonia.Bus.Abstract/Consumer/IRecipient.cs @@ -1,11 +1,9 @@ namespace Nerosoft.Euonia.Bus; /// -/// Interface ISubscriber -/// Implements the +/// /// -/// -public interface ISubscriber : IDisposable +public interface IRecipient : IDisposable { /// /// Occurs when [message received]. diff --git a/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs b/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs index 1a060e0..3829dfc 100644 --- a/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs +++ b/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs @@ -39,13 +39,13 @@ public static void UseInMemory(this IBusConfigurator configurator, Action(provider); + var subscriber = ActivatorUtilities.GetServiceOrCreateInstance(provider); messenger.Register(subscriber, subscription); } } else { - var subscriber = ActivatorUtilities.GetServiceOrCreateInstance(provider); + var subscriber = ActivatorUtilities.GetServiceOrCreateInstance(provider); foreach (var subscription in configurator.GetSubscriptions()) { messenger.Register(subscriber, subscription); diff --git a/Source/Euonia.Bus.InMemory/InMemorySubscriber.cs b/Source/Euonia.Bus.InMemory/InMemoryRecipient.cs similarity index 76% rename from Source/Euonia.Bus.InMemory/InMemorySubscriber.cs rename to Source/Euonia.Bus.InMemory/InMemoryRecipient.cs index 697af30..70363d3 100644 --- a/Source/Euonia.Bus.InMemory/InMemorySubscriber.cs +++ b/Source/Euonia.Bus.InMemory/InMemoryRecipient.cs @@ -3,7 +3,7 @@ /// /// /// -public class InMemorySubscriber : DisposableObject, ISubscriber, IRecipient +public class InMemoryRecipient : DisposableObject, IEventSubscriber, ICommandExecutor, IRecipient { /// /// Occurs when [message received]. @@ -18,16 +18,16 @@ public class InMemorySubscriber : DisposableObject, ISubscriber, IRecipient - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// - public InMemorySubscriber(IHandlerContext handler) + public InMemoryRecipient(IHandlerContext handler) { _handler = handler; } /// - public string Name { get; } = nameof(InMemorySubscriber); + public string Name { get; } = nameof(InMemoryRecipient); #region IDisposable From 2e034fc0ed6634ae11cf17f744a7f29f34b9fc3f Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 27 Nov 2023 13:28:21 +0800 Subject: [PATCH 12/37] [WIP] Remove unused files. --- .../Attributes/CommandAttribute.cs | 9 - .../Attributes/EnqueueAttribute.cs | 27 +++ .../Attributes/QueueAttribute.cs | 22 +- .../{EventAttribute.cs => TopicAttribute.cs} | 2 +- .../IQueue.cs} | 5 +- .../Euonia.Bus.Abstract/Contracts/IRequest.cs | 2 +- .../Contracts/{IMessage.cs => ITopic.cs} | 4 +- .../Euonia.Bus.Abstract.csproj | 2 +- .../Euonia.Bus.Abstract/IMessageSerializer.cs | 50 ++++- .../MessageSerializerSettings.cs | 43 ++++ .../Messages/IMessageTypeCache.cs | 8 - .../IQueueConsumer.cs} | 2 +- .../{Consumer => Recipients}/IRecipient.cs | 0 .../ITopicSubscriber.cs} | 5 +- Source/Euonia.Bus.Abstract/RoutedMessage.cs | 10 + .../BusConfiguratorExtensions.cs | 4 +- .../InMemoryQueueConsumer.cs | 19 ++ .../Euonia.Bus.InMemory/InMemoryRecipient.cs | 5 +- .../InMemoryTopicSubscriber.cs | 19 ++ Source/Euonia.Bus.InMemory/MessageQueue.cs | 6 +- .../Messenger/StrongReferenceMessenger.cs | 2 +- .../BusConfiguratorExtensions.cs | 26 +++ Source/Euonia.Bus.RabbitMq/CommandBus.cs | 186 ---------------- Source/Euonia.Bus.RabbitMq/CommandClient.cs | 172 --------------- Source/Euonia.Bus.RabbitMq/CommandConsumer.cs | 121 ----------- Source/Euonia.Bus.RabbitMq/Constants.cs | 30 ++- .../Euonia.Bus.RabbitMq.csproj | 1 + Source/Euonia.Bus.RabbitMq/EventBus.cs | 201 ------------------ Source/Euonia.Bus.RabbitMq/EventConsumer.cs | 85 -------- Source/Euonia.Bus.RabbitMq/MessageBus.cs | 167 --------------- .../Euonia.Bus.RabbitMq/MessageTypeCache.cs | 31 +++ .../Euonia.Bus.RabbitMq/RabbitMqBusFactory.cs | 17 +- .../Euonia.Bus.RabbitMq/RabbitMqDispatcher.cs | 162 ++++++++++++++ .../RabbitMqMessageBusModule.cs | 20 -- .../RabbitMqMessageBusOptions.cs | 2 +- .../RabbitMqMessageDispatcher.cs | 5 - .../RabbitMqQueueConsumer.cs | 82 +++++++ .../RabbitMqQueueRecipient.cs | 156 ++++++++++++++ .../RabbitMqTopicSubscriber.cs | 64 ++++++ .../ServiceCollectionExtensions.cs | 150 ------------- Source/Euonia.Bus/Constants.cs | 18 +- .../Conventions/AttributeMessageConvention.cs | 8 +- .../Conventions/DefaultMessageConvention.cs | 8 +- .../Conventions/IMessageConvention.cs | 14 +- .../Conventions/MessageConvention.cs | 4 +- .../OverridableMessageConvention.cs | 8 +- Source/Euonia.Bus/Core/ServiceBus.cs | 6 +- Source/Euonia.Bus/Messages/MessageCache.cs | 41 ++++ .../Messages/MessageChannelCache.cs | 28 --- .../Messages/MessageHandlerCache.cs | 5 - .../Serialization/NewtonsoftJsonSerializer.cs | 13 +- .../Serialization/SystemTextJsonSerializer.cs | 39 +++- 52 files changed, 871 insertions(+), 1245 deletions(-) delete mode 100644 Source/Euonia.Bus.Abstract/Attributes/CommandAttribute.cs create mode 100644 Source/Euonia.Bus.Abstract/Attributes/EnqueueAttribute.cs rename Source/Euonia.Bus.Abstract/Attributes/{EventAttribute.cs => TopicAttribute.cs} (82%) rename Source/Euonia.Bus.Abstract/{Consumer/ICommandExecutor.cs => Contracts/IQueue.cs} (56%) rename Source/Euonia.Bus.Abstract/Contracts/{IMessage.cs => ITopic.cs} (52%) create mode 100644 Source/Euonia.Bus.Abstract/MessageSerializerSettings.cs delete mode 100644 Source/Euonia.Bus.Abstract/Messages/IMessageTypeCache.cs rename Source/Euonia.Bus.Abstract/{Contracts/IEvent.cs => Recipients/IQueueConsumer.cs} (61%) rename Source/Euonia.Bus.Abstract/{Consumer => Recipients}/IRecipient.cs (100%) rename Source/Euonia.Bus.Abstract/{Consumer/IEventSubscriber.cs => Recipients/ITopicSubscriber.cs} (64%) create mode 100644 Source/Euonia.Bus.InMemory/InMemoryQueueConsumer.cs create mode 100644 Source/Euonia.Bus.InMemory/InMemoryTopicSubscriber.cs create mode 100644 Source/Euonia.Bus.RabbitMq/BusConfiguratorExtensions.cs delete mode 100644 Source/Euonia.Bus.RabbitMq/CommandBus.cs delete mode 100644 Source/Euonia.Bus.RabbitMq/CommandClient.cs delete mode 100644 Source/Euonia.Bus.RabbitMq/CommandConsumer.cs delete mode 100644 Source/Euonia.Bus.RabbitMq/EventBus.cs delete mode 100644 Source/Euonia.Bus.RabbitMq/EventConsumer.cs delete mode 100644 Source/Euonia.Bus.RabbitMq/MessageBus.cs create mode 100644 Source/Euonia.Bus.RabbitMq/MessageTypeCache.cs create mode 100644 Source/Euonia.Bus.RabbitMq/RabbitMqDispatcher.cs delete mode 100644 Source/Euonia.Bus.RabbitMq/RabbitMqMessageBusModule.cs delete mode 100644 Source/Euonia.Bus.RabbitMq/RabbitMqMessageDispatcher.cs create mode 100644 Source/Euonia.Bus.RabbitMq/RabbitMqQueueConsumer.cs create mode 100644 Source/Euonia.Bus.RabbitMq/RabbitMqQueueRecipient.cs create mode 100644 Source/Euonia.Bus.RabbitMq/RabbitMqTopicSubscriber.cs delete mode 100644 Source/Euonia.Bus.RabbitMq/ServiceCollectionExtensions.cs create mode 100644 Source/Euonia.Bus/Messages/MessageCache.cs delete mode 100644 Source/Euonia.Bus/Messages/MessageChannelCache.cs delete mode 100644 Source/Euonia.Bus/Messages/MessageHandlerCache.cs diff --git a/Source/Euonia.Bus.Abstract/Attributes/CommandAttribute.cs b/Source/Euonia.Bus.Abstract/Attributes/CommandAttribute.cs deleted file mode 100644 index 23991ce..0000000 --- a/Source/Euonia.Bus.Abstract/Attributes/CommandAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Nerosoft.Euonia.Bus; - -/// -/// Represents the class is a command. -/// -[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public class CommandAttribute : Attribute -{ -} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Attributes/EnqueueAttribute.cs b/Source/Euonia.Bus.Abstract/Attributes/EnqueueAttribute.cs new file mode 100644 index 0000000..da37d61 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Attributes/EnqueueAttribute.cs @@ -0,0 +1,27 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// Represents the decorated message type should be enqueued. +/// +[AttributeUsage(AttributeTargets.Class)] +public class EnqueueAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// + public EnqueueAttribute(string name) + { + Name = name; + } + + /// + /// Gets the name of the queue. + /// + public string Name { get; } + + /// + /// Gets or sets the priority of the message. + /// + public int Priority { get; set; } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Attributes/QueueAttribute.cs b/Source/Euonia.Bus.Abstract/Attributes/QueueAttribute.cs index cad4ac8..497ec65 100644 --- a/Source/Euonia.Bus.Abstract/Attributes/QueueAttribute.cs +++ b/Source/Euonia.Bus.Abstract/Attributes/QueueAttribute.cs @@ -1,27 +1,9 @@ namespace Nerosoft.Euonia.Bus; /// -/// Represents the decorated message type should be enqueued. +/// Represents the class is a command. /// -[AttributeUsage(AttributeTargets.Class)] +[AttributeUsage(AttributeTargets.Class, Inherited = false)] public class QueueAttribute : Attribute { - /// - /// Initializes a new instance of the class. - /// - /// - public QueueAttribute(string name) - { - Name = name; - } - - /// - /// Gets the name of the queue. - /// - public string Name { get; } - - /// - /// Gets or sets the priority of the message. - /// - public int Priority { get; set; } } \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Attributes/EventAttribute.cs b/Source/Euonia.Bus.Abstract/Attributes/TopicAttribute.cs similarity index 82% rename from Source/Euonia.Bus.Abstract/Attributes/EventAttribute.cs rename to Source/Euonia.Bus.Abstract/Attributes/TopicAttribute.cs index 598e901..105efb2 100644 --- a/Source/Euonia.Bus.Abstract/Attributes/EventAttribute.cs +++ b/Source/Euonia.Bus.Abstract/Attributes/TopicAttribute.cs @@ -4,6 +4,6 @@ /// Represents the class is a event. /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public class EventAttribute : Attribute +public class TopicAttribute : Attribute { } \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Consumer/ICommandExecutor.cs b/Source/Euonia.Bus.Abstract/Contracts/IQueue.cs similarity index 56% rename from Source/Euonia.Bus.Abstract/Consumer/ICommandExecutor.cs rename to Source/Euonia.Bus.Abstract/Contracts/IQueue.cs index 7321ccb..7d3736f 100644 --- a/Source/Euonia.Bus.Abstract/Consumer/ICommandExecutor.cs +++ b/Source/Euonia.Bus.Abstract/Contracts/IQueue.cs @@ -1,8 +1,9 @@ namespace Nerosoft.Euonia.Bus; /// -/// +/// Represents a topic. /// -public interface ICommandExecutor : IRecipient +public interface IQueue { + } \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Contracts/IRequest.cs b/Source/Euonia.Bus.Abstract/Contracts/IRequest.cs index 4272dca..ec405a1 100644 --- a/Source/Euonia.Bus.Abstract/Contracts/IRequest.cs +++ b/Source/Euonia.Bus.Abstract/Contracts/IRequest.cs @@ -3,6 +3,6 @@ /// /// /// -public interface IRequest : IMessage +public interface IRequest { } \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Contracts/IMessage.cs b/Source/Euonia.Bus.Abstract/Contracts/ITopic.cs similarity index 52% rename from Source/Euonia.Bus.Abstract/Contracts/IMessage.cs rename to Source/Euonia.Bus.Abstract/Contracts/ITopic.cs index ee8a00e..b840c38 100644 --- a/Source/Euonia.Bus.Abstract/Contracts/IMessage.cs +++ b/Source/Euonia.Bus.Abstract/Contracts/ITopic.cs @@ -1,8 +1,8 @@ namespace Nerosoft.Euonia.Bus; /// -/// The base contract of message. +/// Represents a topic. /// -public interface IMessage +public interface ITopic { } \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Euonia.Bus.Abstract.csproj b/Source/Euonia.Bus.Abstract/Euonia.Bus.Abstract.csproj index cbe6f49..4254784 100644 --- a/Source/Euonia.Bus.Abstract/Euonia.Bus.Abstract.csproj +++ b/Source/Euonia.Bus.Abstract/Euonia.Bus.Abstract.csproj @@ -27,7 +27,7 @@ - + diff --git a/Source/Euonia.Bus.Abstract/IMessageSerializer.cs b/Source/Euonia.Bus.Abstract/IMessageSerializer.cs index 47c8cb2..8cbc140 100644 --- a/Source/Euonia.Bus.Abstract/IMessageSerializer.cs +++ b/Source/Euonia.Bus.Abstract/IMessageSerializer.cs @@ -39,12 +39,52 @@ public interface IMessageSerializer /// /// T Deserialize(byte[] bytes); - + + /// + /// Deserializes json text to the specified type. + /// + /// + /// + /// T Deserialize(string json); - + + /// + /// Deserializes the specified stream to the specified type. + /// + /// + /// + /// T Deserialize(Stream stream); - + + /// + /// Serializes the specified object to a JSON string. + /// + /// + /// + /// string Serialize(T obj); - - byte[] SerializeToBytes(T obj); + + /// + /// Serializes the specified object to a JSON byte array. + /// + /// + /// + /// + byte[] SerializeToByteArray(T obj); + + /// + /// Deserializes the json bytes to the specified type. + /// + /// + /// + /// + object Deserialize(byte[] bytes, Type type) => Deserialize(Encoding.UTF8.GetString(bytes), type); + + /// + /// Deserializes the json text to the specified type. + /// + /// + /// + /// + object Deserialize(string json, Type type); } \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/MessageSerializerSettings.cs b/Source/Euonia.Bus.Abstract/MessageSerializerSettings.cs new file mode 100644 index 0000000..259bebc --- /dev/null +++ b/Source/Euonia.Bus.Abstract/MessageSerializerSettings.cs @@ -0,0 +1,43 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// +/// +public class MessageSerializerSettings +{ + /// + /// Gets or sets a value indicating how to handle reference loops. + /// + public ReferenceLoopStrategy? ReferenceLoop { get; set; } + + /// + /// Gets or sets a value indicating whether to use constructor handling. + /// + public bool UseConstructorHandling { get; set; } = true; + + /// + /// Gets or sets the encoding. + /// + public Encoding Encoding { get; set; } = Encoding.UTF8; + + /// + /// Defines how to handle reference loop during deserialization. + /// + public enum ReferenceLoopStrategy + { + /// + /// + /// + Ignore, + + /// + /// + /// + Preserve, + + /// + /// + /// + Serialize + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Messages/IMessageTypeCache.cs b/Source/Euonia.Bus.Abstract/Messages/IMessageTypeCache.cs deleted file mode 100644 index db8b54b..0000000 --- a/Source/Euonia.Bus.Abstract/Messages/IMessageTypeCache.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Reflection; - -namespace Nerosoft.Euonia.Bus; - -public interface IMessageTypeCache -{ - IEnumerable Properties { get; } -} \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Contracts/IEvent.cs b/Source/Euonia.Bus.Abstract/Recipients/IQueueConsumer.cs similarity index 61% rename from Source/Euonia.Bus.Abstract/Contracts/IEvent.cs rename to Source/Euonia.Bus.Abstract/Recipients/IQueueConsumer.cs index 172e18c..4e6ea43 100644 --- a/Source/Euonia.Bus.Abstract/Contracts/IEvent.cs +++ b/Source/Euonia.Bus.Abstract/Recipients/IQueueConsumer.cs @@ -3,6 +3,6 @@ /// /// /// -public interface IEvent : IMessage +public interface IQueueConsumer : IRecipient { } \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Consumer/IRecipient.cs b/Source/Euonia.Bus.Abstract/Recipients/IRecipient.cs similarity index 100% rename from Source/Euonia.Bus.Abstract/Consumer/IRecipient.cs rename to Source/Euonia.Bus.Abstract/Recipients/IRecipient.cs diff --git a/Source/Euonia.Bus.Abstract/Consumer/IEventSubscriber.cs b/Source/Euonia.Bus.Abstract/Recipients/ITopicSubscriber.cs similarity index 64% rename from Source/Euonia.Bus.Abstract/Consumer/IEventSubscriber.cs rename to Source/Euonia.Bus.Abstract/Recipients/ITopicSubscriber.cs index 6464195..4d675a3 100644 --- a/Source/Euonia.Bus.Abstract/Consumer/IEventSubscriber.cs +++ b/Source/Euonia.Bus.Abstract/Recipients/ITopicSubscriber.cs @@ -1,11 +1,10 @@ namespace Nerosoft.Euonia.Bus; /// -/// Interface IEventSubscriber +/// Interface ITopicSubscriber /// Implements the /// /// -public interface IEventSubscriber : IRecipient +public interface ITopicSubscriber : IRecipient { - } \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/RoutedMessage.cs b/Source/Euonia.Bus.Abstract/RoutedMessage.cs index 4adea4a..b20eed2 100644 --- a/Source/Euonia.Bus.Abstract/RoutedMessage.cs +++ b/Source/Euonia.Bus.Abstract/RoutedMessage.cs @@ -123,10 +123,20 @@ public TData Data } } +/// +/// +/// +/// +/// [Serializable] public class RoutedMessage : RoutedMessage where TData : class { + /// + /// Initializes a new instance of the class. + /// + /// + /// public RoutedMessage(TData data, string channel) : base(data, channel) { diff --git a/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs b/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs index 3829dfc..987b0af 100644 --- a/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs +++ b/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs @@ -6,12 +6,12 @@ namespace Nerosoft.Euonia.Bus; /// -/// Message bus extensions for . +/// Service bus extensions for . /// public static class BusConfiguratorExtensions { /// - /// Adds the in-memory message bus to the service collection. + /// Adds the in-memory message transporter. /// /// /// diff --git a/Source/Euonia.Bus.InMemory/InMemoryQueueConsumer.cs b/Source/Euonia.Bus.InMemory/InMemoryQueueConsumer.cs new file mode 100644 index 0000000..3891702 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/InMemoryQueueConsumer.cs @@ -0,0 +1,19 @@ +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// +/// +public class InMemoryQueueConsumer : InMemoryRecipient, IQueueConsumer +{ + /// + /// Initializes a new instance of the class. + /// + /// + public InMemoryQueueConsumer(IHandlerContext handler) + : base(handler) + { + } + + /// + public string Name => nameof(InMemoryQueueConsumer); +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/InMemoryRecipient.cs b/Source/Euonia.Bus.InMemory/InMemoryRecipient.cs index 70363d3..a36b5c1 100644 --- a/Source/Euonia.Bus.InMemory/InMemoryRecipient.cs +++ b/Source/Euonia.Bus.InMemory/InMemoryRecipient.cs @@ -3,7 +3,7 @@ /// /// /// -public class InMemoryRecipient : DisposableObject, IEventSubscriber, ICommandExecutor, IRecipient +public abstract class InMemoryRecipient : DisposableObject, IRecipient { /// /// Occurs when [message received]. @@ -26,9 +26,6 @@ public InMemoryRecipient(IHandlerContext handler) _handler = handler; } - /// - public string Name { get; } = nameof(InMemoryRecipient); - #region IDisposable /// diff --git a/Source/Euonia.Bus.InMemory/InMemoryTopicSubscriber.cs b/Source/Euonia.Bus.InMemory/InMemoryTopicSubscriber.cs new file mode 100644 index 0000000..16376c1 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/InMemoryTopicSubscriber.cs @@ -0,0 +1,19 @@ +namespace Nerosoft.Euonia.Bus.InMemory; + +/// +/// +/// +public class InMemoryTopicSubscriber : InMemoryRecipient, ITopicSubscriber +{ + /// + /// Initializes a new instance of the class. + /// + /// + public InMemoryTopicSubscriber(IHandlerContext handler) + : base(handler) + { + } + + /// + public string Name => nameof(InMemoryTopicSubscriber); +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/MessageQueue.cs b/Source/Euonia.Bus.InMemory/MessageQueue.cs index 0557411..f35da67 100644 --- a/Source/Euonia.Bus.InMemory/MessageQueue.cs +++ b/Source/Euonia.Bus.InMemory/MessageQueue.cs @@ -11,18 +11,18 @@ internal sealed class MessageQueue { private static readonly ConcurrentDictionary _container = new(); - private readonly ConcurrentQueue _queue = new(); + private readonly ConcurrentQueue _queue = new(); public event EventHandler MessagePushed; - internal void Enqueue(IMessage message, IMessageContext messageContext, MessageProcessType processType) + internal void Enqueue(object message, IMessageContext messageContext, MessageProcessType processType) { _queue.Enqueue(message); MessagePushed?.Invoke(this, new MessageProcessedEventArgs(message, messageContext, processType)); } - internal IMessage Dequeue() + internal object Dequeue() { var succeed = _queue.TryDequeue(out var message); return succeed ? message : null; diff --git a/Source/Euonia.Bus.InMemory/Messenger/StrongReferenceMessenger.cs b/Source/Euonia.Bus.InMemory/Messenger/StrongReferenceMessenger.cs index b340dc7..1bf19d2 100644 --- a/Source/Euonia.Bus.InMemory/Messenger/StrongReferenceMessenger.cs +++ b/Source/Euonia.Bus.InMemory/Messenger/StrongReferenceMessenger.cs @@ -81,7 +81,7 @@ public sealed class StrongReferenceMessenger : IMessenger /// The and instance for types combination. /// /// - /// The values are just of type as we don't know the type parameters in advance. + /// The values are just of type as we don't know the type parameters in advance. /// Each method relies on to get the type-safe instance of the /// or class for each pair of generic arguments in use. /// diff --git a/Source/Euonia.Bus.RabbitMq/BusConfiguratorExtensions.cs b/Source/Euonia.Bus.RabbitMq/BusConfiguratorExtensions.cs new file mode 100644 index 0000000..3efd8c2 --- /dev/null +++ b/Source/Euonia.Bus.RabbitMq/BusConfiguratorExtensions.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Nerosoft.Euonia.Bus.RabbitMq; + +/// +/// Service bus extensions for . +/// +public static class BusConfiguratorExtensions +{ + /// + /// Adds the RabbitMQ message transporter. + /// + /// + /// + public static void UseRabbitMq(this IBusConfigurator configurator, Action configuration) + { + configurator.Service.Configure(configuration); + configurator.Service.TryAddSingleton(); + configurator.Service.AddTransient(); + configurator.Service.AddTransient(); + configurator.SerFactory(); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/CommandBus.cs b/Source/Euonia.Bus.RabbitMq/CommandBus.cs deleted file mode 100644 index 89e8ec0..0000000 --- a/Source/Euonia.Bus.RabbitMq/CommandBus.cs +++ /dev/null @@ -1,186 +0,0 @@ -using System.Reflection; -using MediatR; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Nerosoft.Euonia.Domain; -using RabbitMQ.Client; - -namespace Nerosoft.Euonia.Bus.RabbitMq; - -/// -/// The command bus implement using RabbitMQ. -/// -public class CommandBus : MessageBus, ICommandBus -{ - private readonly ConnectionFactory _factory; - private readonly ILogger _logger; - private bool _disposed; - - private readonly Dictionary _consumers = new(); - - /// - /// Initializes a new instance of the . - /// - /// - /// - /// - /// - public CommandBus(IHandlerContext handlerContext, IOptionsMonitor monitor, IServiceAccessor accessor, ILoggerFactory logger) - : base(handlerContext, monitor, accessor) - { - _logger = logger.CreateLogger(); - _factory = new ConnectionFactory { Uri = new Uri(Options.Connection) }; - - HandlerContext.MessageSubscribed += HandleMessageSubscribed; - } - - /// - /// Gets the message mediator. - /// - private IMediator Mediator => ServiceAccessor.GetService(); - - private void HandleMessageSubscribed(object sender, MessageSubscribedEventArgs args) - { - if (args.MessageType.GetCustomAttribute() == null) - { - return; - } - - var consumerType = typeof(CommandConsumer<>).MakeGenericType(args.MessageType); - var consumer = (CommandConsumer)Activator.CreateInstance(consumerType, _factory, Options, HandlerContext); - if (consumer == null) - { - throw new InvalidOperationException($"Could not create consumer for message type {args.MessageType.FullName}"); - } - - consumer.OnMessageAcknowledged = OnMessageAcknowledged; - consumer.OnMessageReceived = OnMessageReceived; - _consumers.Add(args.MessageType, consumer); - - OnMessageSubscribed(args); - } - - /// - public async Task SendAsync(TCommand command, CancellationToken cancellationToken = default) - where TCommand : ICommand - { - if (typeof(TCommand).GetCustomAttribute() != null) - { - await SendCommandAsync(command, cancellationToken); - } - else - { - var request = new CommandRequest(command, false); - await Mediator.Send(request, cancellationToken); - } - } - - /// - public async Task SendAsync(TCommand command, CancellationToken cancellationToken = default) - where TCommand : ICommand - { - TResult result; - - if (typeof(TCommand).GetCustomAttribute() != null) - { - result = await SendCommandAsync(command, cancellationToken).ConfigureAwait(false); - } - else - { - var request = new CommandRequest(command, true); - result = await Mediator.Send(request, cancellationToken).ContinueWith(task => (TResult)task.Result, cancellationToken); - } - - return result; - } - - /// - public async Task SendAsync(TCommand command, Action callback, CancellationToken cancellationToken = default) - where TCommand : ICommand - { - var result = await SendAsync(command, cancellationToken); - callback.Invoke(result); - } - - /// - public async Task SendAsync(ICommand request, CancellationToken cancellationToken = default) - { - return await Mediator.Send(request, cancellationToken); - } - - /// - public void Subscribe() - where TCommand : ICommand - where THandler : ICommandHandler - { - HandlerContext.Register(); - } - - // ReSharper disable once SuggestBaseTypeForParameter - private async Task SendCommandAsync(ICommand command, CancellationToken cancellationToken = default) - { - var type = command.GetType().Name; - - var messageBody = Serialize(command); - using (var client = new CommandClient(Options, _logger)) - { - try - { - var result = await client.CallAsync(messageBody, type, cancellationToken); - - OnMessageDispatched(new MessageDispatchedEventArgs(command, null)); - - return result; - } - catch (Exception exception) - { - _logger.LogError(exception, "Error: {Message}", exception.Message); - throw; - } - } - } - - // ReSharper disable once SuggestBaseTypeForParameter - private async Task SendCommandAsync(ICommand command, CancellationToken cancellationToken = default) - { - var type = command.GetType().Name; - - var messageBody = Serialize(command); - using (var client = new CommandClient(Options, _logger)) - { - try - { - await client.CallAsync(messageBody, type, cancellationToken); - - OnMessageDispatched(new MessageDispatchedEventArgs(command, null)); - } - catch (Exception exception) - { - _logger.LogError(exception, "Error: {Message}", exception.Message); - throw; - } - } - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected override void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - foreach (var (_, consumer) in _consumers) - { - consumer.Dispose(); - } - } - - _disposed = true; - } -} diff --git a/Source/Euonia.Bus.RabbitMq/CommandClient.cs b/Source/Euonia.Bus.RabbitMq/CommandClient.cs deleted file mode 100644 index 61b21df..0000000 --- a/Source/Euonia.Bus.RabbitMq/CommandClient.cs +++ /dev/null @@ -1,172 +0,0 @@ -using Newtonsoft.Json.Linq; -using Newtonsoft.Json; -using Polly; -using RabbitMQ.Client.Events; -using RabbitMQ.Client; -using Microsoft.Extensions.Logging; - -namespace Nerosoft.Euonia.Bus.RabbitMq; - -/// -/// The command message handle client. -/// -public class CommandClient : DisposableObject -{ - private readonly RabbitMqMessageBusOptions _options; - private readonly IConnection _connection; - private readonly IModel _channel; - private readonly string _replyQueueName; - private readonly EventingBasicConsumer _consumer; - private bool _disposed; - private readonly ILogger _logger; - - /// - /// - /// - /// - /// - public CommandClient(RabbitMqMessageBusOptions options, ILogger logger) - { - _options = options; - _logger = logger; - var factory = new ConnectionFactory { Uri = new Uri(options.Connection) }; - - _connection = factory.CreateConnection(); - _channel = _connection.CreateModel(); - _replyQueueName = _channel.QueueDeclare().QueueName; - _consumer = new EventingBasicConsumer(_channel); - } - - /// - /// - /// - /// - /// - /// - /// - /// - public async Task CallAsync(byte[] message, string type, CancellationToken cancellationToken = default) - { - var task = new TaskCompletionSource(); - - _consumer.Received += (_, args) => - { - _logger.LogInformation("Callback message received: {CorrelationId}", args.BasicProperties.CorrelationId); - var body = args.Body.ToArray(); - try - { - var content = Encoding.UTF8.GetString(body); - var settings = new JsonSerializerSettings(); - var response = JsonConvert.DeserializeObject(content, settings); - - task.TrySetResult(response); - } - catch (Exception exception) - { - _logger.LogError("Error deserializing response: {Exception}", exception); - task.TrySetException(exception); - } - }; - - var props = _channel.CreateBasicProperties(); - props.Headers ??= new Dictionary(); - props.Headers[Constants.MessageHeaderCommandType] = type; - var correlationId = Guid.NewGuid().ToString(); - props.CorrelationId = correlationId; - props.ReplyTo = _replyQueueName; - - try - { - Policy.Handle() - .WaitAndRetry(_options.MaxFailureRetries, _ => TimeSpan.FromSeconds(1), (exception, _, retryCount, _) => - { - _logger.LogError(exception, "Retry:{RetryCount}, {Message}", retryCount, exception.Message); - }) - .Execute(() => - { - _channel.BasicPublish("", $"{_options.CommandQueueName}${type}$", props, message); - _channel.BasicConsume(_consumer, _replyQueueName, true); - }); - } - catch (Exception exception) - { - _logger.LogError(exception, "Message publish failed:{Message}", exception.Message); - throw; - } - - cancellationToken.Register(() => task.TrySetCanceled(), false); - - return await task.Task; - } - - /// - /// - /// - /// - /// - /// - /// - public async Task CallAsync(byte[] message, string type, CancellationToken cancellationToken = default) - { - await Task.Run(() => - { - try - { - var props = _channel.CreateBasicProperties(); - props.Headers ??= new Dictionary(); - props.Headers[Constants.MessageHeaderCommandType] = type; - - Policy.Handle() - .WaitAndRetry(_options.MaxFailureRetries, _ => TimeSpan.FromSeconds(1), (exception, _, retryCount, _) => - { - _logger.LogError(exception, "Retry:{RetryCount}, {Message}", retryCount, exception.Message); - }) - .Execute(() => - { - _channel.BasicPublish("", $"{_options.CommandQueueName}${type}$", props, message); - }); - } - catch (Exception exception) - { - _logger.LogError(exception, "Message publish failed:{Message}", exception.Message); - throw; - } - }, cancellationToken); - } - - /// - protected override void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - _channel?.Dispose(); - _connection?.Dispose(); - } - - _disposed = true; - } - - /// - /// - /// - /// - /// - /// - /// - protected virtual Exception GetException(string json, string path, Type type) - { - var jsonObject = JObject.Parse(json); - var message = jsonObject.GetValue(path)?.ToString(); - if (message == null) - { - return null; - } - - return JsonConvert.DeserializeObject(message, type) as Exception; - } -} \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/CommandConsumer.cs b/Source/Euonia.Bus.RabbitMq/CommandConsumer.cs deleted file mode 100644 index 0f3a45d..0000000 --- a/Source/Euonia.Bus.RabbitMq/CommandConsumer.cs +++ /dev/null @@ -1,121 +0,0 @@ -using Newtonsoft.Json; -using RabbitMQ.Client.Events; -using RabbitMQ.Client; -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus.RabbitMq; - -/// -/// -/// -public abstract class CommandConsumer : DisposableObject -{ - /// - /// - /// - public Action OnMessageReceived { get; set; } - - /// - /// - /// - public Action OnMessageAcknowledged { get; set; } -} - -/// -/// -/// -/// -public class CommandConsumer : CommandConsumer - where TCommand : ICommand -{ - // ReSharper disable once StaticMemberInGenericType - private static readonly JsonSerializerSettings _serializerSettings = new() - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - ConstructorHandling = ConstructorHandling.Default, - MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead - }; - - private readonly IModel _channel; - private readonly IConnection _connection; - private readonly EventingBasicConsumer _consumer; - private readonly IHandlerContext _handlerContext; - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - public CommandConsumer(IConnectionFactory factory, RabbitMqMessageBusOptions options, IHandlerContext handlerContext) - { - _handlerContext = handlerContext; - _connection = factory.CreateConnection(); - _channel = _connection.CreateModel(); - - var queueName = $"{options.CommandQueueName}${typeof(TCommand).Name}$"; - - _channel.QueueDeclare(queueName, true, false, false, null); - - _channel.BasicQos(0, 1, false); - - _consumer = new EventingBasicConsumer(_channel); - - _consumer.Received += HandleMessageReceived; - - _channel.BasicConsume(queueName, options.AutoAck, _consumer); - } - - private async void HandleMessageReceived(object _, BasicDeliverEventArgs args) - { - var messageContext = new MessageContext(); - - var body = args.Body; - var props = args.BasicProperties; - var replyNeeded = !string.IsNullOrEmpty(props.CorrelationId); - - var message = Deserialize(body.ToArray()); - OnMessageReceived(new MessageReceivedEventArgs(message, messageContext)); - - var taskCompletion = new TaskCompletionSource(); - messageContext.OnResponse += (_, a) => - { - taskCompletion.TrySetResult(a.Result); - }; - - await _handlerContext.HandleAsync(message, messageContext); - - if (replyNeeded) - { - var result = await taskCompletion.Task; - var replyProps = _channel.CreateBasicProperties(); - replyProps.Headers ??= new Dictionary(); - replyProps.Headers.Add("type", result.GetType().GetFullNameWithAssemblyName()); - replyProps.CorrelationId = props.CorrelationId; - - var response = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(result, _serializerSettings)); - _channel.BasicPublish("", props.ReplyTo, replyProps, response); - _channel.BasicAck(args.DeliveryTag, false); - } - else - { - taskCompletion.SetCanceled(); - } - - OnMessageAcknowledged(new MessageAcknowledgedEventArgs(message, messageContext)); - } - - private static TCommand Deserialize(byte[] value) - { - var json = Encoding.UTF8.GetString(value); - return JsonConvert.DeserializeObject(json, _serializerSettings); - } - - /// - protected override void Dispose(bool disposing) - { - _consumer.Received -= HandleMessageReceived; - _channel.Dispose(); - _connection.Dispose(); - } -} diff --git a/Source/Euonia.Bus.RabbitMq/Constants.cs b/Source/Euonia.Bus.RabbitMq/Constants.cs index 8465a42..a1d7b2a 100644 --- a/Source/Euonia.Bus.RabbitMq/Constants.cs +++ b/Source/Euonia.Bus.RabbitMq/Constants.cs @@ -1,9 +1,29 @@ -namespace Nerosoft.Euonia.Bus.RabbitMq; +using Newtonsoft.Json; + +namespace Nerosoft.Euonia.Bus.RabbitMq; internal class Constants { - internal const string MessageHeaderCommandType = "x-command-type"; - internal const string MessageHeaderEventName = "x-event-name"; - internal const string MessageHeaderEventType = "x-event-type"; - internal const string MessageHeaderEventAttr = "x-event-attr"; + public static readonly JsonSerializerSettings SerializerSettings = new() + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + ConstructorHandling = ConstructorHandling.Default, + MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead + }; + + public class MessageHeaders + { + public const string CorrelationId = "x-correlation-id"; + public const string MessageId = "x-message-id"; + public const string MessageType = "x-message-type"; + public const string ContentType = "x-content-type"; + public const string ContentEncoding = "x-content-encoding"; + public const string DeliveryMode = "x-delivery-mode"; + public const string Priority = "x-priority"; + public const string ReplyTo = "x-reply-to"; + public const string Expiration = "x-expiration"; + public const string Timestamp = "x-timestamp"; + public const string Type = "x-type"; + public const string UserId = "x-user-id"; + } } \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/Euonia.Bus.RabbitMq.csproj b/Source/Euonia.Bus.RabbitMq/Euonia.Bus.RabbitMq.csproj index c27ce22..ba9d082 100644 --- a/Source/Euonia.Bus.RabbitMq/Euonia.Bus.RabbitMq.csproj +++ b/Source/Euonia.Bus.RabbitMq/Euonia.Bus.RabbitMq.csproj @@ -18,6 +18,7 @@ + diff --git a/Source/Euonia.Bus.RabbitMq/EventBus.cs b/Source/Euonia.Bus.RabbitMq/EventBus.cs deleted file mode 100644 index 64f76c5..0000000 --- a/Source/Euonia.Bus.RabbitMq/EventBus.cs +++ /dev/null @@ -1,201 +0,0 @@ -using System.Collections.Concurrent; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Nerosoft.Euonia.Domain; -using Polly; -using RabbitMQ.Client; - -namespace Nerosoft.Euonia.Bus.RabbitMq; - -/// -/// The event bus implementation that uses RabbitMQ for publishing and subscribing to events. -/// -public class EventBus : MessageBus, IEventBus -{ - private readonly ConnectionFactory _factory; - private readonly IMessageStore _messageStore; - private readonly IConnection _connection; - private readonly IModel _channel; - private readonly ILogger _logger; - private bool _disposed; - - private static readonly ConcurrentDictionary _consumers = new(); - - /// - /// Initialize a new instance of . - /// - /// - /// - /// - /// - /// - public EventBus(IHandlerContext handlerContext, IOptionsMonitor monitor, IServiceAccessor accessor, ILoggerFactory logger) - : base(handlerContext, monitor, accessor) - { - _logger = logger.CreateLogger(); - _factory = new ConnectionFactory { Uri = new Uri(Options.Connection) }; - _connection = _factory.CreateConnection(); - _channel = _connection.CreateModel(); - - if (string.IsNullOrEmpty(Options.ExchangeName)) - { - throw new ArgumentNullException(nameof(Options.ExchangeName), Resources.IDS_EXCHANGE_NAME_IS_REQUIRED); - } - - if (string.IsNullOrEmpty(Options.ExchangeType)) - { - throw new ArgumentNullException(nameof(Options.ExchangeType), Resources.IDS_EXCHANGE_TYPE_IS_REQUIRED); - } - - // Declares the exchange - _channel.ExchangeDeclare(Options.ExchangeName, Options.ExchangeType); - - HandlerContext.MessageSubscribed += HandleMessageSubscribed; - } - - /// - /// Initialize a new instance of . - /// - /// - /// - /// - /// - /// - public EventBus(IHandlerContext handlerContext, IOptionsMonitor monitor, IServiceAccessor accessor, IMessageStore messageStore, ILoggerFactory logger) - : this(handlerContext, monitor, accessor, logger) - { - _messageStore = messageStore; - } - - private void HandleMessageSubscribed(object sender, MessageSubscribedEventArgs args) - { - // ReSharper disable once HeapView.CanAvoidClosure - _consumers.GetOrAdd(args.MessageName, name => - { - var consumer = new EventConsumer(_factory, Options, HandlerContext, name) - { - OnMessageAcknowledged = OnMessageAcknowledged, - OnMessageReceived = OnMessageReceived - }; - return consumer; - }); - - OnMessageSubscribed(args); - } - - /// - public async Task PublishAsync(TEvent @event, CancellationToken cancellationToken = default) - where TEvent : IEvent - { - if (_messageStore != null) - { - await _messageStore.SaveAsync(@event, cancellationToken); - } - - var messageContext = new MessageContext(); - - await Task.Run(() => - { - try - { - var messageBody = Serialize(@event); - var props = _channel.CreateBasicProperties(); - props.Headers ??= new Dictionary(); - if (@event.HasAttribute(out ChannelAttribute attribute, false)) - { - props.Headers[Constants.MessageHeaderEventAttr] = attribute.Name; //Encoding.UTF8.GetBytes(attribute.Name); - } - else - { - props.Headers[Constants.MessageHeaderEventType] = @event.Metadata[RoutedMessage<>.MessageTypeKey]; //Encoding.UTF8.GetBytes((@event.Metadata[MessageBase.MESSAGE_TYPE_KEY] as string)!); - } - - Policy.Handle() - .WaitAndRetry(Options.MaxFailureRetries, _ => TimeSpan.FromSeconds(3), (exception, _, retryCount, _) => - { - _logger.LogError(exception, "Retry:{RetryCount}, {Message}", retryCount, exception.Message); - }) - .Execute(() => - { - _channel.BasicPublish(Options.ExchangeName, @event.GetType().FullName, props, messageBody); - }); - } - catch (Exception exception) - { - _logger.LogError(exception, "Message publish failed:{Message}", exception.Message); - throw; - } - }, cancellationToken); - - OnMessageDispatched(new MessageDispatchedEventArgs(@event, messageContext)); - } - - /// - public async Task PublishAsync(string name, TEvent @event, CancellationToken cancellationToken = default) - where TEvent : class - { - var namedEvent = new NamedEvent(name, @event); - if (_messageStore != null) - { - await _messageStore.SaveAsync(namedEvent, cancellationToken); - } - - var messageContext = new MessageContext(); - await Task.Run(() => - { - try - { - var messageBody = Serialize(@event); - var props = _channel.CreateBasicProperties(); - props.Headers ??= new Dictionary(); - props.Headers[Constants.MessageHeaderEventName] = name; - - Policy.Handle() - .WaitAndRetry(Options.MaxFailureRetries, _ => TimeSpan.FromSeconds(3), (exception, _, retryCount, _) => - { - _logger.LogError(exception, "Retry:{RetryCount}, {Message}", retryCount, exception.Message); - }) - .Execute(() => - { - _channel.BasicPublish(Options.ExchangeName, @event.GetType().FullName, props, messageBody); - }); - } - catch (Exception exception) - { - _logger.LogError(exception, "Message publish failed:{Message}", exception.Message); - throw; - } - }, cancellationToken); - - OnMessageDispatched(new MessageDispatchedEventArgs(namedEvent, messageContext)); - } - - /// - public void Subscribe() - where TEvent : IEvent - where THandler : IEventHandler - { - HandlerContext.Register(); - } - - /// - /// Releases unmanaged and - optionally - managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected override void Dispose(bool disposing) - { - _logger.LogInformation("EventBus disposing..."); - if (_disposed) - { - return; - } - - if (disposing) - { - _channel?.Dispose(); - _connection?.Dispose(); - } - - _disposed = true; - } -} \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/EventConsumer.cs b/Source/Euonia.Bus.RabbitMq/EventConsumer.cs deleted file mode 100644 index bb59257..0000000 --- a/Source/Euonia.Bus.RabbitMq/EventConsumer.cs +++ /dev/null @@ -1,85 +0,0 @@ -using RabbitMQ.Client.Events; -using RabbitMQ.Client; -using Nerosoft.Euonia.Domain; - -namespace Nerosoft.Euonia.Bus.RabbitMq; - -/// -/// The event consumer -/// -public class EventConsumer : DisposableObject -{ - private readonly IModel _channel; - private readonly IConnection _connection; - private readonly EventingBasicConsumer _consumer; - private readonly string _messageName; - private readonly RabbitMqMessageBusOptions _options; - private readonly IHandlerContext _handlerContext; - - internal EventConsumer(IConnectionFactory factory, RabbitMqMessageBusOptions options, IHandlerContext handlerContext, string messageName) - { - _options = options; - _handlerContext = handlerContext; - _connection = factory.CreateConnection(); - _channel = _connection.CreateModel(); - _messageName = messageName; - - string queueName; - - if (string.IsNullOrWhiteSpace(options.EventQueueName)) - { - _channel.ExchangeDeclare(messageName, options.ExchangeType); - queueName = _channel.QueueDeclare().QueueName; - } - else - { - _channel.QueueDeclare(options.EventQueueName, true, false, false, null); - queueName = options.EventQueueName; - } - - _channel.QueueBind(queueName, messageName, options.RoutingKey ?? "*"); - - _consumer = new EventingBasicConsumer(_channel); - - _consumer.Received += HandleMessageReceived; - - _channel.BasicConsume(string.Empty, options.AutoAck, _consumer); - } - - private async void HandleMessageReceived(object sender, BasicDeliverEventArgs args) - { - var body = Encoding.UTF8.GetString(args.Body.ToArray()); - - var @event = new NamedEvent(_messageName, body); - OnMessageReceived(new MessageReceivedEventArgs(@event, null)); - - var context = new MessageContext(); - - await _handlerContext.HandleAsync(@event, context); - - if (!_options.AutoAck) - { - _channel.BasicAck(args.DeliveryTag, false); - } - - OnMessageAcknowledged(new MessageAcknowledgedEventArgs(@event, null)); - } - - /// - protected override void Dispose(bool disposing) - { - _consumer.Received -= HandleMessageReceived; - _channel.Dispose(); - _connection.Dispose(); - } - - /// - /// - /// - public Action OnMessageReceived { get; set; } - - /// - /// - /// - public Action OnMessageAcknowledged { get; set; } -} diff --git a/Source/Euonia.Bus.RabbitMq/MessageBus.cs b/Source/Euonia.Bus.RabbitMq/MessageBus.cs deleted file mode 100644 index e4231c4..0000000 --- a/Source/Euonia.Bus.RabbitMq/MessageBus.cs +++ /dev/null @@ -1,167 +0,0 @@ -using Microsoft.Extensions.Options; -using Newtonsoft.Json; - -namespace Nerosoft.Euonia.Bus.RabbitMq; - -/// -/// -/// -public abstract class MessageBus : DisposableObject, IBus -{ - private static readonly JsonSerializerSettings _serializerSettings = new() - { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - ConstructorHandling = ConstructorHandling.Default, - MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead - }; - - /// - public event EventHandler MessageSubscribed; - - /// - public event EventHandler Dispatched; - - /// - public event EventHandler MessageReceived; - - /// - public event EventHandler MessageAcknowledged; - - /// - /// - /// - /// - /// - /// - internal MessageBus(IHandlerContext handlerContext, IOptionsMonitor monitor, IServiceAccessor accessor) - { - HandlerContext = handlerContext; - ServiceAccessor = accessor; - Options = monitor.CurrentValue; - monitor.OnChange((options, _) => - { - Options.Connection = options.Connection; - Options.AutoAck = options.AutoAck; - Options.ExchangeName = options.ExchangeName; - Options.ExchangeType = options.ExchangeType; - Options.CommandQueueName = options.CommandQueueName; - Options.MaxFailureRetries = options.MaxFailureRetries; - }); - } - - /// - /// Gets the service accessor. - /// - protected IServiceAccessor ServiceAccessor { get; } - - /// - /// Gets the message handler context. - /// - protected IHandlerContext HandlerContext { get; } - - /// - /// Gets the message bus options. - /// - protected RabbitMqMessageBusOptions Options { get; } - - /// - /// - /// - /// - protected virtual void OnMessageSubscribed(MessageSubscribedEventArgs args) - { - MessageSubscribed?.Invoke(this, args); - } - - /// - /// - /// - /// - protected virtual void OnMessageDispatched(MessageDispatchedEventArgs args) - { - Dispatched?.Invoke(this, args); - } - - /// - /// - /// - /// - protected virtual void OnMessageReceived(MessageReceivedEventArgs args) - { - MessageReceived?.Invoke(this, args); - } - - /// - /// - /// - /// - protected virtual void OnMessageAcknowledged(MessageAcknowledgedEventArgs args) - { - MessageAcknowledged?.Invoke(this, args); - } - - /// - /// - /// - /// - /// - protected virtual byte[] Serialize(object message) - { - if (message == null) - { - return Array.Empty(); - } - - var type = message.GetType(); - - var stringValue = type.IsClass ? JsonConvert.SerializeObject(message, _serializerSettings) : message.ToString(); - return string.IsNullOrEmpty(stringValue) ? Array.Empty() : Encoding.UTF8.GetBytes(stringValue); - } - - /// - /// - /// - /// - /// - /// - protected virtual string GetHeaderValue(IDictionary header, string key) - { - if (header == null) - { - return string.Empty; - } - - if (header.TryGetValue(key, out var value)) - { - return value switch - { - null => string.Empty, - string @string => @string, - byte[] bytes => Encoding.UTF8.GetString(bytes), - _ => value.ToString() - }; - } - - return string.Empty; - } - - /// - /// - /// - /// - /// - public virtual void Subscribe(Type messageType, Type handlerType) - { - HandlerContext.Register(messageType, handlerType); - } - - /// - /// - /// - /// - /// - public virtual void Subscribe(string messageName, Type handlerType) - { - HandlerContext.Register(messageName, handlerType); - } -} diff --git a/Source/Euonia.Bus.RabbitMq/MessageTypeCache.cs b/Source/Euonia.Bus.RabbitMq/MessageTypeCache.cs new file mode 100644 index 0000000..6607d2f --- /dev/null +++ b/Source/Euonia.Bus.RabbitMq/MessageTypeCache.cs @@ -0,0 +1,31 @@ +using System.Collections.Concurrent; + +namespace Nerosoft.Euonia.Bus.RabbitMq; + +/// +/// +/// +public class MessageTypeCache +{ + private static readonly ConcurrentDictionary _messageTypes = new(); + + /// + /// + /// + /// + /// + /// + public static Type GetMessageType(string messageName) + { + return _messageTypes.GetOrAdd(messageName, name => + { + var type = Type.GetType(name); + if (type == null) + { + throw new InvalidOperationException($"Could not find message type '{name}'."); + } + + return type; + }); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/RabbitMqBusFactory.cs b/Source/Euonia.Bus.RabbitMq/RabbitMqBusFactory.cs index 2cf0e31..69e8dd5 100644 --- a/Source/Euonia.Bus.RabbitMq/RabbitMqBusFactory.cs +++ b/Source/Euonia.Bus.RabbitMq/RabbitMqBusFactory.cs @@ -1,9 +1,24 @@ namespace Nerosoft.Euonia.Bus.RabbitMq; +/// +/// +/// public class RabbitMqBusFactory : IBusFactory { + private readonly RabbitMqDispatcher _dispatcher; + + /// + /// Initializes a new instance of the class. + /// + /// + public RabbitMqBusFactory(RabbitMqDispatcher dispatcher) + { + _dispatcher = dispatcher; + } + + /// public IDispatcher CreateDispatcher() { - throw new NotImplementedException(); + return _dispatcher; } } \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/RabbitMqDispatcher.cs b/Source/Euonia.Bus.RabbitMq/RabbitMqDispatcher.cs new file mode 100644 index 0000000..1e73fa8 --- /dev/null +++ b/Source/Euonia.Bus.RabbitMq/RabbitMqDispatcher.cs @@ -0,0 +1,162 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Polly; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace Nerosoft.Euonia.Bus.RabbitMq; + +/// +/// The implementation using RabbitMQ. +/// +public class RabbitMqDispatcher : IDispatcher +{ + /// + public event EventHandler Delivered; + + private readonly RabbitMqMessageBusOptions _options; + private readonly IConnection _connection; + private readonly ILogger _logger; + + /// + /// Initialize a new instance of . + /// + /// + /// + /// + public RabbitMqDispatcher(IConnection connection, IOptions options, ILoggerFactory logger) + { + _logger = logger.CreateLogger(); + _connection = connection; + _options = options.Value; + } + + /// + public async Task PublishAsync(RoutedMessage message, CancellationToken cancellationToken = default) + where TMessage : class + { + using (var channel = _connection.CreateModel()) + { + var typeName = message.Data.GetType().GetFullNameWithAssemblyName(); + + var props = channel.CreateBasicProperties(); + props.Headers ??= new Dictionary(); + props.Headers[Constants.MessageHeaders.MessageType] = typeName; + props.Type = typeName; + + await Policy.Handle() + .WaitAndRetryAsync(_options.MaxFailureRetries, _ => TimeSpan.FromSeconds(3), (exception, _, retryCount, _) => + { + _logger.LogError(exception, "Retry:{RetryCount}, {Message}", retryCount, exception.Message); + }) + .ExecuteAsync(async () => + { + var messageBody = await SerializeAsync(message, cancellationToken); + + channel.ExchangeDeclare(_options.ExchangeName, _options.ExchangeType); + channel.BasicPublish(_options.ExchangeName, message.Channel, props, messageBody); + + Delivered?.Invoke(this, new MessageDispatchedEventArgs(message.Data, null)); + }); + } + } + + /// + public async Task SendAsync(RoutedMessage message, CancellationToken cancellationToken = default) where TMessage : class + { + using (var channel = _connection.CreateModel()) + { + var typeName = message.Data.GetType().GetFullNameWithAssemblyName(); + + var props = channel.CreateBasicProperties(); + props.Headers ??= new Dictionary(); + props.Headers[Constants.MessageHeaders.MessageType] = typeName; + props.Type = typeName; + + await Policy.Handle() + .WaitAndRetryAsync(_options.MaxFailureRetries, _ => TimeSpan.FromSeconds(3), (exception, _, retryCount, _) => + { + _logger.LogError(exception, "Retry:{RetryCount}, {Message}", retryCount, exception.Message); + }) + .ExecuteAsync(async () => + { + var messageBody = await SerializeAsync(message, cancellationToken); + + channel.BasicPublish("", $"{_options.QueueName}${message.Channel}$", props, messageBody); + + Delivered?.Invoke(this, new MessageDispatchedEventArgs(message.Data, null)); + }); + } + } + + /// + public async Task SendAsync(RoutedMessage message, CancellationToken cancellationToken = default) where TMessage : class + { + var task = new TaskCompletionSource(); + + using (var channel = _connection.CreateModel()) + { + var replyQueueName = channel.QueueDeclare().QueueName; + var consumer = new EventingBasicConsumer(channel); + + consumer.Received += (_, args) => + { + if (args.BasicProperties.CorrelationId != message.CorrelationId) + { + return; + } + + var body = args.Body.ToArray(); + var response = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(body), Constants.SerializerSettings); + + task.SetResult(response); + }; + + var typeName = message.Data.GetType().GetFullNameWithAssemblyName(); + + var props = channel.CreateBasicProperties(); + props.Headers ??= new Dictionary(); + props.Headers[Constants.MessageHeaders.MessageType] = typeName; + props.Type = typeName; + props.CorrelationId = message.CorrelationId; + props.ReplyTo = replyQueueName; + + await Policy.Handle() + .WaitAndRetryAsync(_options.MaxFailureRetries, _ => TimeSpan.FromSeconds(1), (exception, _, retryCount, _) => + { + _logger.LogError(exception, "Retry:{RetryCount}, {Message}", retryCount, exception.Message); + }) + .ExecuteAsync(async () => + { + var messageBody = await SerializeAsync(message, cancellationToken); + channel.BasicPublish("", $"{_options.QueueName}${message.Channel}$", props, messageBody); + channel.BasicConsume(consumer, replyQueueName, true); + + Delivered?.Invoke(this, new MessageDispatchedEventArgs(message.Data, null)); + }); + } + + return await task.Task; + } + + private static async Task SerializeAsync(RoutedMessage message, CancellationToken cancellationToken = default) + where TMessage : class + { + if (message == null) + { + return Array.Empty(); + } + + await using var stream = new MemoryStream(); + await using var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true); + using var jsonWriter = new JsonTextWriter(writer); + + JsonSerializer.Create(Constants.SerializerSettings).Serialize(jsonWriter, message); + + await jsonWriter.FlushAsync(cancellationToken); + await writer.FlushAsync(); + + return stream.ToArray(); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/RabbitMqMessageBusModule.cs b/Source/Euonia.Bus.RabbitMq/RabbitMqMessageBusModule.cs deleted file mode 100644 index 2e5002d..0000000 --- a/Source/Euonia.Bus.RabbitMq/RabbitMqMessageBusModule.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Nerosoft.Euonia.Modularity; - -namespace Nerosoft.Euonia.Bus.RabbitMq; - -/// -/// -/// -[DependsOn(typeof(MessageBusModule))] -public class RabbitMqMessageBusModule : ModuleContextBase -{ - /// - public override void ConfigureServices(ServiceConfigurationContext context) - { - context.Services.TryAddSingleton(_ => MessageConverter.Convert); - context.Services.AddRabbitMqCommandBus(); - context.Services.AddRabbitMqEventBus(); - } -} \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/RabbitMqMessageBusOptions.cs b/Source/Euonia.Bus.RabbitMq/RabbitMqMessageBusOptions.cs index 5c782cd..98a5404 100644 --- a/Source/Euonia.Bus.RabbitMq/RabbitMqMessageBusOptions.cs +++ b/Source/Euonia.Bus.RabbitMq/RabbitMqMessageBusOptions.cs @@ -19,7 +19,7 @@ public class RabbitMqMessageBusOptions /// /// Gets or sets the command queue name. /// - public string CommandQueueName { get; set; } + public string QueueName { get; set; } /// /// Gets or sets the event queue name. diff --git a/Source/Euonia.Bus.RabbitMq/RabbitMqMessageDispatcher.cs b/Source/Euonia.Bus.RabbitMq/RabbitMqMessageDispatcher.cs deleted file mode 100644 index 0314acd..0000000 --- a/Source/Euonia.Bus.RabbitMq/RabbitMqMessageDispatcher.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Nerosoft.Euonia.Bus.RabbitMq; - -public class RabbitMqMessageDispatcher : IDispatcher -{ -} \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/RabbitMqQueueConsumer.cs b/Source/Euonia.Bus.RabbitMq/RabbitMqQueueConsumer.cs new file mode 100644 index 0000000..a52a904 --- /dev/null +++ b/Source/Euonia.Bus.RabbitMq/RabbitMqQueueConsumer.cs @@ -0,0 +1,82 @@ +using Microsoft.Extensions.Options; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace Nerosoft.Euonia.Bus.RabbitMq; + +/// +/// +/// +public class RabbitMqQueueConsumer : RabbitMqQueueRecipient, IQueueConsumer +{ + /// + /// Initializes a new instance of the <see cref="RabbitMqQueueConsumer"/> class. + /// + /// + /// + /// + public RabbitMqQueueConsumer(IConnection connection, IHandlerContext handler, IOptions options) + : base(connection, handler, options) + { + } + + /// + public string Name => nameof(RabbitMqQueueConsumer); + + internal override void Start(string channel) + { + var queueName = $"{Options.QueueName}${channel}$"; + Channel.QueueDeclare(queueName, true, false, false, null); + Channel.BasicQos(0, 1, false); + Channel.BasicConsume(channel, Options.AutoAck, Consumer); + } + + /// + protected override async void HandleMessageReceived(object sender, BasicDeliverEventArgs args) + { + var type = MessageTypeCache.GetMessageType(args.BasicProperties.Type); + + var message = DeserializeMessage(args.Body.ToArray(), type); + + var props = args.BasicProperties; + var replyNeeded = !string.IsNullOrEmpty(props.CorrelationId); + + var context = new MessageContext(); + + OnMessageReceived(new MessageReceivedEventArgs(message.Data, context)); + + var taskCompletion = new TaskCompletionSource(); + context.OnResponse += (_, a) => + { + taskCompletion.TrySetResult(a.Result); + }; + context.Completed += (_, _) => + { + if (!Options.AutoAck) + { + Channel.BasicAck(args.DeliveryTag, false); + } + }; + + await Handler.HandleAsync(message.Channel, message.Data, context); + + if (replyNeeded) + { + var result = await taskCompletion.Task; + var replyProps = Channel.CreateBasicProperties(); + replyProps.Headers ??= new Dictionary(); + replyProps.Headers.Add(Constants.MessageHeaders.MessageType, result.GetType().GetFullNameWithAssemblyName()); + replyProps.CorrelationId = props.CorrelationId; + + var response = SerializeMessage(result); + Channel.BasicPublish(string.Empty, props.ReplyTo, replyProps, response); + Channel.BasicAck(args.DeliveryTag, false); + } + else + { + taskCompletion.SetCanceled(); + } + + OnMessageAcknowledged(new MessageAcknowledgedEventArgs(message.Data, context)); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/RabbitMqQueueRecipient.cs b/Source/Euonia.Bus.RabbitMq/RabbitMqQueueRecipient.cs new file mode 100644 index 0000000..f46db33 --- /dev/null +++ b/Source/Euonia.Bus.RabbitMq/RabbitMqQueueRecipient.cs @@ -0,0 +1,156 @@ +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace Nerosoft.Euonia.Bus.RabbitMq; + +/// +/// The base class for RabbitMQ message recipients. +/// +public abstract class RabbitMqQueueRecipient : DisposableObject +{ + /// + /// Occurs when [message received]. + /// + public event EventHandler MessageReceived; + + /// + /// Occurs when [message acknowledged]. + /// + public event EventHandler MessageAcknowledged; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + protected RabbitMqQueueRecipient(IConnection connection, IHandlerContext handler, IOptions options) + { + Options = options.Value; + Handler = handler; + Channel = connection.CreateModel(); + Consumer = new EventingBasicConsumer(Channel); + Consumer.Received += HandleMessageReceived; + } + + /// + /// Gets the RabbitMQ message bus options. + /// + protected virtual RabbitMqMessageBusOptions Options { get; } + + /// + /// Gets the RabbitMQ message channel. + /// + protected virtual IModel Channel { get; } + + /// + /// Gets the RabbitMQ consumer instance. + /// + protected virtual EventingBasicConsumer Consumer { get; } + + /// + /// Gets the message handler context instance. + /// + protected virtual IHandlerContext Handler { get; } + + // protected virtual void AcknowledgeMessage(ulong deliveryTag) + // { + // Channel.BasicAck(deliveryTag, false); + // } + + internal abstract void Start(string channel); + + /// + /// + /// + /// + /// + protected abstract void HandleMessageReceived(object sender, BasicDeliverEventArgs args); + + /// + /// + /// + /// + protected virtual void OnMessageAcknowledged(MessageAcknowledgedEventArgs args) + { + MessageAcknowledged?.Invoke(this, args); + } + + /// + /// + /// + /// + protected virtual void OnMessageReceived(MessageReceivedEventArgs args) + { + MessageReceived?.Invoke(this, args); + } + + /// + /// Serializes the message. + /// + /// + /// + protected virtual byte[] SerializeMessage(object message) + { + if (message == null) + { + return Array.Empty(); + } + + var json = JsonConvert.SerializeObject(message, Constants.SerializerSettings); + return Encoding.UTF8.GetBytes(json); + } + + /// + /// Deserializes the message. + /// + /// + /// + /// + protected virtual IRoutedMessage DeserializeMessage(byte[] message, Type messageType) + { + var type = typeof(RoutedMessage<>).MakeGenericType(messageType); + return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(message), type, Constants.SerializerSettings) as IRoutedMessage; + } + + /// + /// Gets the header value. + /// + /// + /// + /// + protected virtual string GetHeaderValue(IDictionary header, string key) + { + if (header == null) + { + return string.Empty; + } + + if (header.TryGetValue(key, out var value)) + { + return value switch + { + null => string.Empty, + string @string => @string, + byte[] bytes => Encoding.UTF8.GetString(bytes), + _ => value.ToString() + }; + } + + return string.Empty; + } + + /// + protected override void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + Consumer.Received -= HandleMessageReceived; + Channel?.Dispose(); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/RabbitMqTopicSubscriber.cs b/Source/Euonia.Bus.RabbitMq/RabbitMqTopicSubscriber.cs new file mode 100644 index 0000000..998bf6f --- /dev/null +++ b/Source/Euonia.Bus.RabbitMq/RabbitMqTopicSubscriber.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.Options; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace Nerosoft.Euonia.Bus.RabbitMq; + +/// +/// +/// +public class RabbitMqTopicSubscriber : RabbitMqQueueRecipient, ITopicSubscriber +{ + /// + /// Initializes a new instance of the <see cref="RabbitMqTopicSubscriber"/> class. + /// + /// + /// + /// + public RabbitMqTopicSubscriber(IConnection connection, IHandlerContext handler, IOptions options) + : base(connection, handler, options) + { + } + + /// + public string Name => nameof(RabbitMqTopicSubscriber); + + internal override void Start(string channel) + { + string queueName; + if (string.IsNullOrWhiteSpace(Options.EventQueueName)) + { + Channel.ExchangeDeclare(channel, Options.ExchangeType); + queueName = Channel.QueueDeclare().QueueName; + } + else + { + Channel.QueueDeclare(Options.EventQueueName, true, false, false, null); + queueName = Options.EventQueueName; + } + + Channel.QueueBind(queueName, channel, Options.RoutingKey ?? "*"); + Channel.BasicConsume(string.Empty, Options.AutoAck, Consumer); + } + + /// + protected override async void HandleMessageReceived(object sender, BasicDeliverEventArgs args) + { + var type = MessageTypeCache.GetMessageType(args.BasicProperties.Type); + + var message = DeserializeMessage(args.Body.ToArray(), type); + + var context = new MessageContext(); + + OnMessageReceived(new MessageReceivedEventArgs(message.Data, context)); + + await Handler.HandleAsync(message.Channel, message.Data, context); + + if (!Options.AutoAck) + { + Channel.BasicAck(args.DeliveryTag, false); + } + + OnMessageAcknowledged(new MessageAcknowledgedEventArgs(message.Data, context)); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/ServiceCollectionExtensions.cs b/Source/Euonia.Bus.RabbitMq/ServiceCollectionExtensions.cs deleted file mode 100644 index 23cf206..0000000 --- a/Source/Euonia.Bus.RabbitMq/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,150 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; -using Nerosoft.Euonia.Bus; -using Nerosoft.Euonia.Bus.RabbitMq; - -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// -/// -public static class ServiceCollectionExtensions -{ - /// - /// Add message bus. - /// - /// - /// - /// - /// - public static void AddRabbitMqMessageBus(this IServiceCollection services, IConfiguration configuration, string optionKey, Action action = null) - { - services.Configure(configuration.GetSection(optionKey)); - if (action == null) - { - services.AddSingleton(); - } - else - { - services.AddSingleton(provider => - { - var commandBus = ActivatorUtilities.GetServiceOrCreateInstance(provider); - action(commandBus); - return commandBus; - }); - } - - services.AddSingleton(); - } - - /// - /// Add message bus. - /// - /// - /// - /// - public static void AddRabbitMqMessageBus(this IServiceCollection services, Action configureOptions, Action action = null) - { - services.Configure(configureOptions); - if (action == null) - { - services.AddSingleton(); - } - else - { - services.AddSingleton(provider => - { - var commandBus = ActivatorUtilities.GetServiceOrCreateInstance(provider); - action(commandBus); - return commandBus; - }); - } - } - - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddRabbitMqConfiguration(this IServiceCollection services, Action configureOptions) - { - return services.Configure(configureOptions); - } - - /// - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddRabbitMqConfiguration(this IServiceCollection services, IConfiguration configuration, string key) - { - return services.Configure(configuration.GetSection(key)); - } - - /// - /// - /// - /// - /// - public static IServiceCollection AddRabbitMqCommandBus(this IServiceCollection services) - { - return services.AddSingleton(provider => - { - var bus = ActivatorUtilities.GetServiceOrCreateInstance(provider); - var options = provider.GetService>()?.Value; - if (options != null) - { - foreach (var subscription in options.Registration) - { - bus.Subscribe(subscription.MessageType, subscription.HandlerType); - } - } - - { - } - - return bus; - }); - } - - /// - /// - /// - /// - /// - public static IServiceCollection AddRabbitMqEventBus(this IServiceCollection services) - { - return services.AddSingleton(provider => - { - var bus = ActivatorUtilities.GetServiceOrCreateInstance(provider); - var options = provider.GetService>()?.Value; - if (options != null) - { - foreach (var subscription in options.Registration) - { - if (subscription.MessageType != null) - { - bus.Subscribe(subscription.MessageType, subscription.HandlerType); - } - else - { - bus.Subscribe(subscription.MessageName, subscription.HandlerType); - } - } - } - - { - } - - return bus; - }); - } - - public static void UseRabbitMq(this BusConfigurator configurator, Action configureOptions) - { - configurator.SerFactory(); - } -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Constants.cs b/Source/Euonia.Bus/Constants.cs index ce3917d..04c2675 100644 --- a/Source/Euonia.Bus/Constants.cs +++ b/Source/Euonia.Bus/Constants.cs @@ -2,13 +2,13 @@ internal class Constants { - /// - /// The minimal sequence value. - /// - public const long MinimalSequence = -1L; + /// + /// The minimal sequence value. + /// + public const long MinimalSequence = -1L; - /// - /// The maximum sequence value. - /// - public const long MaximumSequence = long.MaxValue; -} + /// + /// The maximum sequence value. + /// + public const long MaximumSequence = long.MaxValue; +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Conventions/AttributeMessageConvention.cs b/Source/Euonia.Bus/Conventions/AttributeMessageConvention.cs index d2bff74..bff9541 100644 --- a/Source/Euonia.Bus/Conventions/AttributeMessageConvention.cs +++ b/Source/Euonia.Bus/Conventions/AttributeMessageConvention.cs @@ -11,14 +11,14 @@ public class AttributeMessageConvention : IMessageConvention public string Name { get; } = "Attribute decoration message convention"; /// - public bool IsCommandType(Type type) + public bool IsQueueType(Type type) { - return type.GetCustomAttribute(false) != null; + return type.GetCustomAttribute(false) != null; } /// - public bool IsEventType(Type type) + public bool IsTopicType(Type type) { - return type.GetCustomAttribute(false) != null; + return type.GetCustomAttribute(false) != null; } } \ No newline at end of file diff --git a/Source/Euonia.Bus/Conventions/DefaultMessageConvention.cs b/Source/Euonia.Bus/Conventions/DefaultMessageConvention.cs index 06e3db9..6353b14 100644 --- a/Source/Euonia.Bus/Conventions/DefaultMessageConvention.cs +++ b/Source/Euonia.Bus/Conventions/DefaultMessageConvention.cs @@ -9,24 +9,24 @@ public class DefaultMessageConvention : IMessageConvention public string Name => "Default Message Convention"; /// - public bool IsCommandType(Type type) + public bool IsQueueType(Type type) { if (type == null) { throw new ArgumentNullException(nameof(type), "Type cannot be null."); } - return type.IsAssignableTo(typeof(ICommand)) && type != typeof(ICommand); + return type.IsAssignableTo(typeof(IQueue)) && type != typeof(IQueue); } /// - public bool IsEventType(Type type) + public bool IsTopicType(Type type) { if (type == null) { throw new ArgumentNullException(nameof(type), "Type cannot be null."); } - return type.IsAssignableTo(typeof(IEvent)) && type != typeof(IEvent); + return type.IsAssignableTo(typeof(ITopic)) && type != typeof(ITopic); } } \ No newline at end of file diff --git a/Source/Euonia.Bus/Conventions/IMessageConvention.cs b/Source/Euonia.Bus/Conventions/IMessageConvention.cs index 7a948af..bbc9018 100644 --- a/Source/Euonia.Bus/Conventions/IMessageConvention.cs +++ b/Source/Euonia.Bus/Conventions/IMessageConvention.cs @@ -1,7 +1,7 @@ namespace Nerosoft.Euonia.Bus; /// -/// A set of conventions for determining if a class represents a message, command, or event. +/// A set of conventions for determining if a class represents a request, queue, or topic message. /// public interface IMessageConvention { @@ -11,16 +11,16 @@ public interface IMessageConvention string Name { get; } /// - /// Determine if a type is a command type. + /// Determine if a type is a queue type. /// /// The type to check.. - /// true if represents a command. - bool IsCommandType(Type type); + /// true if represents a queue message. + bool IsQueueType(Type type); /// - /// Determine if a type is an event type. + /// Determine if a type is an topic type. /// /// The type to check.. - /// true if represents an event. - bool IsEventType(Type type); + /// true if represents an topic message. + bool IsTopicType(Type type); } \ No newline at end of file diff --git a/Source/Euonia.Bus/Conventions/MessageConvention.cs b/Source/Euonia.Bus/Conventions/MessageConvention.cs index f21f7a8..4d33354 100644 --- a/Source/Euonia.Bus/Conventions/MessageConvention.cs +++ b/Source/Euonia.Bus/Conventions/MessageConvention.cs @@ -25,7 +25,7 @@ public bool IsCommandType(Type type) return _commandConventionCache.Apply(type, handle => { var t = Type.GetTypeFromHandle(handle); - return _conventions.Any(x => x.IsCommandType(t)); + return _conventions.Any(x => x.IsQueueType(t)); }); } @@ -42,7 +42,7 @@ public bool IsEventType(Type type) return _eventConventionCache.Apply(type, handle => { var t = Type.GetTypeFromHandle(handle); - return _conventions.Any(x => x.IsEventType(t)); + return _conventions.Any(x => x.IsTopicType(t)); }); } diff --git a/Source/Euonia.Bus/Conventions/OverridableMessageConvention.cs b/Source/Euonia.Bus/Conventions/OverridableMessageConvention.cs index f636c32..343dfb2 100644 --- a/Source/Euonia.Bus/Conventions/OverridableMessageConvention.cs +++ b/Source/Euonia.Bus/Conventions/OverridableMessageConvention.cs @@ -12,25 +12,25 @@ public OverridableMessageConvention(IMessageConvention innerConvention) public string Name => $"Override with {_innerConvention.Name}"; - bool IMessageConvention.IsCommandType(Type type) + bool IMessageConvention.IsQueueType(Type type) { return IsCommandType(type); } - bool IMessageConvention.IsEventType(Type type) + bool IMessageConvention.IsTopicType(Type type) { return IsEventType(type); } public Func IsCommandType { - get => _isCommandType ?? _innerConvention.IsCommandType; + get => _isCommandType ?? _innerConvention.IsQueueType; set => _isCommandType = value; } public Func IsEventType { - get => _isEventType ?? _innerConvention.IsEventType; + get => _isEventType ?? _innerConvention.IsTopicType; set => _isEventType = value; } diff --git a/Source/Euonia.Bus/Core/ServiceBus.cs b/Source/Euonia.Bus/Core/ServiceBus.cs index f6250e0..e070abf 100644 --- a/Source/Euonia.Bus/Core/ServiceBus.cs +++ b/Source/Euonia.Bus/Core/ServiceBus.cs @@ -28,7 +28,7 @@ public async Task PublishAsync(TMessage message, PublishOptions option throw new InvalidOperationException("The message type is not an event type."); } - var channelName = options?.Channel ?? MessageChannelCache.Default.GetOrAdd(); + var channelName = options?.Channel ?? MessageCache.Default.GetOrAddChannel(); var pack = new RoutedMessage(message, channelName); await _dispatcher.PublishAsync(pack, cancellationToken); } @@ -42,7 +42,7 @@ public async Task SendAsync(TMessage message, SendOptions options, Can throw new InvalidOperationException("The message type is not a command type."); } - var channelName = options?.Channel ?? MessageChannelCache.Default.GetOrAdd(); + var channelName = options?.Channel ?? MessageCache.Default.GetOrAddChannel(); await _dispatcher.SendAsync(new RoutedMessage(message, channelName), cancellationToken); } @@ -55,7 +55,7 @@ public async Task SendAsync(TMessage message, SendOp throw new InvalidOperationException("The message type is not a command type."); } - var channelName = options?.Channel ?? MessageChannelCache.Default.GetOrAdd(); + var channelName = options?.Channel ?? MessageCache.Default.GetOrAddChannel(); var pack = new RoutedMessage(message, channelName); return await _dispatcher.SendAsync(pack, cancellationToken); } diff --git a/Source/Euonia.Bus/Messages/MessageCache.cs b/Source/Euonia.Bus/Messages/MessageCache.cs new file mode 100644 index 0000000..9a78c23 --- /dev/null +++ b/Source/Euonia.Bus/Messages/MessageCache.cs @@ -0,0 +1,41 @@ +using System.Collections.Concurrent; +using System.Reflection; + +namespace Nerosoft.Euonia.Bus; + +/// +/// The message cache. +/// +internal class MessageCache +{ + private static readonly Lazy _instance = new(() => new MessageCache()); + + private readonly ConcurrentDictionary _channels = new(); + + public static MessageCache Default => _instance.Value; + + /// + /// Gets message channel name for the specified message type. + /// + /// + /// + public string GetOrAddChannel() + where TMessage : class + { + return GetOrAddChannel(typeof(TMessage)); + } + + /// + /// Gets message channel name for the specified message type. + /// + /// + /// + public string GetOrAddChannel(Type messageType) + { + return _channels.GetOrAdd(messageType, _ => + { + var channelAttribute = messageType.GetCustomAttribute(); + return channelAttribute != null ? channelAttribute.Name : messageType.FullName; + }); + } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageChannelCache.cs b/Source/Euonia.Bus/Messages/MessageChannelCache.cs deleted file mode 100644 index a23c27d..0000000 --- a/Source/Euonia.Bus/Messages/MessageChannelCache.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Concurrent; -using System.Reflection; - -namespace Nerosoft.Euonia.Bus; - -internal class MessageChannelCache -{ - private static readonly Lazy _instance = new(() => new MessageChannelCache()); - - private readonly ConcurrentDictionary _channels = new(); - - public static MessageChannelCache Default => _instance.Value; - - public string GetOrAdd() - where TMessage : class - { - return GetOrAdd(typeof(TMessage)); - } - - public string GetOrAdd(Type messageType) - { - return _channels.GetOrAdd(messageType, _ => - { - var channelAttribute = messageType.GetCustomAttribute(); - return channelAttribute != null ? channelAttribute.Name : messageType.FullName; - }); - } -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageHandlerCache.cs b/Source/Euonia.Bus/Messages/MessageHandlerCache.cs deleted file mode 100644 index f060d7e..0000000 --- a/Source/Euonia.Bus/Messages/MessageHandlerCache.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Nerosoft.Euonia.Bus; - -internal class MessageHandlerCache -{ -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Serialization/NewtonsoftJsonSerializer.cs b/Source/Euonia.Bus/Serialization/NewtonsoftJsonSerializer.cs index 83096db..0a3f752 100644 --- a/Source/Euonia.Bus/Serialization/NewtonsoftJsonSerializer.cs +++ b/Source/Euonia.Bus/Serialization/NewtonsoftJsonSerializer.cs @@ -40,16 +40,19 @@ public async Task DeserializeAsync(byte[] bytes, CancellationToken cancell return await DeserializeAsync(stream, cancellationToken); } + /// public T Deserialize(byte[] bytes) { return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(bytes)); } + /// public T Deserialize(string json) { return JsonConvert.DeserializeObject(json); } + /// public T Deserialize(Stream stream) { using var reader = new StreamReader(stream, Encoding.UTF8, false, 1024, true); @@ -60,13 +63,21 @@ public T Deserialize(Stream stream) return value; } + /// public string Serialize(T obj) { return JsonConvert.SerializeObject(obj); } - public byte[] SerializeToBytes(T obj) + /// + public byte[] SerializeToByteArray(T obj) { return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(obj)); } + + /// + public object Deserialize(string json, Type type) + { + return JsonConvert.DeserializeObject(json, type); + } } \ No newline at end of file diff --git a/Source/Euonia.Bus/Serialization/SystemTextJsonSerializer.cs b/Source/Euonia.Bus/Serialization/SystemTextJsonSerializer.cs index a039c4d..d467289 100644 --- a/Source/Euonia.Bus/Serialization/SystemTextJsonSerializer.cs +++ b/Source/Euonia.Bus/Serialization/SystemTextJsonSerializer.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Text.Json.Serialization; namespace Nerosoft.Euonia.Bus; @@ -28,28 +29,64 @@ public async Task DeserializeAsync(byte[] bytes, CancellationToken cancell return await DeserializeAsync(stream, cancellationToken); } + /// public T Deserialize(byte[] bytes) { return JsonSerializer.Deserialize(Encoding.UTF8.GetString(bytes)); } + /// public T Deserialize(string json) { return JsonSerializer.Deserialize(json); } + /// public T Deserialize(Stream stream) { return JsonSerializer.Deserialize(stream); } + /// public string Serialize(T obj) { return JsonSerializer.Serialize(obj); } - public byte[] SerializeToBytes(T obj) + /// + public byte[] SerializeToByteArray(T obj) { return Encoding.UTF8.GetBytes(Serialize(obj)); } + + /// + public object Deserialize(string json, Type type) + { + return JsonSerializer.Deserialize(json, type); + } + + private static JsonSerializerOptions ConvertSettings(MessageSerializerSettings settings) + { + if (settings == null) + { + return default; + } + + var options = new JsonSerializerOptions(); + switch (settings.ReferenceLoop) + { + case MessageSerializerSettings.ReferenceLoopStrategy.Ignore: + options.ReferenceHandler = ReferenceHandler.IgnoreCycles; + break; + case MessageSerializerSettings.ReferenceLoopStrategy.Preserve: + options.ReferenceHandler = ReferenceHandler.Preserve; + break; + case MessageSerializerSettings.ReferenceLoopStrategy.Serialize: + case null: + default: + break; + } + + return options; + } } \ No newline at end of file From cfff136110a096df5ff8c67cf582a3a493746013 Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 27 Nov 2023 13:29:36 +0800 Subject: [PATCH 13/37] Remove service bus method from BaseApplicationService.cs --- .../Behaviors/CommandLoggingBehavior.cs | 31 --- .../Services/BaseApplicationService.cs | 194 ++---------------- .../Commands}/ICommand.cs | 2 +- Source/Euonia.Domain/Events/IEvent.cs | 2 +- Source/Euonia.Domain/Seedwork/IMessage.cs | 6 + 5 files changed, 22 insertions(+), 213 deletions(-) delete mode 100644 Source/Euonia.Application/Behaviors/CommandLoggingBehavior.cs rename Source/{Euonia.Bus.Abstract/Contracts => Euonia.Domain/Commands}/ICommand.cs (66%) create mode 100644 Source/Euonia.Domain/Seedwork/IMessage.cs diff --git a/Source/Euonia.Application/Behaviors/CommandLoggingBehavior.cs b/Source/Euonia.Application/Behaviors/CommandLoggingBehavior.cs deleted file mode 100644 index b4c183c..0000000 --- a/Source/Euonia.Application/Behaviors/CommandLoggingBehavior.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.Extensions.Logging; -using Nerosoft.Euonia.Domain; -using Nerosoft.Euonia.Pipeline; - -namespace Nerosoft.Euonia.Application; - -/// -/// A behavior that logs the command context and response. -/// -public class CommandLoggingBehavior : IPipelineBehavior -{ - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The logger service factory. - public CommandLoggingBehavior(ILoggerFactory factory) - { - _logger = factory.CreateLogger(); - } - - /// - public async Task HandleAsync(ICommand context, PipelineDelegate next) - { - _logger.LogInformation("Command {Id} - {FullName}: {Context}", context.Id, context.GetType().FullName, context); - var response = await next(context); - _logger.LogInformation("Command {Id} - {IsSuccess} {Message}", context.Id, response.IsSuccess, response.Message); - return response; - } -} \ No newline at end of file diff --git a/Source/Euonia.Application/Services/BaseApplicationService.cs b/Source/Euonia.Application/Services/BaseApplicationService.cs index eb89858..f10362b 100644 --- a/Source/Euonia.Application/Services/BaseApplicationService.cs +++ b/Source/Euonia.Application/Services/BaseApplicationService.cs @@ -1,7 +1,5 @@ using Nerosoft.Euonia.Bus; using Nerosoft.Euonia.Claims; -using Nerosoft.Euonia.Domain; -using Nerosoft.Euonia.Validation; namespace Nerosoft.Euonia.Application; @@ -10,182 +8,18 @@ namespace Nerosoft.Euonia.Application; /// public abstract class BaseApplicationService : IApplicationService { - /// - /// - /// - public virtual ILazyServiceProvider LazyServiceProvider { get; set; } - - /// - /// Gets the instance. - /// - protected virtual IBus Bus => LazyServiceProvider.GetService(); - - /// - /// Gets the current request user principal. - /// - protected virtual UserPrincipal User => LazyServiceProvider.GetService(); - - /// - /// Gets the default command handle timeout. - /// - protected virtual TimeSpan CommandTimeout => TimeSpan.FromSeconds(300); - - /// - /// Send command message of using . - /// - /// - /// - /// - /// - protected virtual async Task SendCommandAsync(TCommand command, Action responseHandler, CancellationToken cancellationToken = default) - where TCommand : class, ICommand - { - if (cancellationToken == default) - { - cancellationToken = new CancellationTokenSource(CommandTimeout).Token; - } - - Validator.Validate(command); - - await Bus.SendAsync(command, responseHandler, cancellationToken); - } - - /// - /// Sends a command asynchronously and returns the response. - /// - /// The type of command being sent. - /// The command being sent. - /// A cancellation token. - /// A object. - protected virtual async Task SendCommandAsync(TCommand command, CancellationToken cancellationToken = default) - where TCommand : class, ICommand - { - if (cancellationToken == default) - { - cancellationToken = new CancellationTokenSource(CommandTimeout).Token; - } - - Validator.Validate(command); - - return await Bus.SendAsync(command, cancellationToken); - } - - /// - /// Sends a command asynchronously with a typed response. - /// - /// The type of the command to send. - /// The type of the response. - /// The command to send. - /// The handler for the response object. - /// The optional cancellation token. - protected virtual async Task SendCommandAsync(TCommand command, Action> responseHandler, CancellationToken cancellationToken = default) - where TCommand : class, ICommand - { - if (cancellationToken == default) - { - cancellationToken = new CancellationTokenSource(CommandTimeout).Token; - } - - Validator.Validate(command); - - await Bus.SendAsync(command, responseHandler, cancellationToken); - } - - /// - /// - /// - /// - /// - /// - /// - /// - protected virtual async Task> SendCommandAsync(TCommand command, CancellationToken cancellationToken = default) - where TCommand : class, ICommand - { - if (cancellationToken == default) - { - cancellationToken = new CancellationTokenSource(CommandTimeout).Token; - } - - Validator.Validate(command); - - return await Bus.SendAsync>(command, cancellationToken); - } - - /// - /// - /// - /// - /// - /// - /// - protected virtual async Task SendCommandAsync(PipelineCommand context, Action responseHandler, CancellationToken cancellationToken = default) - where TCommand : class, ICommand - { - var response = await context.Pipeline.RunAsync(context.Command, async command => await SendCommandAsync((TCommand)command, cancellationToken)); - responseHandler?.Invoke(response); - } - - /// - /// - /// - /// - /// - /// - /// - /// - protected virtual async Task SendCommandAsync(PipelineCommand context, Action> responseHandler, CancellationToken cancellationToken = default) - where TCommand : class, ICommand - { - var response = await SendCommandAsync(context, cancellationToken); - responseHandler?.Invoke(response); - } - - /// - /// - /// - /// - /// - /// - /// - /// - protected virtual async Task> SendCommandAsync(PipelineCommand context, CancellationToken cancellationToken = default) - where TCommand : class, ICommand - { - var response = await context.Pipeline.RunAsync(context.Command, async command => await SendCommandAsync((TCommand)command, cancellationToken)); - return (CommandResponse)response; - } - - /// - /// Publish application event message using . - /// - /// - /// - protected async void PublishEvent(TEvent @event) - where TEvent : class - { - if (Bus == null) - { - return; - } - - await Bus.PublishAsync(@event); - } - - /// - /// Publish application event message using with specified name. - /// - /// - /// - /// - protected async void PublishEvent(string name, TEvent @event) - where TEvent : class - { - if (Bus == null) - { - return; - } - - await Bus.PublishAsync(name, @event); - } + /// + /// + /// + public virtual ILazyServiceProvider LazyServiceProvider { get; set; } + + /// + /// Gets the instance. + /// + protected virtual IBus Bus => LazyServiceProvider.GetService(); + + /// + /// Gets the current request user principal. + /// + protected virtual UserPrincipal User => LazyServiceProvider.GetService(); } \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/Contracts/ICommand.cs b/Source/Euonia.Domain/Commands/ICommand.cs similarity index 66% rename from Source/Euonia.Bus.Abstract/Contracts/ICommand.cs rename to Source/Euonia.Domain/Commands/ICommand.cs index 8abeff9..58e306c 100644 --- a/Source/Euonia.Bus.Abstract/Contracts/ICommand.cs +++ b/Source/Euonia.Domain/Commands/ICommand.cs @@ -1,4 +1,4 @@ -namespace Nerosoft.Euonia.Bus; +namespace Nerosoft.Euonia.Domain; /// /// diff --git a/Source/Euonia.Domain/Events/IEvent.cs b/Source/Euonia.Domain/Events/IEvent.cs index 65eccbd..1ee6c6a 100644 --- a/Source/Euonia.Domain/Events/IEvent.cs +++ b/Source/Euonia.Domain/Events/IEvent.cs @@ -3,7 +3,7 @@ /// /// The event interface. /// -public interface IEvent +public interface IEvent : IMessage { /// /// Gets the intent of the event. diff --git a/Source/Euonia.Domain/Seedwork/IMessage.cs b/Source/Euonia.Domain/Seedwork/IMessage.cs new file mode 100644 index 0000000..4159825 --- /dev/null +++ b/Source/Euonia.Domain/Seedwork/IMessage.cs @@ -0,0 +1,6 @@ +namespace Nerosoft.Euonia.Domain; + +public interface IMessage +{ + +} \ No newline at end of file From 91a6f1e1c2e537c72f112ab9a69fd1af416badeb Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 27 Nov 2023 13:32:53 +0800 Subject: [PATCH 14/37] Upgrade project version to 7.6.0 --- Samples/common.props | 2 +- Source/common.props | 2 +- Tests/common.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Samples/common.props b/Samples/common.props index a063b65..846cd2e 100644 --- a/Samples/common.props +++ b/Samples/common.props @@ -2,7 +2,7 @@ net7.0 Nerosoft.Euonia.Sample - 7.2.0 + 7.6.0 damon Nerosoft Co., Ltd. Euonia diff --git a/Source/common.props b/Source/common.props index 4791570..3b62baa 100644 --- a/Source/common.props +++ b/Source/common.props @@ -2,7 +2,7 @@ net6.0;net7.0 Nerosoft.$(MSBuildProjectName.Replace(" ", "_")) - 7.2.3 + 7.6.0 damon Nerosoft Co., Ltd. Euonia diff --git a/Tests/common.props b/Tests/common.props index d6d55b4..a8ef460 100644 --- a/Tests/common.props +++ b/Tests/common.props @@ -2,7 +2,7 @@ net6.0;net7.0 Nerosoft.$(MSBuildProjectName.Replace(" ", "_")) - 7.2.0 + 7.6.0 damon Nerosoft Co., Ltd. Euonia From 7fda9984bfeeb7df6885d1e7a18702bdfeb9bc4b Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 27 Nov 2023 20:27:36 +0800 Subject: [PATCH 15/37] Fixes warnings. --- .../Seedwork/PipelineCommand.cs | 125 ++++++------ .../Messenger/IMessenger.cs | 182 ++++++++---------- .../Messenger/MessengerExtensions.cs | 153 +-------------- .../Conventions/DefaultMessageConvention.cs | 4 +- Source/Euonia.Domain/Seedwork/IMessage.cs | 4 +- 5 files changed, 153 insertions(+), 315 deletions(-) diff --git a/Source/Euonia.Application/Seedwork/PipelineCommand.cs b/Source/Euonia.Application/Seedwork/PipelineCommand.cs index 39d4191..3577b4e 100644 --- a/Source/Euonia.Application/Seedwork/PipelineCommand.cs +++ b/Source/Euonia.Application/Seedwork/PipelineCommand.cs @@ -1,5 +1,4 @@ -using Nerosoft.Euonia.Bus; -using Nerosoft.Euonia.Domain; +using Nerosoft.Euonia.Domain; using Nerosoft.Euonia.Pipeline; namespace Nerosoft.Euonia.Application; @@ -9,61 +8,61 @@ namespace Nerosoft.Euonia.Application; /// /// public class PipelineCommand - where TCommand : ICommand + where TCommand : ICommand { - /// - /// - /// - /// - public PipelineCommand(TCommand command) - { - Command = command; - } + /// + /// + /// + /// + public PipelineCommand(TCommand command) + { + Command = command; + } - /// - /// - /// - /// - /// - public PipelineCommand(TCommand command, IPipeline pipeline) - { - Command = command; - Pipeline = pipeline; - } + /// + /// + /// + /// + /// + public PipelineCommand(TCommand command, IPipeline pipeline) + { + Command = command; + Pipeline = pipeline; + } - /// - /// - /// - public TCommand Command { get; } + /// + /// + /// + public TCommand Command { get; } - /// - /// - /// - public IPipeline Pipeline { get; private set; } + /// + /// + /// + public IPipeline Pipeline { get; private set; } - /// - /// - /// - /// - /// - /// - public PipelineCommand Use(Type type, params object[] args) - { - Pipeline = Pipeline.Use(type, args); - return this; - } + /// + /// + /// + /// + /// + /// + public PipelineCommand Use(Type type, params object[] args) + { + Pipeline = Pipeline.Use(type, args); + return this; + } - /// - /// - /// - /// - /// - public PipelineCommand Use() - where TBehavior : IPipelineBehavior - { - Pipeline = Pipeline.Use(); - return this; - } + /// + /// + /// + /// + /// + public PipelineCommand Use() + where TBehavior : IPipelineBehavior + { + Pipeline = Pipeline.Use(); + return this; + } } /// @@ -71,16 +70,16 @@ public PipelineCommand Use() /// public static class PipelineCommandExtensions { - /// - /// Use pipeline. - /// - /// - /// - /// - /// - public static PipelineCommand UsePipeline(this TCommand command, IPipeline pipeline) - where TCommand : ICommand - { - return new PipelineCommand(command, pipeline); - } + /// + /// Use pipeline. + /// + /// + /// + /// + /// + public static PipelineCommand UsePipeline(this TCommand command, IPipeline pipeline) + where TCommand : ICommand + { + return new PipelineCommand(command, pipeline); + } } \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Messenger/IMessenger.cs b/Source/Euonia.Bus.InMemory/Messenger/IMessenger.cs index 14b5888..325095c 100644 --- a/Source/Euonia.Bus.InMemory/Messenger/IMessenger.cs +++ b/Source/Euonia.Bus.InMemory/Messenger/IMessenger.cs @@ -1,9 +1,3 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; - namespace Nerosoft.Euonia.Bus.InMemory; /// @@ -48,105 +42,97 @@ namespace Nerosoft.Euonia.Bus.InMemory; /// /// Messenger.Default.Register(this, Receive); /// -/// The C# compiler will automatically convert that expression to a instance -/// compatible with . -/// This will also work if multiple overloads of that method are available, each handling a different -/// message type: the C# compiler will automatically pick the right one for the current message type. -/// It is also possible to register message handlers explicitly using the interface. -/// To do so, the recipient just needs to implement the interface and then call the -/// extension, which will automatically register -/// all the handlers that are declared by the recipient type. Registration for individual handlers is supported as well. /// public interface IMessenger { - /// - /// Checks whether or not a given recipient has already been registered for a message. - /// - /// The type of message to check for the given recipient. - /// The type of token to check the channel for. - /// The target recipient to check the registration for. - /// The token used to identify the target channel to check. - /// Whether or not has already been registered for the specified message. - /// Thrown if or are . - bool IsRegistered(object recipient, TToken token) - where TMessage : class - where TToken : IEquatable; + /// + /// Checks whether or not a given recipient has already been registered for a message. + /// + /// The type of message to check for the given recipient. + /// The type of token to check the channel for. + /// The target recipient to check the registration for. + /// The token used to identify the target channel to check. + /// Whether or not has already been registered for the specified message. + /// Thrown if or are . + bool IsRegistered(object recipient, TToken token) + where TMessage : class + where TToken : IEquatable; - /// - /// Registers a recipient for a given type of message. - /// - /// The type of recipient for the message. - /// The type of message to receive. - /// The type of token to use to pick the messages to receive. - /// The recipient that will receive the messages. - /// A token used to determine the receiving channel to use. - /// The to invoke when a message is received. - /// Thrown if , or are . - /// Thrown when trying to register the same message twice. - void Register(TRecipient recipient, TToken token, MessageHandler handler) - where TRecipient : class - where TMessage : class - where TToken : IEquatable; + /// + /// Registers a recipient for a given type of message. + /// + /// The type of recipient for the message. + /// The type of message to receive. + /// The type of token to use to pick the messages to receive. + /// The recipient that will receive the messages. + /// A token used to determine the receiving channel to use. + /// The to invoke when a message is received. + /// Thrown if , or are . + /// Thrown when trying to register the same message twice. + void Register(TRecipient recipient, TToken token, MessageHandler handler) + where TRecipient : class + where TMessage : class + where TToken : IEquatable; - /// - /// Unregisters a recipient from all registered messages. - /// - /// The recipient to unregister. - /// - /// This method will unregister the target recipient across all channels. - /// Use this method as an easy way to lose all references to a target recipient. - /// If the recipient has no registered handler, this method does nothing. - /// - /// Thrown if is . - void UnregisterAll(object recipient); + /// + /// Unregisters a recipient from all registered messages. + /// + /// The recipient to unregister. + /// + /// This method will unregister the target recipient across all channels. + /// Use this method as an easy way to lose all references to a target recipient. + /// If the recipient has no registered handler, this method does nothing. + /// + /// Thrown if is . + void UnregisterAll(object recipient); - /// - /// Unregisters a recipient from all messages on a specific channel. - /// - /// The type of token to identify what channel to unregister from. - /// The recipient to unregister. - /// The token to use to identify which handlers to unregister. - /// If the recipient has no registered handler, this method does nothing. - /// Thrown if or are . - void UnregisterAll(object recipient, TToken token) - where TToken : IEquatable; + /// + /// Unregisters a recipient from all messages on a specific channel. + /// + /// The type of token to identify what channel to unregister from. + /// The recipient to unregister. + /// The token to use to identify which handlers to unregister. + /// If the recipient has no registered handler, this method does nothing. + /// Thrown if or are . + void UnregisterAll(object recipient, TToken token) + where TToken : IEquatable; - /// - /// Unregisters a recipient from messages of a given type. - /// - /// The type of message to stop receiving. - /// The type of token to identify what channel to unregister from. - /// The recipient to unregister. - /// The token to use to identify which handlers to unregister. - /// If the recipient has no registered handler, this method does nothing. - /// Thrown if or are . - void Unregister(object recipient, TToken token) - where TMessage : class - where TToken : IEquatable; + /// + /// Unregisters a recipient from messages of a given type. + /// + /// The type of message to stop receiving. + /// The type of token to identify what channel to unregister from. + /// The recipient to unregister. + /// The token to use to identify which handlers to unregister. + /// If the recipient has no registered handler, this method does nothing. + /// Thrown if or are . + void Unregister(object recipient, TToken token) + where TMessage : class + where TToken : IEquatable; - /// - /// Sends a message of the specified type to all registered recipients. - /// - /// The type of message to send. - /// The type of token to identify what channel to use to send the message. - /// The message to send. - /// The token indicating what channel to use. - /// The message that was sent (ie. ). - /// Thrown if or are . - TMessage Send(TMessage message, TToken token) - where TMessage : class - where TToken : IEquatable; + /// + /// Sends a message of the specified type to all registered recipients. + /// + /// The type of message to send. + /// The type of token to identify what channel to use to send the message. + /// The message to send. + /// The token indicating what channel to use. + /// The message that was sent (ie. ). + /// Thrown if or are . + TMessage Send(TMessage message, TToken token) + where TMessage : class + where TToken : IEquatable; - /// - /// Performs a cleanup on the current messenger. - /// Invoking this method does not unregister any of the currently registered - /// recipient, and it can be used to perform cleanup operations such as - /// trimming the internal data structures of a messenger implementation. - /// - void Cleanup(); + /// + /// Performs a cleanup on the current messenger. + /// Invoking this method does not unregister any of the currently registered + /// recipient, and it can be used to perform cleanup operations such as + /// trimming the internal data structures of a messenger implementation. + /// + void Cleanup(); - /// - /// Resets the instance and unregisters all the existing recipients. - /// - void Reset(); -} + /// + /// Resets the instance and unregisters all the existing recipients. + /// + void Reset(); +} \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/Messenger/MessengerExtensions.cs b/Source/Euonia.Bus.InMemory/Messenger/MessengerExtensions.cs index 9049b23..9c45bbc 100644 --- a/Source/Euonia.Bus.InMemory/Messenger/MessengerExtensions.cs +++ b/Source/Euonia.Bus.InMemory/Messenger/MessengerExtensions.cs @@ -1,6 +1,7 @@ -using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; +// ReSharper disable UnusedType.Local +// ReSharper disable UnusedMember.Local namespace Nerosoft.Euonia.Bus.InMemory; @@ -71,156 +72,6 @@ public static bool IsRegistered(this IMessenger messenger, object reci return messenger.IsRegistered(recipient, default); } - /// - /// Registers all declared message handlers for a given recipient, using the default channel. - /// - /// The instance to use to register the recipient. - /// The recipient that will receive the messages. - /// See notes for for more info. - /// Thrown if or are . - public static void RegisterAll(this IMessenger messenger, object recipient) - { - ArgumentAssert.ThrowIfNull(messenger); - ArgumentAssert.ThrowIfNull(recipient); - - // We use this method as a callback for the conditional weak table, which will handle - // thread-safety for us. This first callback will try to find a generated method for the - // target recipient type, and just invoke it to get the delegate to cache and use later. - static Action LoadRegistrationMethodsForType(Type recipientType) - { - if (recipientType.Assembly.GetType("CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions") is Type extensionsType && - extensionsType.GetMethod("CreateAllMessagesRegistrator", new[] { recipientType }) is MethodInfo methodInfo) - { - return (Action)methodInfo.Invoke(null, new object[] { null })!; - } - - return null; - } - - // Try to get the cached delegate, if the generator has run correctly - Action registrationAction = DiscoveredRecipients.RegistrationMethods.GetValue( - recipient.GetType(), - static (t) => LoadRegistrationMethodsForType(t)); - - if (registrationAction is not null) - { - registrationAction(messenger, recipient); - } - else - { - messenger.RegisterAll(recipient, default(Unit)); - } - } - - /// - /// Registers all declared message handlers for a given recipient. - /// - /// The type of token to identify what channel to use to receive messages. - /// The instance to use to register the recipient. - /// The recipient that will receive the messages. - /// The token indicating what channel to use. - /// - /// This method will register all messages corresponding to the interfaces - /// being implemented by . If none are present, this method will do nothing. - /// Note that unlike all other extensions, this method will use reflection to find the handlers to register. - /// Once the registration is complete though, the performance will be exactly the same as with handlers - /// registered directly through any of the other generic extensions for the interface. - /// - /// Thrown if , or are . - public static void RegisterAll(this IMessenger messenger, object recipient, TToken token) - where TToken : IEquatable - { - ArgumentAssert.ThrowIfNull(messenger); - ArgumentAssert.ThrowIfNull(recipient); - ArgumentAssert.For.ThrowIfNull(token); - - // We use this method as a callback for the conditional weak table, which will handle - // thread-safety for us. This first callback will try to find a generated method for the - // target recipient type, and just invoke it to get the delegate to cache and use later. - // In this case we also need to create a generic instantiation of the target method first. - static Action LoadRegistrationMethodsForType(Type recipientType) - { - if (recipientType.Assembly.GetType("CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions") is Type extensionsType && - extensionsType.GetMethod("CreateAllMessagesRegistratorWithToken", new[] { recipientType }) is MethodInfo methodInfo) - { - MethodInfo genericMethodInfo = methodInfo.MakeGenericMethod(typeof(TToken)); - - return (Action)genericMethodInfo.Invoke(null, new object[] { null })!; - } - - return LoadRegistrationMethodsForTypeFallback(recipientType); - } - - // Fallback method when a generated method is not found. - // This method is only invoked once per recipient type and token type, so we're not - // worried about making it super efficient, and we can use the LINQ code for clarity. - // The LINQ codegen bloat is not really important for the same reason. - static Action LoadRegistrationMethodsForTypeFallback(Type recipientType) - { - // Get the collection of validation methods - MethodInfo[] registrationMethods = ( - from interfaceType in recipientType.GetInterfaces() - where interfaceType.IsGenericType && - interfaceType.GetGenericTypeDefinition() == typeof(IRecipient<>) - let messageType = interfaceType.GenericTypeArguments[0] - select MethodInfos.RegisterIRecipient.MakeGenericMethod(messageType, typeof(TToken))).ToArray(); - - // Short path if there are no message handlers to register - if (registrationMethods.Length == 0) - { - return static (_, _, _) => - { - }; - } - - // Input parameters (IMessenger instance, non-generic recipient, token) - ParameterExpression arg0 = Expression.Parameter(typeof(IMessenger)); - ParameterExpression arg1 = Expression.Parameter(typeof(object)); - ParameterExpression arg2 = Expression.Parameter(typeof(TToken)); - - // Declare a local resulting from the (RecipientType)recipient cast - UnaryExpression inst1 = Expression.Convert(arg1, recipientType); - - // We want a single compiled LINQ expression that executes the registration for all - // the declared message types in the input type. To do so, we create a block with the - // unrolled invocations for the individual message registration (for each IRecipient). - // The code below will generate the following block expression: - // =============================================================================== - // { - // var inst1 = (RecipientType)arg1; - // IMessengerExtensions.Register(arg0, inst1, arg2); - // IMessengerExtensions.Register(arg0, inst1, arg2); - // ... - // IMessengerExtensions.Register(arg0, inst1, arg2); - // } - // =============================================================================== - // We also add an explicit object conversion to cast the input recipient type to - // the actual specific type, so that the exposed message handlers are accessible. - BlockExpression body = Expression.Block( - from registrationMethod in registrationMethods - select Expression.Call(registrationMethod, new Expression[] - { - arg0, - inst1, - arg2 - })); - - return Expression.Lambda>(body, arg0, arg1, arg2).Compile(); - } - - // Get or compute the registration method for the current recipient type. - // As in CommunityToolkit.Diagnostics.TypeExtensions.ToTypeString, we use a lambda - // expression instead of a method group expression to leverage the statically initialized - // delegate and avoid repeated allocations for each invocation of this method. - // For more info on this, see the related issue at https://github.com/dotnet/roslyn/issues/5835. - Action registrationAction = DiscoveredRecipients.RegistrationMethods.GetValue( - recipient.GetType(), - static (t) => LoadRegistrationMethodsForType(t)); - - // Invoke the cached delegate to actually execute the message registration - registrationAction(messenger, recipient, token); - } - /// /// Registers a recipient for a given type of message. /// diff --git a/Source/Euonia.Bus/Conventions/DefaultMessageConvention.cs b/Source/Euonia.Bus/Conventions/DefaultMessageConvention.cs index 6353b14..3ebcb23 100644 --- a/Source/Euonia.Bus/Conventions/DefaultMessageConvention.cs +++ b/Source/Euonia.Bus/Conventions/DefaultMessageConvention.cs @@ -13,7 +13,7 @@ public bool IsQueueType(Type type) { if (type == null) { - throw new ArgumentNullException(nameof(type), "Type cannot be null."); + throw new ArgumentNullException(nameof(type), Resources.IDS_TYPE_CANNOT_NULL); } return type.IsAssignableTo(typeof(IQueue)) && type != typeof(IQueue); @@ -24,7 +24,7 @@ public bool IsTopicType(Type type) { if (type == null) { - throw new ArgumentNullException(nameof(type), "Type cannot be null."); + throw new ArgumentNullException(nameof(type), Resources.IDS_TYPE_CANNOT_NULL); } return type.IsAssignableTo(typeof(ITopic)) && type != typeof(ITopic); diff --git a/Source/Euonia.Domain/Seedwork/IMessage.cs b/Source/Euonia.Domain/Seedwork/IMessage.cs index 4159825..53482c5 100644 --- a/Source/Euonia.Domain/Seedwork/IMessage.cs +++ b/Source/Euonia.Domain/Seedwork/IMessage.cs @@ -1,6 +1,8 @@ namespace Nerosoft.Euonia.Domain; +/// +/// +/// public interface IMessage { - } \ No newline at end of file From 0d7c358e2d4b819b19e594d325cf7c1e136c4adc Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 27 Nov 2023 20:28:14 +0800 Subject: [PATCH 16/37] Rename interface TypeDictionary to ITypeDictionary. --- .../Internal/TypeDictionary.Interface.cs | 12 ++++++------ .../Internal/TypeDictionary.cs | 4 ++-- .../Messenger/StrongReferenceMessenger.cs | 16 ++++++++-------- .../Messenger/WeakReferenceMessenger.cs | 8 ++++---- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Source/Euonia.Bus.InMemory/Internal/TypeDictionary.Interface.cs b/Source/Euonia.Bus.InMemory/Internal/TypeDictionary.Interface.cs index b4dd9bd..061c73f 100644 --- a/Source/Euonia.Bus.InMemory/Internal/TypeDictionary.Interface.cs +++ b/Source/Euonia.Bus.InMemory/Internal/TypeDictionary.Interface.cs @@ -1,9 +1,9 @@ namespace Nerosoft.Euonia.Bus.InMemory; /// -/// A base interface masking instances and exposing non-generic functionalities. +/// A base interface masking instances and exposing non-generic functionalities. /// -internal interface TypeDictionary +internal interface ITypeDictionary { /// /// Gets the count of entries in the dictionary. @@ -17,10 +17,10 @@ internal interface TypeDictionary } /// -/// An interface providing key type contravariant access to a instance. +/// An interface providing key type contravariant access to a instance. /// /// The contravariant type of keys in the dictionary. -internal interface TypeDictionary : TypeDictionary +internal interface ITypeDictionary : ITypeDictionary where TKey : IEquatable { /// @@ -33,11 +33,11 @@ internal interface TypeDictionary : TypeDictionary /// /// An interface providing key type contravariant and value type covariant access -/// to a instance. +/// to a instance. /// /// The contravariant type of keys in the dictionary. /// The covariant type of values in the dictionary. -internal interface ITypeDictionary : TypeDictionary +internal interface ITypeDictionary : ITypeDictionary where TKey : IEquatable where TValue : class { diff --git a/Source/Euonia.Bus.InMemory/Internal/TypeDictionary.cs b/Source/Euonia.Bus.InMemory/Internal/TypeDictionary.cs index eff66f5..09959ce 100644 --- a/Source/Euonia.Bus.InMemory/Internal/TypeDictionary.cs +++ b/Source/Euonia.Bus.InMemory/Internal/TypeDictionary.cs @@ -52,7 +52,7 @@ internal class TypeDictionary : ITypeDictionary private int _freeCount; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public TypeDictionary() { @@ -256,7 +256,7 @@ public ref TValue GetOrAddValueRef(TKey key) public Enumerator GetEnumerator() => new(this); /// - /// Enumerator for . + /// Enumerator for . /// public ref struct Enumerator { diff --git a/Source/Euonia.Bus.InMemory/Messenger/StrongReferenceMessenger.cs b/Source/Euonia.Bus.InMemory/Messenger/StrongReferenceMessenger.cs index 1bf19d2..3a3c30d 100644 --- a/Source/Euonia.Bus.InMemory/Messenger/StrongReferenceMessenger.cs +++ b/Source/Euonia.Bus.InMemory/Messenger/StrongReferenceMessenger.cs @@ -15,7 +15,7 @@ public sealed class StrongReferenceMessenger : IMessenger { // This messenger uses the following logic to link stored instances together: // -------------------------------------------------------------------------------------------------------- - // TypeDictionary> recipientsMap; + // ITypeDictionary> recipientsMap; // | \________________[*]ITypeDictionary> // | \_______________[*]ITypeDictionary / // | \_________/_________/___ / @@ -23,16 +23,16 @@ public sealed class StrongReferenceMessenger : IMessenger // | \__________________ / _____(channel registrations)_____/______\____/ // | \ / / __________________________/ \ // | / / / \ - // | TypeDictionary mapping = Mapping________________\ + // | ITypeDictionary mapping = Mapping________________\ // | __________________/ / | / \ // |/ / | / \ - // TypeDictionary> mapping = Mapping____________\ + // ITypeDictionary> mapping = Mapping____________\ // / / / / // ___(EquatableType.TToken)____/ / / / // /________________(EquatableType.TMessage)_______/_______/__/ // / ________________________________/ // / / - // TypeDictionary typesMap; + // ITypeDictionary typesMap; // -------------------------------------------------------------------------------------------------------- // Each combination of results in a concrete Mapping type (if TToken is Unit) or Mapping type, // which holds the references from registered recipients to handlers. Mapping is used when the default channel is being @@ -81,7 +81,7 @@ public sealed class StrongReferenceMessenger : IMessenger /// The and instance for types combination. /// /// - /// The values are just of type as we don't know the type parameters in advance. + /// The values are just of type as we don't know the type parameters in advance. /// Each method relies on to get the type-safe instance of the /// or class for each pair of generic arguments in use. /// @@ -307,7 +307,7 @@ public void UnregisterAll(object recipient, TToken token) foreach (var item in set) { // Select all mappings using the same token type - if (item is ITypeDictionary> mapping) + if (item is ITypeDictionary> mapping) { maps[i++] = mapping; } @@ -322,7 +322,7 @@ public void UnregisterAll(object recipient, TToken token) // matches with the token type currently in use, and operate on those instances. foreach (var obj in maps.AsSpan(0, i)) { - var handlersMap = Unsafe.As>>(obj); + var handlersMap = Unsafe.As>>(obj); // We don't need whether or not the map contains the recipient, as the // sequence of maps has already been copied from the set containing all @@ -783,7 +783,7 @@ public static Mapping Create() /// An interface for the and types which allows to retrieve /// the type arguments from a given generic instance without having any prior knowledge about those arguments. /// - private interface IMapping : TypeDictionary + private interface IMapping : ITypeDictionary { /// /// Gets the instance representing the current type arguments. diff --git a/Source/Euonia.Bus.InMemory/Messenger/WeakReferenceMessenger.cs b/Source/Euonia.Bus.InMemory/Messenger/WeakReferenceMessenger.cs index 6245173..1eb9801 100644 --- a/Source/Euonia.Bus.InMemory/Messenger/WeakReferenceMessenger.cs +++ b/Source/Euonia.Bus.InMemory/Messenger/WeakReferenceMessenger.cs @@ -21,20 +21,20 @@ public sealed class WeakReferenceMessenger : IMessenger { // This messenger uses the following logic to link stored instances together: // -------------------------------------------------------------------------------------------------------- - // TypeDictionary mapping + // ITypeDictionary mapping // / / / // ___(EquatableType.TToken)___/ / / ___(if EquatableType.TToken is Unit) // /_________(EquatableType.TMessage)______________/ / / // / _________________/___MessageHandlerDispatcher? // / / \ - // TypeDictionary> recipientsMap; \___(null if using IRecipient) + // ITypeDictionary> recipientsMap; \___(null if using IRecipient) // -------------------------------------------------------------------------------------------------------- // Just like in the strong reference variant, each pair of message and token types is used as a key in the // recipients map. In this case, the values in the dictionary are ConditionalWeakTable2<,> instances, that // link each registered recipient to a map of currently registered handlers, through dependent handles. This // ensures that handlers will remain alive as long as their associated recipient is also alive (so there is no // need for users to manually indicate whether a given handler should be kept alive in case it creates a closure). - // The value in each conditional table can either be TypeDictionary or object. The + // The value in each conditional table can either be ITypeDictionary or object. The // first case is used when any token type other than the default Unit type is used, as in this case there could be // multiple handlers for each recipient that need to be tracked separately. In order to invoke all the handlers from // a context where their type parameters is not known, handlers are stored as MessageHandlerDispatcher instances. There @@ -498,7 +498,7 @@ private void CleanupWithoutLock() { while (recipientsEnumerator.MoveNext()) { - if (Unsafe.As(recipientsEnumerator.GetValue()!).Count == 0) + if (Unsafe.As(recipientsEnumerator.GetValue()!).Count == 0) { emptyRecipients.Add(recipientsEnumerator.GetKey()); } From e237b6ab8d5019206962fa5077f240678031ea66 Mon Sep 17 00:00:00 2001 From: damon Date: Mon, 27 Nov 2023 20:28:53 +0800 Subject: [PATCH 17/37] Rename. --- .../Conventions/MessageConvention.cs | 6 ++--- .../OverridableMessageConvention.cs | 22 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Source/Euonia.Bus/Conventions/MessageConvention.cs b/Source/Euonia.Bus/Conventions/MessageConvention.cs index 4d33354..65870fd 100644 --- a/Source/Euonia.Bus/Conventions/MessageConvention.cs +++ b/Source/Euonia.Bus/Conventions/MessageConvention.cs @@ -18,7 +18,7 @@ public class MessageConvention /// /// /// - public bool IsCommandType(Type type) + public bool IsQueueType(Type type) { ArgumentAssert.ThrowIfNull(type); @@ -35,7 +35,7 @@ public bool IsCommandType(Type type) /// /// /// - public bool IsEventType(Type type) + public bool IsTopicType(Type type) { ArgumentAssert.ThrowIfNull(type); @@ -60,7 +60,7 @@ internal void Add(params IMessageConvention[] conventions) { if (conventions == null || conventions.Length == 0) { - throw new ArgumentException("At least one convention must be provided.", nameof(conventions)); + throw new ArgumentException(Resources.IDS_CONVENTION_PROVIDER_REQUIRED, nameof(conventions)); } _conventions.AddRange(conventions); diff --git a/Source/Euonia.Bus/Conventions/OverridableMessageConvention.cs b/Source/Euonia.Bus/Conventions/OverridableMessageConvention.cs index 343dfb2..11826ae 100644 --- a/Source/Euonia.Bus/Conventions/OverridableMessageConvention.cs +++ b/Source/Euonia.Bus/Conventions/OverridableMessageConvention.cs @@ -3,7 +3,7 @@ internal class OverridableMessageConvention : IMessageConvention { private readonly IMessageConvention _innerConvention; - private Func _isCommandType, _isEventType; + private Func _isQueueType, _isTopicType; public OverridableMessageConvention(IMessageConvention innerConvention) { @@ -14,33 +14,33 @@ public OverridableMessageConvention(IMessageConvention innerConvention) bool IMessageConvention.IsQueueType(Type type) { - return IsCommandType(type); + return IsQueueType(type); } bool IMessageConvention.IsTopicType(Type type) { - return IsEventType(type); + return IsTopicType(type); } - public Func IsCommandType + public Func IsQueueType { - get => _isCommandType ?? _innerConvention.IsQueueType; - set => _isCommandType = value; + get => _isQueueType ?? _innerConvention.IsQueueType; + set => _isQueueType = value; } - public Func IsEventType + public Func IsTopicType { - get => _isEventType ?? _innerConvention.IsTopicType; - set => _isEventType = value; + get => _isTopicType ?? _innerConvention.IsTopicType; + set => _isTopicType = value; } public void DefineCommandType(Func convention) { - _isCommandType = convention; + _isQueueType = convention; } public void DefineEventType(Func convention) { - _isEventType = convention; + _isTopicType = convention; } } \ No newline at end of file From 6e9948394f5f07e8d86da3ce58e652ba25936dcb Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 28 Nov 2023 00:45:56 +0800 Subject: [PATCH 18/37] Use Expression.Call instead of reflecting. --- Source/Euonia.Bus/BusConfigurator.cs | 44 +++- Source/Euonia.Bus/Core/HandlerContext.cs | 104 ++++---- Source/Euonia.Bus/Core/IBus.cs | 19 ++ Source/Euonia.Bus/Core/IHandler.cs | 17 -- Source/Euonia.Bus/Core/ServiceBus.cs | 37 ++- .../Messages/MessageHandlerFactory.cs | 6 + .../Messages/MessageRegistration.cs | 44 ++++ .../Messages/MessageSubscription.cs | 54 ----- Source/Euonia.Bus/Properties/Resources.resx | 223 ++++++++++-------- .../Serialization}/IMessageSerializer.cs | 0 .../MessageSerializerSettings.cs | 0 .../Serialization/SystemTextJsonSerializer.cs | 18 +- .../Euonia.Bus/ServiceCollectionExtensions.cs | 16 +- 13 files changed, 326 insertions(+), 256 deletions(-) create mode 100644 Source/Euonia.Bus/Messages/MessageHandlerFactory.cs create mode 100644 Source/Euonia.Bus/Messages/MessageRegistration.cs delete mode 100644 Source/Euonia.Bus/Messages/MessageSubscription.cs rename Source/{Euonia.Bus.Abstract => Euonia.Bus/Serialization}/IMessageSerializer.cs (100%) rename Source/{Euonia.Bus.Abstract => Euonia.Bus/Serialization}/MessageSerializerSettings.cs (100%) diff --git a/Source/Euonia.Bus/BusConfigurator.cs b/Source/Euonia.Bus/BusConfigurator.cs index b10dcff..e6e5943 100644 --- a/Source/Euonia.Bus/BusConfigurator.cs +++ b/Source/Euonia.Bus/BusConfigurator.cs @@ -18,7 +18,7 @@ public class BusConfigurator : IBusConfigurator /// /// The message handler types. /// - internal List Registrations { get; } = new(); + internal List Registrations { get; } = new(); /// /// Initialize a new instance of @@ -35,7 +35,7 @@ public BusConfigurator(IServiceCollection service) /// public IEnumerable GetSubscriptions() { - return Registrations.Select(t => t.Name); + return Registrations.Select(t => t.Channel); } /// @@ -162,7 +162,7 @@ public BusConfigurator RegisterHandlers(IEnumerable handlerTypes) if (handlerType.IsImplementsGeneric(typeof(IHandler<>))) { - var interfaces = handlerType.GetInterfaces().Where(t => t.IsImplementsGeneric(typeof(IHandler<>))); + var interfaces = handlerType.GetInterfaces().Where(t => t.IsGenericType); foreach (var @interface in interfaces) { @@ -173,10 +173,16 @@ public BusConfigurator RegisterHandlers(IEnumerable handlerTypes) continue; } - Registrations.Add(new MessageSubscription(messageType, handlerType, @interface.GetRuntimeMethod(nameof(IHandler.HandleAsync), new[] { messageType, typeof(MessageContext), typeof(CancellationToken) }))); + var method = @interface.GetMethod(nameof(IHandler.HandleAsync), BINDING_FLAGS, null, new[] { messageType, typeof(MessageContext), typeof(CancellationToken) }, null); + + var registration = new MessageRegistration(MessageCache.Default.GetOrAddChannel(messageType), messageType, handlerType, method); + + Registrations.Add(registration); + + Service.TryAddScoped(typeof(IHandler<>).MakeGenericType(messageType), handlerType); } - Service.TryAddScoped(typeof(IHandler<>), handlerType); + Service.TryAddScoped(handlerType); } else { @@ -193,7 +199,7 @@ public BusConfigurator RegisterHandlers(IEnumerable handlerTypes) if (!parameters.Any(t => t.ParameterType != typeof(CancellationToken) && t.ParameterType != typeof(MessageContext))) { - throw new InvalidOperationException("Invalid handler method"); + throw new InvalidOperationException("Invalid handler method."); } var firstParameter = parameters[0]; @@ -215,7 +221,8 @@ public BusConfigurator RegisterHandlers(IEnumerable handlerTypes) foreach (var attribute in attributes) { - Registrations.Add(new MessageSubscription(attribute.Name, handlerType, method)); + var registration = new MessageRegistration(attribute.Name, firstParameter.ParameterType, handlerType, method); + Registrations.Add(registration); } } @@ -224,6 +231,29 @@ public BusConfigurator RegisterHandlers(IEnumerable handlerTypes) } return this; + + void ValidateMessageType(Type messageType) + { + if (messageType.IsPrimitiveType()) + { + throw new InvalidOperationException("The message type cannot be a primitive type."); + } + + if (messageType.IsClass) + { + throw new InvalidOperationException("The message type must be a class."); + } + + if (messageType.IsAbstract) + { + throw new InvalidOperationException("The message type cannot be an abstract class."); + } + + if (messageType.IsInterface) + { + throw new InvalidOperationException("The message type cannot be an interface."); + } + } } /// diff --git a/Source/Euonia.Bus/Core/HandlerContext.cs b/Source/Euonia.Bus/Core/HandlerContext.cs index 7560e97..1864812 100644 --- a/Source/Euonia.Bus/Core/HandlerContext.cs +++ b/Source/Euonia.Bus/Core/HandlerContext.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Linq.Expressions; using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -15,46 +16,55 @@ public class HandlerContext : IHandlerContext /// public event EventHandler MessageSubscribed; - private readonly ConcurrentDictionary> _handlerContainer = new(); - private static readonly ConcurrentDictionary _messageTypeMapping = new(); + private readonly ConcurrentDictionary> _handlerContainer = new(); private readonly IServiceProvider _provider; - private readonly MessageConvert _convert; private readonly ILogger _logger; /// /// Initialize a new instance of /// /// - /// - public HandlerContext(IServiceProvider provider, MessageConvert convert) + public HandlerContext(IServiceProvider provider) { _provider = provider; - _convert = convert; _logger = provider.GetService()?.CreateLogger(); } - #region Handler register + #region Handling register internal virtual void Register() where TMessage : class where THandler : IHandler { - Register(typeof(TMessage), typeof(THandler), typeof(THandler).GetMethod(nameof(IHandler.HandleAsync), BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)); - } + var channel = MessageCache.Default.GetOrAddChannel(); - internal virtual void Register(Type messageType, Type handlerType, MethodInfo method) - { - var messageName = messageType.FullName; + MessageHandler Handling(IServiceProvider provider) + { + var handler = provider.GetService(); + return (message, context, token) => handler.HandleAsync((TMessage)message, context, token); + } - _messageTypeMapping.GetOrAdd(messageName, messageType); - ConcurrentDictionarySafeRegister(messageName, (handlerType, method), _handlerContainer); - MessageSubscribed?.Invoke(this, new MessageSubscribedEventArgs(messageType, handlerType)); + ConcurrentDictionarySafeRegister(channel, Handling, _handlerContainer); + MessageSubscribed?.Invoke(this, new MessageSubscribedEventArgs(channel, typeof(TMessage), typeof(THandler))); } - internal void Register(string messageName, Type handlerType, MethodInfo method) + internal void Register(MessageRegistration registration) { - ConcurrentDictionarySafeRegister(messageName, (handlerType, method), _handlerContainer); - MessageSubscribed?.Invoke(this, new MessageSubscribedEventArgs(messageName, method.DeclaringType)); + MessageHandler Handling(IServiceProvider provider) + { + var handler = ActivatorUtilities.GetServiceOrCreateInstance(provider, registration.HandlerType); + + return (message, context, token) => + { + var arguments = GetArguments(registration.Method, message, context, token); + var expression = Expression.Call(Expression.Constant(handler), registration.Method, arguments); + + return Expression.Lambda>(expression).Compile()(); + }; + } + + ConcurrentDictionarySafeRegister(registration.Channel, Handling, _handlerContainer); + MessageSubscribed?.Invoke(this, new MessageSubscribedEventArgs(registration.Channel, registration.MessageType, registration.HandlerType)); } #endregion @@ -69,12 +79,12 @@ public virtual async Task HandleAsync(object message, MessageContext context, Ca return; } - var name = message.GetType().FullName; + var name = MessageCache.Default.GetOrAddChannel(message.GetType()); await HandleAsync(name, message, context, cancellationToken); } /// - public virtual async Task HandleAsync(string name, object message, MessageContext context, CancellationToken cancellationToken = default) + public virtual async Task HandleAsync(string channel, object message, MessageContext context, CancellationToken cancellationToken = default) { if (message == null) { @@ -84,24 +94,28 @@ public virtual async Task HandleAsync(string name, object message, MessageContex var tasks = new List(); using var scope = _provider.GetRequiredService().CreateScope(); - if (!_handlerContainer.TryGetValue(name, out var handlerTypes)) + if (!_handlerContainer.TryGetValue(channel, out var handling)) { throw new InvalidOperationException("No handler registered for message"); } - foreach (var (handlerType, handleMethod) in handlerTypes) - { - var handler = ActivatorUtilities.GetServiceOrCreateInstance(scope.ServiceProvider, handlerType); + // Get handler instance from service provider using Expression Tree - var parameters = GetMethodArguments(handleMethod, context, context, cancellationToken); - if (parameters == null) - { - _logger.LogWarning("Method '{Name}' parameter number not matches", handleMethod.Name); - } - else - { - tasks.Add(Invoke(handleMethod, handler, parameters)); - } + foreach (var factory in handling) + { + var handler = factory(scope.ServiceProvider); + tasks.Add(handler(message, context, cancellationToken)); + // var handler = ActivatorUtilities.GetServiceOrCreateInstance(scope.ServiceProvider, handlerType); + // + // var arguments = GetMethodArguments(handleMethod, message, context, cancellationToken); + // if (arguments == null) + // { + // _logger.LogWarning("Method '{Name}' parameter number not matches", handleMethod.Name); + // } + // else + // { + // tasks.Add(Invoke(handleMethod, handler, arguments)); + // } } if (tasks.Count == 0) @@ -113,6 +127,8 @@ await Task.WhenAll(tasks).ContinueWith(_ => { _logger?.LogInformation("Message {Id} was completed handled", context.MessageId); }, cancellationToken); + + context.Dispose(); } #endregion @@ -144,10 +160,10 @@ private void ConcurrentDictionarySafeRegister(TKey key, TValue val } } - private object[] GetMethodArguments(MethodBase method, object message, MessageContext context, CancellationToken cancellationToken) + private static Expression[] GetArguments(MethodBase method, object message, MessageContext context, CancellationToken cancellationToken) { var parameterInfos = method.GetParameters(); - var parameters = new object[parameterInfos.Length]; + var arguments = new Expression[parameterInfos.Length]; switch (parameterInfos.Length) { case 0: @@ -158,42 +174,42 @@ private object[] GetMethodArguments(MethodBase method, object message, MessageCo if (parameterType == typeof(MessageContext)) { - parameters[0] = context; + arguments[0] = Expression.Constant(context); } else if (parameterType == typeof(CancellationToken)) { - parameters[0] = cancellationToken; + arguments[0] = Expression.Constant(cancellationToken); } else { - parameters[0] = _convert(message, parameterType); + arguments[0] = Expression.Constant(message); } } break; case 2: case 3: { - for (var index = 0; index < parameterInfos.Length; index++) + arguments[0] ??= Expression.Constant(message); + + for (var index = 1; index < parameterInfos.Length; index++) { if (parameterInfos[index].ParameterType == typeof(MessageContext)) { - parameters[index] = context; + arguments[index] = Expression.Constant(context); } if (parameterInfos[index].ParameterType == typeof(CancellationToken)) { - parameters[index] = cancellationToken; + arguments[index] = Expression.Constant(cancellationToken); } } - - parameters[0] ??= _convert(message, parameterInfos[0].ParameterType); } break; default: return null; } - return parameters; + return arguments; } private static Task Invoke(MethodInfo method, object handler, params object[] parameters) diff --git a/Source/Euonia.Bus/Core/IBus.cs b/Source/Euonia.Bus/Core/IBus.cs index fde6311..be8d303 100644 --- a/Source/Euonia.Bus/Core/IBus.cs +++ b/Source/Euonia.Bus/Core/IBus.cs @@ -80,4 +80,23 @@ Task SendAsync(TMessage message, CancellationToken c /// Task SendAsync(TMessage message, SendOptions options, CancellationToken cancellationToken = default) where TMessage : class; + + /// + /// Sends the specified message. + /// + /// + /// + /// + /// + Task SendAsync(IQueue message, CancellationToken cancellationToken = default) => SendAsync(message, new SendOptions(), cancellationToken); + + /// + /// Sends the specified message. + /// + /// + /// + /// + /// + /// + Task SendAsync(IQueue message, SendOptions options, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/Source/Euonia.Bus/Core/IHandler.cs b/Source/Euonia.Bus/Core/IHandler.cs index c43ba0e..02387f4 100644 --- a/Source/Euonia.Bus/Core/IHandler.cs +++ b/Source/Euonia.Bus/Core/IHandler.cs @@ -11,15 +11,6 @@ public interface IHandler /// Type of the message to be checked. /// true if the current message handler can handle the message with the specified message type; otherwise, false. bool CanHandle(Type messageType); - - /// - /// Handle message. - /// - /// - /// - /// - /// - Task HandleAsync(object message, MessageContext messageContext, CancellationToken cancellationToken = default); } /// @@ -29,14 +20,6 @@ public interface IHandler public interface IHandler : IHandler where TMessage : class { - async Task IHandler.HandleAsync(object message, MessageContext messageContext, CancellationToken cancellationToken) - { - if (message is TMessage knownMessage) - { - await HandleAsync(knownMessage, messageContext, cancellationToken); - } - } - /// /// Handle message. /// diff --git a/Source/Euonia.Bus/Core/ServiceBus.cs b/Source/Euonia.Bus/Core/ServiceBus.cs index e070abf..4da0d9b 100644 --- a/Source/Euonia.Bus/Core/ServiceBus.cs +++ b/Source/Euonia.Bus/Core/ServiceBus.cs @@ -23,13 +23,16 @@ public ServiceBus(IBusFactory factory, MessageConvention convention) public async Task PublishAsync(TMessage message, PublishOptions options, CancellationToken cancellationToken = default) where TMessage : class { - if (!_convention.IsEventType(message.GetType())) + if (!_convention.IsTopicType(message.GetType())) { throw new InvalidOperationException("The message type is not an event type."); } var channelName = options?.Channel ?? MessageCache.Default.GetOrAddChannel(); - var pack = new RoutedMessage(message, channelName); + var pack = new RoutedMessage(message, channelName) + { + MessageId = options?.MessageId ?? Guid.NewGuid().ToString() + }; await _dispatcher.PublishAsync(pack, cancellationToken); } @@ -37,26 +40,44 @@ public async Task PublishAsync(TMessage message, PublishOptions option public async Task SendAsync(TMessage message, SendOptions options, CancellationToken cancellationToken = default) where TMessage : class { - if (!_convention.IsCommandType(message.GetType())) + if (!_convention.IsQueueType(message.GetType())) { - throw new InvalidOperationException("The message type is not a command type."); + throw new InvalidOperationException("The message type is not a queue type."); } var channelName = options?.Channel ?? MessageCache.Default.GetOrAddChannel(); - await _dispatcher.SendAsync(new RoutedMessage(message, channelName), cancellationToken); + var pack = new RoutedMessage(message, channelName) + { + MessageId = options?.MessageId ?? Guid.NewGuid().ToString() + }; + await _dispatcher.SendAsync(pack, cancellationToken); } /// public async Task SendAsync(TMessage message, SendOptions options, CancellationToken cancellationToken = default) where TMessage : class { - if (!_convention.IsCommandType(message.GetType())) + if (!_convention.IsQueueType(message.GetType())) { - throw new InvalidOperationException("The message type is not a command type."); + throw new InvalidOperationException("The message type is not a queue type."); } var channelName = options?.Channel ?? MessageCache.Default.GetOrAddChannel(); - var pack = new RoutedMessage(message, channelName); + var pack = new RoutedMessage(message, channelName) + { + MessageId = options?.MessageId ?? Guid.NewGuid().ToString() + }; + return await _dispatcher.SendAsync(pack, cancellationToken); + } + + /// + public async Task SendAsync(IQueue message, SendOptions options, CancellationToken cancellationToken = default) + { + var channelName = options?.Channel ?? MessageCache.Default.GetOrAddChannel(message.GetType()); + var pack = new RoutedMessage, TResult>(message, channelName) + { + MessageId = options?.MessageId ?? Guid.NewGuid().ToString() + }; return await _dispatcher.SendAsync(pack, cancellationToken); } } \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageHandlerFactory.cs b/Source/Euonia.Bus/Messages/MessageHandlerFactory.cs new file mode 100644 index 0000000..4130f45 --- /dev/null +++ b/Source/Euonia.Bus/Messages/MessageHandlerFactory.cs @@ -0,0 +1,6 @@ +namespace Nerosoft.Euonia.Bus; + +/// +/// The delegate to create message handler. +/// +public delegate MessageHandler MessageHandlerFactory(IServiceProvider provider); \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageRegistration.cs b/Source/Euonia.Bus/Messages/MessageRegistration.cs new file mode 100644 index 0000000..5ff0a28 --- /dev/null +++ b/Source/Euonia.Bus/Messages/MessageRegistration.cs @@ -0,0 +1,44 @@ +using System.Reflection; + +namespace Nerosoft.Euonia.Bus; + +/// +/// The message subscription. +/// +internal class MessageRegistration +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + public MessageRegistration(string channel, Type messageType, Type handlerType, MethodInfo method) + { + Channel = channel; + MessageType = messageType; + HandlerType = handlerType; + Method = method; + } + + /// + /// Gets or sets the message name. + /// + public string Channel { get; set; } + + /// + /// Gets or sets the message type. + /// + public Type MessageType { get; set; } + + /// + /// Gets or sets the handler type. + /// + public Type HandlerType { get; set; } + + /// + /// Gets or sets the handler method. + /// + public MethodInfo Method { get; set; } +} \ No newline at end of file diff --git a/Source/Euonia.Bus/Messages/MessageSubscription.cs b/Source/Euonia.Bus/Messages/MessageSubscription.cs deleted file mode 100644 index 213b6fc..0000000 --- a/Source/Euonia.Bus/Messages/MessageSubscription.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Reflection; - -namespace Nerosoft.Euonia.Bus; - -/// -/// The message subscription. -/// -internal class MessageSubscription -{ - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - public MessageSubscription(Type type, Type handlerType, MethodInfo handleMethod) - : this(type.FullName, handlerType, handleMethod) - { - Type = type; - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - public MessageSubscription(string name, Type handlerType, MethodInfo handleMethod) - { - Name = name; - HandlerType = handlerType; - HandleMethod = handleMethod; - } - - /// - /// Gets or sets the message name. - /// - public string Name { get; set; } - - /// - /// Gets or sets the message type. - /// - public Type Type { get; set; } - - /// - /// Gets or sets the message handler type. - /// - public Type HandlerType { get; set; } - - /// - /// Gets or sets the message handler method. - /// - public MethodInfo HandleMethod { get; set; } -} \ No newline at end of file diff --git a/Source/Euonia.Bus/Properties/Resources.resx b/Source/Euonia.Bus/Properties/Resources.resx index 443e4fa..268e134 100644 --- a/Source/Euonia.Bus/Properties/Resources.resx +++ b/Source/Euonia.Bus/Properties/Resources.resx @@ -1,107 +1,126 @@  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 1.3 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - The type cannot be null. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The type cannot be null. + + + At least one convention must be provided. + \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/IMessageSerializer.cs b/Source/Euonia.Bus/Serialization/IMessageSerializer.cs similarity index 100% rename from Source/Euonia.Bus.Abstract/IMessageSerializer.cs rename to Source/Euonia.Bus/Serialization/IMessageSerializer.cs diff --git a/Source/Euonia.Bus.Abstract/MessageSerializerSettings.cs b/Source/Euonia.Bus/Serialization/MessageSerializerSettings.cs similarity index 100% rename from Source/Euonia.Bus.Abstract/MessageSerializerSettings.cs rename to Source/Euonia.Bus/Serialization/MessageSerializerSettings.cs diff --git a/Source/Euonia.Bus/Serialization/SystemTextJsonSerializer.cs b/Source/Euonia.Bus/Serialization/SystemTextJsonSerializer.cs index d467289..f98d12b 100644 --- a/Source/Euonia.Bus/Serialization/SystemTextJsonSerializer.cs +++ b/Source/Euonia.Bus/Serialization/SystemTextJsonSerializer.cs @@ -65,6 +65,7 @@ public object Deserialize(string json, Type type) return JsonSerializer.Deserialize(json, type); } + // ReSharper disable once UnusedMember.Local private static JsonSerializerOptions ConvertSettings(MessageSerializerSettings settings) { if (settings == null) @@ -73,19 +74,12 @@ private static JsonSerializerOptions ConvertSettings(MessageSerializerSettings s } var options = new JsonSerializerOptions(); - switch (settings.ReferenceLoop) + options.ReferenceHandler = settings.ReferenceLoop switch { - case MessageSerializerSettings.ReferenceLoopStrategy.Ignore: - options.ReferenceHandler = ReferenceHandler.IgnoreCycles; - break; - case MessageSerializerSettings.ReferenceLoopStrategy.Preserve: - options.ReferenceHandler = ReferenceHandler.Preserve; - break; - case MessageSerializerSettings.ReferenceLoopStrategy.Serialize: - case null: - default: - break; - } + MessageSerializerSettings.ReferenceLoopStrategy.Ignore => ReferenceHandler.IgnoreCycles, + MessageSerializerSettings.ReferenceLoopStrategy.Preserve => ReferenceHandler.Preserve, + _ => options.ReferenceHandler + }; return options; } diff --git a/Source/Euonia.Bus/ServiceCollectionExtensions.cs b/Source/Euonia.Bus/ServiceCollectionExtensions.cs index 4d87bb5..8171341 100644 --- a/Source/Euonia.Bus/ServiceCollectionExtensions.cs +++ b/Source/Euonia.Bus/ServiceCollectionExtensions.cs @@ -17,8 +17,7 @@ internal static void AddMessageHandler(this IServiceCollection services, Action< { services.AddSingleton(provider => { - var @delegate = provider.GetService(); - var context = new HandlerContext(provider, @delegate); + var context = new HandlerContext(provider); context.MessageSubscribed += (sender, args) => { callback?.Invoke(sender, args); @@ -110,22 +109,15 @@ public static void AddServiceBus(this IServiceCollection services, Action(provider => { - var @delegate = provider.GetService(); - var context = new HandlerContext(provider, @delegate); + var context = new HandlerContext(provider); foreach (var subscription in configurator.Registrations) { - if (subscription.Type != null) - { - context.Register(subscription.Type, subscription.HandlerType, subscription.HandleMethod); - } - else - { - context.Register(subscription.Name, subscription.HandlerType, subscription.HandleMethod); - } + context.Register(subscription); } return context; }); + services.TryAddSingleton(); services.AddSingleton(); } } \ No newline at end of file From c760e329815f73006f33629e7df6129a6c0d5a58 Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 28 Nov 2023 10:06:30 +0800 Subject: [PATCH 19/37] [WIP] Refactoring: Add Authorization. --- Source/Euonia.Bus.Abstract/IMessageContext.cs | 9 +++-- Source/Euonia.Bus.Abstract/IRoutedMessage.cs | 5 +++ Source/Euonia.Bus.Abstract/MessageContext.cs | 36 +++++++++++++++---- Source/Euonia.Bus.Abstract/RoutedMessage.cs | 6 ++++ 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/Source/Euonia.Bus.Abstract/IMessageContext.cs b/Source/Euonia.Bus.Abstract/IMessageContext.cs index 0545f9e..20daab1 100644 --- a/Source/Euonia.Bus.Abstract/IMessageContext.cs +++ b/Source/Euonia.Bus.Abstract/IMessageContext.cs @@ -31,7 +31,12 @@ public interface IMessageContext : IDisposable string RequestTraceId { get; set; } /// - /// Gets or sets the message request headers. + /// Gets or sets the authorization. /// - IReadOnlyDictionary Headers { get; set; } + string Authorization { get; set; } + + /// + /// Gets the message request headers. + /// + IReadOnlyDictionary Headers { get; } } \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/IRoutedMessage.cs b/Source/Euonia.Bus.Abstract/IRoutedMessage.cs index 68f7ad7..9679602 100644 --- a/Source/Euonia.Bus.Abstract/IRoutedMessage.cs +++ b/Source/Euonia.Bus.Abstract/IRoutedMessage.cs @@ -19,4 +19,9 @@ public interface IRoutedMessage : IMessageEnvelope /// Gets the data of the message. /// object Data { get; } + + /// + /// Gets the request authorization. + /// + string Authorization { get; set; } } \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/MessageContext.cs b/Source/Euonia.Bus.Abstract/MessageContext.cs index 11db180..3028caa 100644 --- a/Source/Euonia.Bus.Abstract/MessageContext.cs +++ b/Source/Euonia.Bus.Abstract/MessageContext.cs @@ -7,7 +7,7 @@ public sealed class MessageContext : IMessageContext { private readonly WeakEventManager _events = new(); - private readonly IDictionary _headers = new Dictionary(); + private readonly Dictionary _headers = new(); private bool _disposedValue; @@ -38,6 +38,7 @@ public MessageContext(IRoutedMessage pack) CorrelationId = pack.CorrelationId; ConversationId = pack.ConversationId; RequestTraceId = pack.RequestTraceId; + Authorization = pack.Authorization; } /// @@ -62,19 +63,42 @@ public event EventHandler Completed public object Message { get; } /// - public string MessageId { get; set; } + public string MessageId + { + get => _headers.TryGetValue(nameof(MessageId), out var value) ? value : null; + set => _headers[nameof(MessageId)] = value; + } + + /// + public string CorrelationId + { + get => _headers.TryGetValue(nameof(CorrelationId), out var value) ? value : null; + set => _headers[nameof(CorrelationId)] = value; + } /// - public string CorrelationId { get; set; } + public string ConversationId + { + get => _headers.TryGetValue(nameof(ConversationId), out var value) ? value : null; + set => _headers[nameof(ConversationId)] = value; + } /// - public string ConversationId { get; set; } + public string RequestTraceId + { + get => _headers.TryGetValue(nameof(RequestTraceId), out var value) ? value : null; + set => _headers[nameof(RequestTraceId)] = value; + } /// - public string RequestTraceId { get; set; } + public string Authorization + { + get => _headers.TryGetValue(nameof(Authorization), out var value) ? value : null; + set => _headers[nameof(Authorization)] = value; + } /// - public IReadOnlyDictionary Headers { get; set; } + public IReadOnlyDictionary Headers => _headers; /// /// Replies message handling result to message dispatcher. diff --git a/Source/Euonia.Bus.Abstract/RoutedMessage.cs b/Source/Euonia.Bus.Abstract/RoutedMessage.cs index b20eed2..d215e98 100644 --- a/Source/Euonia.Bus.Abstract/RoutedMessage.cs +++ b/Source/Euonia.Bus.Abstract/RoutedMessage.cs @@ -50,6 +50,12 @@ protected RoutedMessage() [DataMember] public virtual string Channel { get; set; } + /// + /// + /// + [DataMember] + public virtual string Authorization { get; set; } + /// /// Gets or sets the timestamp that describes when the message occurs. /// From 28311c1337fb683323589e3221a92a8da9732984 Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 28 Nov 2023 11:29:40 +0800 Subject: [PATCH 20/37] Remove unused constructor. --- .../Events/MessageSubscribedEventArgs.cs | 65 ++++++++----------- 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/Source/Euonia.Bus.Abstract/Events/MessageSubscribedEventArgs.cs b/Source/Euonia.Bus.Abstract/Events/MessageSubscribedEventArgs.cs index e68518f..d153292 100644 --- a/Source/Euonia.Bus.Abstract/Events/MessageSubscribedEventArgs.cs +++ b/Source/Euonia.Bus.Abstract/Events/MessageSubscribedEventArgs.cs @@ -1,48 +1,37 @@ namespace Nerosoft.Euonia.Bus; /// -/// Class MessageSubscribedEventArgs. -/// Implements the +/// Represents the message was subscribed. /// -/// public class MessageSubscribedEventArgs : EventArgs { - /// - /// Initializes a new instance of the class. - /// - /// Type of the message. - /// Type of the handler. - public MessageSubscribedEventArgs(Type messageType, Type handlerType) - : this(messageType.FullName, handlerType) - { - MessageType = messageType; - } + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + public MessageSubscribedEventArgs(string channel, Type messageType, Type handlerType) + { + Channel = channel; + MessageType = messageType; + HandlerType = handlerType; + } - /// - /// Initializes a new instance of the class. - /// - /// - /// - public MessageSubscribedEventArgs(string messageName, Type handlerType) - { - MessageName = messageName; - HandlerType = handlerType; - } + /// + /// Gets the message name. + /// + public string Channel { get; } - /// - /// Gets the type of the message. - /// - /// The type of the message. - public Type MessageType { get; } + /// + /// Gets the type of the message. + /// + /// The type of the message. + public Type MessageType { get; } - /// - /// Gets the message name. - /// - public string MessageName { get; } - - /// - /// Gets the type of the handler. - /// - /// The type of the handler. - public Type HandlerType { get; } + /// + /// Gets the type of the handler. + /// + /// The type of the handler. + public Type HandlerType { get; } } \ No newline at end of file From 3b68f7cb3f0657be610ce7979d2b94a09b1a279c Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 28 Nov 2023 11:31:00 +0800 Subject: [PATCH 21/37] Rename parameter. --- .../Contracts/IBusConfigurator.cs | 6 ------ Source/Euonia.Bus.Abstract/Contracts/IQueue.cs | 13 +++++++++---- Source/Euonia.Bus.Abstract/IHandlerContext.cs | 4 ++-- Source/Euonia.Bus/BusConfigurator.cs | 6 ------ 4 files changed, 11 insertions(+), 18 deletions(-) diff --git a/Source/Euonia.Bus.Abstract/Contracts/IBusConfigurator.cs b/Source/Euonia.Bus.Abstract/Contracts/IBusConfigurator.cs index 34a4104..0ca20c3 100644 --- a/Source/Euonia.Bus.Abstract/Contracts/IBusConfigurator.cs +++ b/Source/Euonia.Bus.Abstract/Contracts/IBusConfigurator.cs @@ -12,12 +12,6 @@ public interface IBusConfigurator /// IServiceCollection Service { get; } - /// - /// Get the message subscriptions. - /// - /// - IEnumerable GetSubscriptions(); - /// /// Set the service bus factory. /// diff --git a/Source/Euonia.Bus.Abstract/Contracts/IQueue.cs b/Source/Euonia.Bus.Abstract/Contracts/IQueue.cs index 7d3736f..49d2cde 100644 --- a/Source/Euonia.Bus.Abstract/Contracts/IQueue.cs +++ b/Source/Euonia.Bus.Abstract/Contracts/IQueue.cs @@ -1,9 +1,14 @@ namespace Nerosoft.Euonia.Bus; /// -/// Represents a topic. +/// Represents a queue message. /// public interface IQueue -{ - -} \ No newline at end of file +{ } + +/// +/// Represents a queue message. +/// +/// +public interface IQueue : IQueue +{ } \ No newline at end of file diff --git a/Source/Euonia.Bus.Abstract/IHandlerContext.cs b/Source/Euonia.Bus.Abstract/IHandlerContext.cs index 61707a1..2b2268f 100644 --- a/Source/Euonia.Bus.Abstract/IHandlerContext.cs +++ b/Source/Euonia.Bus.Abstract/IHandlerContext.cs @@ -22,10 +22,10 @@ public interface IHandlerContext /// /// Handle message asynchronously. /// - /// + /// /// /// /// /// - Task HandleAsync(string name, object message, MessageContext context, CancellationToken cancellationToken = default); + Task HandleAsync(string channel, object message, MessageContext context, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/Source/Euonia.Bus/BusConfigurator.cs b/Source/Euonia.Bus/BusConfigurator.cs index e6e5943..4770737 100644 --- a/Source/Euonia.Bus/BusConfigurator.cs +++ b/Source/Euonia.Bus/BusConfigurator.cs @@ -32,12 +32,6 @@ public BusConfigurator(IServiceCollection service) /// public IServiceCollection Service { get; } - /// - public IEnumerable GetSubscriptions() - { - return Registrations.Select(t => t.Channel); - } - /// /// /// From 9ca187f55828d75b415dd2b80fbd0adaeca90c3f Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 28 Nov 2023 12:16:50 +0800 Subject: [PATCH 22/37] Move IMessageConvention to abstract project. --- .../IMessageConvention.cs | 0 Source/Euonia.Bus/BusConfigurator.cs | 2 +- Source/Euonia.Bus/Conventions/MessageConvention.cs | 7 +++++-- 3 files changed, 6 insertions(+), 3 deletions(-) rename Source/{Euonia.Bus/Conventions => Euonia.Bus.Abstract}/IMessageConvention.cs (100%) diff --git a/Source/Euonia.Bus/Conventions/IMessageConvention.cs b/Source/Euonia.Bus.Abstract/IMessageConvention.cs similarity index 100% rename from Source/Euonia.Bus/Conventions/IMessageConvention.cs rename to Source/Euonia.Bus.Abstract/IMessageConvention.cs diff --git a/Source/Euonia.Bus/BusConfigurator.cs b/Source/Euonia.Bus/BusConfigurator.cs index 4770737..a11a9fd 100644 --- a/Source/Euonia.Bus/BusConfigurator.cs +++ b/Source/Euonia.Bus/BusConfigurator.cs @@ -258,7 +258,7 @@ void ValidateMessageType(Type messageType) public BusConfigurator SetConventions(Action configure) { configure?.Invoke(ConventionBuilder); - Service.TryAddSingleton(ConventionBuilder.Convention); + Service.TryAddSingleton(ConventionBuilder.Convention); return this; } } \ No newline at end of file diff --git a/Source/Euonia.Bus/Conventions/MessageConvention.cs b/Source/Euonia.Bus/Conventions/MessageConvention.cs index 65870fd..ee91aee 100644 --- a/Source/Euonia.Bus/Conventions/MessageConvention.cs +++ b/Source/Euonia.Bus/Conventions/MessageConvention.cs @@ -5,10 +5,10 @@ namespace Nerosoft.Euonia.Bus; /// /// /// -public class MessageConvention +public class MessageConvention : IMessageConvention { private readonly OverridableMessageConvention _defaultConvention = new(new DefaultMessageConvention()); - private readonly List _conventions = new(); + private readonly List _conventions = []; private readonly ConventionCache _commandConventionCache = new(); private readonly ConventionCache _eventConventionCache = new(); @@ -71,6 +71,9 @@ internal void Add(params IMessageConvention[] conventions) /// internal string[] RegisteredConventions => _conventions.Select(x => x.Name).ToArray(); + /// + public string Name { get; } + private class ConventionCache { public bool Apply(Type type, Func convention) From bf6d93c6ab293b1a14da5aeed0ac61e58aa46a2e Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 28 Nov 2023 22:00:32 +0800 Subject: [PATCH 23/37] Move MessageRegistration to abstract project. --- .../Contracts/IBusConfigurator.cs | 5 + .../MessageConventionType.cs | 23 ++++ .../MessageRegistration.cs | 36 ++++++ Source/Euonia.Bus/BusConfigurator.cs | 121 ++---------------- .../Conventions/MessageConvention.cs | 21 +-- .../Conventions/MessageConventionBuilder.cs | 8 +- .../OverridableMessageConvention.cs | 4 +- .../Euonia.Bus/Core/MessageHandlerFinder.cs | 87 +++++++++++++ Source/Euonia.Bus/Core/ServiceBus.cs | 21 +-- .../Messages/MessageRegistration.cs | 44 ------- 10 files changed, 190 insertions(+), 180 deletions(-) create mode 100644 Source/Euonia.Bus.Abstract/MessageConventionType.cs create mode 100644 Source/Euonia.Bus.Abstract/MessageRegistration.cs create mode 100644 Source/Euonia.Bus/Core/MessageHandlerFinder.cs delete mode 100644 Source/Euonia.Bus/Messages/MessageRegistration.cs diff --git a/Source/Euonia.Bus.Abstract/Contracts/IBusConfigurator.cs b/Source/Euonia.Bus.Abstract/Contracts/IBusConfigurator.cs index 0ca20c3..62bb2f9 100644 --- a/Source/Euonia.Bus.Abstract/Contracts/IBusConfigurator.cs +++ b/Source/Euonia.Bus.Abstract/Contracts/IBusConfigurator.cs @@ -12,6 +12,11 @@ public interface IBusConfigurator /// IServiceCollection Service { get; } + /// + /// Gets the message handle registrations. + /// + IReadOnlyList Registrations { get; } + /// /// Set the service bus factory. /// diff --git a/Source/Euonia.Bus.Abstract/MessageConventionType.cs b/Source/Euonia.Bus.Abstract/MessageConventionType.cs new file mode 100644 index 0000000..799699d --- /dev/null +++ b/Source/Euonia.Bus.Abstract/MessageConventionType.cs @@ -0,0 +1,23 @@ +namespace Nerosoft.Euonia.Bus; + +public enum MessageConventionType +{ + /// + /// + /// + None, + + /// + /// + /// + Queue, + /// + /// + /// + Topic, + + /// + /// + /// + Request, +} diff --git a/Source/Euonia.Bus.Abstract/MessageRegistration.cs b/Source/Euonia.Bus.Abstract/MessageRegistration.cs new file mode 100644 index 0000000..83c6e5c --- /dev/null +++ b/Source/Euonia.Bus.Abstract/MessageRegistration.cs @@ -0,0 +1,36 @@ +using System.Reflection; + +namespace Nerosoft.Euonia.Bus; + +/// +/// The message subscription. +/// +/// +/// Initializes a new instance of the class. +/// +/// +/// +/// +/// +public class MessageRegistration(string channel, Type messageType, Type handlerType, MethodInfo method) +{ + /// + /// Gets or sets the message name. + /// + public string Channel { get; set; } = channel; + + /// + /// Gets or sets the message type. + /// + public Type MessageType { get; set; } = messageType; + + /// + /// Gets or sets the handler type. + /// + public Type HandlerType { get; set; } = handlerType; + + /// + /// Gets or sets the handler method. + /// + public MethodInfo Method { get; set; } = method; +} \ No newline at end of file diff --git a/Source/Euonia.Bus/BusConfigurator.cs b/Source/Euonia.Bus/BusConfigurator.cs index a11a9fd..cd9379c 100644 --- a/Source/Euonia.Bus/BusConfigurator.cs +++ b/Source/Euonia.Bus/BusConfigurator.cs @@ -11,14 +11,14 @@ namespace Nerosoft.Euonia.Bus; /// public class BusConfigurator : IBusConfigurator { - private const BindingFlags BINDING_FLAGS = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; + private readonly List _registrations = new(); private MessageConventionBuilder ConventionBuilder { get; } = new(); /// /// The message handler types. /// - internal List Registrations { get; } = new(); + public IReadOnlyList Registrations => _registrations; /// /// Initialize a new instance of @@ -123,131 +123,38 @@ public IBusConfigurator SetMessageStore(Func s /// /// Register the message handlers. /// - /// + /// /// - public BusConfigurator RegisterHandlers(Assembly assembly) + public BusConfigurator RegisterHandlers(params Assembly[] assemblies) { - return RegisterHandlers(() => assembly.DefinedTypes); + return RegisterHandlers(() => assemblies.SelectMany(assembly => assembly.DefinedTypes)); } /// /// Register the message handlers. /// - /// + /// /// - public BusConfigurator RegisterHandlers(Func> handlerTypesFactory) + public BusConfigurator RegisterHandlers(Func> typesFactory) { - return RegisterHandlers(handlerTypesFactory()); + return RegisterHandlers(typesFactory()); } /// /// Register the message handlers. /// - /// + /// /// - public BusConfigurator RegisterHandlers(IEnumerable handlerTypes) + public BusConfigurator RegisterHandlers(IEnumerable types) { + var registrations = MessageHandlerFinder.Find(types); + var handlerTypes = registrations.Select(x => x.HandlerType).Distinct(); foreach (var handlerType in handlerTypes) { - if (!handlerType.IsClass || handlerType.IsInterface || handlerType.IsAbstract) - { - continue; - } - - if (handlerType.IsImplementsGeneric(typeof(IHandler<>))) - { - var interfaces = handlerType.GetInterfaces().Where(t => t.IsGenericType); - - foreach (var @interface in interfaces) - { - var messageType = @interface.GetGenericArguments().FirstOrDefault(); - - if (messageType == null) - { - continue; - } - - var method = @interface.GetMethod(nameof(IHandler.HandleAsync), BINDING_FLAGS, null, new[] { messageType, typeof(MessageContext), typeof(CancellationToken) }, null); - - var registration = new MessageRegistration(MessageCache.Default.GetOrAddChannel(messageType), messageType, handlerType, method); - - Registrations.Add(registration); - - Service.TryAddScoped(typeof(IHandler<>).MakeGenericType(messageType), handlerType); - } - - Service.TryAddScoped(handlerType); - } - else - { - var methods = handlerType.GetMethods(BINDING_FLAGS).Where(method => method.GetCustomAttributes().Any()); - - if (!methods.Any()) - { - continue; - } - - foreach (var method in methods) - { - var parameters = method.GetParameters(); - - if (!parameters.Any(t => t.ParameterType != typeof(CancellationToken) && t.ParameterType != typeof(MessageContext))) - { - throw new InvalidOperationException("Invalid handler method."); - } - - var firstParameter = parameters[0]; - - if (firstParameter.ParameterType.IsPrimitiveType()) - { - throw new InvalidOperationException("The first parameter of handler method must be message type"); - } - - switch (parameters.Length) - { - case 2 when parameters[1].ParameterType != typeof(MessageContext) || parameters[1].ParameterType != typeof(CancellationToken): - throw new InvalidOperationException("The second parameter of handler method must be MessageContext or CancellationToken if the method contains 2 parameters"); - case 3 when parameters[1].ParameterType != typeof(MessageContext) && parameters[2].ParameterType != typeof(CancellationToken): - throw new InvalidOperationException("The second and third parameter of handler method must be MessageContext and CancellationToken if the method contains 3 parameters"); - } - - var attributes = method.GetCustomAttributes(); - - foreach (var attribute in attributes) - { - var registration = new MessageRegistration(attribute.Name, firstParameter.ParameterType, handlerType, method); - Registrations.Add(registration); - } - } - - Service.TryAddScoped(handlerType); - } + Service.TryAddScoped(handlerType); } - + _registrations.AddRange(registrations); return this; - - void ValidateMessageType(Type messageType) - { - if (messageType.IsPrimitiveType()) - { - throw new InvalidOperationException("The message type cannot be a primitive type."); - } - - if (messageType.IsClass) - { - throw new InvalidOperationException("The message type must be a class."); - } - - if (messageType.IsAbstract) - { - throw new InvalidOperationException("The message type cannot be an abstract class."); - } - - if (messageType.IsInterface) - { - throw new InvalidOperationException("The message type cannot be an interface."); - } - } } /// diff --git a/Source/Euonia.Bus/Conventions/MessageConvention.cs b/Source/Euonia.Bus/Conventions/MessageConvention.cs index ee91aee..ec4fb50 100644 --- a/Source/Euonia.Bus/Conventions/MessageConvention.cs +++ b/Source/Euonia.Bus/Conventions/MessageConvention.cs @@ -9,8 +9,8 @@ public class MessageConvention : IMessageConvention { private readonly OverridableMessageConvention _defaultConvention = new(new DefaultMessageConvention()); private readonly List _conventions = []; - private readonly ConventionCache _commandConventionCache = new(); - private readonly ConventionCache _eventConventionCache = new(); + private readonly ConventionCache _topicConventionCache = new(); + private readonly ConventionCache _queueConventionCache = new(); /// /// Determines whether the specified type is a command. @@ -22,7 +22,7 @@ public bool IsQueueType(Type type) { ArgumentAssert.ThrowIfNull(type); - return _commandConventionCache.Apply(type, handle => + return _topicConventionCache.Apply(type, handle => { var t = Type.GetTypeFromHandle(handle); return _conventions.Any(x => x.IsQueueType(t)); @@ -39,21 +39,26 @@ public bool IsTopicType(Type type) { ArgumentAssert.ThrowIfNull(type); - return _eventConventionCache.Apply(type, handle => + return _queueConventionCache.Apply(type, handle => { var t = Type.GetTypeFromHandle(handle); return _conventions.Any(x => x.IsTopicType(t)); }); } - internal void DefineCommandTypeConvention(Func convention) + internal void DefineQueueTypeConvention(Func convention) { - _defaultConvention.DefineCommandType(convention); + _defaultConvention.DefineQueueType(convention); } - internal void DefineEventTypeConvention(Func convention) + internal void DefineTopicTypeConvention(Func convention) { - _defaultConvention.DefineEventType(convention); + _defaultConvention.DefineTopicType(convention); + } + + internal void DefineTypeConvention(Func convention) + { + } internal void Add(params IMessageConvention[] conventions) diff --git a/Source/Euonia.Bus/Conventions/MessageConventionBuilder.cs b/Source/Euonia.Bus/Conventions/MessageConventionBuilder.cs index 591c2fb..2eb399e 100644 --- a/Source/Euonia.Bus/Conventions/MessageConventionBuilder.cs +++ b/Source/Euonia.Bus/Conventions/MessageConventionBuilder.cs @@ -12,10 +12,10 @@ public class MessageConventionBuilder /// /// /// - public MessageConventionBuilder EvaluateCommand(Func convention) + public MessageConventionBuilder EvaluateQueue(Func convention) { ArgumentAssert.ThrowIfNull(convention); - Convention.DefineCommandTypeConvention(convention); + Convention.DefineQueueTypeConvention(convention); return this; } @@ -24,10 +24,10 @@ public MessageConventionBuilder EvaluateCommand(Func convention) /// /// /// - public MessageConventionBuilder EvaluateEvent(Func convention) + public MessageConventionBuilder EvaluateTopic(Func convention) { ArgumentAssert.ThrowIfNull(convention); - Convention.DefineEventTypeConvention(convention); + Convention.DefineTopicTypeConvention(convention); return this; } diff --git a/Source/Euonia.Bus/Conventions/OverridableMessageConvention.cs b/Source/Euonia.Bus/Conventions/OverridableMessageConvention.cs index 11826ae..b44f08d 100644 --- a/Source/Euonia.Bus/Conventions/OverridableMessageConvention.cs +++ b/Source/Euonia.Bus/Conventions/OverridableMessageConvention.cs @@ -34,12 +34,12 @@ public Func IsTopicType set => _isTopicType = value; } - public void DefineCommandType(Func convention) + public void DefineQueueType(Func convention) { _isQueueType = convention; } - public void DefineEventType(Func convention) + public void DefineTopicType(Func convention) { _isTopicType = convention; } diff --git a/Source/Euonia.Bus/Core/MessageHandlerFinder.cs b/Source/Euonia.Bus/Core/MessageHandlerFinder.cs new file mode 100644 index 0000000..8835f83 --- /dev/null +++ b/Source/Euonia.Bus/Core/MessageHandlerFinder.cs @@ -0,0 +1,87 @@ +using System.Reflection; + +namespace Nerosoft.Euonia.Bus; + +internal class MessageHandlerFinder +{ + private const BindingFlags BINDING_FLAGS = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; + + public static IEnumerable Find(IEnumerable types) + { + return types.SelectMany(Resolve); + } + + public static IEnumerable Find(params Assembly[] assemblies) + { + var types = assemblies.SelectMany(x => x.DefinedTypes); + + return Find(types); + } + + public static IEnumerable Find(params Type[] types) + { + return Find(types.AsEnumerable()); + } + + private static IEnumerable Resolve(Type type) + { + if (!type.IsClass || type.IsInterface || type.IsAbstract) + { + return Enumerable.Empty(); + } + + var registrations = new List(); + + var methods = type.GetMethods(BINDING_FLAGS).Where(method => method.HasAttribute()); + + if (methods.Any()) + { + foreach (var method in methods) + { + var parameters = method.GetParameters(); + + if (!parameters.Any(t => t.ParameterType != typeof(CancellationToken) && t.ParameterType != typeof(MessageContext))) + { + throw new InvalidOperationException("Invalid handler method."); + } + + var firstParameter = parameters[0]; + + if (firstParameter.ParameterType.IsPrimitiveType()) + { + throw new InvalidOperationException("The first parameter of handler method must be message type"); + } + + switch (parameters.Length) + { + case 2 when parameters[1].ParameterType != typeof(MessageContext) || parameters[1].ParameterType != typeof(CancellationToken): + throw new InvalidOperationException("The second parameter of handler method must be MessageContext or CancellationToken if the method contains 2 parameters"); + case 3 when parameters[1].ParameterType != typeof(MessageContext) && parameters[2].ParameterType != typeof(CancellationToken): + throw new InvalidOperationException("The second and third parameter of handler method must be MessageContext and CancellationToken if the method contains 3 parameters"); + } + + var attributes = method.GetCustomAttributes(); + + foreach (var attribute in attributes) + { + var registration = new MessageRegistration(attribute.Name, firstParameter.ParameterType, type, method); + registrations.Add(registration); + } + } + } + + var interfaces = type.GetInterfaces().Where(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IHandler<>)); + if (interfaces.Any()) + { + foreach (var @interface in interfaces) + { + var messageType = @interface.GetGenericArguments()[0]; + var method = @interface.GetMethod(nameof(IHandler.HandleAsync), BINDING_FLAGS, null, new[] { messageType, typeof(MessageContext), typeof(CancellationToken) }, null); + var registration = new MessageRegistration(MessageCache.Default.GetOrAddChannel(messageType), messageType, type, method); + registrations.Add(registration); + } + } + + return registrations; + } +} diff --git a/Source/Euonia.Bus/Core/ServiceBus.cs b/Source/Euonia.Bus/Core/ServiceBus.cs index 4da0d9b..378cc74 100644 --- a/Source/Euonia.Bus/Core/ServiceBus.cs +++ b/Source/Euonia.Bus/Core/ServiceBus.cs @@ -1,23 +1,14 @@ namespace Nerosoft.Euonia.Bus; /// -/// +/// The implementation of interface. /// -public sealed class ServiceBus : IBus +/// +/// +public sealed class ServiceBus(IBusFactory factory, IMessageConvention convention) : IBus { - private readonly IDispatcher _dispatcher; - private readonly MessageConvention _convention; - - /// - /// Initialize a new instance of - /// - /// - /// - public ServiceBus(IBusFactory factory, MessageConvention convention) - { - _convention = convention; - _dispatcher = factory.CreateDispatcher(); - } + private readonly IDispatcher _dispatcher = factory.CreateDispatcher(); + private readonly IMessageConvention _convention = convention; /// public async Task PublishAsync(TMessage message, PublishOptions options, CancellationToken cancellationToken = default) diff --git a/Source/Euonia.Bus/Messages/MessageRegistration.cs b/Source/Euonia.Bus/Messages/MessageRegistration.cs deleted file mode 100644 index 5ff0a28..0000000 --- a/Source/Euonia.Bus/Messages/MessageRegistration.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Reflection; - -namespace Nerosoft.Euonia.Bus; - -/// -/// The message subscription. -/// -internal class MessageRegistration -{ - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - /// - public MessageRegistration(string channel, Type messageType, Type handlerType, MethodInfo method) - { - Channel = channel; - MessageType = messageType; - HandlerType = handlerType; - Method = method; - } - - /// - /// Gets or sets the message name. - /// - public string Channel { get; set; } - - /// - /// Gets or sets the message type. - /// - public Type MessageType { get; set; } - - /// - /// Gets or sets the handler type. - /// - public Type HandlerType { get; set; } - - /// - /// Gets or sets the handler method. - /// - public MethodInfo Method { get; set; } -} \ No newline at end of file From f22e2c985680245d212e238f057202b5f10fb50b Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 28 Nov 2023 22:01:24 +0800 Subject: [PATCH 24/37] InMemory message transportation. --- .../BusConfiguratorExtensions.cs | 38 ++++++++++++++++--- .../Euonia.Bus.InMemory/InMemoryBusOptions.cs | 2 +- .../Euonia.Bus.InMemory/InMemoryDispatcher.cs | 12 ++---- .../Euonia.Bus.InMemory/InMemoryRecipient.cs | 2 +- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs b/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs index 987b0af..9073ce6 100644 --- a/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs +++ b/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs @@ -35,20 +35,46 @@ public static void UseInMemory(this IBusConfigurator configurator, Action throw new ArgumentOutOfRangeException(nameof(options.MessengerReference), options.MessengerReference, null) }; + var convention = provider.GetService(); + if (options.MultipleSubscriberInstance) { - foreach (var subscription in configurator.GetSubscriptions()) + foreach (var registration in configurator.Registrations) { - var subscriber = ActivatorUtilities.GetServiceOrCreateInstance(provider); - messenger.Register(subscriber, subscription); + InMemoryRecipient recipient; + if (convention.IsQueueType(registration.MessageType)) + { + recipient = ActivatorUtilities.GetServiceOrCreateInstance(provider); + } + else if (convention.IsTopicType(registration.MessageType)) + { + recipient = ActivatorUtilities.GetServiceOrCreateInstance(provider); + } + else + { + throw new InvalidOperationException(); + } + messenger.Register(recipient, registration.Channel); } } else { - var subscriber = ActivatorUtilities.GetServiceOrCreateInstance(provider); - foreach (var subscription in configurator.GetSubscriptions()) + foreach (var registration in configurator.Registrations) { - messenger.Register(subscriber, subscription); + InMemoryRecipient recipient; + if (convention.IsQueueType(registration.MessageType)) + { + recipient = Singleton.Get(() => ActivatorUtilities.GetServiceOrCreateInstance(provider)); + } + else if (convention.IsTopicType(registration.MessageType)) + { + recipient = Singleton.Get(() => ActivatorUtilities.GetServiceOrCreateInstance(provider)); + } + else + { + throw new InvalidOperationException(); + } + messenger.Register(recipient, registration.Channel); } } diff --git a/Source/Euonia.Bus.InMemory/InMemoryBusOptions.cs b/Source/Euonia.Bus.InMemory/InMemoryBusOptions.cs index 3c4dd5e..109b819 100644 --- a/Source/Euonia.Bus.InMemory/InMemoryBusOptions.cs +++ b/Source/Euonia.Bus.InMemory/InMemoryBusOptions.cs @@ -26,5 +26,5 @@ public class InMemoryBusOptions /// /// Gets or sets the messenger reference type. /// - public MessengerReferenceType MessengerReference { get; init; } = MessengerReferenceType.StrongReference; + public MessengerReferenceType MessengerReference { get; set; } = MessengerReferenceType.StrongReference; } \ No newline at end of file diff --git a/Source/Euonia.Bus.InMemory/InMemoryDispatcher.cs b/Source/Euonia.Bus.InMemory/InMemoryDispatcher.cs index 7a552b6..4b3c7df 100644 --- a/Source/Euonia.Bus.InMemory/InMemoryDispatcher.cs +++ b/Source/Euonia.Bus.InMemory/InMemoryDispatcher.cs @@ -1,6 +1,4 @@ -using Microsoft.Extensions.Options; - -namespace Nerosoft.Euonia.Bus.InMemory; +namespace Nerosoft.Euonia.Bus.InMemory; /// /// @@ -10,18 +8,14 @@ public class InMemoryDispatcher : DisposableObject, IDispatcher /// public event EventHandler Delivered; - private readonly InMemoryBusOptions _options; - private readonly IMessenger _messenger; /// /// Initializes a new instance of the class. /// /// - /// - public InMemoryDispatcher(IMessenger messenger, IOptions options) + public InMemoryDispatcher(IMessenger messenger) { - _options = options.Value; _messenger = messenger; } @@ -76,7 +70,7 @@ public async Task SendAsync(RoutedMessage(); if (cancellationToken != default) diff --git a/Source/Euonia.Bus.InMemory/InMemoryRecipient.cs b/Source/Euonia.Bus.InMemory/InMemoryRecipient.cs index a36b5c1..765751b 100644 --- a/Source/Euonia.Bus.InMemory/InMemoryRecipient.cs +++ b/Source/Euonia.Bus.InMemory/InMemoryRecipient.cs @@ -39,7 +39,7 @@ protected override void Dispose(bool disposing) public async void Receive(MessagePack pack) { MessageReceived?.Invoke(this, new MessageReceivedEventArgs(pack.Message, pack.Context)); - await _handler.HandleAsync(pack.Message.Data, pack.Context, pack.Aborted); + await _handler.HandleAsync(pack.Message.Channel, pack.Message.Data, pack.Context, pack.Aborted); MessageAcknowledged?.Invoke(this, new MessageAcknowledgedEventArgs(pack.Message, pack.Context)); } } \ No newline at end of file From b21655fbcf90f3cbaff2aa5f80e0dbb95c33ade7 Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 28 Nov 2023 22:01:53 +0800 Subject: [PATCH 25/37] Add extension method. --- .../Extensions/Extensions.Object.cs | 309 +++++++++--------- 1 file changed, 161 insertions(+), 148 deletions(-) diff --git a/Source/Euonia.Core/Extensions/Extensions.Object.cs b/Source/Euonia.Core/Extensions/Extensions.Object.cs index 6fe8870..34e6220 100644 --- a/Source/Euonia.Core/Extensions/Extensions.Object.cs +++ b/Source/Euonia.Core/Extensions/Extensions.Object.cs @@ -4,165 +4,178 @@ public static partial class Extensions { - /// - /// Used to simplify and beautify casting an object to a type. - /// - /// Type to be casted - /// Object to cast - /// Casted object - public static T As(this object obj) - where T : class - { - return (T)obj; - } + /// + /// Used to simplify and beautify casting an object to a type. + /// + /// Type to be casted + /// Object to cast + /// Casted object + public static T As(this object obj) + where T : class + { + return (T)obj; + } - /// - /// Converts given object to a value type using method. - /// - /// Object to be converted - /// Type of the target object - /// Converted object - public static T To(this object obj) - where T : struct - { - if (obj == null) - { - throw new NullReferenceException(); - } + /// + /// Converts given object to a value type using method. + /// + /// Object to be converted + /// Type of the target object + /// Converted object + public static T To(this object obj) + where T : struct + { + if (obj == null) + { + throw new NullReferenceException(); + } - if (typeof(T) == obj.GetType()) - { - return (T)obj; - } + if (typeof(T) == obj.GetType()) + { + return (T)obj; + } - if (typeof(T) == typeof(Guid)) - { - // ReSharper disable once PossibleNullReferenceException - return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFromInvariantString(obj.ToString()); - } + if (typeof(T) == typeof(Guid)) + { + // ReSharper disable once PossibleNullReferenceException + return (T)TypeDescriptor.GetConverter(typeof(T)).ConvertFromInvariantString(obj.ToString()); + } - return (T)System.Convert.ChangeType(obj, typeof(T), CultureInfo.InvariantCulture); - } + return (T)System.Convert.ChangeType(obj, typeof(T), CultureInfo.InvariantCulture); + } - /// - /// Check if an item is in a list. - /// - /// Item to check - /// List of items - /// Type of the items - public static bool IsIn(this T item, params T[] list) - { - return list.Contains(item); - } + /// + /// Check if an item is in a list. + /// + /// Item to check + /// List of items + /// Type of the items + public static bool IsIn(this T item, params T[] list) + { + return list.Contains(item); + } - /// - /// Check if an item is in the given enumerable. - /// - /// Item to check - /// Items - /// Type of the items - public static bool IsIn(this T item, IEnumerable items) - { - return items.Contains(item); - } + /// + /// Check if an item is in the given enumerable. + /// + /// Item to check + /// Items + /// Type of the items + public static bool IsIn(this T item, IEnumerable items) + { + return items.Contains(item); + } - /// - /// - /// - /// - /// - /// - /// - /// - public static bool IsIn(this T item, IEnumerable items, IEqualityComparer comparer) - { - return items.Contains(item, comparer); - } + /// + /// + /// + /// + /// + /// + /// + /// + public static bool IsIn(this T item, IEnumerable items, IEqualityComparer comparer) + { + return items.Contains(item, comparer); + } - /// - /// Can be used to conditionally perform a function - /// on an object and return the modified or the original object. - /// It is useful for chained calls. - /// - /// An object - /// A condition - /// A function that is executed only if the condition is true - /// Type of the object - /// - /// Returns the modified object (by the if the is true) - /// or the original object if the is false - /// - public static T If(this T obj, bool condition, Func func) - { - return condition ? func(obj) : obj; - } + /// + /// Can be used to conditionally perform a function + /// on an object and return the modified or the original object. + /// It is useful for chained calls. + /// + /// An object + /// A condition + /// A function that is executed only if the condition is true + /// Type of the object + /// + /// Returns the modified object (by the if the is true) + /// or the original object if the is false + /// + public static T If(this T obj, bool condition, Func func) + { + return condition ? func(obj) : obj; + } - /// - /// Can be used to conditionally perform an action - /// on an object and return the original object. - /// It is useful for chained calls on the object. - /// - /// An object - /// A condition - /// An action that is executed only if the condition is true - /// Type of the object - /// - /// Returns the original object. - /// - public static T If(this T obj, bool condition, Action action) - { - if (condition) - { - action(obj); - } + /// + /// Can be used to conditionally perform an action + /// on an object and return the original object. + /// It is useful for chained calls on the object. + /// + /// An object + /// A condition + /// An action that is executed only if the condition is true + /// Type of the object + /// + /// Returns the original object. + /// + public static T If(this T obj, bool condition, Action action) + { + if (condition) + { + action(obj); + } - return obj; - } + return obj; + } - /// - /// - /// - /// - /// - /// - /// - public static bool HasAttribute(this object source, bool inherit = true) - where TAttribute : Attribute - { - var type = source.GetType(); - var attribute = type.GetCustomAttributes(inherit); - return attribute.Any(); - } + /// + /// + /// + /// + /// + /// + /// + public static bool HasAttribute(this Type type, bool inherit = true) + where TAttribute : Attribute + { + var attribute = type.GetCustomAttributes(inherit); + return attribute.Any(); + } - /// - /// - /// - /// - /// - /// - /// - /// - public static bool HasAttribute(this object source, out TAttribute attribute, bool inherit = true) - where TAttribute : Attribute - { - var type = source.GetType(); - attribute = type.GetCustomAttribute(inherit); - return attribute != null; - } + /// + /// + /// + /// + /// + /// + /// + public static bool HasAttribute(this MethodInfo method, bool inherit = true) + where TAttribute : Attribute + { + var attribute = method.GetCustomAttributes(inherit); + return attribute.Any(); + } - /// - /// - /// - /// - /// - /// - /// - /// - public static bool HasAttribute(this object source, out IEnumerable attributes, bool inherit = true) - where TAttribute : Attribute - { - var type = source.GetType(); - attributes = type.GetCustomAttributes(inherit); - return attributes.Any(); - } + /// + /// + /// + /// + /// + /// + /// + /// + public static bool HasAttribute(this object source, out TAttribute attribute, bool inherit = true) + where TAttribute : Attribute + { + var type = source.GetType(); + attribute = type.GetCustomAttribute(inherit); + return attribute != null; + } + + /// + /// + /// + /// + /// + /// + /// + /// + public static bool HasAttribute(this object source, out IEnumerable attributes, bool inherit = true) + where TAttribute : Attribute + { + var type = source.GetType(); + attributes = type.GetCustomAttributes(inherit); + return attributes.Any(); + } } From 096b2a8ce9faac9fdb6dc25d344de559e670f8fd Mon Sep 17 00:00:00 2001 From: damon Date: Tue, 28 Nov 2023 22:02:18 +0800 Subject: [PATCH 26/37] Add testings. --- Euonia.sln | 21 +++++++ .../Commands/FooCreateCommand.cs | 12 ++++ .../Commands/UserCreateCommand.cs | 12 ++++ .../Commands/UserUpdateCommand.cs | 12 ++++ .../Euonia.Bus.InMemory.Tests.csproj | 16 ++++++ .../Handlers/FooCommandHandler.cs | 19 +++++++ .../Handlers/UserCommandHandler.cs | 30 ++++++++++ .../ServiceBusTests.cs | 46 +++++++++++++++ Tests/Euonia.Bus.InMemory.Tests/Startup.cs | 57 +++++++++++++++++++ Tests/Euonia.Bus.InMemory.Tests/Usings.cs | 1 + .../appsettings.json | 1 + .../Euonia.Bus.Tests/Euonia.Bus.Tests.csproj | 16 ++++++ Tests/Euonia.Bus.Tests/Startup.cs | 40 +++++++++++++ Tests/Euonia.Bus.Tests/Usings.cs | 1 + Tests/Euonia.Bus.Tests/appsettings.json | 1 + 15 files changed, 285 insertions(+) create mode 100644 Tests/Euonia.Bus.InMemory.Tests/Commands/FooCreateCommand.cs create mode 100644 Tests/Euonia.Bus.InMemory.Tests/Commands/UserCreateCommand.cs create mode 100644 Tests/Euonia.Bus.InMemory.Tests/Commands/UserUpdateCommand.cs create mode 100644 Tests/Euonia.Bus.InMemory.Tests/Euonia.Bus.InMemory.Tests.csproj create mode 100644 Tests/Euonia.Bus.InMemory.Tests/Handlers/FooCommandHandler.cs create mode 100644 Tests/Euonia.Bus.InMemory.Tests/Handlers/UserCommandHandler.cs create mode 100644 Tests/Euonia.Bus.InMemory.Tests/ServiceBusTests.cs create mode 100644 Tests/Euonia.Bus.InMemory.Tests/Startup.cs create mode 100644 Tests/Euonia.Bus.InMemory.Tests/Usings.cs create mode 100644 Tests/Euonia.Bus.InMemory.Tests/appsettings.json create mode 100644 Tests/Euonia.Bus.Tests/Euonia.Bus.Tests.csproj create mode 100644 Tests/Euonia.Bus.Tests/Startup.cs create mode 100644 Tests/Euonia.Bus.Tests/Usings.cs create mode 100644 Tests/Euonia.Bus.Tests/appsettings.json diff --git a/Euonia.sln b/Euonia.sln index 957716f..ad160a3 100644 --- a/Euonia.sln +++ b/Euonia.sln @@ -121,6 +121,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Euonia.Bus.ActiveMq", "Sour EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Euonia.Bus.Abstract", "Source\Euonia.Bus.Abstract\Euonia.Bus.Abstract.csproj", "{31CDB3D0-A7CE-435F-A2A8-CD5F9E7EB8B2}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Bus", "Bus", "{66B49F2D-FBC6-45A0-82B0-A099CABD37C6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Euonia.Bus.Tests", "Tests\Euonia.Bus.Tests\Euonia.Bus.Tests.csproj", "{F7ABE0BA-9659-4975-B03D-C383CA13B3D2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Euonia.Bus.InMemory.Tests", "Tests\Euonia.Bus.InMemory.Tests\Euonia.Bus.InMemory.Tests.csproj", "{AA073AAD-9290-4D55-BFF1-DC1B3357FF8B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -356,6 +362,18 @@ Global {31CDB3D0-A7CE-435F-A2A8-CD5F9E7EB8B2}.Product|Any CPU.Build.0 = Debug|Any CPU {31CDB3D0-A7CE-435F-A2A8-CD5F9E7EB8B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {31CDB3D0-A7CE-435F-A2A8-CD5F9E7EB8B2}.Release|Any CPU.Build.0 = Release|Any CPU + {F7ABE0BA-9659-4975-B03D-C383CA13B3D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7ABE0BA-9659-4975-B03D-C383CA13B3D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7ABE0BA-9659-4975-B03D-C383CA13B3D2}.Product|Any CPU.ActiveCfg = Debug|Any CPU + {F7ABE0BA-9659-4975-B03D-C383CA13B3D2}.Product|Any CPU.Build.0 = Debug|Any CPU + {F7ABE0BA-9659-4975-B03D-C383CA13B3D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7ABE0BA-9659-4975-B03D-C383CA13B3D2}.Release|Any CPU.Build.0 = Release|Any CPU + {AA073AAD-9290-4D55-BFF1-DC1B3357FF8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA073AAD-9290-4D55-BFF1-DC1B3357FF8B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA073AAD-9290-4D55-BFF1-DC1B3357FF8B}.Product|Any CPU.ActiveCfg = Debug|Any CPU + {AA073AAD-9290-4D55-BFF1-DC1B3357FF8B}.Product|Any CPU.Build.0 = Debug|Any CPU + {AA073AAD-9290-4D55-BFF1-DC1B3357FF8B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA073AAD-9290-4D55-BFF1-DC1B3357FF8B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -409,6 +427,9 @@ Global {81D1403E-24B7-47DD-BD55-1D22B8E7756B} = {E048931D-EC51-448A-A737-3C62CF100813} {EFABA5DF-BD24-4880-A9FE-242AACF5B599} = {273D1F47-F6AF-4ED5-AAB5-977BD9906B2E} {31CDB3D0-A7CE-435F-A2A8-CD5F9E7EB8B2} = {273D1F47-F6AF-4ED5-AAB5-977BD9906B2E} + {66B49F2D-FBC6-45A0-82B0-A099CABD37C6} = {E048931D-EC51-448A-A737-3C62CF100813} + {F7ABE0BA-9659-4975-B03D-C383CA13B3D2} = {66B49F2D-FBC6-45A0-82B0-A099CABD37C6} + {AA073AAD-9290-4D55-BFF1-DC1B3357FF8B} = {66B49F2D-FBC6-45A0-82B0-A099CABD37C6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {84CDDCF4-F3D0-45FC-87C5-557845F58F55} diff --git a/Tests/Euonia.Bus.InMemory.Tests/Commands/FooCreateCommand.cs b/Tests/Euonia.Bus.InMemory.Tests/Commands/FooCreateCommand.cs new file mode 100644 index 0000000..50a4307 --- /dev/null +++ b/Tests/Euonia.Bus.InMemory.Tests/Commands/FooCreateCommand.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Nerosoft.Euonia.Bus.InMemory.Tests.Commands; + +//[Channel("foo.create")] +public class FooCreateCommand : IQueue +{ +} diff --git a/Tests/Euonia.Bus.InMemory.Tests/Commands/UserCreateCommand.cs b/Tests/Euonia.Bus.InMemory.Tests/Commands/UserCreateCommand.cs new file mode 100644 index 0000000..8af5357 --- /dev/null +++ b/Tests/Euonia.Bus.InMemory.Tests/Commands/UserCreateCommand.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Nerosoft.Euonia.Bus.InMemory.Tests.Commands; + +[Queue] +public class UserCreateCommand +{ +} diff --git a/Tests/Euonia.Bus.InMemory.Tests/Commands/UserUpdateCommand.cs b/Tests/Euonia.Bus.InMemory.Tests/Commands/UserUpdateCommand.cs new file mode 100644 index 0000000..5e0ddb9 --- /dev/null +++ b/Tests/Euonia.Bus.InMemory.Tests/Commands/UserUpdateCommand.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Nerosoft.Euonia.Bus.InMemory.Tests.Commands; + +[Queue] +public class UserUpdateCommand +{ +} diff --git a/Tests/Euonia.Bus.InMemory.Tests/Euonia.Bus.InMemory.Tests.csproj b/Tests/Euonia.Bus.InMemory.Tests/Euonia.Bus.InMemory.Tests.csproj new file mode 100644 index 0000000..bcc3c58 --- /dev/null +++ b/Tests/Euonia.Bus.InMemory.Tests/Euonia.Bus.InMemory.Tests.csproj @@ -0,0 +1,16 @@ + + + + + + + + + + + + Always + + + + diff --git a/Tests/Euonia.Bus.InMemory.Tests/Handlers/FooCommandHandler.cs b/Tests/Euonia.Bus.InMemory.Tests/Handlers/FooCommandHandler.cs new file mode 100644 index 0000000..872f1e1 --- /dev/null +++ b/Tests/Euonia.Bus.InMemory.Tests/Handlers/FooCommandHandler.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Nerosoft.Euonia.Bus.InMemory.Tests.Commands; + +namespace Nerosoft.Euonia.Bus.InMemory.Tests.Handlers; + +public class FooCommandHandler +{ + [Subscribe("foo.create")] + public async Task HandleAsync(FooCreateCommand message, MessageContext messageContext, CancellationToken cancellationToken = default) + { + Console.WriteLine("FooCreateCommand handled"); + messageContext.Response(1); + await Task.CompletedTask; + } +} diff --git a/Tests/Euonia.Bus.InMemory.Tests/Handlers/UserCommandHandler.cs b/Tests/Euonia.Bus.InMemory.Tests/Handlers/UserCommandHandler.cs new file mode 100644 index 0000000..dadc6f3 --- /dev/null +++ b/Tests/Euonia.Bus.InMemory.Tests/Handlers/UserCommandHandler.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Nerosoft.Euonia.Bus.InMemory.Tests.Commands; + +namespace Nerosoft.Euonia.Bus.InMemory.Tests.Handlers +{ + public class UserCommandHandler : IHandler, IHandler + { + public bool CanHandle(Type messageType) + { + return true; + } + + public async Task HandleAsync(UserCreateCommand message, MessageContext messageContext, CancellationToken cancellationToken = default) + { + Console.WriteLine("UserCreateCommand handled"); + messageContext.Response(1); + await Task.CompletedTask; + } + + public async Task HandleAsync(UserUpdateCommand message, MessageContext messageContext, CancellationToken cancellationToken = default) + { + Console.WriteLine("UserUpdateCommand handled"); + await Task.CompletedTask; + } + } +} diff --git a/Tests/Euonia.Bus.InMemory.Tests/ServiceBusTests.cs b/Tests/Euonia.Bus.InMemory.Tests/ServiceBusTests.cs new file mode 100644 index 0000000..495ae4e --- /dev/null +++ b/Tests/Euonia.Bus.InMemory.Tests/ServiceBusTests.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Nerosoft.Euonia.Bus.InMemory.Tests.Commands; + +namespace Nerosoft.Euonia.Bus.InMemory.Tests; + +public class ServiceBusTests +{ + private readonly IBus _bus; + + public ServiceBusTests(IBus bus) + { + _bus = bus; + } + + [Fact] + public async Task TestSendCommand_HasReponse() + { + var result = await _bus.SendAsync(new UserCreateCommand()); + Assert.Equal(1, result); + } + + [Fact] + public async Task TestSendCommand_NoReponse() + { + await _bus.SendAsync(new UserCreateCommand()); + Assert.True(true); + } + + [Fact] + public async Task TestSendCommand_HasReponse_UseSubscribeAttribute() + { + var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); + Assert.Equal(1, result); + } + + [Fact] + public async Task TestSendCommand_HasReponse_MessageHasResultInherites() + { + var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); + Assert.Equal(1, result); + } +} diff --git a/Tests/Euonia.Bus.InMemory.Tests/Startup.cs b/Tests/Euonia.Bus.InMemory.Tests/Startup.cs new file mode 100644 index 0000000..7cc7464 --- /dev/null +++ b/Tests/Euonia.Bus.InMemory.Tests/Startup.cs @@ -0,0 +1,57 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Nerosoft.Euonia.Bus.InMemory.Tests; + +[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "")] +[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "")] +public class Startup +{ + public void ConfigureHost(IHostBuilder hostBuilder) + { + hostBuilder.ConfigureAppConfiguration(builder => + { + builder.AddJsonFile("appsettings.json"); + }) + .ConfigureServices((_, _) => + { + // Register service here. + }); + } + + // ConfigureServices(IServiceCollection services) + // ConfigureServices(IServiceCollection services, HostBuilderContext hostBuilderContext) + // ConfigureServices(HostBuilderContext hostBuilderContext, IServiceCollection services) + public void ConfigureServices(IServiceCollection services, HostBuilderContext hostBuilderContext) + { + services.AddServiceBus(config => + { + config.RegisterHandlers(Assembly.GetExecutingAssembly()); + config.SetConventions(builder => + { + builder.Add(); + builder.Add(); + builder.EvaluateQueue(t => t.Name.EndsWith("Command")); + builder.EvaluateTopic(t => t.Name.EndsWith("Event")); + }); + config.UseInMemory(options => + { + options.MessengerReference = MessengerReferenceType.StrongReference; + options.MultipleSubscriberInstance = false; + }); + }); + } + + //public void Configure(IServiceProvider applicationServices, IIdGenerator idGenerator) + //{ + // InitData(); + //} + + public void Configure(IServiceProvider applicationServices) + { + //var config = applicationServices.GetService(); + } +} diff --git a/Tests/Euonia.Bus.InMemory.Tests/Usings.cs b/Tests/Euonia.Bus.InMemory.Tests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/Tests/Euonia.Bus.InMemory.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/Tests/Euonia.Bus.InMemory.Tests/appsettings.json b/Tests/Euonia.Bus.InMemory.Tests/appsettings.json new file mode 100644 index 0000000..22fdca1 --- /dev/null +++ b/Tests/Euonia.Bus.InMemory.Tests/appsettings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/Tests/Euonia.Bus.Tests/Euonia.Bus.Tests.csproj b/Tests/Euonia.Bus.Tests/Euonia.Bus.Tests.csproj new file mode 100644 index 0000000..040711c --- /dev/null +++ b/Tests/Euonia.Bus.Tests/Euonia.Bus.Tests.csproj @@ -0,0 +1,16 @@ + + + + + + + + + + + + Always + + + + diff --git a/Tests/Euonia.Bus.Tests/Startup.cs b/Tests/Euonia.Bus.Tests/Startup.cs new file mode 100644 index 0000000..f151dd4 --- /dev/null +++ b/Tests/Euonia.Bus.Tests/Startup.cs @@ -0,0 +1,40 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Nerosoft.Euonia.Bus.Tests; + +[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "")] +[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "")] +public class Startup +{ + public void ConfigureHost(IHostBuilder hostBuilder) + { + hostBuilder.ConfigureAppConfiguration(builder => + { + builder.AddJsonFile("appsettings.json"); + }) + .ConfigureServices((_, _) => + { + // Register service here. + }); + } + + // ConfigureServices(IServiceCollection services) + // ConfigureServices(IServiceCollection services, HostBuilderContext hostBuilderContext) + // ConfigureServices(HostBuilderContext hostBuilderContext, IServiceCollection services) + public void ConfigureServices(IServiceCollection services, HostBuilderContext hostBuilderContext) + { + } + + //public void Configure(IServiceProvider applicationServices, IIdGenerator idGenerator) + //{ + // InitData(); + //} + + public void Configure(IServiceProvider applicationServices) + { + //var config = applicationServices.GetService(); + } +} diff --git a/Tests/Euonia.Bus.Tests/Usings.cs b/Tests/Euonia.Bus.Tests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/Tests/Euonia.Bus.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/Tests/Euonia.Bus.Tests/appsettings.json b/Tests/Euonia.Bus.Tests/appsettings.json new file mode 100644 index 0000000..22fdca1 --- /dev/null +++ b/Tests/Euonia.Bus.Tests/appsettings.json @@ -0,0 +1 @@ +{} \ No newline at end of file From f6c0dac1b92530c204fd2da2c4303d28983aad4d Mon Sep 17 00:00:00 2001 From: damon Date: Wed, 29 Nov 2023 18:15:54 +0800 Subject: [PATCH 27/37] Add RabbitMq transporter tests. --- Euonia.sln | 19 +++++- .../Recipients/IRecipientRegistrar.cs | 6 ++ .../InMemoryRecipientRegistrar.cs | 64 +++++++++++++++++++ .../RabbitMqRecipientRegistrar.cs | 39 +++++++++++ Source/Euonia.Bus/RecipientActivator.cs | 23 +++++++ .../Euonia.Bus.InMemory.Tests.csproj | 12 ++-- Tests/Euonia.Bus.InMemory.Tests/Startup.cs | 3 +- .../Euonia.Bus.RabbitMq.Tests.csproj | 22 +++++++ Tests/Euonia.Bus.RabbitMq.Tests/Startup.cs | 61 ++++++++++++++++++ Tests/Euonia.Bus.RabbitMq.Tests/Usings.cs | 1 + .../appsettings.json | 1 + .../Commands/FooCreateCommand.cs | 2 +- .../Commands/UserCreateCommand.cs | 2 +- .../Commands/UserUpdateCommand.cs | 2 +- .../Euonia.Bus.Tests.Shared.projitems | 20 ++++++ .../Euonia.Bus.Tests.Shared.shproj | 13 ++++ .../Handlers/FooCommandHandler.cs | 4 +- .../Handlers/UserCommandHandler.cs | 4 +- .../ServiceBusTests.cs | 6 +- .../TestNotificationBucket.cs | 6 ++ 20 files changed, 292 insertions(+), 18 deletions(-) create mode 100644 Source/Euonia.Bus.Abstract/Recipients/IRecipientRegistrar.cs create mode 100644 Source/Euonia.Bus.InMemory/InMemoryRecipientRegistrar.cs create mode 100644 Source/Euonia.Bus.RabbitMq/RabbitMqRecipientRegistrar.cs create mode 100644 Source/Euonia.Bus/RecipientActivator.cs create mode 100644 Tests/Euonia.Bus.RabbitMq.Tests/Euonia.Bus.RabbitMq.Tests.csproj create mode 100644 Tests/Euonia.Bus.RabbitMq.Tests/Startup.cs create mode 100644 Tests/Euonia.Bus.RabbitMq.Tests/Usings.cs create mode 100644 Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json rename Tests/{Euonia.Bus.InMemory.Tests => Euonia.Bus.Tests.Shared}/Commands/FooCreateCommand.cs (78%) rename Tests/{Euonia.Bus.InMemory.Tests => Euonia.Bus.Tests.Shared}/Commands/UserCreateCommand.cs (74%) rename Tests/{Euonia.Bus.InMemory.Tests => Euonia.Bus.Tests.Shared}/Commands/UserUpdateCommand.cs (74%) create mode 100644 Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.projitems create mode 100644 Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.shproj rename Tests/{Euonia.Bus.InMemory.Tests => Euonia.Bus.Tests.Shared}/Handlers/FooCommandHandler.cs (80%) rename Tests/{Euonia.Bus.InMemory.Tests => Euonia.Bus.Tests.Shared}/Handlers/UserCommandHandler.cs (88%) rename Tests/{Euonia.Bus.InMemory.Tests => Euonia.Bus.Tests.Shared}/ServiceBusTests.cs (91%) create mode 100644 Tests/Euonia.Bus.Tests.Shared/TestNotificationBucket.cs diff --git a/Euonia.sln b/Euonia.sln index ad160a3..9b59f38 100644 --- a/Euonia.sln +++ b/Euonia.sln @@ -123,9 +123,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Euonia.Bus.Abstract", "Sour EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Bus", "Bus", "{66B49F2D-FBC6-45A0-82B0-A099CABD37C6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Euonia.Bus.Tests", "Tests\Euonia.Bus.Tests\Euonia.Bus.Tests.csproj", "{F7ABE0BA-9659-4975-B03D-C383CA13B3D2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Euonia.Bus.Tests", "Tests\Euonia.Bus.Tests\Euonia.Bus.Tests.csproj", "{F7ABE0BA-9659-4975-B03D-C383CA13B3D2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Euonia.Bus.InMemory.Tests", "Tests\Euonia.Bus.InMemory.Tests\Euonia.Bus.InMemory.Tests.csproj", "{AA073AAD-9290-4D55-BFF1-DC1B3357FF8B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Euonia.Bus.InMemory.Tests", "Tests\Euonia.Bus.InMemory.Tests\Euonia.Bus.InMemory.Tests.csproj", "{AA073AAD-9290-4D55-BFF1-DC1B3357FF8B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Euonia.Bus.RabbitMq.Tests", "Tests\Euonia.Bus.RabbitMq.Tests\Euonia.Bus.RabbitMq.Tests.csproj", "{C1262DAB-43DB-480A-8064-634C50872258}" +EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Euonia.Bus.Tests.Shared", "Tests\Euonia.Bus.Tests.Shared\Euonia.Bus.Tests.Shared.shproj", "{B2B6A902-17A7-4926-9B7F-22DE8A092231}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -374,6 +378,12 @@ Global {AA073AAD-9290-4D55-BFF1-DC1B3357FF8B}.Product|Any CPU.Build.0 = Debug|Any CPU {AA073AAD-9290-4D55-BFF1-DC1B3357FF8B}.Release|Any CPU.ActiveCfg = Release|Any CPU {AA073AAD-9290-4D55-BFF1-DC1B3357FF8B}.Release|Any CPU.Build.0 = Release|Any CPU + {C1262DAB-43DB-480A-8064-634C50872258}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C1262DAB-43DB-480A-8064-634C50872258}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C1262DAB-43DB-480A-8064-634C50872258}.Product|Any CPU.ActiveCfg = Debug|Any CPU + {C1262DAB-43DB-480A-8064-634C50872258}.Product|Any CPU.Build.0 = Debug|Any CPU + {C1262DAB-43DB-480A-8064-634C50872258}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C1262DAB-43DB-480A-8064-634C50872258}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -430,6 +440,8 @@ Global {66B49F2D-FBC6-45A0-82B0-A099CABD37C6} = {E048931D-EC51-448A-A737-3C62CF100813} {F7ABE0BA-9659-4975-B03D-C383CA13B3D2} = {66B49F2D-FBC6-45A0-82B0-A099CABD37C6} {AA073AAD-9290-4D55-BFF1-DC1B3357FF8B} = {66B49F2D-FBC6-45A0-82B0-A099CABD37C6} + {C1262DAB-43DB-480A-8064-634C50872258} = {66B49F2D-FBC6-45A0-82B0-A099CABD37C6} + {B2B6A902-17A7-4926-9B7F-22DE8A092231} = {66B49F2D-FBC6-45A0-82B0-A099CABD37C6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {84CDDCF4-F3D0-45FC-87C5-557845F58F55} @@ -438,6 +450,9 @@ Global Tests\Euonia.Mapping.Tests.Shared\Euonia.Mapping.Tests.Shared.projitems*{34b067d7-7126-4b02-a4e8-e1afc77f3485}*SharedItemsImports = 5 Tests\Euonia.Caching.Tests.Shared\Euonia.Caching.Tests.Shared.projitems*{4a28cd6b-0c75-4d39-b613-66de6b693675}*SharedItemsImports = 13 Tests\Euonia.Mapping.Tests.Shared\Euonia.Mapping.Tests.Shared.projitems*{4c27eeef-6837-47ba-bb5c-b0e96fa3504d}*SharedItemsImports = 5 + Tests\Euonia.Bus.Tests.Shared\Euonia.Bus.Tests.Shared.projitems*{aa073aad-9290-4d55-bff1-dc1b3357ff8b}*SharedItemsImports = 5 + Tests\Euonia.Bus.Tests.Shared\Euonia.Bus.Tests.Shared.projitems*{b2b6a902-17a7-4926-9b7f-22de8a092231}*SharedItemsImports = 13 + Tests\Euonia.Bus.Tests.Shared\Euonia.Bus.Tests.Shared.projitems*{c1262dab-43db-480a-8064-634c50872258}*SharedItemsImports = 5 Tests\Euonia.Mapping.Tests.Shared\Euonia.Mapping.Tests.Shared.projitems*{de31e135-48a1-40d8-afef-768cebb4819a}*SharedItemsImports = 13 Tests\Euonia.Caching.Tests.Shared\Euonia.Caching.Tests.Shared.projitems*{efa1cd9d-4b53-483c-bf9d-f21b9b2c6fde}*SharedItemsImports = 5 Tests\Euonia.Caching.Tests.Shared\Euonia.Caching.Tests.Shared.projitems*{f827303e-92c2-46d3-bd8a-50db8885e6ce}*SharedItemsImports = 5 diff --git a/Source/Euonia.Bus.Abstract/Recipients/IRecipientRegistrar.cs b/Source/Euonia.Bus.Abstract/Recipients/IRecipientRegistrar.cs new file mode 100644 index 0000000..7f7aad4 --- /dev/null +++ b/Source/Euonia.Bus.Abstract/Recipients/IRecipientRegistrar.cs @@ -0,0 +1,6 @@ +namespace Nerosoft.Euonia.Bus; + +public interface IRecipientRegistrar +{ + Task RegisterAsync(IReadOnlyList registrations, CancellationToken cancellationToken = default); +} diff --git a/Source/Euonia.Bus.InMemory/InMemoryRecipientRegistrar.cs b/Source/Euonia.Bus.InMemory/InMemoryRecipientRegistrar.cs new file mode 100644 index 0000000..c4dda06 --- /dev/null +++ b/Source/Euonia.Bus.InMemory/InMemoryRecipientRegistrar.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Nerosoft.Euonia.Bus.InMemory; + +public sealed class InMemoryRecipientRegistrar : IRecipientRegistrar +{ + private readonly InMemoryBusOptions _options; + private readonly IMessageConvention _convention; + private readonly IServiceProvider _provider; + private readonly IMessenger _messenger; + + public InMemoryRecipientRegistrar(IMessenger messenger, IMessageConvention convention, IServiceProvider provider, IOptions options) + { + _options = options.Value; + _convention = convention; + _provider = provider; + _messenger = messenger; + } + + public async Task RegisterAsync(IReadOnlyList registrations, CancellationToken cancellationToken = default) + { + if (_options.MultipleSubscriberInstance) + { + foreach (var registration in registrations) + { + InMemoryRecipient recipient; + if (_convention.IsQueueType(registration.MessageType)) + { + recipient = ActivatorUtilities.GetServiceOrCreateInstance(_provider); + } + else if (_convention.IsTopicType(registration.MessageType)) + { + recipient = ActivatorUtilities.GetServiceOrCreateInstance(_provider); + } + else + { + throw new InvalidOperationException(); + } + _messenger.Register(recipient, registration.Channel); + } + } + else + { + foreach (var registration in registrations) + { + InMemoryRecipient recipient; + if (_convention.IsQueueType(registration.MessageType)) + { + recipient = Singleton.Get(() => ActivatorUtilities.GetServiceOrCreateInstance(_provider)); + } + else if (_convention.IsTopicType(registration.MessageType)) + { + recipient = Singleton.Get(() => ActivatorUtilities.GetServiceOrCreateInstance(_provider)); + } + else + { + throw new InvalidOperationException(); + } + _messenger.Register(recipient, registration.Channel); + } + } + } +} diff --git a/Source/Euonia.Bus.RabbitMq/RabbitMqRecipientRegistrar.cs b/Source/Euonia.Bus.RabbitMq/RabbitMqRecipientRegistrar.cs new file mode 100644 index 0000000..1d5730b --- /dev/null +++ b/Source/Euonia.Bus.RabbitMq/RabbitMqRecipientRegistrar.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Nerosoft.Euonia.Bus.RabbitMq; + +public sealed class RabbitMqRecipientRegistrar : IRecipientRegistrar +{ + private readonly IMessageConvention _convention; + private readonly IServiceProvider _provider; + + public RabbitMqRecipientRegistrar(IMessageConvention convention, IServiceProvider provider) + { + _convention = convention; + _provider = provider; + } + + /// + public async Task RegisterAsync(IReadOnlyList registrations, CancellationToken cancellationToken = default) + { + foreach (var registration in registrations) + { + RabbitMqQueueRecipient recipient; + if (_convention.IsQueueType(registration.MessageType)) + { + recipient = ActivatorUtilities.GetServiceOrCreateInstance(_provider); + } + else if (_convention.IsTopicType(registration.MessageType)) + { + recipient = ActivatorUtilities.GetServiceOrCreateInstance(_provider); + } + else + { + throw new InvalidOperationException(); + } + + recipient.Start(registration.Channel); + } + await Task.CompletedTask; + } +} diff --git a/Source/Euonia.Bus/RecipientActivator.cs b/Source/Euonia.Bus/RecipientActivator.cs new file mode 100644 index 0000000..f83ebd0 --- /dev/null +++ b/Source/Euonia.Bus/RecipientActivator.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Nerosoft.Euonia.Bus; + +/// +/// The background service to active the recipients. +/// +/// +public class RecipientActivator(IServiceProvider provider) : BackgroundService +{ + private readonly IServiceProvider _provider = provider; + + /// + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + var registrations = Singleton.Instance.Registrations; + + var registrars = _provider.GetServices(); + + return Task.WhenAll(registrars.Select(x => x.RegisterAsync(registrations, stoppingToken))); + } +} diff --git a/Tests/Euonia.Bus.InMemory.Tests/Euonia.Bus.InMemory.Tests.csproj b/Tests/Euonia.Bus.InMemory.Tests/Euonia.Bus.InMemory.Tests.csproj index bcc3c58..c329d7c 100644 --- a/Tests/Euonia.Bus.InMemory.Tests/Euonia.Bus.InMemory.Tests.csproj +++ b/Tests/Euonia.Bus.InMemory.Tests/Euonia.Bus.InMemory.Tests.csproj @@ -1,11 +1,8 @@ - + - - - - + @@ -13,4 +10,9 @@ + + + + + diff --git a/Tests/Euonia.Bus.InMemory.Tests/Startup.cs b/Tests/Euonia.Bus.InMemory.Tests/Startup.cs index 7cc7464..f2e32e1 100644 --- a/Tests/Euonia.Bus.InMemory.Tests/Startup.cs +++ b/Tests/Euonia.Bus.InMemory.Tests/Startup.cs @@ -3,8 +3,9 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Nerosoft.Euonia.Bus.InMemory; -namespace Nerosoft.Euonia.Bus.InMemory.Tests; +namespace Nerosoft.Euonia.Bus.Tests; [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "")] [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "")] diff --git a/Tests/Euonia.Bus.RabbitMq.Tests/Euonia.Bus.RabbitMq.Tests.csproj b/Tests/Euonia.Bus.RabbitMq.Tests/Euonia.Bus.RabbitMq.Tests.csproj new file mode 100644 index 0000000..f3ae61d --- /dev/null +++ b/Tests/Euonia.Bus.RabbitMq.Tests/Euonia.Bus.RabbitMq.Tests.csproj @@ -0,0 +1,22 @@ + + + + $(DefineConstants);RABBIT_MQ + + + + + + + + + + + + + + Always + + + + diff --git a/Tests/Euonia.Bus.RabbitMq.Tests/Startup.cs b/Tests/Euonia.Bus.RabbitMq.Tests/Startup.cs new file mode 100644 index 0000000..a087125 --- /dev/null +++ b/Tests/Euonia.Bus.RabbitMq.Tests/Startup.cs @@ -0,0 +1,61 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Nerosoft.Euonia.Bus.RabbitMq; + +namespace Nerosoft.Euonia.Bus.Tests; + +[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "")] +[SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "")] +public class Startup +{ + public void ConfigureHost(IHostBuilder hostBuilder) + { + hostBuilder.ConfigureAppConfiguration(builder => + { + builder.AddJsonFile("appsettings.json"); + }) + .ConfigureServices((_, _) => + { + // Register service here. + }); + } + + // ConfigureServices(IServiceCollection services) + // ConfigureServices(IServiceCollection services, HostBuilderContext hostBuilderContext) + // ConfigureServices(HostBuilderContext hostBuilderContext, IServiceCollection services) + public void ConfigureServices(IServiceCollection services, HostBuilderContext hostBuilderContext) + { + services.AddServiceBus(config => + { + config.RegisterHandlers(Assembly.GetExecutingAssembly()); + config.SetConventions(builder => + { + builder.Add(); + builder.Add(); + builder.EvaluateQueue(t => t.Name.EndsWith("Command")); + builder.EvaluateTopic(t => t.Name.EndsWith("Event")); + }); + config.UseRabbitMq(options => + { + options.Connection = "amqp://127.0.0.1"; + options.QueueName = "nerosoft.euonia.test.command"; + options.TopicName = "nerosoft.euonia.test.event"; + options.ExchangeName = $"nerosoft.euonia.test.exchange.{options.ExchangeType}"; + options.RoutingKey = "*"; + }); + }); + } + + //public void Configure(IServiceProvider applicationServices, IIdGenerator idGenerator) + //{ + // InitData(); + //} + + public void Configure(IServiceProvider applicationServices) + { + //var config = applicationServices.GetService(); + } +} diff --git a/Tests/Euonia.Bus.RabbitMq.Tests/Usings.cs b/Tests/Euonia.Bus.RabbitMq.Tests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/Tests/Euonia.Bus.RabbitMq.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json b/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json new file mode 100644 index 0000000..22fdca1 --- /dev/null +++ b/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/Tests/Euonia.Bus.InMemory.Tests/Commands/FooCreateCommand.cs b/Tests/Euonia.Bus.Tests.Shared/Commands/FooCreateCommand.cs similarity index 78% rename from Tests/Euonia.Bus.InMemory.Tests/Commands/FooCreateCommand.cs rename to Tests/Euonia.Bus.Tests.Shared/Commands/FooCreateCommand.cs index 50a4307..e021714 100644 --- a/Tests/Euonia.Bus.InMemory.Tests/Commands/FooCreateCommand.cs +++ b/Tests/Euonia.Bus.Tests.Shared/Commands/FooCreateCommand.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Nerosoft.Euonia.Bus.InMemory.Tests.Commands; +namespace Nerosoft.Euonia.Bus.Tests.Commands; //[Channel("foo.create")] public class FooCreateCommand : IQueue diff --git a/Tests/Euonia.Bus.InMemory.Tests/Commands/UserCreateCommand.cs b/Tests/Euonia.Bus.Tests.Shared/Commands/UserCreateCommand.cs similarity index 74% rename from Tests/Euonia.Bus.InMemory.Tests/Commands/UserCreateCommand.cs rename to Tests/Euonia.Bus.Tests.Shared/Commands/UserCreateCommand.cs index 8af5357..6ac85ee 100644 --- a/Tests/Euonia.Bus.InMemory.Tests/Commands/UserCreateCommand.cs +++ b/Tests/Euonia.Bus.Tests.Shared/Commands/UserCreateCommand.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Nerosoft.Euonia.Bus.InMemory.Tests.Commands; +namespace Nerosoft.Euonia.Bus.Tests.Commands; [Queue] public class UserCreateCommand diff --git a/Tests/Euonia.Bus.InMemory.Tests/Commands/UserUpdateCommand.cs b/Tests/Euonia.Bus.Tests.Shared/Commands/UserUpdateCommand.cs similarity index 74% rename from Tests/Euonia.Bus.InMemory.Tests/Commands/UserUpdateCommand.cs rename to Tests/Euonia.Bus.Tests.Shared/Commands/UserUpdateCommand.cs index 5e0ddb9..8a102b4 100644 --- a/Tests/Euonia.Bus.InMemory.Tests/Commands/UserUpdateCommand.cs +++ b/Tests/Euonia.Bus.Tests.Shared/Commands/UserUpdateCommand.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Nerosoft.Euonia.Bus.InMemory.Tests.Commands; +namespace Nerosoft.Euonia.Bus.Tests.Commands; [Queue] public class UserUpdateCommand diff --git a/Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.projitems b/Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.projitems new file mode 100644 index 0000000..485842f --- /dev/null +++ b/Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.projitems @@ -0,0 +1,20 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + b2b6a902-17a7-4926-9b7f-22de8a092231 + + + Nerosoft.Euonia.Bus.Tests + + + + + + + + + + + \ No newline at end of file diff --git a/Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.shproj b/Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.shproj new file mode 100644 index 0000000..16c1e04 --- /dev/null +++ b/Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.shproj @@ -0,0 +1,13 @@ + + + + b2b6a902-17a7-4926-9b7f-22de8a092231 + 14.0 + + + + + + + + diff --git a/Tests/Euonia.Bus.InMemory.Tests/Handlers/FooCommandHandler.cs b/Tests/Euonia.Bus.Tests.Shared/Handlers/FooCommandHandler.cs similarity index 80% rename from Tests/Euonia.Bus.InMemory.Tests/Handlers/FooCommandHandler.cs rename to Tests/Euonia.Bus.Tests.Shared/Handlers/FooCommandHandler.cs index 872f1e1..974989f 100644 --- a/Tests/Euonia.Bus.InMemory.Tests/Handlers/FooCommandHandler.cs +++ b/Tests/Euonia.Bus.Tests.Shared/Handlers/FooCommandHandler.cs @@ -3,9 +3,9 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using Nerosoft.Euonia.Bus.InMemory.Tests.Commands; +using Nerosoft.Euonia.Bus.Tests.Commands; -namespace Nerosoft.Euonia.Bus.InMemory.Tests.Handlers; +namespace Nerosoft.Euonia.Bus.Tests.Handlers; public class FooCommandHandler { diff --git a/Tests/Euonia.Bus.InMemory.Tests/Handlers/UserCommandHandler.cs b/Tests/Euonia.Bus.Tests.Shared/Handlers/UserCommandHandler.cs similarity index 88% rename from Tests/Euonia.Bus.InMemory.Tests/Handlers/UserCommandHandler.cs rename to Tests/Euonia.Bus.Tests.Shared/Handlers/UserCommandHandler.cs index dadc6f3..9894381 100644 --- a/Tests/Euonia.Bus.InMemory.Tests/Handlers/UserCommandHandler.cs +++ b/Tests/Euonia.Bus.Tests.Shared/Handlers/UserCommandHandler.cs @@ -3,9 +3,9 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using Nerosoft.Euonia.Bus.InMemory.Tests.Commands; +using Nerosoft.Euonia.Bus.Tests.Commands; -namespace Nerosoft.Euonia.Bus.InMemory.Tests.Handlers +namespace Nerosoft.Euonia.Bus.Tests.Handlers { public class UserCommandHandler : IHandler, IHandler { diff --git a/Tests/Euonia.Bus.InMemory.Tests/ServiceBusTests.cs b/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs similarity index 91% rename from Tests/Euonia.Bus.InMemory.Tests/ServiceBusTests.cs rename to Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs index 495ae4e..99f2cf6 100644 --- a/Tests/Euonia.Bus.InMemory.Tests/ServiceBusTests.cs +++ b/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs @@ -3,9 +3,9 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using Nerosoft.Euonia.Bus.InMemory.Tests.Commands; +using Nerosoft.Euonia.Bus.Tests.Commands; -namespace Nerosoft.Euonia.Bus.InMemory.Tests; +namespace Nerosoft.Euonia.Bus.Tests; public class ServiceBusTests { @@ -43,4 +43,4 @@ public async Task TestSendCommand_HasReponse_MessageHasResultInherites() var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); Assert.Equal(1, result); } -} +} \ No newline at end of file diff --git a/Tests/Euonia.Bus.Tests.Shared/TestNotificationBucket.cs b/Tests/Euonia.Bus.Tests.Shared/TestNotificationBucket.cs new file mode 100644 index 0000000..72f5403 --- /dev/null +++ b/Tests/Euonia.Bus.Tests.Shared/TestNotificationBucket.cs @@ -0,0 +1,6 @@ +namespace Nerosoft.Euonia.Bus.Tests; + +public class TestNotificationBucket +{ + +} \ No newline at end of file From c9dda2ed036cef505d62ec349a836db3528db154 Mon Sep 17 00:00:00 2001 From: damon Date: Wed, 29 Nov 2023 18:17:01 +0800 Subject: [PATCH 28/37] Update RabbitMq consumer. --- .../Contracts/IBusConfigurator.cs | 5 - Source/Euonia.Bus.Abstract/RoutedMessage.cs | 4 +- .../BusConfiguratorExtensions.cs | 45 +--- .../BusConfiguratorExtensions.cs | 24 +- .../Euonia.Bus.RabbitMq/RabbitMqDispatcher.cs | 210 ++++++++++-------- .../RabbitMqMessageBusOptions.cs | 82 +++---- .../RabbitMqQueueConsumer.cs | 63 ++++-- .../RabbitMqQueueRecipient.cs | 36 +-- .../RabbitMqTopicSubscriber.cs | 41 +++- Source/Euonia.Bus/BusConfigurator.cs | 8 +- Source/Euonia.Bus/Core/SendOptions.cs | 1 + Source/Euonia.Bus/Core/ServiceBus.cs | 9 +- .../Euonia.Bus/ServiceCollectionExtensions.cs | 93 +------- 13 files changed, 281 insertions(+), 340 deletions(-) diff --git a/Source/Euonia.Bus.Abstract/Contracts/IBusConfigurator.cs b/Source/Euonia.Bus.Abstract/Contracts/IBusConfigurator.cs index 62bb2f9..0ca20c3 100644 --- a/Source/Euonia.Bus.Abstract/Contracts/IBusConfigurator.cs +++ b/Source/Euonia.Bus.Abstract/Contracts/IBusConfigurator.cs @@ -12,11 +12,6 @@ public interface IBusConfigurator /// IServiceCollection Service { get; } - /// - /// Gets the message handle registrations. - /// - IReadOnlyList Registrations { get; } - /// /// Set the service bus factory. /// diff --git a/Source/Euonia.Bus.Abstract/RoutedMessage.cs b/Source/Euonia.Bus.Abstract/RoutedMessage.cs index d215e98..626a67e 100644 --- a/Source/Euonia.Bus.Abstract/RoutedMessage.cs +++ b/Source/Euonia.Bus.Abstract/RoutedMessage.cs @@ -30,7 +30,7 @@ protected RoutedMessage() /// Gets or sets the correlation identifier. /// [DataMember] - public virtual string CorrelationId { get; } + public virtual string CorrelationId { get; set; } = Guid.NewGuid().ToString(); /// /// Gets or sets the conversation identifier. @@ -123,7 +123,7 @@ public TData Data _data = value; if (value != null) { - Metadata[MessageTypeKey] = value.GetType().AssemblyQualifiedName; + Metadata[MessageTypeKey] = value.GetType().GetFullNameWithAssemblyName(); } } } diff --git a/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs b/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs index 9073ce6..18b3443 100644 --- a/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs +++ b/Source/Euonia.Bus.InMemory/BusConfiguratorExtensions.cs @@ -34,53 +34,10 @@ public static void UseInMemory(this IBusConfigurator configurator, Action WeakReferenceMessenger.Default, _ => throw new ArgumentOutOfRangeException(nameof(options.MessengerReference), options.MessengerReference, null) }; - - var convention = provider.GetService(); - - if (options.MultipleSubscriberInstance) - { - foreach (var registration in configurator.Registrations) - { - InMemoryRecipient recipient; - if (convention.IsQueueType(registration.MessageType)) - { - recipient = ActivatorUtilities.GetServiceOrCreateInstance(provider); - } - else if (convention.IsTopicType(registration.MessageType)) - { - recipient = ActivatorUtilities.GetServiceOrCreateInstance(provider); - } - else - { - throw new InvalidOperationException(); - } - messenger.Register(recipient, registration.Channel); - } - } - else - { - foreach (var registration in configurator.Registrations) - { - InMemoryRecipient recipient; - if (convention.IsQueueType(registration.MessageType)) - { - recipient = Singleton.Get(() => ActivatorUtilities.GetServiceOrCreateInstance(provider)); - } - else if (convention.IsTopicType(registration.MessageType)) - { - recipient = Singleton.Get(() => ActivatorUtilities.GetServiceOrCreateInstance(provider)); - } - else - { - throw new InvalidOperationException(); - } - messenger.Register(recipient, registration.Channel); - } - } - return messenger; }); configurator.Service.TryAddSingleton(); + configurator.Service.AddTransient(); configurator.SerFactory(); } } \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/BusConfiguratorExtensions.cs b/Source/Euonia.Bus.RabbitMq/BusConfiguratorExtensions.cs index 3efd8c2..c823e9b 100644 --- a/Source/Euonia.Bus.RabbitMq/BusConfiguratorExtensions.cs +++ b/Source/Euonia.Bus.RabbitMq/BusConfiguratorExtensions.cs @@ -1,7 +1,7 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; +using RabbitMQ.Client; namespace Nerosoft.Euonia.Bus.RabbitMq; @@ -18,9 +18,25 @@ public static class BusConfiguratorExtensions public static void UseRabbitMq(this IBusConfigurator configurator, Action configuration) { configurator.Service.Configure(configuration); + + configurator.Service.TryAddSingleton(provider => + { + var options = provider.GetService>()?.Value; + + if (options == null) + { + throw new InvalidOperationException("RabbitMqMessageBusOptions was not configured."); + } + + var factory = new ConnectionFactory { Uri = new Uri(options.Connection) }; + return factory; + }); + + configurator.Service.TryAddTransient(); + configurator.Service.TryAddTransient(); + configurator.Service.TryAddSingleton(); - configurator.Service.AddTransient(); - configurator.Service.AddTransient(); + configurator.Service.AddTransient(); configurator.SerFactory(); } } \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/RabbitMqDispatcher.cs b/Source/Euonia.Bus.RabbitMq/RabbitMqDispatcher.cs index 1e73fa8..b0973e6 100644 --- a/Source/Euonia.Bus.RabbitMq/RabbitMqDispatcher.cs +++ b/Source/Euonia.Bus.RabbitMq/RabbitMqDispatcher.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Polly; using RabbitMQ.Client; using RabbitMQ.Client.Events; @@ -16,19 +17,19 @@ public class RabbitMqDispatcher : IDispatcher public event EventHandler Delivered; private readonly RabbitMqMessageBusOptions _options; - private readonly IConnection _connection; + private readonly ConnectionFactory _factory; private readonly ILogger _logger; /// /// Initialize a new instance of . /// - /// + /// /// /// - public RabbitMqDispatcher(IConnection connection, IOptions options, ILoggerFactory logger) + public RabbitMqDispatcher(ConnectionFactory factory, IOptions options, ILoggerFactory logger) { _logger = logger.CreateLogger(); - _connection = connection; + _factory = factory; _options = options.Value; } @@ -36,58 +37,57 @@ public RabbitMqDispatcher(IConnection connection, IOptions(RoutedMessage message, CancellationToken cancellationToken = default) where TMessage : class { - using (var channel = _connection.CreateModel()) - { - var typeName = message.Data.GetType().GetFullNameWithAssemblyName(); - - var props = channel.CreateBasicProperties(); - props.Headers ??= new Dictionary(); - props.Headers[Constants.MessageHeaders.MessageType] = typeName; - props.Type = typeName; - - await Policy.Handle() - .WaitAndRetryAsync(_options.MaxFailureRetries, _ => TimeSpan.FromSeconds(3), (exception, _, retryCount, _) => - { - _logger.LogError(exception, "Retry:{RetryCount}, {Message}", retryCount, exception.Message); - }) - .ExecuteAsync(async () => - { - var messageBody = await SerializeAsync(message, cancellationToken); - - channel.ExchangeDeclare(_options.ExchangeName, _options.ExchangeType); - channel.BasicPublish(_options.ExchangeName, message.Channel, props, messageBody); - - Delivered?.Invoke(this, new MessageDispatchedEventArgs(message.Data, null)); - }); - } + using var connection = _factory.CreateConnection(); + using var channel = connection.CreateModel(); + + var typeName = message.GetTypeName(); + + var props = channel.CreateBasicProperties(); + props.Headers ??= new Dictionary(); + props.Headers[Constants.MessageHeaders.MessageType] = typeName; + props.Type = typeName; + + await Policy.Handle() + .WaitAndRetryAsync(_options.MaxFailureRetries, _ => TimeSpan.FromSeconds(3), (exception, _, retryCount, _) => + { + _logger.LogError(exception, "Retry:{RetryCount}, {Message}", retryCount, exception.Message); + }) + .ExecuteAsync(async () => + { + var messageBody = await SerializeAsync(message, cancellationToken); + + channel.ExchangeDeclare(_options.ExchangeName, _options.ExchangeType); + channel.BasicPublish(_options.ExchangeName, $"{_options.TopicName}${message.Channel}$", props, messageBody); + + Delivered?.Invoke(this, new MessageDispatchedEventArgs(message.Data, null)); + }); } /// public async Task SendAsync(RoutedMessage message, CancellationToken cancellationToken = default) where TMessage : class { - using (var channel = _connection.CreateModel()) - { - var typeName = message.Data.GetType().GetFullNameWithAssemblyName(); - - var props = channel.CreateBasicProperties(); - props.Headers ??= new Dictionary(); - props.Headers[Constants.MessageHeaders.MessageType] = typeName; - props.Type = typeName; - - await Policy.Handle() - .WaitAndRetryAsync(_options.MaxFailureRetries, _ => TimeSpan.FromSeconds(3), (exception, _, retryCount, _) => - { - _logger.LogError(exception, "Retry:{RetryCount}, {Message}", retryCount, exception.Message); - }) - .ExecuteAsync(async () => - { - var messageBody = await SerializeAsync(message, cancellationToken); - - channel.BasicPublish("", $"{_options.QueueName}${message.Channel}$", props, messageBody); - - Delivered?.Invoke(this, new MessageDispatchedEventArgs(message.Data, null)); - }); - } + using var connection = _factory.CreateConnection(); + using var channel = connection.CreateModel(); + var typeName = message.GetTypeName(); + + var props = channel.CreateBasicProperties(); + props.Headers ??= new Dictionary(); + props.Headers[Constants.MessageHeaders.MessageType] = typeName; + props.Type = typeName; + + await Policy.Handle() + .WaitAndRetryAsync(_options.MaxFailureRetries, _ => TimeSpan.FromSeconds(3), (exception, _, retryCount, _) => + { + _logger.LogError(exception, "Retry:{RetryCount}, {Message}", retryCount, exception.Message); + }) + .ExecuteAsync(async () => + { + var messageBody = await SerializeAsync(message, cancellationToken); + + channel.BasicPublish("", $"{_options.QueueName}${message.Channel}$", props, messageBody); + + Delivered?.Invoke(this, new MessageDispatchedEventArgs(message.Data, null)); + }); } /// @@ -95,68 +95,82 @@ public async Task SendAsync(RoutedMessage(); - using (var channel = _connection.CreateModel()) - { - var replyQueueName = channel.QueueDeclare().QueueName; - var consumer = new EventingBasicConsumer(channel); + using var connection = _factory.CreateConnection(); + + using var channel = connection.CreateModel(); + + var replyQueueName = channel.QueueDeclare().QueueName; + var consumer = new EventingBasicConsumer(channel); + + consumer.Received += OnReceived; + + var typeName = message.GetTypeName(); + + var props = channel.CreateBasicProperties(); + props.Headers ??= new Dictionary(); + props.Headers[Constants.MessageHeaders.MessageType] = typeName; + props.Type = typeName; + props.CorrelationId = message.CorrelationId; + props.ReplyTo = replyQueueName; + + await Policy.Handle() + .WaitAndRetryAsync(_options.MaxFailureRetries, _ => TimeSpan.FromSeconds(1), (exception, _, retryCount, _) => + { + _logger.LogError(exception, "Retry:{RetryCount}, {Message}", retryCount, exception.Message); + }) + .ExecuteAsync(async () => + { + var messageBody = await SerializeAsync(message, cancellationToken); + channel.BasicPublish("", $"{_options.QueueName}${message.Channel}$", props, messageBody); + channel.BasicConsume(consumer, replyQueueName, true); - consumer.Received += (_, args) => + Delivered?.Invoke(this, new MessageDispatchedEventArgs(message.Data, null)); + }); + + var result = await task.Task; + consumer.Received -= OnReceived; + return result; + + void OnReceived(object sender, BasicDeliverEventArgs args) + { + if (args.BasicProperties.CorrelationId != message.CorrelationId) { - if (args.BasicProperties.CorrelationId != message.CorrelationId) - { - return; - } - - var body = args.Body.ToArray(); - var response = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(body), Constants.SerializerSettings); - - task.SetResult(response); - }; - - var typeName = message.Data.GetType().GetFullNameWithAssemblyName(); - - var props = channel.CreateBasicProperties(); - props.Headers ??= new Dictionary(); - props.Headers[Constants.MessageHeaders.MessageType] = typeName; - props.Type = typeName; - props.CorrelationId = message.CorrelationId; - props.ReplyTo = replyQueueName; - - await Policy.Handle() - .WaitAndRetryAsync(_options.MaxFailureRetries, _ => TimeSpan.FromSeconds(1), (exception, _, retryCount, _) => - { - _logger.LogError(exception, "Retry:{RetryCount}, {Message}", retryCount, exception.Message); - }) - .ExecuteAsync(async () => - { - var messageBody = await SerializeAsync(message, cancellationToken); - channel.BasicPublish("", $"{_options.QueueName}${message.Channel}$", props, messageBody); - channel.BasicConsume(consumer, replyQueueName, true); - - Delivered?.Invoke(this, new MessageDispatchedEventArgs(message.Data, null)); - }); - } + return; + } + + var body = args.Body.ToArray(); + var response = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(body), Constants.SerializerSettings); - return await task.Task; + task.SetResult(response); + } } + /// + /// Serializes the message to bytes. + /// + /// + /// + /// + /// private static async Task SerializeAsync(RoutedMessage message, CancellationToken cancellationToken = default) where TMessage : class { if (message == null) { - return Array.Empty(); + return []; } await using var stream = new MemoryStream(); - await using var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true); - using var jsonWriter = new JsonTextWriter(writer); - - JsonSerializer.Create(Constants.SerializerSettings).Serialize(jsonWriter, message); + // The default UTF8Encoding emits the BOM will cause the RabbitMQ client to fail to deserialize the message. + using (var writer = new StreamWriter(stream, new UTF8Encoding(false))) + { + using var jsonWriter = new JsonTextWriter(writer); - await jsonWriter.FlushAsync(cancellationToken); - await writer.FlushAsync(); + JsonSerializer.CreateDefault().Serialize(jsonWriter, message); + await jsonWriter.FlushAsync(cancellationToken); + await writer.FlushAsync(); + } return stream.ToArray(); } } \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/RabbitMqMessageBusOptions.cs b/Source/Euonia.Bus.RabbitMq/RabbitMqMessageBusOptions.cs index 98a5404..142ab6d 100644 --- a/Source/Euonia.Bus.RabbitMq/RabbitMqMessageBusOptions.cs +++ b/Source/Euonia.Bus.RabbitMq/RabbitMqMessageBusOptions.cs @@ -5,45 +5,45 @@ /// public class RabbitMqMessageBusOptions { - /// - /// Gets or sets the RabbitMQ connection string. - /// amqp://user:password@host:port - /// - public string Connection { get; set; } - - /// - /// Gets or sets the exchange name. - /// - public string ExchangeName { get; set; } - - /// - /// Gets or sets the command queue name. - /// - public string QueueName { get; set; } - - /// - /// Gets or sets the event queue name. - /// - public string EventQueueName { get; set; } - - /// - /// Gets or sets the exchange type. - /// Values: fanout, direct, headers, topic - /// - public string ExchangeType { get; set; } = RabbitMQ.Client.ExchangeType.Fanout; - - /// - /// - /// - public string RoutingKey { get; set; } - - /// - /// - /// - public bool AutoAck { get; set; } = false; - - /// - /// - /// - public int MaxFailureRetries { get; set; } = 3; + /// + /// Gets or sets the RabbitMQ connection string. + /// amqp://user:password@host:port + /// + public string Connection { get; set; } + + /// + /// Gets or sets the exchange name. + /// + public string ExchangeName { get; set; } + + /// + /// Gets or sets the queue name. + /// + public string QueueName { get; set; } + + /// + /// Gets or sets the topic name. + /// + public string TopicName { get; set; } + + /// + /// Gets or sets the exchange type. + /// Values: fanout, direct, headers, topic + /// + public string ExchangeType { get; set; } = RabbitMQ.Client.ExchangeType.Fanout; + + /// + /// + /// + public string RoutingKey { get; set; } = "*"; + + /// + /// + /// + public bool AutoAck { get; set; } = false; + + /// + /// + /// + public int MaxFailureRetries { get; set; } = 3; } diff --git a/Source/Euonia.Bus.RabbitMq/RabbitMqQueueConsumer.cs b/Source/Euonia.Bus.RabbitMq/RabbitMqQueueConsumer.cs index a52a904..3a11a0f 100644 --- a/Source/Euonia.Bus.RabbitMq/RabbitMqQueueConsumer.cs +++ b/Source/Euonia.Bus.RabbitMq/RabbitMqQueueConsumer.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Options; +using System.Threading.Channels; +using Microsoft.Extensions.Options; using RabbitMQ.Client; using RabbitMQ.Client.Events; @@ -10,25 +11,46 @@ namespace Nerosoft.Euonia.Bus.RabbitMq; public class RabbitMqQueueConsumer : RabbitMqQueueRecipient, IQueueConsumer { /// - /// Initializes a new instance of the <see cref="RabbitMqQueueConsumer"/> class. + /// Initializes a new instance of the class. /// - /// + /// /// /// - public RabbitMqQueueConsumer(IConnection connection, IHandlerContext handler, IOptions options) - : base(connection, handler, options) + public RabbitMqQueueConsumer(ConnectionFactory factory, IHandlerContext handler, IOptions options) + : base(factory, handler, options) { } /// public string Name => nameof(RabbitMqQueueConsumer); + private IConnection Connection { get; set; } + + /// + /// Gets the RabbitMQ message channel. + /// + private IModel Channel { get; set; } + + /// + /// Gets the RabbitMQ consumer instance. + /// + private EventingBasicConsumer Consumer { get; set; } + internal override void Start(string channel) { var queueName = $"{Options.QueueName}${channel}$"; + + Connection = ConnectionFactory.CreateConnection(); + + Channel = Connection.CreateModel(); + Channel.QueueDeclare(queueName, true, false, false, null); Channel.BasicQos(0, 1, false); - Channel.BasicConsume(channel, Options.AutoAck, Consumer); + + Consumer = new EventingBasicConsumer(Channel); + Consumer.Received += HandleMessageReceived; + + Channel.BasicConsume(queueName, Options.AutoAck, Consumer); } /// @@ -39,7 +61,6 @@ protected override async void HandleMessageReceived(object sender, BasicDeliverE var message = DeserializeMessage(args.Body.ToArray(), type); var props = args.BasicProperties; - var replyNeeded = !string.IsNullOrEmpty(props.CorrelationId); var context = new MessageContext(); @@ -52,17 +73,15 @@ protected override async void HandleMessageReceived(object sender, BasicDeliverE }; context.Completed += (_, _) => { - if (!Options.AutoAck) - { - Channel.BasicAck(args.DeliveryTag, false); - } + taskCompletion.TryCompleteFromCompletedTask(Task.FromResult(default(object))); }; await Handler.HandleAsync(message.Channel, message.Data, context); - if (replyNeeded) + var result = await taskCompletion.Task; + + if (!string.IsNullOrEmpty(props.CorrelationId) || !string.IsNullOrWhiteSpace(props.ReplyTo)) { - var result = await taskCompletion.Task; var replyProps = Channel.CreateBasicProperties(); replyProps.Headers ??= new Dictionary(); replyProps.Headers.Add(Constants.MessageHeaders.MessageType, result.GetType().GetFullNameWithAssemblyName()); @@ -70,13 +89,23 @@ protected override async void HandleMessageReceived(object sender, BasicDeliverE var response = SerializeMessage(result); Channel.BasicPublish(string.Empty, props.ReplyTo, replyProps, response); - Channel.BasicAck(args.DeliveryTag, false); } - else + + Channel.BasicAck(args.DeliveryTag, false); + + OnMessageAcknowledged(new MessageAcknowledgedEventArgs(message.Data, context)); + } + + /// + protected override void Dispose(bool disposing) + { + if (!disposing) { - taskCompletion.SetCanceled(); + return; } - OnMessageAcknowledged(new MessageAcknowledgedEventArgs(message.Data, context)); + Consumer.Received -= HandleMessageReceived; + Channel?.Dispose(); + Connection?.Dispose(); } } \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/RabbitMqQueueRecipient.cs b/Source/Euonia.Bus.RabbitMq/RabbitMqQueueRecipient.cs index f46db33..137da57 100644 --- a/Source/Euonia.Bus.RabbitMq/RabbitMqQueueRecipient.cs +++ b/Source/Euonia.Bus.RabbitMq/RabbitMqQueueRecipient.cs @@ -23,32 +23,25 @@ public abstract class RabbitMqQueueRecipient : DisposableObject /// /// Initializes a new instance of the class. /// - /// + /// /// /// - protected RabbitMqQueueRecipient(IConnection connection, IHandlerContext handler, IOptions options) + protected RabbitMqQueueRecipient(ConnectionFactory factory, IHandlerContext handler, IOptions options) { Options = options.Value; Handler = handler; - Channel = connection.CreateModel(); - Consumer = new EventingBasicConsumer(Channel); - Consumer.Received += HandleMessageReceived; + ConnectionFactory = factory; } /// - /// Gets the RabbitMQ message bus options. - /// - protected virtual RabbitMqMessageBusOptions Options { get; } - - /// - /// Gets the RabbitMQ message channel. + /// /// - protected virtual IModel Channel { get; } + protected ConnectionFactory ConnectionFactory { get; } /// - /// Gets the RabbitMQ consumer instance. + /// Gets the RabbitMQ message bus options. /// - protected virtual EventingBasicConsumer Consumer { get; } + protected virtual RabbitMqMessageBusOptions Options { get; } /// /// Gets the message handler context instance. @@ -112,7 +105,8 @@ protected virtual byte[] SerializeMessage(object message) protected virtual IRoutedMessage DeserializeMessage(byte[] message, Type messageType) { var type = typeof(RoutedMessage<>).MakeGenericType(messageType); - return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(message), type, Constants.SerializerSettings) as IRoutedMessage; + var json = Encoding.UTF8.GetString(message); + return JsonConvert.DeserializeObject(json, type, Constants.SerializerSettings) as IRoutedMessage; } /// @@ -141,16 +135,4 @@ protected virtual string GetHeaderValue(IDictionary header, stri return string.Empty; } - - /// - protected override void Dispose(bool disposing) - { - if (!disposing) - { - return; - } - - Consumer.Received -= HandleMessageReceived; - Channel?.Dispose(); - } } \ No newline at end of file diff --git a/Source/Euonia.Bus.RabbitMq/RabbitMqTopicSubscriber.cs b/Source/Euonia.Bus.RabbitMq/RabbitMqTopicSubscriber.cs index 998bf6f..4e9e1c3 100644 --- a/Source/Euonia.Bus.RabbitMq/RabbitMqTopicSubscriber.cs +++ b/Source/Euonia.Bus.RabbitMq/RabbitMqTopicSubscriber.cs @@ -15,7 +15,7 @@ public class RabbitMqTopicSubscriber : RabbitMqQueueRecipient, ITopicSubscriber /// /// /// - public RabbitMqTopicSubscriber(IConnection connection, IHandlerContext handler, IOptions options) + public RabbitMqTopicSubscriber(ConnectionFactory connection, IHandlerContext handler, IOptions options) : base(connection, handler, options) { } @@ -23,20 +23,38 @@ public RabbitMqTopicSubscriber(IConnection connection, IHandlerContext handler, /// public string Name => nameof(RabbitMqTopicSubscriber); + private IConnection Connection { get; set; } + + /// + /// Gets the RabbitMQ message channel. + /// + private IModel Channel { get; set; } + + /// + /// Gets the RabbitMQ consumer instance. + /// + private EventingBasicConsumer Consumer { get; set; } + internal override void Start(string channel) { + Connection = ConnectionFactory.CreateConnection(); + Channel = Connection.CreateModel(); + string queueName; - if (string.IsNullOrWhiteSpace(Options.EventQueueName)) + if (string.IsNullOrWhiteSpace(Options.TopicName)) { Channel.ExchangeDeclare(channel, Options.ExchangeType); queueName = Channel.QueueDeclare().QueueName; } else { - Channel.QueueDeclare(Options.EventQueueName, true, false, false, null); - queueName = Options.EventQueueName; + Channel.QueueDeclare(Options.TopicName, true, false, false, null); + queueName = Options.TopicName; } - + + Consumer = new EventingBasicConsumer(Channel); + Consumer.Received += HandleMessageReceived; + Channel.QueueBind(queueName, channel, Options.RoutingKey ?? "*"); Channel.BasicConsume(string.Empty, Options.AutoAck, Consumer); } @@ -61,4 +79,17 @@ protected override async void HandleMessageReceived(object sender, BasicDeliverE OnMessageAcknowledged(new MessageAcknowledgedEventArgs(message.Data, context)); } + + /// + protected override void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + Consumer.Received -= HandleMessageReceived; + Channel?.Dispose(); + Connection?.Dispose(); + } } \ No newline at end of file diff --git a/Source/Euonia.Bus/BusConfigurator.cs b/Source/Euonia.Bus/BusConfigurator.cs index cd9379c..ecb3209 100644 --- a/Source/Euonia.Bus/BusConfigurator.cs +++ b/Source/Euonia.Bus/BusConfigurator.cs @@ -11,12 +11,12 @@ namespace Nerosoft.Euonia.Bus; /// public class BusConfigurator : IBusConfigurator { - private readonly List _registrations = new(); + private readonly List _registrations = []; private MessageConventionBuilder ConventionBuilder { get; } = new(); /// - /// The message handler types. + /// Gets the message handle registrations. /// public IReadOnlyList Registrations => _registrations; @@ -40,7 +40,7 @@ public BusConfigurator(IServiceCollection service) public IBusConfigurator SerFactory() where TFactory : class, IBusFactory { - Service.AddSingleton(); + Service.TryAddSingleton(); return this; } @@ -53,7 +53,7 @@ public IBusConfigurator SerFactory() public IBusConfigurator SerFactory(TFactory factory) where TFactory : class, IBusFactory { - Service.AddSingleton(factory); + Service.TryAddSingleton(factory); return this; } diff --git a/Source/Euonia.Bus/Core/SendOptions.cs b/Source/Euonia.Bus/Core/SendOptions.cs index 8e2f8d6..f81c838 100644 --- a/Source/Euonia.Bus/Core/SendOptions.cs +++ b/Source/Euonia.Bus/Core/SendOptions.cs @@ -5,4 +5,5 @@ /// public class SendOptions : ExtendableOptions { + public string CorrelationId { get; set; } } \ No newline at end of file diff --git a/Source/Euonia.Bus/Core/ServiceBus.cs b/Source/Euonia.Bus/Core/ServiceBus.cs index 378cc74..ba77ff8 100644 --- a/Source/Euonia.Bus/Core/ServiceBus.cs +++ b/Source/Euonia.Bus/Core/ServiceBus.cs @@ -39,7 +39,8 @@ public async Task SendAsync(TMessage message, SendOptions options, Can var channelName = options?.Channel ?? MessageCache.Default.GetOrAddChannel(); var pack = new RoutedMessage(message, channelName) { - MessageId = options?.MessageId ?? Guid.NewGuid().ToString() + MessageId = options?.MessageId ?? Guid.NewGuid().ToString(), + CorrelationId = options?.CorrelationId ?? Guid.NewGuid().ToString() }; await _dispatcher.SendAsync(pack, cancellationToken); } @@ -56,7 +57,8 @@ public async Task SendAsync(TMessage message, SendOp var channelName = options?.Channel ?? MessageCache.Default.GetOrAddChannel(); var pack = new RoutedMessage(message, channelName) { - MessageId = options?.MessageId ?? Guid.NewGuid().ToString() + MessageId = options?.MessageId ?? Guid.NewGuid().ToString(), + CorrelationId = options?.CorrelationId ?? Guid.NewGuid().ToString() }; return await _dispatcher.SendAsync(pack, cancellationToken); } @@ -67,7 +69,8 @@ public async Task SendAsync(IQueue message, SendOptio var channelName = options?.Channel ?? MessageCache.Default.GetOrAddChannel(message.GetType()); var pack = new RoutedMessage, TResult>(message, channelName) { - MessageId = options?.MessageId ?? Guid.NewGuid().ToString() + MessageId = options?.MessageId ?? Guid.NewGuid().ToString(), + CorrelationId = options?.CorrelationId ?? Guid.NewGuid().ToString() }; return await _dispatcher.SendAsync(pack, cancellationToken); } diff --git a/Source/Euonia.Bus/ServiceCollectionExtensions.cs b/Source/Euonia.Bus/ServiceCollectionExtensions.cs index 8171341..0fa56fe 100644 --- a/Source/Euonia.Bus/ServiceCollectionExtensions.cs +++ b/Source/Euonia.Bus/ServiceCollectionExtensions.cs @@ -8,94 +8,6 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class ServiceCollectionExtensions { - /// - /// Add message handler. - /// - /// - /// - internal static void AddMessageHandler(this IServiceCollection services, Action callback = null) - { - services.AddSingleton(provider => - { - var context = new HandlerContext(provider); - context.MessageSubscribed += (sender, args) => - { - callback?.Invoke(sender, args); - }; - return context; - }); - } - - /// - /// Add message handler. - /// - /// - /// - /// - internal static void AddMessageHandler(this IServiceCollection services, Func> handlerTypesFactory, Action callback = null) - { - var handlerTypes = handlerTypesFactory?.Invoke(); - - services.AddMessageHandler(handlerTypes, callback); - } - - /// - /// Add message handler. - /// - /// - /// - /// - internal static void AddMessageHandler(this IServiceCollection services, IEnumerable handlerTypes, Action callback = null) - { - services.AddMessageHandler(callback); - - if (handlerTypes == null) - { - return; - } - - if (!handlerTypes.Any()) - { - return; - } - - foreach (var handlerType in handlerTypes) - { - if (!handlerType.IsClass) - { - continue; - } - - if (handlerType.IsAbstract) - { - continue; - } - - if (handlerType.GetInterface(nameof(IHandler)) == null) - { - continue; - } - - var inheritedTypes = handlerType.GetInterfaces().Where(t => t.IsGenericType); - - foreach (var inheritedType in inheritedTypes) - { - if (inheritedType.Name.Contains(nameof(IHandler))) - { - continue; - } - - if (inheritedType.GenericTypeArguments.Length == 0) - { - continue; - } - - services.TryAddScoped(inheritedType, handlerType); - services.TryAddScoped(handlerType); - } - } - } - /// /// Register message bus. /// @@ -110,14 +22,15 @@ public static void AddServiceBus(this IServiceCollection services, Action(provider => { var context = new HandlerContext(provider); - foreach (var subscription in configurator.Registrations) + foreach (var registration in configurator.Registrations) { - context.Register(subscription); + context.Register(registration); } return context; }); services.TryAddSingleton(); services.AddSingleton(); + services.AddHostedService(); } } \ No newline at end of file From 2630801a591c19cef8437cbaf3fbe49801f54887 Mon Sep 17 00:00:00 2001 From: damon Date: Wed, 29 Nov 2023 20:34:25 +0800 Subject: [PATCH 29/37] Optimize code. --- .../Euonia.Bus.Tests.Shared/Commands/FooCreateCommand.cs | 8 +------- .../Euonia.Bus.Tests.Shared/Commands/UserCreateCommand.cs | 8 +------- .../Euonia.Bus.Tests.Shared/Commands/UserUpdateCommand.cs | 8 +------- .../Euonia.Bus.Tests.Shared/Handlers/FooCommandHandler.cs | 7 +------ .../Handlers/UserCommandHandler.cs | 7 +------ Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs | 7 +------ 6 files changed, 6 insertions(+), 39 deletions(-) diff --git a/Tests/Euonia.Bus.Tests.Shared/Commands/FooCreateCommand.cs b/Tests/Euonia.Bus.Tests.Shared/Commands/FooCreateCommand.cs index e021714..a0f574f 100644 --- a/Tests/Euonia.Bus.Tests.Shared/Commands/FooCreateCommand.cs +++ b/Tests/Euonia.Bus.Tests.Shared/Commands/FooCreateCommand.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Nerosoft.Euonia.Bus.Tests.Commands; +namespace Nerosoft.Euonia.Bus.Tests.Commands; //[Channel("foo.create")] public class FooCreateCommand : IQueue diff --git a/Tests/Euonia.Bus.Tests.Shared/Commands/UserCreateCommand.cs b/Tests/Euonia.Bus.Tests.Shared/Commands/UserCreateCommand.cs index 6ac85ee..d76bb4b 100644 --- a/Tests/Euonia.Bus.Tests.Shared/Commands/UserCreateCommand.cs +++ b/Tests/Euonia.Bus.Tests.Shared/Commands/UserCreateCommand.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Nerosoft.Euonia.Bus.Tests.Commands; +namespace Nerosoft.Euonia.Bus.Tests.Commands; [Queue] public class UserCreateCommand diff --git a/Tests/Euonia.Bus.Tests.Shared/Commands/UserUpdateCommand.cs b/Tests/Euonia.Bus.Tests.Shared/Commands/UserUpdateCommand.cs index 8a102b4..d6f19cf 100644 --- a/Tests/Euonia.Bus.Tests.Shared/Commands/UserUpdateCommand.cs +++ b/Tests/Euonia.Bus.Tests.Shared/Commands/UserUpdateCommand.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Nerosoft.Euonia.Bus.Tests.Commands; +namespace Nerosoft.Euonia.Bus.Tests.Commands; [Queue] public class UserUpdateCommand diff --git a/Tests/Euonia.Bus.Tests.Shared/Handlers/FooCommandHandler.cs b/Tests/Euonia.Bus.Tests.Shared/Handlers/FooCommandHandler.cs index 974989f..0f684bc 100644 --- a/Tests/Euonia.Bus.Tests.Shared/Handlers/FooCommandHandler.cs +++ b/Tests/Euonia.Bus.Tests.Shared/Handlers/FooCommandHandler.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Nerosoft.Euonia.Bus.Tests.Commands; +using Nerosoft.Euonia.Bus.Tests.Commands; namespace Nerosoft.Euonia.Bus.Tests.Handlers; diff --git a/Tests/Euonia.Bus.Tests.Shared/Handlers/UserCommandHandler.cs b/Tests/Euonia.Bus.Tests.Shared/Handlers/UserCommandHandler.cs index 9894381..fde0a4b 100644 --- a/Tests/Euonia.Bus.Tests.Shared/Handlers/UserCommandHandler.cs +++ b/Tests/Euonia.Bus.Tests.Shared/Handlers/UserCommandHandler.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Nerosoft.Euonia.Bus.Tests.Commands; +using Nerosoft.Euonia.Bus.Tests.Commands; namespace Nerosoft.Euonia.Bus.Tests.Handlers { diff --git a/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs b/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs index 99f2cf6..6ff5436 100644 --- a/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs +++ b/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Nerosoft.Euonia.Bus.Tests.Commands; +using Nerosoft.Euonia.Bus.Tests.Commands; namespace Nerosoft.Euonia.Bus.Tests; From 0925202f70d744367772ab4b8da29228049179a9 Mon Sep 17 00:00:00 2001 From: damon Date: Wed, 29 Nov 2023 20:35:51 +0800 Subject: [PATCH 30/37] Summaries. --- .../MessageConventionType.cs | 3 + .../Recipients/IRecipientRegistrar.cs | 9 ++ .../InMemoryRecipientRegistrar.cs | 13 ++ .../RabbitMqRecipientRegistrar.cs | 11 +- Source/Euonia.Bus/BusConfigurator.cs | 1 + .../Conventions/MessageConvention.cs | 2 +- Source/Euonia.Bus/Core/HandlerContext.cs | 12 -- Source/Euonia.Bus/Core/SendOptions.cs | 5 +- .../Properties/Resources.zh-CN.resx | 126 ++++++++++++++++++ 9 files changed, 167 insertions(+), 15 deletions(-) create mode 100644 Source/Euonia.Bus/Properties/Resources.zh-CN.resx diff --git a/Source/Euonia.Bus.Abstract/MessageConventionType.cs b/Source/Euonia.Bus.Abstract/MessageConventionType.cs index 799699d..083f376 100644 --- a/Source/Euonia.Bus.Abstract/MessageConventionType.cs +++ b/Source/Euonia.Bus.Abstract/MessageConventionType.cs @@ -1,5 +1,8 @@ namespace Nerosoft.Euonia.Bus; +/// +/// Defines the message convention type. +/// public enum MessageConventionType { /// diff --git a/Source/Euonia.Bus.Abstract/Recipients/IRecipientRegistrar.cs b/Source/Euonia.Bus.Abstract/Recipients/IRecipientRegistrar.cs index 7f7aad4..9565b63 100644 --- a/Source/Euonia.Bus.Abstract/Recipients/IRecipientRegistrar.cs +++ b/Source/Euonia.Bus.Abstract/Recipients/IRecipientRegistrar.cs @@ -1,6 +1,15 @@ namespace Nerosoft.Euonia.Bus; +/// +/// Defines interface for message recipient registrar. +/// public interface IRecipientRegistrar { + /// + /// + /// + /// + /// + /// Task RegisterAsync(IReadOnlyList registrations, CancellationToken cancellationToken = default); } diff --git a/Source/Euonia.Bus.InMemory/InMemoryRecipientRegistrar.cs b/Source/Euonia.Bus.InMemory/InMemoryRecipientRegistrar.cs index c4dda06..2d22117 100644 --- a/Source/Euonia.Bus.InMemory/InMemoryRecipientRegistrar.cs +++ b/Source/Euonia.Bus.InMemory/InMemoryRecipientRegistrar.cs @@ -3,6 +3,9 @@ namespace Nerosoft.Euonia.Bus.InMemory; +/// +/// The in-memory message recipient registrar. +/// public sealed class InMemoryRecipientRegistrar : IRecipientRegistrar { private readonly InMemoryBusOptions _options; @@ -10,6 +13,13 @@ public sealed class InMemoryRecipientRegistrar : IRecipientRegistrar private readonly IServiceProvider _provider; private readonly IMessenger _messenger; + /// + /// Initializes a new instance of the . + /// + /// + /// + /// + /// public InMemoryRecipientRegistrar(IMessenger messenger, IMessageConvention convention, IServiceProvider provider, IOptions options) { _options = options.Value; @@ -18,6 +28,7 @@ public InMemoryRecipientRegistrar(IMessenger messenger, IMessageConvention conve _messenger = messenger; } + /// public async Task RegisterAsync(IReadOnlyList registrations, CancellationToken cancellationToken = default) { if (_options.MultipleSubscriberInstance) @@ -60,5 +71,7 @@ public async Task RegisterAsync(IReadOnlyList registrations _messenger.Register(recipient, registration.Channel); } } + + await Task.CompletedTask; } } diff --git a/Source/Euonia.Bus.RabbitMq/RabbitMqRecipientRegistrar.cs b/Source/Euonia.Bus.RabbitMq/RabbitMqRecipientRegistrar.cs index 1d5730b..2149a78 100644 --- a/Source/Euonia.Bus.RabbitMq/RabbitMqRecipientRegistrar.cs +++ b/Source/Euonia.Bus.RabbitMq/RabbitMqRecipientRegistrar.cs @@ -2,11 +2,19 @@ namespace Nerosoft.Euonia.Bus.RabbitMq; +/// +/// The RabbitMQ recipient registrar. +/// public sealed class RabbitMqRecipientRegistrar : IRecipientRegistrar { private readonly IMessageConvention _convention; private readonly IServiceProvider _provider; + /// + /// Initialize a new instance of . + /// + /// + /// public RabbitMqRecipientRegistrar(IMessageConvention convention, IServiceProvider provider) { _convention = convention; @@ -34,6 +42,7 @@ public async Task RegisterAsync(IReadOnlyList registrations recipient.Start(registration.Channel); } + await Task.CompletedTask; } -} +} \ No newline at end of file diff --git a/Source/Euonia.Bus/BusConfigurator.cs b/Source/Euonia.Bus/BusConfigurator.cs index ecb3209..431754c 100644 --- a/Source/Euonia.Bus/BusConfigurator.cs +++ b/Source/Euonia.Bus/BusConfigurator.cs @@ -153,6 +153,7 @@ public BusConfigurator RegisterHandlers(IEnumerable types) { Service.TryAddScoped(handlerType); } + _registrations.AddRange(registrations); return this; } diff --git a/Source/Euonia.Bus/Conventions/MessageConvention.cs b/Source/Euonia.Bus/Conventions/MessageConvention.cs index ec4fb50..d61175f 100644 --- a/Source/Euonia.Bus/Conventions/MessageConvention.cs +++ b/Source/Euonia.Bus/Conventions/MessageConvention.cs @@ -77,7 +77,7 @@ internal void Add(params IMessageConvention[] conventions) internal string[] RegisteredConventions => _conventions.Select(x => x.Name).ToArray(); /// - public string Name { get; } + public string Name => "Default"; private class ConventionCache { diff --git a/Source/Euonia.Bus/Core/HandlerContext.cs b/Source/Euonia.Bus/Core/HandlerContext.cs index 1864812..09e94ef 100644 --- a/Source/Euonia.Bus/Core/HandlerContext.cs +++ b/Source/Euonia.Bus/Core/HandlerContext.cs @@ -212,17 +212,5 @@ private static Expression[] GetArguments(MethodBase method, object message, Mess return arguments; } - private static Task Invoke(MethodInfo method, object handler, params object[] parameters) - { - if (method.ReturnType.IsAssignableTo(typeof(IAsyncResult))) - { - return (Task)method.Invoke(handler, parameters); - } - else - { - return Task.Run(() => method.Invoke(handler, parameters)); - } - } - #endregion } \ No newline at end of file diff --git a/Source/Euonia.Bus/Core/SendOptions.cs b/Source/Euonia.Bus/Core/SendOptions.cs index f81c838..0452541 100644 --- a/Source/Euonia.Bus/Core/SendOptions.cs +++ b/Source/Euonia.Bus/Core/SendOptions.cs @@ -1,9 +1,12 @@ namespace Nerosoft.Euonia.Bus; /// -/// +/// The options for send message. /// public class SendOptions : ExtendableOptions { + /// + /// Gets or sets the correlation identifier. + /// public string CorrelationId { get; set; } } \ No newline at end of file diff --git a/Source/Euonia.Bus/Properties/Resources.zh-CN.resx b/Source/Euonia.Bus/Properties/Resources.zh-CN.resx new file mode 100644 index 0000000..cf2d5d7 --- /dev/null +++ b/Source/Euonia.Bus/Properties/Resources.zh-CN.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 类型不能为空。 + + + 至少需要一个Convention。 + + \ No newline at end of file From aa24c8db94c9fb091bb8ab47db537e473a51d79e Mon Sep 17 00:00:00 2001 From: damon Date: Wed, 29 Nov 2023 20:55:21 +0800 Subject: [PATCH 31/37] Prevent run rabbitmq tests on github workflow. --- Tests/Euonia.Bus.InMemory.Tests/Defines.cs | 7 +++++++ Tests/Euonia.Bus.RabbitMq.Tests/Defines.cs | 13 +++++++++++++ Tests/Euonia.Bus.Tests.Shared/Defines.cs | 6 ++++++ .../Euonia.Bus.Tests.Shared.projitems | 1 + Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs | 16 ++++++++++++++++ 5 files changed, 43 insertions(+) create mode 100644 Tests/Euonia.Bus.InMemory.Tests/Defines.cs create mode 100644 Tests/Euonia.Bus.RabbitMq.Tests/Defines.cs create mode 100644 Tests/Euonia.Bus.Tests.Shared/Defines.cs diff --git a/Tests/Euonia.Bus.InMemory.Tests/Defines.cs b/Tests/Euonia.Bus.InMemory.Tests/Defines.cs new file mode 100644 index 0000000..20c8292 --- /dev/null +++ b/Tests/Euonia.Bus.InMemory.Tests/Defines.cs @@ -0,0 +1,7 @@ +namespace Nerosoft.Euonia.Bus.Tests; + +internal partial class Defines +{ + public const bool DontRunTests = false; + +} diff --git a/Tests/Euonia.Bus.RabbitMq.Tests/Defines.cs b/Tests/Euonia.Bus.RabbitMq.Tests/Defines.cs new file mode 100644 index 0000000..f69f38c --- /dev/null +++ b/Tests/Euonia.Bus.RabbitMq.Tests/Defines.cs @@ -0,0 +1,13 @@ +namespace Nerosoft.Euonia.Bus.Tests; + +internal partial class Defines +{ + public const bool DontRunTests = +#if DEBUG + false +#else + true +#endif + ; + +} diff --git a/Tests/Euonia.Bus.Tests.Shared/Defines.cs b/Tests/Euonia.Bus.Tests.Shared/Defines.cs new file mode 100644 index 0000000..f4f49a6 --- /dev/null +++ b/Tests/Euonia.Bus.Tests.Shared/Defines.cs @@ -0,0 +1,6 @@ +namespace Nerosoft.Euonia.Bus.Tests; + +internal partial class Defines +{ + +} \ No newline at end of file diff --git a/Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.projitems b/Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.projitems index 485842f..413a03d 100644 --- a/Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.projitems +++ b/Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.projitems @@ -12,6 +12,7 @@ + diff --git a/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs b/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs index 6ff5436..0e141b7 100644 --- a/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs +++ b/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs @@ -14,6 +14,10 @@ public ServiceBusTests(IBus bus) [Fact] public async Task TestSendCommand_HasReponse() { + if (Defines.DontRunTests) + { + return; + } var result = await _bus.SendAsync(new UserCreateCommand()); Assert.Equal(1, result); } @@ -21,6 +25,10 @@ public async Task TestSendCommand_HasReponse() [Fact] public async Task TestSendCommand_NoReponse() { + if (Defines.DontRunTests) + { + return; + } await _bus.SendAsync(new UserCreateCommand()); Assert.True(true); } @@ -28,6 +36,10 @@ public async Task TestSendCommand_NoReponse() [Fact] public async Task TestSendCommand_HasReponse_UseSubscribeAttribute() { + if (Defines.DontRunTests) + { + return; + } var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); Assert.Equal(1, result); } @@ -35,6 +47,10 @@ public async Task TestSendCommand_HasReponse_UseSubscribeAttribute() [Fact] public async Task TestSendCommand_HasReponse_MessageHasResultInherites() { + if (Defines.DontRunTests) + { + return; + } var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); Assert.Equal(1, result); } From 4bb1b128ab279889eb42ca419a7b1180c47be794 Mon Sep 17 00:00:00 2001 From: damon Date: Wed, 29 Nov 2023 20:55:21 +0800 Subject: [PATCH 32/37] Prevent run rabbitmq tests on github workflow. --- Tests/Euonia.Bus.InMemory.Tests/Defines.cs | 7 ++++++ .../appsettings.json | 4 +++- Tests/Euonia.Bus.RabbitMq.Tests/Defines.cs | 6 +++++ .../appsettings.json | 4 +++- Tests/Euonia.Bus.Tests.Shared/Defines.cs | 6 +++++ .../Euonia.Bus.Tests.Shared.projitems | 1 + .../ServiceBusTests.cs | 23 +++++++++++++++++-- 7 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 Tests/Euonia.Bus.InMemory.Tests/Defines.cs create mode 100644 Tests/Euonia.Bus.RabbitMq.Tests/Defines.cs create mode 100644 Tests/Euonia.Bus.Tests.Shared/Defines.cs diff --git a/Tests/Euonia.Bus.InMemory.Tests/Defines.cs b/Tests/Euonia.Bus.InMemory.Tests/Defines.cs new file mode 100644 index 0000000..20c8292 --- /dev/null +++ b/Tests/Euonia.Bus.InMemory.Tests/Defines.cs @@ -0,0 +1,7 @@ +namespace Nerosoft.Euonia.Bus.Tests; + +internal partial class Defines +{ + public const bool DontRunTests = false; + +} diff --git a/Tests/Euonia.Bus.InMemory.Tests/appsettings.json b/Tests/Euonia.Bus.InMemory.Tests/appsettings.json index 22fdca1..704e408 100644 --- a/Tests/Euonia.Bus.InMemory.Tests/appsettings.json +++ b/Tests/Euonia.Bus.InMemory.Tests/appsettings.json @@ -1 +1,3 @@ -{} \ No newline at end of file +{ + "DontRunTests": false +} \ No newline at end of file diff --git a/Tests/Euonia.Bus.RabbitMq.Tests/Defines.cs b/Tests/Euonia.Bus.RabbitMq.Tests/Defines.cs new file mode 100644 index 0000000..bfacf30 --- /dev/null +++ b/Tests/Euonia.Bus.RabbitMq.Tests/Defines.cs @@ -0,0 +1,6 @@ +namespace Nerosoft.Euonia.Bus.Tests; + +internal partial class Defines +{ + public const bool DontRunTests = true; +} diff --git a/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json b/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json index 22fdca1..f180afc 100644 --- a/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json +++ b/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json @@ -1 +1,3 @@ -{} \ No newline at end of file +{ + "DontRunTests": true +} \ No newline at end of file diff --git a/Tests/Euonia.Bus.Tests.Shared/Defines.cs b/Tests/Euonia.Bus.Tests.Shared/Defines.cs new file mode 100644 index 0000000..f4f49a6 --- /dev/null +++ b/Tests/Euonia.Bus.Tests.Shared/Defines.cs @@ -0,0 +1,6 @@ +namespace Nerosoft.Euonia.Bus.Tests; + +internal partial class Defines +{ + +} \ No newline at end of file diff --git a/Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.projitems b/Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.projitems index 485842f..413a03d 100644 --- a/Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.projitems +++ b/Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.projitems @@ -12,6 +12,7 @@ + diff --git a/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs b/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs index 6ff5436..60aaa62 100644 --- a/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs +++ b/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs @@ -1,19 +1,26 @@ -using Nerosoft.Euonia.Bus.Tests.Commands; +using Microsoft.Extensions.Configuration; +using Nerosoft.Euonia.Bus.Tests.Commands; namespace Nerosoft.Euonia.Bus.Tests; public class ServiceBusTests { private readonly IBus _bus; + private readonly bool _dontRunTests; - public ServiceBusTests(IBus bus) + public ServiceBusTests(IBus bus, IConfiguration configuration) { _bus = bus; + _dontRunTests = configuration.GetValue("DontRunTests"); } [Fact] public async Task TestSendCommand_HasReponse() { + if (_dontRunTests) + { + return; + } var result = await _bus.SendAsync(new UserCreateCommand()); Assert.Equal(1, result); } @@ -21,6 +28,10 @@ public async Task TestSendCommand_HasReponse() [Fact] public async Task TestSendCommand_NoReponse() { + if (_dontRunTests) + { + return; + } await _bus.SendAsync(new UserCreateCommand()); Assert.True(true); } @@ -28,6 +39,10 @@ public async Task TestSendCommand_NoReponse() [Fact] public async Task TestSendCommand_HasReponse_UseSubscribeAttribute() { + if (_dontRunTests) + { + return; + } var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); Assert.Equal(1, result); } @@ -35,6 +50,10 @@ public async Task TestSendCommand_HasReponse_UseSubscribeAttribute() [Fact] public async Task TestSendCommand_HasReponse_MessageHasResultInherites() { + if (_dontRunTests) + { + return; + } var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); Assert.Equal(1, result); } From f0f10ebcae96f2477b75f949a125e7676248eef9 Mon Sep 17 00:00:00 2001 From: damon Date: Wed, 29 Nov 2023 21:22:59 +0800 Subject: [PATCH 33/37] Revert "Prevent run rabbitmq tests on github workflow." This reverts commit 4bb1b128ab279889eb42ca419a7b1180c47be794. --- Tests/Euonia.Bus.InMemory.Tests/Defines.cs | 7 ------ .../appsettings.json | 4 +--- Tests/Euonia.Bus.RabbitMq.Tests/Defines.cs | 6 ----- .../appsettings.json | 4 +--- Tests/Euonia.Bus.Tests.Shared/Defines.cs | 6 ----- .../Euonia.Bus.Tests.Shared.projitems | 1 - .../ServiceBusTests.cs | 23 ++----------------- 7 files changed, 4 insertions(+), 47 deletions(-) delete mode 100644 Tests/Euonia.Bus.InMemory.Tests/Defines.cs delete mode 100644 Tests/Euonia.Bus.RabbitMq.Tests/Defines.cs delete mode 100644 Tests/Euonia.Bus.Tests.Shared/Defines.cs diff --git a/Tests/Euonia.Bus.InMemory.Tests/Defines.cs b/Tests/Euonia.Bus.InMemory.Tests/Defines.cs deleted file mode 100644 index 20c8292..0000000 --- a/Tests/Euonia.Bus.InMemory.Tests/Defines.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Nerosoft.Euonia.Bus.Tests; - -internal partial class Defines -{ - public const bool DontRunTests = false; - -} diff --git a/Tests/Euonia.Bus.InMemory.Tests/appsettings.json b/Tests/Euonia.Bus.InMemory.Tests/appsettings.json index 704e408..22fdca1 100644 --- a/Tests/Euonia.Bus.InMemory.Tests/appsettings.json +++ b/Tests/Euonia.Bus.InMemory.Tests/appsettings.json @@ -1,3 +1 @@ -{ - "DontRunTests": false -} \ No newline at end of file +{} \ No newline at end of file diff --git a/Tests/Euonia.Bus.RabbitMq.Tests/Defines.cs b/Tests/Euonia.Bus.RabbitMq.Tests/Defines.cs deleted file mode 100644 index bfacf30..0000000 --- a/Tests/Euonia.Bus.RabbitMq.Tests/Defines.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Nerosoft.Euonia.Bus.Tests; - -internal partial class Defines -{ - public const bool DontRunTests = true; -} diff --git a/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json b/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json index f180afc..22fdca1 100644 --- a/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json +++ b/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json @@ -1,3 +1 @@ -{ - "DontRunTests": true -} \ No newline at end of file +{} \ No newline at end of file diff --git a/Tests/Euonia.Bus.Tests.Shared/Defines.cs b/Tests/Euonia.Bus.Tests.Shared/Defines.cs deleted file mode 100644 index f4f49a6..0000000 --- a/Tests/Euonia.Bus.Tests.Shared/Defines.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Nerosoft.Euonia.Bus.Tests; - -internal partial class Defines -{ - -} \ No newline at end of file diff --git a/Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.projitems b/Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.projitems index 413a03d..485842f 100644 --- a/Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.projitems +++ b/Tests/Euonia.Bus.Tests.Shared/Euonia.Bus.Tests.Shared.projitems @@ -12,7 +12,6 @@ - diff --git a/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs b/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs index 60aaa62..6ff5436 100644 --- a/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs +++ b/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs @@ -1,26 +1,19 @@ -using Microsoft.Extensions.Configuration; -using Nerosoft.Euonia.Bus.Tests.Commands; +using Nerosoft.Euonia.Bus.Tests.Commands; namespace Nerosoft.Euonia.Bus.Tests; public class ServiceBusTests { private readonly IBus _bus; - private readonly bool _dontRunTests; - public ServiceBusTests(IBus bus, IConfiguration configuration) + public ServiceBusTests(IBus bus) { _bus = bus; - _dontRunTests = configuration.GetValue("DontRunTests"); } [Fact] public async Task TestSendCommand_HasReponse() { - if (_dontRunTests) - { - return; - } var result = await _bus.SendAsync(new UserCreateCommand()); Assert.Equal(1, result); } @@ -28,10 +21,6 @@ public async Task TestSendCommand_HasReponse() [Fact] public async Task TestSendCommand_NoReponse() { - if (_dontRunTests) - { - return; - } await _bus.SendAsync(new UserCreateCommand()); Assert.True(true); } @@ -39,10 +28,6 @@ public async Task TestSendCommand_NoReponse() [Fact] public async Task TestSendCommand_HasReponse_UseSubscribeAttribute() { - if (_dontRunTests) - { - return; - } var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); Assert.Equal(1, result); } @@ -50,10 +35,6 @@ public async Task TestSendCommand_HasReponse_UseSubscribeAttribute() [Fact] public async Task TestSendCommand_HasReponse_MessageHasResultInherites() { - if (_dontRunTests) - { - return; - } var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); Assert.Equal(1, result); } From de9d24b7b2cc7306319448cfabe14816e4e02a32 Mon Sep 17 00:00:00 2001 From: damon Date: Wed, 29 Nov 2023 21:26:54 +0800 Subject: [PATCH 34/37] Prevent test run by appsettings. --- Tests/Euonia.Bus.InMemory.Tests/appsettings.json | 4 +++- Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json | 4 +++- Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs | 15 +++++++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Tests/Euonia.Bus.InMemory.Tests/appsettings.json b/Tests/Euonia.Bus.InMemory.Tests/appsettings.json index 22fdca1..704e408 100644 --- a/Tests/Euonia.Bus.InMemory.Tests/appsettings.json +++ b/Tests/Euonia.Bus.InMemory.Tests/appsettings.json @@ -1 +1,3 @@ -{} \ No newline at end of file +{ + "DontRunTests": false +} \ No newline at end of file diff --git a/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json b/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json index 22fdca1..f180afc 100644 --- a/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json +++ b/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json @@ -1 +1,3 @@ -{} \ No newline at end of file +{ + "DontRunTests": true +} \ No newline at end of file diff --git a/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs b/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs index 0e141b7..60aaa62 100644 --- a/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs +++ b/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs @@ -1,20 +1,23 @@ -using Nerosoft.Euonia.Bus.Tests.Commands; +using Microsoft.Extensions.Configuration; +using Nerosoft.Euonia.Bus.Tests.Commands; namespace Nerosoft.Euonia.Bus.Tests; public class ServiceBusTests { private readonly IBus _bus; + private readonly bool _dontRunTests; - public ServiceBusTests(IBus bus) + public ServiceBusTests(IBus bus, IConfiguration configuration) { _bus = bus; + _dontRunTests = configuration.GetValue("DontRunTests"); } [Fact] public async Task TestSendCommand_HasReponse() { - if (Defines.DontRunTests) + if (_dontRunTests) { return; } @@ -25,7 +28,7 @@ public async Task TestSendCommand_HasReponse() [Fact] public async Task TestSendCommand_NoReponse() { - if (Defines.DontRunTests) + if (_dontRunTests) { return; } @@ -36,7 +39,7 @@ public async Task TestSendCommand_NoReponse() [Fact] public async Task TestSendCommand_HasReponse_UseSubscribeAttribute() { - if (Defines.DontRunTests) + if (_dontRunTests) { return; } @@ -47,7 +50,7 @@ public async Task TestSendCommand_HasReponse_UseSubscribeAttribute() [Fact] public async Task TestSendCommand_HasReponse_MessageHasResultInherites() { - if (Defines.DontRunTests) + if (_dontRunTests) { return; } From 2ee48cafbbbf9f8c7d018e713bdebef983bcbf24 Mon Sep 17 00:00:00 2001 From: damon Date: Wed, 29 Nov 2023 21:38:05 +0800 Subject: [PATCH 35/37] Disable rabbitmq service bus unit tests on release mode. --- Tests/Euonia.Bus.RabbitMq.Tests/Startup.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/Euonia.Bus.RabbitMq.Tests/Startup.cs b/Tests/Euonia.Bus.RabbitMq.Tests/Startup.cs index a087125..7fad1a9 100644 --- a/Tests/Euonia.Bus.RabbitMq.Tests/Startup.cs +++ b/Tests/Euonia.Bus.RabbitMq.Tests/Startup.cs @@ -28,6 +28,7 @@ public void ConfigureHost(IHostBuilder hostBuilder) // ConfigureServices(HostBuilderContext hostBuilderContext, IServiceCollection services) public void ConfigureServices(IServiceCollection services, HostBuilderContext hostBuilderContext) { +#if DEBUG services.AddServiceBus(config => { config.RegisterHandlers(Assembly.GetExecutingAssembly()); @@ -47,6 +48,7 @@ public void ConfigureServices(IServiceCollection services, HostBuilderContext ho options.RoutingKey = "*"; }); }); +#endif } //public void Configure(IServiceProvider applicationServices, IIdGenerator idGenerator) From 5ada141cb6eb44b50327a352d181efca09808578 Mon Sep 17 00:00:00 2001 From: damon Date: Wed, 29 Nov 2023 22:00:08 +0800 Subject: [PATCH 36/37] Refactor unit test. --- .../ServiceBusTests.cs | 40 +++++++++ .../ServiceBusTests.cs | 82 +++++++++++++++++++ .../ServiceBusTests.cs | 56 ++----------- 3 files changed, 128 insertions(+), 50 deletions(-) create mode 100644 Tests/Euonia.Bus.InMemory.Tests/ServiceBusTests.cs create mode 100644 Tests/Euonia.Bus.RabbitMq.Tests/ServiceBusTests.cs diff --git a/Tests/Euonia.Bus.InMemory.Tests/ServiceBusTests.cs b/Tests/Euonia.Bus.InMemory.Tests/ServiceBusTests.cs new file mode 100644 index 0000000..eca67f1 --- /dev/null +++ b/Tests/Euonia.Bus.InMemory.Tests/ServiceBusTests.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.Configuration; +using Nerosoft.Euonia.Bus.Tests.Commands; + +namespace Nerosoft.Euonia.Bus.Tests; + +public partial class ServiceBusTests +{ + private readonly IBus _bus; + private readonly bool _dontRunTests; + + public ServiceBusTests(IBus bus, IConfiguration configuration) + { + _bus = bus; + _dontRunTests = configuration.GetValue("DontRunTests"); + } + + public partial async Task TestSendCommand_HasReponse() + { + var result = await _bus.SendAsync(new UserCreateCommand()); + Assert.Equal(1, result); + } + + public partial async Task TestSendCommand_NoReponse() + { + await _bus.SendAsync(new UserCreateCommand()); + Assert.True(true); + } + + public partial async Task TestSendCommand_HasReponse_UseSubscribeAttribute() + { + var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); + Assert.Equal(1, result); + } + + public partial async Task TestSendCommand_HasReponse_MessageHasResultInherites() + { + var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); + Assert.Equal(1, result); + } +} \ No newline at end of file diff --git a/Tests/Euonia.Bus.RabbitMq.Tests/ServiceBusTests.cs b/Tests/Euonia.Bus.RabbitMq.Tests/ServiceBusTests.cs new file mode 100644 index 0000000..b3f5706 --- /dev/null +++ b/Tests/Euonia.Bus.RabbitMq.Tests/ServiceBusTests.cs @@ -0,0 +1,82 @@ +using Microsoft.Extensions.Configuration; +using Nerosoft.Euonia.Bus.Tests.Commands; + +namespace Nerosoft.Euonia.Bus.Tests; + +#if DEBUG +public partial class ServiceBusTests +{ + private readonly IBus _bus; + private readonly bool _dontRunTests; + + public ServiceBusTests(IBus bus, IConfiguration configuration) + { + _bus = bus; + _dontRunTests = configuration.GetValue("DontRunTests"); + } + + + public partial async Task TestSendCommand_HasReponse() + { + if (_dontRunTests) + { + return; + } + var result = await _bus.SendAsync(new UserCreateCommand()); + Assert.Equal(1, result); + } + + public partial async Task TestSendCommand_NoReponse() + { + if (_dontRunTests) + { + return; + } + await _bus.SendAsync(new UserCreateCommand()); + Assert.True(true); + } + + public partial async Task TestSendCommand_HasReponse_UseSubscribeAttribute() + { + if (_dontRunTests) + { + return; + } + var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); + Assert.Equal(1, result); + } + + public partial async Task TestSendCommand_HasReponse_MessageHasResultInherites() + { + if (_dontRunTests) + { + return; + } + var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); + Assert.Equal(1, result); + } +} +#else +public partial class ServiceBusTests +{ + public partial async Task TestSendCommand_HasReponse() + { + Assert.True(true); + } + + public partial async Task TestSendCommand_NoReponse() + { + Assert.True(true); + } + + public partial async Task TestSendCommand_HasReponse_UseSubscribeAttribute() + { + Assert.True(true); + } + + public partial async Task TestSendCommand_HasReponse_MessageHasResultInherites() + { + Assert.True(true); + } +} +#endif \ No newline at end of file diff --git a/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs b/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs index 60aaa62..488ca85 100644 --- a/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs +++ b/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs @@ -1,60 +1,16 @@ -using Microsoft.Extensions.Configuration; -using Nerosoft.Euonia.Bus.Tests.Commands; +namespace Nerosoft.Euonia.Bus.Tests; -namespace Nerosoft.Euonia.Bus.Tests; - -public class ServiceBusTests +public partial class ServiceBusTests { - private readonly IBus _bus; - private readonly bool _dontRunTests; - - public ServiceBusTests(IBus bus, IConfiguration configuration) - { - _bus = bus; - _dontRunTests = configuration.GetValue("DontRunTests"); - } - [Fact] - public async Task TestSendCommand_HasReponse() - { - if (_dontRunTests) - { - return; - } - var result = await _bus.SendAsync(new UserCreateCommand()); - Assert.Equal(1, result); - } + public partial Task TestSendCommand_HasReponse(); [Fact] - public async Task TestSendCommand_NoReponse() - { - if (_dontRunTests) - { - return; - } - await _bus.SendAsync(new UserCreateCommand()); - Assert.True(true); - } + public partial Task TestSendCommand_NoReponse(); [Fact] - public async Task TestSendCommand_HasReponse_UseSubscribeAttribute() - { - if (_dontRunTests) - { - return; - } - var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); - Assert.Equal(1, result); - } + public partial Task TestSendCommand_HasReponse_UseSubscribeAttribute(); [Fact] - public async Task TestSendCommand_HasReponse_MessageHasResultInherites() - { - if (_dontRunTests) - { - return; - } - var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); - Assert.Equal(1, result); - } + public partial Task TestSendCommand_HasReponse_MessageHasResultInherites(); } \ No newline at end of file From b3db8e5bd098a45f7485357c11ee0c0f1e02a8d8 Mon Sep 17 00:00:00 2001 From: damon Date: Wed, 29 Nov 2023 22:34:57 +0800 Subject: [PATCH 37/37] Tesing. --- .../ServiceBusTests.cs | 40 --------- .../appsettings.json | 2 +- .../ServiceBusTests.cs | 82 ------------------- Tests/Euonia.Bus.RabbitMq.Tests/Startup.cs | 38 +++++---- .../appsettings.json | 2 +- .../ServiceBusTests.cs | 67 +++++++++++++-- 6 files changed, 84 insertions(+), 147 deletions(-) delete mode 100644 Tests/Euonia.Bus.InMemory.Tests/ServiceBusTests.cs delete mode 100644 Tests/Euonia.Bus.RabbitMq.Tests/ServiceBusTests.cs diff --git a/Tests/Euonia.Bus.InMemory.Tests/ServiceBusTests.cs b/Tests/Euonia.Bus.InMemory.Tests/ServiceBusTests.cs deleted file mode 100644 index eca67f1..0000000 --- a/Tests/Euonia.Bus.InMemory.Tests/ServiceBusTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Nerosoft.Euonia.Bus.Tests.Commands; - -namespace Nerosoft.Euonia.Bus.Tests; - -public partial class ServiceBusTests -{ - private readonly IBus _bus; - private readonly bool _dontRunTests; - - public ServiceBusTests(IBus bus, IConfiguration configuration) - { - _bus = bus; - _dontRunTests = configuration.GetValue("DontRunTests"); - } - - public partial async Task TestSendCommand_HasReponse() - { - var result = await _bus.SendAsync(new UserCreateCommand()); - Assert.Equal(1, result); - } - - public partial async Task TestSendCommand_NoReponse() - { - await _bus.SendAsync(new UserCreateCommand()); - Assert.True(true); - } - - public partial async Task TestSendCommand_HasReponse_UseSubscribeAttribute() - { - var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); - Assert.Equal(1, result); - } - - public partial async Task TestSendCommand_HasReponse_MessageHasResultInherites() - { - var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); - Assert.Equal(1, result); - } -} \ No newline at end of file diff --git a/Tests/Euonia.Bus.InMemory.Tests/appsettings.json b/Tests/Euonia.Bus.InMemory.Tests/appsettings.json index 704e408..6f1f1a7 100644 --- a/Tests/Euonia.Bus.InMemory.Tests/appsettings.json +++ b/Tests/Euonia.Bus.InMemory.Tests/appsettings.json @@ -1,3 +1,3 @@ { - "DontRunTests": false + "PreventRunTests": false } \ No newline at end of file diff --git a/Tests/Euonia.Bus.RabbitMq.Tests/ServiceBusTests.cs b/Tests/Euonia.Bus.RabbitMq.Tests/ServiceBusTests.cs deleted file mode 100644 index b3f5706..0000000 --- a/Tests/Euonia.Bus.RabbitMq.Tests/ServiceBusTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Microsoft.Extensions.Configuration; -using Nerosoft.Euonia.Bus.Tests.Commands; - -namespace Nerosoft.Euonia.Bus.Tests; - -#if DEBUG -public partial class ServiceBusTests -{ - private readonly IBus _bus; - private readonly bool _dontRunTests; - - public ServiceBusTests(IBus bus, IConfiguration configuration) - { - _bus = bus; - _dontRunTests = configuration.GetValue("DontRunTests"); - } - - - public partial async Task TestSendCommand_HasReponse() - { - if (_dontRunTests) - { - return; - } - var result = await _bus.SendAsync(new UserCreateCommand()); - Assert.Equal(1, result); - } - - public partial async Task TestSendCommand_NoReponse() - { - if (_dontRunTests) - { - return; - } - await _bus.SendAsync(new UserCreateCommand()); - Assert.True(true); - } - - public partial async Task TestSendCommand_HasReponse_UseSubscribeAttribute() - { - if (_dontRunTests) - { - return; - } - var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); - Assert.Equal(1, result); - } - - public partial async Task TestSendCommand_HasReponse_MessageHasResultInherites() - { - if (_dontRunTests) - { - return; - } - var result = await _bus.SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); - Assert.Equal(1, result); - } -} -#else -public partial class ServiceBusTests -{ - public partial async Task TestSendCommand_HasReponse() - { - Assert.True(true); - } - - public partial async Task TestSendCommand_NoReponse() - { - Assert.True(true); - } - - public partial async Task TestSendCommand_HasReponse_UseSubscribeAttribute() - { - Assert.True(true); - } - - public partial async Task TestSendCommand_HasReponse_MessageHasResultInherites() - { - Assert.True(true); - } -} -#endif \ No newline at end of file diff --git a/Tests/Euonia.Bus.RabbitMq.Tests/Startup.cs b/Tests/Euonia.Bus.RabbitMq.Tests/Startup.cs index 7fad1a9..cfcb9ed 100644 --- a/Tests/Euonia.Bus.RabbitMq.Tests/Startup.cs +++ b/Tests/Euonia.Bus.RabbitMq.Tests/Startup.cs @@ -28,27 +28,29 @@ public void ConfigureHost(IHostBuilder hostBuilder) // ConfigureServices(HostBuilderContext hostBuilderContext, IServiceCollection services) public void ConfigureServices(IServiceCollection services, HostBuilderContext hostBuilderContext) { -#if DEBUG - services.AddServiceBus(config => + var preventUnitTest = hostBuilderContext.Configuration.GetValue("PreventRunTests"); + if (!preventUnitTest) { - config.RegisterHandlers(Assembly.GetExecutingAssembly()); - config.SetConventions(builder => + services.AddServiceBus(config => { - builder.Add(); - builder.Add(); - builder.EvaluateQueue(t => t.Name.EndsWith("Command")); - builder.EvaluateTopic(t => t.Name.EndsWith("Event")); + config.RegisterHandlers(Assembly.GetExecutingAssembly()); + config.SetConventions(builder => + { + builder.Add(); + builder.Add(); + builder.EvaluateQueue(t => t.Name.EndsWith("Command")); + builder.EvaluateTopic(t => t.Name.EndsWith("Event")); + }); + config.UseRabbitMq(options => + { + options.Connection = "amqp://127.0.0.1"; + options.QueueName = "nerosoft.euonia.test.command"; + options.TopicName = "nerosoft.euonia.test.event"; + options.ExchangeName = $"nerosoft.euonia.test.exchange.{options.ExchangeType}"; + options.RoutingKey = "*"; + }); }); - config.UseRabbitMq(options => - { - options.Connection = "amqp://127.0.0.1"; - options.QueueName = "nerosoft.euonia.test.command"; - options.TopicName = "nerosoft.euonia.test.event"; - options.ExchangeName = $"nerosoft.euonia.test.exchange.{options.ExchangeType}"; - options.RoutingKey = "*"; - }); - }); -#endif + } } //public void Configure(IServiceProvider applicationServices, IIdGenerator idGenerator) diff --git a/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json b/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json index f180afc..8b1afaf 100644 --- a/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json +++ b/Tests/Euonia.Bus.RabbitMq.Tests/appsettings.json @@ -1,3 +1,3 @@ { - "DontRunTests": true + "PreventRunTests": true } \ No newline at end of file diff --git a/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs b/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs index 488ca85..fac500a 100644 --- a/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs +++ b/Tests/Euonia.Bus.Tests.Shared/ServiceBusTests.cs @@ -1,16 +1,73 @@ -namespace Nerosoft.Euonia.Bus.Tests; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Nerosoft.Euonia.Bus.Tests.Commands; + +namespace Nerosoft.Euonia.Bus.Tests; public partial class ServiceBusTests { + private readonly IServiceProvider _provider; + private readonly bool _preventRunTests; + + public ServiceBusTests(IServiceProvider provider, IConfiguration configuration) + { + _provider = provider; + _preventRunTests = configuration.GetValue("PreventRunTests"); + } + [Fact] - public partial Task TestSendCommand_HasReponse(); + public async Task TestSendCommand_HasReponse() + { + if (_preventRunTests) + { + Assert.True(true); + } + else + { + var result = await _provider.GetService().SendAsync(new UserCreateCommand()); + Assert.Equal(1, result); + } + } [Fact] - public partial Task TestSendCommand_NoReponse(); + public async Task TestSendCommand_NoReponse() + { + if (_preventRunTests) + { + Assert.True(true); + } + else + { + await _provider.GetService().SendAsync(new UserCreateCommand()); + Assert.True(true); + } + } [Fact] - public partial Task TestSendCommand_HasReponse_UseSubscribeAttribute(); + public async Task TestSendCommand_HasReponse_UseSubscribeAttribute() + { + if (_preventRunTests) + { + Assert.True(true); + } + else + { + var result = await _provider.GetService().SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); + Assert.Equal(1, result); + } + } [Fact] - public partial Task TestSendCommand_HasReponse_MessageHasResultInherites(); + public async Task TestSendCommand_HasReponse_MessageHasResultInherites() + { + if (_preventRunTests) + { + Assert.True(true); + } + else + { + var result = await _provider.GetService().SendAsync(new FooCreateCommand(), new SendOptions { Channel = "foo.create" }); + Assert.Equal(1, result); + } + } } \ No newline at end of file