diff --git a/.docfx/Dockerfile.docfx b/.docfx/Dockerfile.docfx
index 2229c730..7d97dde5 100644
--- a/.docfx/Dockerfile.docfx
+++ b/.docfx/Dockerfile.docfx
@@ -1,4 +1,4 @@
-ARG NGINX_VERSION=1.31.2-alpine
+ARG NGINX_VERSION=1.31-alpine
FROM --platform=$BUILDPLATFORM nginx:${NGINX_VERSION} AS base
RUN rm -rf /usr/share/nginx/html/*
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..a083db5c
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,176 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+# Default: prefer spaces for data/markup
+indent_style = space
+indent_size = 2
+tab_width = 2
+
+# This style rule concern the use of the range operator, which is available in C# 8.0 and later.
+# https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0057
+[*.{cs,vb}]
+dotnet_diagnostic.IDE0057.severity = none
+
+# This style rule concerns the use of switch expressions versus switch statements.
+# https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0066
+[*.{cs,vb}]
+dotnet_diagnostic.IDE0066.severity = none
+
+# Performance rules
+# https://docs.microsoft.com/da-dk/dotnet/fundamentals/code-analysis/quality-rules/performance-warnings
+[*.{cs,vb}]
+dotnet_analyzer_diagnostic.category-Performance.severity = none # Because many of the suggestions by performance analyzers are not compatible with .NET Standard 2.0
+
+# This style rule concerns the use of using statements without curly braces, also known as using declarations. This alternative syntax was introduced in C# 8.0.
+# https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0063
+[*.{cs,vb}]
+dotnet_diagnostic.IDE0063.severity = none
+
+# This style rule concerns with simplification of interpolated strings to improve code readability. It recommends removal of certain explicit method calls, such as ToString(), when the same method would be implicitly invoked by the compiler if the explicit method call is removed.
+# https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0071
+[*.{cs,vb}]
+dotnet_diagnostic.IDE0071.severity = none
+
+# S3267: Loops should be simplified with "LINQ" expressions
+# https://rules.sonarsource.com/csharp/RSPEC-3267
+dotnet_diagnostic.S3267.severity = none
+
+# CA1859: Use concrete types when possible for improved performance
+# This is a violation of Framework Design Guidelines.
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1859
+[*.{cs,vb}]
+dotnet_diagnostic.CA1859.severity = none
+
+# IDE0008: Use explicit type
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0007-ide0008
+[*.{cs,vb}]
+dotnet_diagnostic.IDE0008.severity = none
+
+[*.{cs,vb}]
+indent_style = space
+indent_size = 4
+
+# IDE0161: Namespace declaration preferences
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0161
+# Prefer file-scoped namespaces for new files; existing block-scoped files should not be converted unless explicitly asked
+[*.cs]
+csharp_style_namespace_declarations = file_scoped:suggestion
+
+# Top-level statements: DO NOT USE
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0210-ide0211
+# This is enforced via a style preference set to error severity, not a language-level prohibition.
+# Always use explicit class declarations with a proper namespace and Main method where applicable.
+[*.cs]
+csharp_style_prefer_top_level_statements = false:error
+
+[*.xml]
+indent_style = space
+indent_size = 2
+
+# IDE0078: Use pattern matching
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0078
+[*.{cs,vb}]
+dotnet_diagnostic.IDE0078.severity = none
+
+# IDE0290: Use primary constructor
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0290
+[*.{cs,vb}]
+dotnet_diagnostic.IDE0290.severity = none
+
+# IDE0305: Use collection expression for fluent
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0305
+[*.{cs,vb}]
+dotnet_diagnostic.IDE0305.severity = none
+
+# IDE0011: Add braces
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0011
+[*.{cs,vb}]
+dotnet_diagnostic.IDE0011.severity = none
+
+# IDE0028: Use collection initializers or expressions
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0028
+[*.{cs,vb}]
+dotnet_diagnostic.IDE0028.severity = none
+
+# IDE0039: Use collection expression for array
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0300
+[*.{cs,vb}]
+dotnet_diagnostic.IDE0300.severity = none
+
+# IDE0031: Use collection expression for empty
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0301
+[*.{cs,vb}]
+dotnet_diagnostic.IDE0301.severity = none
+
+# IDE0046: Use conditional expression for return
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0046
+[*.{cs,vb}]
+dotnet_diagnostic.IDE0046.severity = none
+
+# IDE0047: Parentheses preferences
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0047-ide0048
+[*.{cs,vb}]
+dotnet_diagnostic.IDE0047.severity = none
+
+# CA1716: Identifiers should not match keywords
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1716
+[*.{cs,vb}]
+dotnet_diagnostic.CA1716.severity = none
+
+# CA1720: Identifiers should not contain type names
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1720
+[*.{cs,vb}]
+dotnet_diagnostic.CA1720.severity = none
+
+# CA1846: Prefer AsSpan over Substring
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1846
+# Excluded while TFMs include netstandard2.0
+[*.{cs,vb}]
+dotnet_diagnostic.CA1846.severity = none
+
+# CA1847: Use String.Contains(char) instead of String.Contains(string) with single characters
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1847
+# Excluded while TFMs include netstandard2.0
+[*.{cs,vb}]
+dotnet_diagnostic.CA1847.severity = none
+
+# CA1865-CA1867: Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1865-ca1867
+# Excluded while TFMs include netstandard2.0
+[*.{cs,vb}]
+dotnet_diagnostic.CA1865.severity = none
+dotnet_diagnostic.CA1866.severity = none
+dotnet_diagnostic.CA1867.severity = none
+
+# CA2263: Prefer generic overload when type is known
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2263
+# Excluded while TFMs include netstandard2.0
+[*.{cs,vb}]
+dotnet_diagnostic.CA2263.severity = none
+
+# CA2249: Consider using String.Contains instead of String.IndexOf
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2249
+# Excluded while TFMs include netstandard2.0
+[*.{cs,vb}]
+dotnet_diagnostic.CA2249.severity = none
+
+# IDE0022: Use expression body for methods
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0022
+[*.{cs,vb}]
+dotnet_diagnostic.IDE0022.severity = none
+
+# IDE0032: Use auto-property
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0032
+[*.{cs,vb}]
+dotnet_diagnostic.IDE0032.severity = none
+
+# Order modifiers
+# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0036
+# Excluded becuase of inconsistency with other analyzers
+[*.{cs,vb}]
+dotnet_diagnostic.IDE0036.severity = none
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 4cd6ee6c..f114fb5b 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -541,9 +541,9 @@ namespace Cuemon.Security
}
///
- /// Computes the hash value for the specified .
+ /// Computes the hash value for the specified IConvertible[].
///
- /// The to compute the hash code for.
+ /// The IConvertible[] to compute the hash code for.
/// A containing the computed hash code of the specified .
public virtual HashResult ComputeHash(params IConvertible[] input)
{
@@ -561,9 +561,9 @@ namespace Cuemon.Security
}
///
- /// Computes the hash value for the specified .
+ /// Computes the hash value for the specified byte[].
///
- /// The to compute the hash code for.
+ /// The byte[] to compute the hash code for.
/// A containing the computed hash code of the specified .
public abstract HashResult ComputeHash(byte[] input);
diff --git a/.nuget/Savvyio.App/PackageReleaseNotes.txt b/.nuget/Savvyio.App/PackageReleaseNotes.txt
index dedd7801..3fe93ed3 100644
--- a/.nuget/Savvyio.App/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.App/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Commands.Messaging/PackageReleaseNotes.txt b/.nuget/Savvyio.Commands.Messaging/PackageReleaseNotes.txt
index c0a47ced..c7d85cba 100644
--- a/.nuget/Savvyio.Commands.Messaging/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Commands.Messaging/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Commands/PackageReleaseNotes.txt b/.nuget/Savvyio.Commands/PackageReleaseNotes.txt
index cbf0b510..7999e77e 100644
--- a/.nuget/Savvyio.Commands/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Commands/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Core/PackageReleaseNotes.txt b/.nuget/Savvyio.Core/PackageReleaseNotes.txt
index 2318ce77..274c5cd9 100644
--- a/.nuget/Savvyio.Core/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Core/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Domain.EventSourcing/PackageReleaseNotes.txt b/.nuget/Savvyio.Domain.EventSourcing/PackageReleaseNotes.txt
index 86e2b46f..f84f83b0 100644
--- a/.nuget/Savvyio.Domain.EventSourcing/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Domain.EventSourcing/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Domain/PackageReleaseNotes.txt b/.nuget/Savvyio.Domain/PackageReleaseNotes.txt
index ac683a87..5605fcce 100644
--- a/.nuget/Savvyio.Domain/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Domain/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.EventDriven.Messaging/PackageReleaseNotes.txt b/.nuget/Savvyio.EventDriven.Messaging/PackageReleaseNotes.txt
index 480e5a8d..fe013290 100644
--- a/.nuget/Savvyio.EventDriven.Messaging/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.EventDriven.Messaging/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.EventDriven/PackageReleaseNotes.txt b/.nuget/Savvyio.EventDriven/PackageReleaseNotes.txt
index b5c5a7cc..87a6fa8d 100644
--- a/.nuget/Savvyio.EventDriven/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.EventDriven/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.Dapper/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.Dapper/PackageReleaseNotes.txt
index 0612ddec..2ee3221e 100644
--- a/.nuget/Savvyio.Extensions.Dapper/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.Dapper/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.DapperExtensions/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.DapperExtensions/PackageReleaseNotes.txt
index 3fa6c31c..bf55a821 100644
--- a/.nuget/Savvyio.Extensions.DapperExtensions/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.DapperExtensions/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.DependencyInjection.Dapper/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.DependencyInjection.Dapper/PackageReleaseNotes.txt
index 136c8394..88badf4f 100644
--- a/.nuget/Savvyio.Extensions.DependencyInjection.Dapper/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.DependencyInjection.Dapper/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.DependencyInjection.DapperExtensions/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.DependencyInjection.DapperExtensions/PackageReleaseNotes.txt
index 20e9b0b3..ebdc4f5e 100644
--- a/.nuget/Savvyio.Extensions.DependencyInjection.DapperExtensions/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.DependencyInjection.DapperExtensions/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.DependencyInjection.Domain/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.DependencyInjection.Domain/PackageReleaseNotes.txt
index 76814b21..ceecad55 100644
--- a/.nuget/Savvyio.Extensions.DependencyInjection.Domain/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.DependencyInjection.Domain/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.DependencyInjection.EFCore.Domain.EventSourcing/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.DependencyInjection.EFCore.Domain.EventSourcing/PackageReleaseNotes.txt
index 07ae9e65..4bf0d7f0 100644
--- a/.nuget/Savvyio.Extensions.DependencyInjection.EFCore.Domain.EventSourcing/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.DependencyInjection.EFCore.Domain.EventSourcing/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.DependencyInjection.EFCore.Domain/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.DependencyInjection.EFCore.Domain/PackageReleaseNotes.txt
index 7f71dc73..e7122be4 100644
--- a/.nuget/Savvyio.Extensions.DependencyInjection.EFCore.Domain/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.DependencyInjection.EFCore.Domain/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.DependencyInjection.EFCore/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.DependencyInjection.EFCore/PackageReleaseNotes.txt
index c2addbda..5e007f77 100644
--- a/.nuget/Savvyio.Extensions.DependencyInjection.EFCore/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.DependencyInjection.EFCore/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.DependencyInjection.NATS/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.DependencyInjection.NATS/PackageReleaseNotes.txt
index fe79f84d..4c6533ea 100644
--- a/.nuget/Savvyio.Extensions.DependencyInjection.NATS/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.DependencyInjection.NATS/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.DependencyInjection.Newtonsoft.Json/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.DependencyInjection.Newtonsoft.Json/PackageReleaseNotes.txt
index f7ff5297..a109b330 100644
--- a/.nuget/Savvyio.Extensions.DependencyInjection.Newtonsoft.Json/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.DependencyInjection.Newtonsoft.Json/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.DependencyInjection.QueueStorage/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.DependencyInjection.QueueStorage/PackageReleaseNotes.txt
index 3944cf3b..70f81ef4 100644
--- a/.nuget/Savvyio.Extensions.DependencyInjection.QueueStorage/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.DependencyInjection.QueueStorage/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.DependencyInjection.RabbitMQ/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.DependencyInjection.RabbitMQ/PackageReleaseNotes.txt
index 3e508673..244842d8 100644
--- a/.nuget/Savvyio.Extensions.DependencyInjection.RabbitMQ/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.DependencyInjection.RabbitMQ/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.DependencyInjection.SimpleQueueService/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.DependencyInjection.SimpleQueueService/PackageReleaseNotes.txt
index 392d3ac0..92381490 100644
--- a/.nuget/Savvyio.Extensions.DependencyInjection.SimpleQueueService/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.DependencyInjection.SimpleQueueService/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.DependencyInjection.Text.Json/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.DependencyInjection.Text.Json/PackageReleaseNotes.txt
index c9071f16..f78f5d5d 100644
--- a/.nuget/Savvyio.Extensions.DependencyInjection.Text.Json/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.DependencyInjection.Text.Json/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.DependencyInjection/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.DependencyInjection/PackageReleaseNotes.txt
index cc12a26d..c947e377 100644
--- a/.nuget/Savvyio.Extensions.DependencyInjection/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.DependencyInjection/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.Dispatchers/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.Dispatchers/PackageReleaseNotes.txt
index f6748edc..14e1e680 100644
--- a/.nuget/Savvyio.Extensions.Dispatchers/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.Dispatchers/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.EFCore.Domain.EventSourcing/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.EFCore.Domain.EventSourcing/PackageReleaseNotes.txt
index fcf54906..cd9e2316 100644
--- a/.nuget/Savvyio.Extensions.EFCore.Domain.EventSourcing/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.EFCore.Domain.EventSourcing/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.EFCore.Domain/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.EFCore.Domain/PackageReleaseNotes.txt
index 3d9c0694..84fac6c9 100644
--- a/.nuget/Savvyio.Extensions.EFCore.Domain/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.EFCore.Domain/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.EFCore/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.EFCore/PackageReleaseNotes.txt
index 4c4b29c4..bd6d58c8 100644
--- a/.nuget/Savvyio.Extensions.EFCore/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.EFCore/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.NATS/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.NATS/PackageReleaseNotes.txt
index c63adf63..7e9679d2 100644
--- a/.nuget/Savvyio.Extensions.NATS/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.NATS/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.Newtonsoft.Json/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.Newtonsoft.Json/PackageReleaseNotes.txt
index 9752d9ab..cd10d8d8 100644
--- a/.nuget/Savvyio.Extensions.Newtonsoft.Json/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.Newtonsoft.Json/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.QueueStorage/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.QueueStorage/PackageReleaseNotes.txt
index b8a631a2..2029f864 100644
--- a/.nuget/Savvyio.Extensions.QueueStorage/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.QueueStorage/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.RabbitMQ/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.RabbitMQ/PackageReleaseNotes.txt
index 815ff8f5..021fc527 100644
--- a/.nuget/Savvyio.Extensions.RabbitMQ/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.RabbitMQ/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.SimpleQueueService/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.SimpleQueueService/PackageReleaseNotes.txt
index d66e30b6..574e6393 100644
--- a/.nuget/Savvyio.Extensions.SimpleQueueService/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.SimpleQueueService/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Extensions.Text.Json/PackageReleaseNotes.txt b/.nuget/Savvyio.Extensions.Text.Json/PackageReleaseNotes.txt
index 2b78a72c..0c7bb11c 100644
--- a/.nuget/Savvyio.Extensions.Text.Json/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Extensions.Text.Json/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Messaging/PackageReleaseNotes.txt b/.nuget/Savvyio.Messaging/PackageReleaseNotes.txt
index e2809f6e..23ec0b71 100644
--- a/.nuget/Savvyio.Messaging/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Messaging/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/.nuget/Savvyio.Queries/PackageReleaseNotes.txt b/.nuget/Savvyio.Queries/PackageReleaseNotes.txt
index c8be8576..a14ca8e1 100644
--- a/.nuget/Savvyio.Queries/PackageReleaseNotes.txt
+++ b/.nuget/Savvyio.Queries/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 5.0.10
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 5.0.9
Availability: .NET 10 and .NET 9
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3413a64..2fa79204 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
For more details, please refer to `PackageReleaseNotes.txt` on a per assembly basis in the `.nuget` folder.
+## [5.0.10] - 2026-07-26
+
+This is a patch release focused on dependency updates, code analysis hardening, .NET 9 modernization, code style enforcement, and test coverage expansion.
+
+### Added
+
+- SQLitePCLRaw.bundle_e_sqlite3 package to resolve transitive security warnings in test projects,
+- Code analysis enforcement in build pipeline with latest analyzer level and recommended mode,
+- `.editorconfig` configuration for consistent code style and formatting rules across the codebase,
+- Comprehensive tests for Text.Json converters (MessageConverter, RequestConverter),
+- Extended Newtonsoft.Json RequestConverter tests with writable properties coverage.
+
+### Changed
+
+- Dependency versions bumped to latest stable releases: AWSSDK.SQS and AWSSDK.SimpleNotificationService (4.0.100 → 4.0.100.6), NATS.Client packages (2.8.2 → 3.0.1), Microsoft.Data.Sqlite (10.0.9 → 10.0.10), Microsoft.Extensions.Logging.Abstractions (10.0.9 → 10.0.10), Microsoft.NET.Test.Sdk (18.7.0 → 18.8.1), EntityFrameworkCore net9 (9.0.17 → 9.0.18), EntityFrameworkCore net10 (10.0.9 → 10.0.10), Codebelt extensions (10.1.5 → 10.1.6, 11.1.1 → 11.1.2), Cuemon extensions (10.5.4 → 10.5.5),
+- NGINX base image for DocFX container (1.31.2-alpine → 1.31-alpine),
+- Core platform code modernized for .NET 9: replaced object locks with System.Threading.Lock, added CultureInfo.InvariantCulture to culture-aware StringBuilder and string operations, optimized Enumerable.Any() checks to Count > 0 on known collection types,
+- Roslyn analyzer suppressions added for CA1711, CA1725, CA1848, and CA1873 with justifications,
+- XML documentation examples corrected in Copilot instructions to use proper see cref and c tag formats,
+- HandlerServicesDescriptor and converter helper methods extracted for improved code organization and maintainability.
+
## [5.0.9] - 2026-07-01
This is a patch release focused on API documentation expansion with comprehensive namespace and type examples, DocFX infrastructure restructuring, clear documentation maintenance standards for agents, and multiple NuGet package updates to latest stable versions.
@@ -1060,7 +1081,7 @@ Noticeable highlights:
- QueryHandler class in the Savvyio.Queries namespace that defines a generic and consistent way of handling Query objects that implements the IQuery interface
- SavvyioOptionsExtensions class in the Savvyio.Queries namespace that consist of extension methods for the SavvyioOptions class: AddQueryHandler, AddQueryDispatcher
-[Unreleased]: https://github.com/codebeltnet/savvyio/compare/v5.0.9...HEAD
+[5.0.10]: https://github.com/codebeltnet/savvyio/compare/v5.0.9...v5.0.10
[5.0.9]: https://github.com/codebeltnet/savvyio/compare/v5.0.8...v5.0.9
[5.0.8]: https://github.com/codebeltnet/savvyio/compare/v5.0.7...v5.0.8
[5.0.7]: https://github.com/codebeltnet/savvyio/compare/v5.0.6...v5.0.7
diff --git a/Directory.Build.props b/Directory.Build.props
index f6525a0c..a86adfa4 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -32,8 +32,12 @@
true
true
$(MSBuildThisFileDirectory)savvyio.snk
- 7035
+ true
+ latest
+ Recommended
+ 7035,CA2260,S6618,CA1711,CA1725,CA1848,CA1873
v
+ true
@@ -62,7 +66,7 @@
false
true
0
- none
+ none
NU1701,NETSDK1206
false
true
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 03ea2ce1..508fe832 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,31 +4,32 @@
true
-
-
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
-
-
-
+
+
+
@@ -39,14 +40,14 @@
-
-
-
+
+
+
-
-
-
+
+
+
\ No newline at end of file
diff --git a/src/Savvyio.Core/GlobalSuppressions.cs b/src/Savvyio.Core/GlobalSuppressions.cs
index 1afa65df..1b0f0f9a 100644
--- a/src/Savvyio.Core/GlobalSuppressions.cs
+++ b/src/Savvyio.Core/GlobalSuppressions.cs
@@ -1,4 +1,4 @@
-// This file is used by Code Analysis to maintain SuppressMessage
+// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
@@ -8,4 +8,5 @@
[assembly: SuppressMessage("Major Code Smell", "S2326:Unused type parameters should be removed", Justification = "By design; marker interface - inheriting interface requires specification of DTO.", Scope = "type", Target = "~T:Savvyio.Data.IDataStore`1")]
[assembly: SuppressMessage("Major Code Smell", "S2326:Unused type parameters should be removed", Justification = "By design; marker interface - inheriting interface requires specification of Entity.", Scope = "type", Target = "~T:Savvyio.Domain.IRepository`2")]
[assembly: SuppressMessage("Major Code Smell", "S2326:Unused type parameters should be removed", Justification = "By design; marker interface - inheriting interface requires specification of Request.", Scope = "type", Target = "~T:Savvyio.IHandler`1")]
-[assembly: SuppressMessage("Major Code Smell", "S2326:Unused type parameters should be removed", Justification = "By design; marker interface - inheriting interface requires specification of Request.", Scope = "type", Target = "~T:Savvyio.Queries.IQuery`1")]
\ No newline at end of file
+[assembly: SuppressMessage("Major Code Smell", "S2326:Unused type parameters should be removed", Justification = "By design; marker interface - inheriting interface requires specification of Request.", Scope = "type", Target = "~T:Savvyio.Queries.IQuery`1")]
+[assembly: SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = "By design - Dictionary suffix confuses context.", Scope = "type", Target = "~T:Savvyio.EventDriven.Messaging.CloudEvents.ICloudEvent`1")]
diff --git a/src/Savvyio.Core/HandlerServicesDescriptor.cs b/src/Savvyio.Core/HandlerServicesDescriptor.cs
index 88f646b9..e07b7f59 100644
--- a/src/Savvyio.Core/HandlerServicesDescriptor.cs
+++ b/src/Savvyio.Core/HandlerServicesDescriptor.cs
@@ -1,7 +1,9 @@
-using System;
+using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Text;
+using System.Threading;
using Cuemon;
using Cuemon.Extensions;
using Cuemon.Extensions.Runtime;
@@ -27,7 +29,7 @@ namespace Savvyio
public class HandlerServicesDescriptor : IHandlerServicesDescriptor
{
private List _models;
- private readonly object _locker = new();
+ private readonly Lock _locker = new();
///
/// Initializes a new instance of the class.
@@ -53,25 +55,12 @@ public override string ToString()
var builder = new StringBuilder();
foreach (var model in GenerateHandlerDiscoveries())
{
- var discovery = $"Discovered {model.ImplementationsCount} {model.AbstractionType} implementation{(model.ImplementationsCount > 1 ? "s" : "")} covering a total of {model.DelegatesCount} {model.DelegateType} method{(model.DelegatesCount > 1 ? "s" : "")}";
+ var discovery = FormatDiscoveryHeader(model);
builder.Append(discovery);
builder.AppendLine();
foreach (var assembly in model.Assemblies)
{
- builder.AppendLine();
- builder.AppendLine($"Assembly: {assembly.Name}");
- builder.AppendLine($"Namespace: {assembly.Namespace}");
- builder.AppendLine();
-
- foreach (var implementation in assembly.Implementations)
- {
- builder.AppendLine($"<{implementation.Name}>");
- foreach (var @delegate in implementation.Delegates)
- {
- builder.AppendLine($"\t*{@delegate.Type} --> &{@delegate.Handler}");
- }
- builder.AppendLine();
- }
+ AppendAssembly(builder, assembly);
}
builder.AppendLine(Generate.FixedString('-', discovery.Length.Max(discovery.Length.Max(discovery.Length))));
builder.AppendLine();
@@ -79,36 +68,61 @@ public override string ToString()
return builder.ToString().TrimEnd();
}
+ private static string FormatDiscoveryHeader(HandlerDiscoveryModel model)
+ {
+ return $"Discovered {model.ImplementationsCount} {model.AbstractionType} implementation{(model.ImplementationsCount > 1 ? "s" : "")} covering a total of {model.DelegatesCount} {model.DelegateType} method{(model.DelegatesCount > 1 ? "s" : "")}";
+ }
+
+ private static void AppendAssembly(StringBuilder builder, HandlerServiceAssemblyModel assembly)
+ {
+ builder.AppendLine();
+ builder.AppendLine(CultureInfo.InvariantCulture, $"Assembly: {assembly.Name}");
+ builder.AppendLine(CultureInfo.InvariantCulture, $"Namespace: {assembly.Namespace}");
+ builder.AppendLine();
+
+ foreach (var implementation in assembly.Implementations)
+ {
+ builder.AppendLine(CultureInfo.InvariantCulture, $"<{implementation.Name}>");
+ foreach (var @delegate in implementation.Delegates)
+ {
+ builder.AppendLine(CultureInfo.InvariantCulture, $"\t*{@delegate.Type} --> &{@delegate.Handler}");
+ }
+ builder.AppendLine();
+ }
+ }
+
///
/// Generates the handler discoveries.
///
/// A collection of representing the handler discoveries.
public IEnumerable GenerateHandlerDiscoveries()
{
- if (_models == null)
+ if (_models != null) { return _models; }
+
+ lock (_locker)
{
- lock (_locker)
+ _models ??= BuildHandlerDiscoveries();
+ }
+ return _models;
+ }
+
+ private List BuildHandlerDiscoveries()
+ {
+ var models = new List();
+ foreach (var serviceType in ServiceTypes)
+ {
+ var serviceRequestType = serviceType.GetInterface("IHandler`1")?.GenericTypeArguments.Single();
+ if (serviceRequestType == null) { continue; }
+ foreach (var discoveredServicesGroup in DiscoveredServices)
{
- if (_models == null)
+ var model = new HandlerDiscoveryModel(serviceType, serviceRequestType, discoveredServicesGroup);
+ if (model.ImplementationsCount > 0)
{
- _models = new List();
- foreach (var serviceType in ServiceTypes)
- {
- var serviceRequestType = serviceType.GetInterface("IHandler`1")?.GenericTypeArguments.Single();
- if (serviceRequestType == null) { continue; }
- foreach (var discoveredServicesGroup in DiscoveredServices)
- {
- var model = new HandlerDiscoveryModel(serviceType, serviceRequestType, discoveredServicesGroup);
- if (model.ImplementationsCount > 0)
- {
- _models.Add(model);
- }
- }
- }
+ models.Add(model);
}
}
}
- return _models;
+ return models;
}
}
}
diff --git a/src/Savvyio.Core/Handlers/FireForgetManager.cs b/src/Savvyio.Core/Handlers/FireForgetManager.cs
index 4c517cc3..661dd93c 100644
--- a/src/Savvyio.Core/Handlers/FireForgetManager.cs
+++ b/src/Savvyio.Core/Handlers/FireForgetManager.cs
@@ -6,7 +6,7 @@
namespace Savvyio.Handlers
{
- internal class FireForgetManager : IFireForgetRegistry, IFireForgetActivator
+ internal sealed class FireForgetManager : IFireForgetRegistry, IFireForgetActivator
{
private readonly ConcurrentDictionary> _handlers = new();
private readonly ConcurrentDictionary> _asyncHandlers = new();
diff --git a/src/Savvyio.Core/Handlers/RequestReplyManager.cs b/src/Savvyio.Core/Handlers/RequestReplyManager.cs
index b1e523e8..b8c19f17 100644
--- a/src/Savvyio.Core/Handlers/RequestReplyManager.cs
+++ b/src/Savvyio.Core/Handlers/RequestReplyManager.cs
@@ -6,7 +6,7 @@
namespace Savvyio.Handlers
{
- internal class RequestReplyManager : IRequestReplyRegistry, IRequestReplyActivator
+ internal sealed class RequestReplyManager : IRequestReplyRegistry, IRequestReplyActivator
{
private readonly ConcurrentDictionary> _handlers = new();
private readonly ConcurrentDictionary>> _asyncHandlers = new();
diff --git a/src/Savvyio.Core/MetadataDictionary.cs b/src/Savvyio.Core/MetadataDictionary.cs
index 0f2429b8..a362ee17 100644
--- a/src/Savvyio.Core/MetadataDictionary.cs
+++ b/src/Savvyio.Core/MetadataDictionary.cs
@@ -95,13 +95,13 @@ public object this[string key]
public bool IsReadOnly => _dictionary.IsReadOnly;
///
- /// Gets an containing the keys of the .
+ /// Gets an containing the keys of the .
///
/// A containing the keys in the .
public ICollection Keys => _dictionary.Keys;
///
- /// Gets an containing the values in the .
+ /// Gets an containing the values in the .
///
/// A containing the values in the .
public ICollection
/// A builder used to create or modify options for this context. Databases (and other extensions)
@@ -44,10 +44,10 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
///
/// Override this method to set defaults and configure conventions before they run. This method is invoked before
- /// .
+ /// .
///
/// The builder being used to set defaults and configure conventions that will be used to build the model for this context.
- /// If a model is explicitly set on the options for this context (via )
+ /// If a model is explicitly set on the options for this context (via )
/// then this method will not be run.
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
@@ -57,14 +57,14 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura
///
/// Override this method to further configure the model that was discovered by convention from the entity types
- /// exposed in properties on your derived context. The resulting model may be cached
+ /// exposed in properties on your derived context. The resulting model may be cached
/// and re-used for subsequent instances of your derived context.
///
/// The builder being used to construct the model for this context. Databases (and other extensions) typically
/// define extension methods on this object that allow you to configure aspects of the model that are specific
/// to a given database.
///
- /// If a model is explicitly set on the options for this context (via )
+ /// If a model is explicitly set on the options for this context (via )
/// then this method will not be run.
///
///
diff --git a/src/Savvyio.Extensions.EFCore/GlobalSuppressions.cs b/src/Savvyio.Extensions.EFCore/GlobalSuppressions.cs
index afb01e85..a1dfb4a6 100644
--- a/src/Savvyio.Extensions.EFCore/GlobalSuppressions.cs
+++ b/src/Savvyio.Extensions.EFCore/GlobalSuppressions.cs
@@ -1,4 +1,4 @@
-// This file is used by Code Analysis to maintain SuppressMessage
+// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
@@ -6,6 +6,4 @@
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Major Code Smell", "S3358:Ternary operators should not be nested", Justification = "By design. No added complexity in this scope.", Scope = "member", Target = "~M:Savvyio.Extensions.EFCore.EfCoreRepository`2.FindAllAsync(System.Linq.Expressions.Expression{System.Func{`0,System.Boolean}},System.Action{Cuemon.Threading.AsyncOptions})~System.Threading.Tasks.Task{System.Collections.Generic.IEnumerable{`0}}")]
-[assembly: SuppressMessage("Major Code Smell", "S3358:Ternary operators should not be nested", Justification = "By design. No added complexity in this scope.", Scope = "member", Target = "~M:Savvyio.Extensions.EFCore.DefaultEfCoreDataStore`1.FindAllAsync(System.Action{Savvyio.Extensions.EFCore.EfCoreQueryOptions{`0}})~System.Threading.Tasks.Task{System.Collections.Generic.IEnumerable{`0}}")]
-[assembly: SuppressMessage("Major Code Smell", "S6966:Awaitable method should be used", Justification = "https://github.com/SonarSource/sonar-dotnet/pull/9318", Scope = "member", Target = "~M:Savvyio.Extensions.EFCore.DefaultEfCoreDataStore`1.CreateAsync(`0,System.Action{Cuemon.Threading.AsyncOptions})~System.Threading.Tasks.Task")]
[assembly: SuppressMessage("Major Code Smell", "S6966:Awaitable method should be used", Justification = "https://github.com/SonarSource/sonar-dotnet/pull/9318", Scope = "member", Target = "~M:Savvyio.Extensions.EFCore.EfCoreDataStore`1.CreateAsync(`0,System.Action{Cuemon.Threading.AsyncOptions})~System.Threading.Tasks.Task")]
diff --git a/src/Savvyio.Extensions.Newtonsoft.Json/Bootstrapper.cs b/src/Savvyio.Extensions.Newtonsoft.Json/Bootstrapper.cs
index 6eb35067..44d2a3fe 100644
--- a/src/Savvyio.Extensions.Newtonsoft.Json/Bootstrapper.cs
+++ b/src/Savvyio.Extensions.Newtonsoft.Json/Bootstrapper.cs
@@ -1,10 +1,11 @@
-using Codebelt.Extensions.Newtonsoft.Json.Formatters;
+using Codebelt.Extensions.Newtonsoft.Json.Formatters;
+using System.Threading;
namespace Savvyio.Extensions.Newtonsoft.Json
{
internal static class Bootstrapper
{
- private static readonly object PadLock = new();
+ private static readonly Lock PadLock = new();
private static bool _initialized;
internal static void Initialize()
diff --git a/src/Savvyio.Extensions.Newtonsoft.Json/Converters/AggregateRootConverter.cs b/src/Savvyio.Extensions.Newtonsoft.Json/Converters/AggregateRootConverter.cs
index a36b66de..003c02c3 100644
--- a/src/Savvyio.Extensions.Newtonsoft.Json/Converters/AggregateRootConverter.cs
+++ b/src/Savvyio.Extensions.Newtonsoft.Json/Converters/AggregateRootConverter.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
@@ -35,7 +35,7 @@ public AggregateRootConverter(Action setup = null)
///
/// Writes the JSON representation of the object.
///
- /// The to write to.
+ /// The to write to.
/// The value.
/// The calling serializer.
public override void WriteJson(JsonWriter writer, AggregateRoot value, JsonSerializer serializer)
@@ -57,7 +57,7 @@ public override void WriteJson(JsonWriter writer, AggregateRoot value, Jso
///
/// Reads the JSON representation of the object.
///
- /// The to read from.
+ /// The to read from.
/// Type of the object.
/// The existing value of object being read. If there is no existing value then null will be used.
/// The existing value has a value.
@@ -69,69 +69,86 @@ public override AggregateRoot ReadJson(JsonReader reader, Type objectType,
var properties = objectType.GetRuntimePropertiesExceptOf>().Where(pi => pi.CanRead).ToList();
if (idProperty != null) { properties.Insert(0, idProperty); }
+ var propertyData = ReadPropertyData(reader, properties, serializer);
+
+ var result = CreateAggregateRoot(objectType, properties, propertyData);
+ if (result != null) { return result; }
+
+ throw ExceptionInsights.Embed(new InvalidOperationException($"Unable to deserialize {objectType.FullName}; consider adding a custom converter for this type."), MethodBase.GetCurrentMethod(), Arguments.ToArray(reader, objectType, existingValue, hasExistingValue, serializer));
+ }
+
+ private List ReadPropertyData(JsonReader reader, List properties, JsonSerializer serializer)
+ {
var propertyData = new List();
- if (reader.TokenType == JsonToken.StartObject)
+ if (reader.TokenType != JsonToken.StartObject) { return propertyData; }
+
+ var depth = reader.Depth;
+ while (reader.Read())
{
- var depth = reader.Depth;
- while (reader.Read())
+ switch (reader.TokenType)
{
- switch (reader.TokenType)
- {
- case JsonToken.PropertyName:
- var propertyName = (string)reader.Value;
- var matchingProperty = properties.FirstOrDefault(pi => pi.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase));
- if (matchingProperty != null)
- {
- reader.Read();
- if (matchingProperty.PropertyType.HasTypes(typeof(SingleValueObject<>)))
- {
- propertyData.Add(new DataPair(matchingProperty.Name, serializer.Deserialize(reader, matchingProperty.PropertyType), matchingProperty.PropertyType));
- }
- else
- {
- propertyData.Add(new DataPair(matchingProperty.Name, ParserFactory.FromObject().Parse(reader.Value?.ToString(), matchingProperty.PropertyType, o => o.FormatProvider = _options.FormatProvider) ?? serializer.Deserialize(reader, matchingProperty.PropertyType), matchingProperty.PropertyType));
- }
- }
- break;
- }
-
- if (reader.Depth == depth && reader.TokenType == JsonToken.EndObject) { break; }
+ case JsonToken.PropertyName:
+ var propertyName = (string)reader.Value;
+ var matchingProperty = properties.FirstOrDefault(pi => pi.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase));
+ if (matchingProperty != null)
+ {
+ reader.Read();
+ propertyData.Add(ReadDataPair(reader, matchingProperty, serializer));
+ }
+ break;
}
+
+ if (reader.Depth == depth && reader.TokenType == JsonToken.EndObject) { break; }
}
+ return propertyData;
+ }
+ private DataPair ReadDataPair(JsonReader reader, PropertyInfo matchingProperty, JsonSerializer serializer)
+ {
+ if (matchingProperty.PropertyType.HasTypes(typeof(SingleValueObject<>)))
+ {
+ return new DataPair(matchingProperty.Name, serializer.Deserialize(reader, matchingProperty.PropertyType), matchingProperty.PropertyType);
+ }
+
+ return new DataPair(matchingProperty.Name, ParserFactory.FromObject().Parse(reader.Value?.ToString(), matchingProperty.PropertyType, o => o.FormatProvider = _options.FormatProvider) ?? serializer.Deserialize(reader, matchingProperty.PropertyType), matchingProperty.PropertyType);
+ }
+
+ private AggregateRoot CreateAggregateRoot(Type objectType, List properties, List propertyData)
+ {
var ctors = objectType.GetConstructors(_options.Flags).ToList();
- if (ctors.Any())
+ if (ctors.Count == 0) { return null; }
+
+ var matchingCtor = ctors.SingleOrDefault(info => MatchesConstructor(info, propertyData));
+ if (matchingCtor != null)
{
- var matchingCtor = ctors.SingleOrDefault(info =>
- {
- var paramters = info.GetParameters().ToList();
- return paramters.Count == propertyData.Count && paramters.Select(pi => pi.ParameterType).SequenceEqual(propertyData.Select(pair => pair.Type));
- });
+ return matchingCtor.Invoke(propertyData.Select(pair => pair.Value).ToArray()) as AggregateRoot;
+ }
- if (matchingCtor != null)
- {
- return matchingCtor.Invoke(propertyData.Select(pair => pair.Value).ToArray()) as AggregateRoot;
- }
- else
+ var defaultCtor = ctors.SingleOrDefault(ci => ci.GetParameters().Length == 0);
+ if (defaultCtor == null) { return null; }
+
+ return PopulateDefaultConstructed(defaultCtor, properties, propertyData);
+ }
+
+ private static bool MatchesConstructor(ConstructorInfo info, List propertyData)
+ {
+ var paramters = info.GetParameters().ToList();
+ return paramters.Count == propertyData.Count && paramters.Select(pi => pi.ParameterType).SequenceEqual(propertyData.Select(pair => pair.Type));
+ }
+
+ private static AggregateRoot PopulateDefaultConstructed(ConstructorInfo defaultCtor, List properties, List propertyData)
+ {
+ var ar = defaultCtor.Invoke(Array.Empty()) as AggregateRoot;
+ foreach (var property in properties)
+ {
+ if (property.CanWrite)
{
- var defaultCtor = ctors.SingleOrDefault(ci => ci.GetParameters().Length == 0);
- if (defaultCtor != null)
- {
- var ar = defaultCtor.Invoke(Array.Empty()) as AggregateRoot;
- foreach (var property in properties)
- {
- if (property.CanWrite)
- {
- property.SetValue(ar, propertyData.SingleOrDefault(pair => pair.Name.Equals(property.Name, StringComparison.OrdinalIgnoreCase))?.Value);
- }
- }
- return ar;
- }
+ property.SetValue(ar, propertyData.SingleOrDefault(pair => pair.Name.Equals(property.Name, StringComparison.OrdinalIgnoreCase))?.Value);
}
}
- throw ExceptionInsights.Embed(new InvalidOperationException($"Unable to deserialize {objectType.FullName}; consider adding a custom converter for this type."), MethodBase.GetCurrentMethod(), Arguments.ToArray(reader, objectType, existingValue, hasExistingValue, serializer));
+ return ar;
}
}
}
diff --git a/src/Savvyio.Extensions.Newtonsoft.Json/Converters/MessageConverter.cs b/src/Savvyio.Extensions.Newtonsoft.Json/Converters/MessageConverter.cs
index 5c58c1a3..516abe48 100644
--- a/src/Savvyio.Extensions.Newtonsoft.Json/Converters/MessageConverter.cs
+++ b/src/Savvyio.Extensions.Newtonsoft.Json/Converters/MessageConverter.cs
@@ -27,7 +27,7 @@ public class MessageConverter : JsonConverter
///
/// Writes the JSON representation of the object.
///
- /// The to write to.
+ /// The to write to.
/// The value.
/// The calling serializer.
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
@@ -47,7 +47,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
///
/// Reads the JSON representation of the object.
///
- /// The to read from.
+ /// The to read from.
/// Type of the object.
/// The existing value of object being read.
/// The calling serializer.
@@ -76,7 +76,7 @@ public override bool CanConvert(Type objectType)
}
}
- internal class MessageConverter : JsonConverter> where T : IRequest
+ internal sealed class MessageConverter : JsonConverter> where T : IRequest
{
public MessageConverter()
{
@@ -147,13 +147,13 @@ public override IMessage ReadJson(JsonReader reader, Type objectType, IMessag
var specVersionKey = serializer.ResolvePropertyKeyByConvention(nameof(ICloudEvent.Specversion));
var requestType = objectType.GetGenericArguments()[0];
- var cloudEventType = MessageConverter.CloudEventTypes.Value.Single(ti => ti.FullName!.StartsWith("Savvyio.EventDriven.Messaging.CloudEvents.CloudEvent"));
+ var cloudEventType = MessageConverter.CloudEventTypes.Value.Single(ti => ti.FullName!.StartsWith("Savvyio.EventDriven.Messaging.CloudEvents.CloudEvent", StringComparison.Ordinal));
var specVersion = document.Root[specVersionKey]!.Value();
var cloudEvent = Activator.CreateInstance(cloudEventType.MakeGenericType(requestType), [message, specVersion]) as IMessage;
if (objectType.HasInterfaces(typeof(ISignedCloudEvent<>)))
{
- var signedCloudEventType = MessageConverter.CloudEventTypes.Value.Single(ti => ti.FullName!.StartsWith("Savvyio.EventDriven.Messaging.CloudEvents.Cryptography.SignedCloudEvent"));
+ var signedCloudEventType = MessageConverter.CloudEventTypes.Value.Single(ti => ti.FullName!.StartsWith("Savvyio.EventDriven.Messaging.CloudEvents.Cryptography.SignedCloudEvent", StringComparison.Ordinal));
var signature = document.Root[signatureKey]!.Value();
return Activator.CreateInstance(signedCloudEventType.MakeGenericType(requestType), [cloudEvent, signature]) as IMessage;
diff --git a/src/Savvyio.Extensions.Newtonsoft.Json/Converters/RequestConverter.cs b/src/Savvyio.Extensions.Newtonsoft.Json/Converters/RequestConverter.cs
index c20be9fa..ea4c41d8 100644
--- a/src/Savvyio.Extensions.Newtonsoft.Json/Converters/RequestConverter.cs
+++ b/src/Savvyio.Extensions.Newtonsoft.Json/Converters/RequestConverter.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using System.Reflection;
using System.Runtime.CompilerServices;
using Cuemon.Extensions.Reflection;
using Newtonsoft.Json;
@@ -23,7 +24,7 @@ public RequestConverter()
///
/// Writes the JSON representation of the object.
///
- /// The to write to.
+ /// The to write to.
/// The value.
/// The calling serializer.
///
@@ -35,16 +36,16 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
}
///
- /// Gets a value indicating whether this can write JSON.
+ /// Gets a value indicating whether this can write JSON.
///
- /// true if this can write JSON; otherwise, false.
+ /// true if this can write JSON; otherwise, false.
public override bool CanWrite { get; } = false;
///
/// Reads the JSON representation of the object.
///
- /// The to read from.
+ /// The to read from.
/// Type of the object.
/// The existing value of object being read.
/// The calling serializer.
@@ -64,30 +65,32 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
if (document.Root[jProperty.Name] != null)
{
var value = serializer.Deserialize(document.Root[jProperty.Name].CreateReader(), property.PropertyType);
- if (property.CanWrite)
- {
- property.SetValue(instance, value);
- }
- else
- {
- var field = property.IsAutoProperty()
- ? objectType.GetAllFields().SingleOrDefault(fi => fi.Name.StartsWith($"<{property.Name}>"))
- : objectType.GetAllFields().SingleOrDefault(fi => fi.Name.Equals($"_{property.Name}>", StringComparison.OrdinalIgnoreCase));
- if (field != null)
- {
- field.SetValue(instance, value);
- }
- else
- {
- throw new NotSupportedException($"This deserializer only supports rehydration of {nameof(IRequest)} implementations that either use auto-properties or have a naming convention that makes it possible to tie non-writable properties with the backing field equivalent.");
- }
- }
+ SetPropertyOrField(instance, objectType, property, value);
}
}
return instance as IRequest;
}
+ private static void SetPropertyOrField(object instance, Type objectType, PropertyInfo property, object value)
+ {
+ if (property.CanWrite)
+ {
+ property.SetValue(instance, value);
+ return;
+ }
+
+ var field = property.IsAutoProperty()
+ ? objectType.GetAllFields().SingleOrDefault(fi => fi.Name.StartsWith($"<{property.Name}>", StringComparison.Ordinal))
+ : objectType.GetAllFields().SingleOrDefault(fi => fi.Name.Equals($"_{property.Name}>", StringComparison.OrdinalIgnoreCase));
+ if (field == null)
+ {
+ throw new NotSupportedException($"This deserializer only supports rehydration of {nameof(IRequest)} implementations that either use auto-properties or have a naming convention that makes it possible to tie non-writable properties with the backing field equivalent.");
+ }
+
+ field.SetValue(instance, value);
+ }
+
///
/// Determines whether this instance can convert the specified object type.
///
diff --git a/src/Savvyio.Extensions.Newtonsoft.Json/Converters/SingleValueObjectConverter.cs b/src/Savvyio.Extensions.Newtonsoft.Json/Converters/SingleValueObjectConverter.cs
index 026a01f4..18b7a226 100644
--- a/src/Savvyio.Extensions.Newtonsoft.Json/Converters/SingleValueObjectConverter.cs
+++ b/src/Savvyio.Extensions.Newtonsoft.Json/Converters/SingleValueObjectConverter.cs
@@ -25,7 +25,7 @@ public SingleValueObjectConverter()
///
/// Writes the JSON representation of the object.
///
- /// The to write to.
+ /// The to write to.
/// The value.
/// The calling serializer.
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
@@ -46,7 +46,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s
///
/// Reads the JSON representation of the object.
///
- /// The to read from.
+ /// The to read from.
/// Type of the object.
/// The existing value of object being read.
/// The calling serializer.
@@ -75,7 +75,7 @@ public override bool CanConvert(Type objectType)
}
}
- internal class SingleValueObjectConverter : JsonConverter>
+ internal sealed class SingleValueObjectConverter : JsonConverter>
{
public SingleValueObjectConverter()
{
diff --git a/src/Savvyio.Extensions.Newtonsoft.Json/Converters/ValueObjectConverter.cs b/src/Savvyio.Extensions.Newtonsoft.Json/Converters/ValueObjectConverter.cs
index 371c6038..757a49eb 100644
--- a/src/Savvyio.Extensions.Newtonsoft.Json/Converters/ValueObjectConverter.cs
+++ b/src/Savvyio.Extensions.Newtonsoft.Json/Converters/ValueObjectConverter.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -35,7 +35,7 @@ public ValueObjectConverter(Action setup = null)
///
/// Writes the JSON representation of the object.
///
- /// The to write to.
+ /// The to write to.
/// The value.
/// The calling serializer.
public override void WriteJson(JsonWriter writer, ValueObject value, JsonSerializer serializer)
@@ -63,7 +63,7 @@ public override void WriteJson(JsonWriter writer, ValueObject value, JsonSeriali
///
/// Reads the JSON representation of the object.
///
- /// The to read from.
+ /// The to read from.
/// Type of the object.
/// The existing value of object being read. If there is no existing value then null will be used.
/// The existing value has a value.
@@ -81,68 +81,86 @@ public override ValueObject ReadJson(JsonReader reader, Type objectType, ValueOb
}
}
+ var propertyData = ReadPropertyData(reader, properties, serializer);
+
+ var result = CreateValueObject(objectType, properties, propertyData);
+ if (result != null) { return result; }
+
+ throw ExceptionInsights.Embed(new InvalidOperationException($"Unable to deserialize {objectType.FullName}; consider adding a custom converter for this type."), MethodBase.GetCurrentMethod(), Arguments.ToArray(reader, objectType, existingValue, hasExistingValue, serializer));
+ }
+
+ private List ReadPropertyData(JsonReader reader, List properties, JsonSerializer serializer)
+ {
var propertyData = new List();
- if (reader.TokenType == JsonToken.StartObject)
+ if (reader.TokenType != JsonToken.StartObject) { return propertyData; }
+
+ var depth = reader.Depth;
+ while (reader.Read())
{
- var depth = reader.Depth;
- while (reader.Read())
+ switch (reader.TokenType)
{
- switch (reader.TokenType)
- {
- case JsonToken.PropertyName:
- var propertyName = (string)reader.Value;
- var matchingProperty = properties.FirstOrDefault(pi => pi.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase));
- if (matchingProperty != null)
- {
- reader.Read();
- if (matchingProperty.PropertyType.HasTypes(typeof(SingleValueObject<>)))
- {
- propertyData.Add(new DataPair(matchingProperty.Name, serializer.Deserialize(reader, matchingProperty.PropertyType), matchingProperty.PropertyType));
- }
- else
- {
- propertyData.Add(new DataPair(matchingProperty.Name, ParserFactory.FromObject().Parse(reader.Value?.ToString(), matchingProperty.PropertyType, o => o.FormatProvider = _options.FormatProvider) ?? serializer.Deserialize(reader, matchingProperty.PropertyType), matchingProperty.PropertyType));
- }
- }
- break;
- }
-
- if (reader.Depth == depth && reader.TokenType == JsonToken.EndObject) { break; }
+ case JsonToken.PropertyName:
+ var propertyName = (string)reader.Value;
+ var matchingProperty = properties.FirstOrDefault(pi => pi.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase));
+ if (matchingProperty != null)
+ {
+ reader.Read();
+ propertyData.Add(ReadDataPair(reader, matchingProperty, serializer));
+ }
+ break;
}
+
+ if (reader.Depth == depth && reader.TokenType == JsonToken.EndObject) { break; }
}
+ return propertyData;
+ }
+
+ private DataPair ReadDataPair(JsonReader reader, PropertyInfo matchingProperty, JsonSerializer serializer)
+ {
+ if (matchingProperty.PropertyType.HasTypes(typeof(SingleValueObject<>)))
+ {
+ return new DataPair(matchingProperty.Name, serializer.Deserialize(reader, matchingProperty.PropertyType), matchingProperty.PropertyType);
+ }
+
+ return new DataPair(matchingProperty.Name, ParserFactory.FromObject().Parse(reader.Value?.ToString(), matchingProperty.PropertyType, o => o.FormatProvider = _options.FormatProvider) ?? serializer.Deserialize(reader, matchingProperty.PropertyType), matchingProperty.PropertyType);
+ }
+
+ private ValueObject CreateValueObject(Type objectType, List properties, List propertyData)
+ {
var ctors = objectType.GetConstructors(_options.Flags).ToList();
- if (ctors.Any())
+ if (ctors.Count == 0) { return null; }
+
+ var matchingCtor = ctors.SingleOrDefault(info => MatchesConstructor(info, propertyData));
+ if (matchingCtor != null)
{
- var matchingCtor = ctors.SingleOrDefault(info =>
- {
- var paramters = info.GetParameters().ToList();
- return paramters.Count == propertyData.Count && paramters.Select(pi => pi.ParameterType).SequenceEqual(propertyData.Select(pair => pair.Type));
- });
+ return matchingCtor.Invoke(propertyData.Select(pair => pair.Value).ToArray()) as ValueObject;
+ }
- if (matchingCtor != null)
- {
- return matchingCtor.Invoke(propertyData.Select(pair => pair.Value).ToArray()) as ValueObject;
- }
- else
+ var defaultCtor = ctors.SingleOrDefault(ci => ci.GetParameters().Length == 0);
+ if (defaultCtor == null) { return null; }
+
+ return PopulateDefaultConstructed(defaultCtor, properties, propertyData);
+ }
+
+ private static bool MatchesConstructor(ConstructorInfo info, List propertyData)
+ {
+ var paramters = info.GetParameters().ToList();
+ return paramters.Count == propertyData.Count && paramters.Select(pi => pi.ParameterType).SequenceEqual(propertyData.Select(pair => pair.Type));
+ }
+
+ private static ValueObject PopulateDefaultConstructed(ConstructorInfo defaultCtor, List properties, List propertyData)
+ {
+ var vo = defaultCtor.Invoke(Array.Empty()) as ValueObject;
+ foreach (var property in properties)
+ {
+ if (property.CanWrite)
{
- var defaultCtor = ctors.SingleOrDefault(ci => ci.GetParameters().Length == 0);
- if (defaultCtor != null)
- {
- var vo = defaultCtor.Invoke(Array.Empty()) as ValueObject;
- foreach (var property in properties)
- {
- if (property.CanWrite)
- {
- property.SetValue(vo, propertyData.SingleOrDefault(pair => pair.Name.Equals(property.Name, StringComparison.OrdinalIgnoreCase))?.Value);
- }
- }
- return vo;
- }
+ property.SetValue(vo, propertyData.SingleOrDefault(pair => pair.Name.Equals(property.Name, StringComparison.OrdinalIgnoreCase))?.Value);
}
}
- throw ExceptionInsights.Embed(new InvalidOperationException($"Unable to deserialize {objectType.FullName}; consider adding a custom converter for this type."), MethodBase.GetCurrentMethod(), Arguments.ToArray(reader, objectType, existingValue, hasExistingValue, serializer));
+ return vo;
}
}
}
diff --git a/src/Savvyio.Extensions.Newtonsoft.Json/GlobalSuppressions.cs b/src/Savvyio.Extensions.Newtonsoft.Json/GlobalSuppressions.cs
new file mode 100644
index 00000000..c648670e
--- /dev/null
+++ b/src/Savvyio.Extensions.Newtonsoft.Json/GlobalSuppressions.cs
@@ -0,0 +1,8 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Performance", "CA1805:Do not initialize unnecessarily", Justification = "False-Positive.", Scope = "member", Target = "~P:Savvyio.Extensions.Newtonsoft.Json.Converters.RequestConverter.CanWrite")]
diff --git a/src/Savvyio.Extensions.SimpleQueueService/AmazonResourceNameOptions.cs b/src/Savvyio.Extensions.SimpleQueueService/AmazonResourceNameOptions.cs
index 19548e80..087ce66e 100644
--- a/src/Savvyio.Extensions.SimpleQueueService/AmazonResourceNameOptions.cs
+++ b/src/Savvyio.Extensions.SimpleQueueService/AmazonResourceNameOptions.cs
@@ -73,7 +73,7 @@ public void ValidateOptions()
{
Validator.ThrowIfInvalidState(Partition.IsNullOrWhiteSpace());
Validator.ThrowIfInvalidState(Region.IsNullOrWhiteSpace());
- Validator.ThrowIfInvalidState(AccountId.IsNullOrWhiteSpace() || AccountId.Length != 12 || !AccountId.IsNumeric(NumberStyles.Integer));
+ Validator.ThrowIfInvalidState(AccountId.IsNullOrWhiteSpace() || AccountId.Length != 12 || !AccountId.IsNumeric(NumberStyles.Integer, CultureInfo.InvariantCulture));
}
}
}
diff --git a/src/Savvyio.Extensions.Text.Json/Bootstrapper.cs b/src/Savvyio.Extensions.Text.Json/Bootstrapper.cs
index 46dcfe7e..732a75db 100644
--- a/src/Savvyio.Extensions.Text.Json/Bootstrapper.cs
+++ b/src/Savvyio.Extensions.Text.Json/Bootstrapper.cs
@@ -1,10 +1,11 @@
-using Cuemon.Extensions.Text.Json.Formatters;
+using Cuemon.Extensions.Text.Json.Formatters;
+using System.Threading;
namespace Savvyio.Extensions.Text.Json
{
internal static class Bootstrapper
{
- private static readonly object PadLock = new();
+ private static readonly Lock PadLock = new();
private static bool _initialized;
internal static void Initialize()
diff --git a/src/Savvyio.Extensions.Text.Json/Converters/MessageConverter.cs b/src/Savvyio.Extensions.Text.Json/Converters/MessageConverter.cs
index 37929a37..06dbfae5 100644
--- a/src/Savvyio.Extensions.Text.Json/Converters/MessageConverter.cs
+++ b/src/Savvyio.Extensions.Text.Json/Converters/MessageConverter.cs
@@ -61,7 +61,7 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer
}
}
- internal class MessageConverter : JsonConverter> where T : IRequest
+ internal sealed class MessageConverter : JsonConverter> where T : IRequest
{
public MessageConverter()
{
@@ -85,53 +85,15 @@ public override IMessage Read(ref Utf8JsonReader reader, Type typeToConvert,
var memberType = typeToConvert.GenericTypeArguments[0];
var time = document.RootElement.GetProperty(timeKey).GetDateTimeOffset().UtcDateTime;
var data = (T)document.RootElement.GetProperty(dataKey).Deserialize(memberType!, options);
- if (data is IMetadata) // for unknown reasons, Microsoft does not use the custom converter for IMetadataDictionary here; have to fiddle extra around as seen below .. for the record; this just works with Newtonsoft!
- {
- var md = document.RootElement.GetProperty(dataKey).GetProperty(metadataKey).Deserialize(options);
- var property = memberType.GetAllProperties().SingleOrDefault(pi => pi.Name == nameof(IMetadata.Metadata));
- if (property != null)
- {
- if (property.CanWrite)
- {
- property.SetValue(data, md);
- }
- else
- {
- memberType.GetAllFields().SingleOrDefault(fi => fi.Name.Contains(nameof(IMetadata.Metadata)))?.SetValue(data, md);
- }
- }
- }
+
+ ApplyMetadata(data, memberType, document.RootElement.GetProperty(dataKey), metadataKey, options);
var message = new Message(id, source, type, data, time);
if (typeToConvert.HasInterfaces(typeof(ICloudEvent<>)))
{
- var specVersionKey = options.PropertyNamingPolicy.ConvertName(nameof(ICloudEvent.Specversion));
-
- var requestType = typeToConvert.GetGenericArguments()[0];
- var cloudEventType = MessageConverter.CloudEventTypes.Value.Single(ti => ti.FullName!.StartsWith("Savvyio.EventDriven.Messaging.CloudEvents.CloudEvent"));
- var specVersion = document.RootElement.GetProperty(specVersionKey).GetString();
- var cloudEvent = Activator.CreateInstance(cloudEventType.MakeGenericType(requestType), [message, specVersion]) as IMessage;
-
- if (typeToConvert.HasInterfaces(typeof(ISignedCloudEvent<>)))
- {
- var signedCloudEventType = MessageConverter.CloudEventTypes.Value.Single(ti => ti.FullName!.StartsWith("Savvyio.EventDriven.Messaging.CloudEvents.Cryptography.SignedCloudEvent"));
- var signature = document.RootElement.GetProperty(signatureKey).GetString();
-
- return Activator.CreateInstance(signedCloudEventType.MakeGenericType(requestType), [cloudEvent, signature]) as IMessage;
- }
-
- var reservedKeys = new[] { idKey, sourceKey, timeKey, typeKey, dataKey, metadataKey, signatureKey, specVersionKey };
-
- if (cloudEvent is IDictionary dictionary)
- {
- foreach (var property in document.RootElement.EnumerateObject().Where(jp => !reservedKeys.Contains(jp.Name)))
- {
- dictionary.Add(property.Name, property.Value.Deserialize(property.Value.GetType(), options));
- }
- }
-
- return cloudEvent;
+ var reservedKeys = new[] { idKey, sourceKey, timeKey, typeKey, dataKey, metadataKey, signatureKey };
+ return CreateCloudEvent(typeToConvert, message, document.RootElement, signatureKey, reservedKeys, options);
}
if (typeToConvert.HasInterfaces(typeof(ISignedMessage<>)))
@@ -144,6 +106,55 @@ public override IMessage Read(ref Utf8JsonReader reader, Type typeToConvert,
}
}
+ // for unknown reasons, Microsoft does not use the custom converter for IMetadataDictionary here; have to fiddle extra around as seen below .. for the record; this just works with Newtonsoft!
+ private static void ApplyMetadata(T data, Type memberType, JsonElement dataElement, string metadataKey, JsonSerializerOptions options)
+ {
+ if (data is not IMetadata) { return; }
+
+ var md = dataElement.GetProperty(metadataKey).Deserialize(options);
+ var property = memberType.GetAllProperties().SingleOrDefault(pi => pi.Name == nameof(IMetadata.Metadata));
+ if (property == null) { return; }
+
+ if (property.CanWrite)
+ {
+ property.SetValue(data, md);
+ }
+ else
+ {
+ memberType.GetAllFields().SingleOrDefault(fi => fi.Name.Contains(nameof(IMetadata.Metadata)))?.SetValue(data, md);
+ }
+ }
+
+ private static IMessage CreateCloudEvent(Type typeToConvert, Message message, JsonElement root, string signatureKey, string[] reservedKeys, JsonSerializerOptions options)
+ {
+ var specVersionKey = options.PropertyNamingPolicy!.ConvertName(nameof(ICloudEvent.Specversion));
+ var requestType = typeToConvert.GetGenericArguments()[0];
+ var cloudEventType = MessageConverter.CloudEventTypes.Value.Single(ti => ti.FullName!.StartsWith("Savvyio.EventDriven.Messaging.CloudEvents.CloudEvent", StringComparison.Ordinal));
+ var specVersion = root.GetProperty(specVersionKey).GetString();
+ var cloudEvent = Activator.CreateInstance(cloudEventType.MakeGenericType(requestType), [message, specVersion]) as IMessage;
+
+ if (typeToConvert.HasInterfaces(typeof(ISignedCloudEvent<>)))
+ {
+ var signedCloudEventType = MessageConverter.CloudEventTypes.Value.Single(ti => ti.FullName!.StartsWith("Savvyio.EventDriven.Messaging.CloudEvents.Cryptography.SignedCloudEvent", StringComparison.Ordinal));
+ var signature = root.GetProperty(signatureKey).GetString();
+
+ return Activator.CreateInstance(signedCloudEventType.MakeGenericType(requestType), [cloudEvent, signature]) as IMessage;
+ }
+
+ AddExtensionAttributes(cloudEvent, root, [.. reservedKeys, specVersionKey], options);
+ return cloudEvent;
+ }
+
+ private static void AddExtensionAttributes(IMessage cloudEvent, JsonElement root, string[] reservedKeys, JsonSerializerOptions options)
+ {
+ if (cloudEvent is not IDictionary dictionary) { return; }
+
+ foreach (var property in root.EnumerateObject().Where(jp => !reservedKeys.Contains(jp.Name)))
+ {
+ dictionary.Add(property.Name, property.Value.Deserialize(property.Value.GetType(), options));
+ }
+ }
+
public override void Write(Utf8JsonWriter writer, IMessage value, JsonSerializerOptions options)
{
writer.WriteStartObject();
diff --git a/src/Savvyio.Extensions.Text.Json/Converters/RequestConverter.cs b/src/Savvyio.Extensions.Text.Json/Converters/RequestConverter.cs
index a8bf4ed7..0b5f67bb 100644
--- a/src/Savvyio.Extensions.Text.Json/Converters/RequestConverter.cs
+++ b/src/Savvyio.Extensions.Text.Json/Converters/RequestConverter.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -46,30 +47,32 @@ public override IRequest Read(ref Utf8JsonReader reader, Type typeToConvert, Jso
if (document.RootElement.TryGetProperty(jProperty.Name, out var element))
{
var value = element.Deserialize(property.PropertyType, options);
- if (property.CanWrite)
- {
- property.SetValue(instance, value);
- }
- else
- {
- var field = property.IsAutoProperty()
- ? typeToConvert.GetAllFields().SingleOrDefault(fi => fi.Name.StartsWith($"<{property.Name}>"))
- : typeToConvert.GetAllFields().SingleOrDefault(fi => fi.Name.Equals($"_{property.Name}>", StringComparison.OrdinalIgnoreCase));
- if (field != null)
- {
- field.SetValue(instance, value);
- }
- else
- {
- throw new NotSupportedException($"This deserializer only supports rehydration of {nameof(IRequest)} implementations that either use auto-properties or have a naming convention that makes it possible to tie non-writable properties with the backing field equivalent.");
- }
- }
+ SetPropertyOrField(instance, typeToConvert, property, value);
}
}
return instance as IRequest;
}
}
+ private static void SetPropertyOrField(object instance, Type typeToConvert, PropertyInfo property, object value)
+ {
+ if (property.CanWrite)
+ {
+ property.SetValue(instance, value);
+ return;
+ }
+
+ var field = property.IsAutoProperty()
+ ? typeToConvert.GetAllFields().SingleOrDefault(fi => fi.Name.StartsWith($"<{property.Name}>", StringComparison.Ordinal))
+ : typeToConvert.GetAllFields().SingleOrDefault(fi => fi.Name.Equals($"_{property.Name}>", StringComparison.OrdinalIgnoreCase));
+ if (field == null)
+ {
+ throw new NotSupportedException($"This deserializer only supports rehydration of {nameof(IRequest)} implementations that either use auto-properties or have a naming convention that makes it possible to tie non-writable properties with the backing field equivalent.");
+ }
+
+ field.SetValue(instance, value);
+ }
+
///
/// Writes a specified as JSON.
///
diff --git a/src/Savvyio.Extensions.Text.Json/Converters/SingleValueObjectConverter.cs b/src/Savvyio.Extensions.Text.Json/Converters/SingleValueObjectConverter.cs
index 558e8df8..b75e20ff 100644
--- a/src/Savvyio.Extensions.Text.Json/Converters/SingleValueObjectConverter.cs
+++ b/src/Savvyio.Extensions.Text.Json/Converters/SingleValueObjectConverter.cs
@@ -52,7 +52,7 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer
}
}
- internal class SingleValueObjectConverter : JsonConverter>
+ internal sealed class SingleValueObjectConverter : JsonConverter>
{
public SingleValueObjectConverter()
{
diff --git a/src/Savvyio.Messaging/Cryptography/SignedMessageExtensions.cs b/src/Savvyio.Messaging/Cryptography/SignedMessageExtensions.cs
index 7f7369ac..6d9768c4 100644
--- a/src/Savvyio.Messaging/Cryptography/SignedMessageExtensions.cs
+++ b/src/Savvyio.Messaging/Cryptography/SignedMessageExtensions.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Cuemon;
namespace Savvyio.Messaging.Cryptography
@@ -32,7 +32,7 @@ public static void CheckSignature(this ISignedMessage message, IMarshaller
Validator.ThrowIfNull(marshaller);
Validator.ThrowIfInvalidConfigurator(setup, out _);
var baseMessage = message.Clone().Sign(marshaller, setup);
- if (!message.Signature.Equals(baseMessage.Signature))
+ if (!message.Signature.Equals(baseMessage.Signature, StringComparison.Ordinal))
{
throw new ArgumentOutOfRangeException(nameof(message), message.Signature, "The signature of the message does not match the cryptographically calculated value. Either you are using an incorrect secret and/or algorithm or the message has been tampered with.");
}
diff --git a/src/Savvyio.Messaging/MessageAsyncEnumerable.cs b/src/Savvyio.Messaging/MessageAsyncEnumerable.cs
index a2f9cb27..d40c7d1a 100644
--- a/src/Savvyio.Messaging/MessageAsyncEnumerable.cs
+++ b/src/Savvyio.Messaging/MessageAsyncEnumerable.cs
@@ -48,7 +48,7 @@ public MessageAsyncEnumerable(IAsyncEnumerable> source, Action
/// Returns an enumerator that iterates asynchronously through the collection.
///
- /// A that may be used to cancel the asynchronous iteration.
+ /// A that may be used to cancel the asynchronous iteration.
/// An enumerator that can be used to iterate asynchronously through the collection.
public virtual IAsyncEnumerator> GetAsyncEnumerator(CancellationToken cancellationToken = default)
{
diff --git a/src/Savvyio.Messaging/MessageAsyncEnumerator.cs b/src/Savvyio.Messaging/MessageAsyncEnumerator.cs
index bf83dc93..8f7be43a 100644
--- a/src/Savvyio.Messaging/MessageAsyncEnumerator.cs
+++ b/src/Savvyio.Messaging/MessageAsyncEnumerator.cs
@@ -4,7 +4,7 @@
namespace Savvyio.Messaging
{
- internal class MessageAsyncEnumerator : IAsyncEnumerator> where T : IRequest
+ internal sealed class MessageAsyncEnumerator : IAsyncEnumerator> where T : IRequest
{
private readonly IAsyncEnumerator> _source;
private readonly MessageAsyncEnumerableOptions _options;
diff --git a/test/Savvyio.Assets.Dapper.Tests/Savvyio.Assets.Dapper.Tests.csproj b/test/Savvyio.Assets.Dapper.Tests/Savvyio.Assets.Dapper.Tests.csproj
index 3870b566..8ffc88b5 100644
--- a/test/Savvyio.Assets.Dapper.Tests/Savvyio.Assets.Dapper.Tests.csproj
+++ b/test/Savvyio.Assets.Dapper.Tests/Savvyio.Assets.Dapper.Tests.csproj
@@ -6,6 +6,7 @@
+
diff --git a/test/Savvyio.Assets.EfCore.Tests/Savvyio.Assets.EfCore.Tests.csproj b/test/Savvyio.Assets.EfCore.Tests/Savvyio.Assets.EfCore.Tests.csproj
index b7f86170..f693c479 100644
--- a/test/Savvyio.Assets.EfCore.Tests/Savvyio.Assets.EfCore.Tests.csproj
+++ b/test/Savvyio.Assets.EfCore.Tests/Savvyio.Assets.EfCore.Tests.csproj
@@ -11,6 +11,7 @@
+
diff --git a/test/Savvyio.Extensions.Newtonsoft.Json.Tests/RequestConverterTest.cs b/test/Savvyio.Extensions.Newtonsoft.Json.Tests/RequestConverterTest.cs
index efef9e31..0eeecf6c 100644
--- a/test/Savvyio.Extensions.Newtonsoft.Json.Tests/RequestConverterTest.cs
+++ b/test/Savvyio.Extensions.Newtonsoft.Json.Tests/RequestConverterTest.cs
@@ -47,6 +47,18 @@ public void RequestConverter_ShouldRehydrateAutoPropertyRequests()
Assert.Equal("jd@office.com", sut.EmailAddress);
}
+ [Fact]
+ public void RequestConverter_ShouldRehydrateWritableProperties()
+ {
+ var settings = new JsonSerializerSettings();
+ settings.Converters.Add(new RequestConverter());
+
+ var sut = JsonConvert.DeserializeObject("{\"name\":\"Jane Doe\"}", settings);
+
+ Assert.NotNull(sut);
+ Assert.Equal("Jane Doe", sut.Name);
+ }
+
[Fact]
public void RequestConverter_ShouldFailWhenNoSupportedBackingFieldExists()
{
@@ -58,6 +70,11 @@ public void RequestConverter_ShouldFailWhenNoSupportedBackingFieldExists()
Assert.StartsWith("This deserializer only supports rehydration", ex.Message);
}
+ private sealed class WritableRequest : IRequest
+ {
+ public string Name { get; set; }
+ }
+
private sealed class UnsupportedRequest : IRequest
{
private readonly string _name = string.Empty;
diff --git a/test/Savvyio.Extensions.SimpleQueueService.Tests/AmazonResourceNameOptionsTest.cs b/test/Savvyio.Extensions.SimpleQueueService.Tests/AmazonResourceNameOptionsTest.cs
index fbffc678..6fc48c1e 100644
--- a/test/Savvyio.Extensions.SimpleQueueService.Tests/AmazonResourceNameOptionsTest.cs
+++ b/test/Savvyio.Extensions.SimpleQueueService.Tests/AmazonResourceNameOptionsTest.cs
@@ -37,7 +37,7 @@ public void ValidateOptions_ThrowsInvalidOperationException_WhenAccountIdIsNullO
var sut2 = Assert.Throws(() => sut1.ValidateOptions());
var sut3 = Assert.Throws(() => Validator.ThrowIfInvalidOptions(sut1));
- Assert.Equal($"Operation is not valid due to the current state of the object. (Expression '{nameof(AmazonResourceNameOptions.AccountId)}.IsNullOrWhiteSpace() || {nameof(AmazonResourceNameOptions.AccountId)}.Length != 12 || !{nameof(AmazonResourceNameOptions.AccountId)}.IsNumeric(NumberStyles.Integer)')", sut2.Message);
+ Assert.Equal($"Operation is not valid due to the current state of the object. (Expression '{nameof(AmazonResourceNameOptions.AccountId)}.IsNullOrWhiteSpace() || {nameof(AmazonResourceNameOptions.AccountId)}.Length != 12 || !{nameof(AmazonResourceNameOptions.AccountId)}.IsNumeric(NumberStyles.Integer, CultureInfo.InvariantCulture)')", sut2.Message);
Assert.Equal($"{nameof(AmazonResourceNameOptions)} are not in a valid state. (Parameter '{nameof(sut1)}')", sut3.Message);
Assert.IsType(sut3.InnerException);
}
diff --git a/test/Savvyio.Extensions.Text.Json.Tests/Converters/MessageConverterTest.cs b/test/Savvyio.Extensions.Text.Json.Tests/Converters/MessageConverterTest.cs
new file mode 100644
index 00000000..ea6a5514
--- /dev/null
+++ b/test/Savvyio.Extensions.Text.Json.Tests/Converters/MessageConverterTest.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using Cuemon.Extensions;
+using Cuemon.Extensions.IO;
+using Codebelt.Extensions.Xunit;
+using Savvyio.Assets.EventDriven;
+using Savvyio.EventDriven.Messaging;
+using Savvyio.EventDriven.Messaging.CloudEvents;
+using Savvyio.Messaging;
+using Xunit;
+
+namespace Savvyio.Extensions.Text.Json.Converters
+{
+ public class MessageConverterTest : Test
+ {
+ public MessageConverterTest(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ [Fact]
+ public void MessageConverter_ShouldConvertOnlyMessages()
+ {
+ var sut = new MessageConverter();
+
+ Assert.True(sut.CanConvert(typeof(Message)));
+ Assert.False(sut.CanConvert(typeof(MemberCreated)));
+ Assert.False(sut.CanConvert(typeof(string)));
+ }
+
+ [Fact]
+ public void MessageConverter_ShouldRoundtripCloudEventExtensionAttributes()
+ {
+ var utc = DateTime.Parse("2023-11-16T23:24:17.8414532Z", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
+ var cloudEvent = new MemberCreated("Jane Doe", "jd@office.com").SetEventId("69bccf3b1117425397c5ed9ed757bb0f").SetTimestamp(utc)
+ .ToMessage("https://fancy.api/members".ToUri(), nameof(MemberCreated), o =>
+ {
+ o.MessageId = "2d4030d32a254ee8a27046e5bafe696a";
+ o.Time = utc;
+ }).ToCloudEvent();
+
+ ((IDictionary)cloudEvent).Add("traceparent", "00-abc-def-01");
+
+ var marshaller = new JsonMarshaller();
+ var json = marshaller.Serialize(cloudEvent);
+ var jsonString = json.ToEncodedString(o => o.LeaveOpen = true);
+
+ TestOutput.WriteLine(jsonString);
+
+ var result = marshaller.Deserialize>(json);
+
+ Assert.Contains("traceparent", jsonString);
+ Assert.True(((IDictionary)result).ContainsKey("traceparent"));
+ }
+
+ [Fact]
+ public void MessageConverter_ShouldRoundtripDataWithWritableMetadata()
+ {
+ var utc = DateTime.Parse("2023-11-16T23:24:17.8414532Z", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
+ var data = new WritableMetadataRequest { Name = "Jane Doe" };
+ data.Metadata.Add("custom", "value");
+ var message = new Message("2d4030d32a254ee8a27046e5bafe696a", "https://fancy.api/members".ToUri(), nameof(WritableMetadataRequest), data, utc);
+
+ var marshaller = new JsonMarshaller();
+ var json = marshaller.Serialize(message);
+ var jsonString = json.ToEncodedString(o => o.LeaveOpen = true);
+
+ TestOutput.WriteLine(jsonString);
+
+ var result = marshaller.Deserialize>(json);
+
+ Assert.NotNull(result);
+ Assert.Equal("Jane Doe", result.Data.Name);
+ Assert.NotNull(result.Data.Metadata);
+ Assert.True(result.Data.Metadata.ContainsKey("custom"));
+ Assert.Equal("value", result.Data.Metadata["custom"].ToString());
+ }
+
+ private sealed record WritableMetadataRequest : IRequest, IMetadata
+ {
+ public string Name { get; set; }
+
+ public IMetadataDictionary Metadata { get; set; } = new MetadataDictionary();
+ }
+ }
+}
diff --git a/test/Savvyio.Extensions.Text.Json.Tests/Converters/RequestConverterTest.cs b/test/Savvyio.Extensions.Text.Json.Tests/Converters/RequestConverterTest.cs
new file mode 100644
index 00000000..15e417a8
--- /dev/null
+++ b/test/Savvyio.Extensions.Text.Json.Tests/Converters/RequestConverterTest.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Codebelt.Extensions.Xunit;
+using Savvyio.Assets.Commands;
+using Xunit;
+
+namespace Savvyio.Extensions.Text.Json.Converters
+{
+ public class RequestConverterTest : Test
+ {
+ public RequestConverterTest(ITestOutputHelper output) : base(output)
+ {
+ }
+
+ [Fact]
+ public void RequestConverter_ShouldConvertOnlyRequests()
+ {
+ var sut = new RequestConverter();
+
+ Assert.True(sut.CanConvert(typeof(CreateMemberCommand)));
+ Assert.False(sut.CanConvert(typeof(string)));
+ }
+
+ [Fact]
+ public void RequestConverter_ShouldRehydrateAutoPropertyRequests()
+ {
+ var sut = JsonSerializer.Deserialize("{\"name\":\"Jane Doe\",\"age\":21,\"emailAddress\":\"jd@office.com\"}", CreateOptions());
+
+ Assert.NotNull(sut);
+ Assert.Equal("Jane Doe", sut.Name);
+ Assert.Equal((byte)21, sut.Age);
+ Assert.Equal("jd@office.com", sut.EmailAddress);
+ }
+
+ [Fact]
+ public void RequestConverter_ShouldRehydrateWritableProperties()
+ {
+ var sut = JsonSerializer.Deserialize("{\"name\":\"Jane Doe\"}", CreateOptions());
+
+ Assert.NotNull(sut);
+ Assert.Equal("Jane Doe", sut.Name);
+ }
+
+ [Fact]
+ public void RequestConverter_ShouldFailWhenNoSupportedBackingFieldExists()
+ {
+ var ex = Assert.Throws(() => JsonSerializer.Deserialize("{\"name\":\"Jane Doe\"}", CreateOptions()));
+
+ Assert.StartsWith("This deserializer only supports rehydration", ex.Message);
+ }
+
+ [Fact]
+ public void RequestConverter_ShouldRoundtripThroughWrite()
+ {
+ var options = CreateOptions();
+ var json = JsonSerializer.Serialize(new WritableRequest { Name = "Jane Doe" }, options);
+
+ TestOutput.WriteLine(json);
+
+ var sut = JsonSerializer.Deserialize(json, options);
+
+ Assert.NotNull(sut);
+ Assert.Equal("Jane Doe", sut.Name);
+ }
+
+ private static JsonSerializerOptions CreateOptions()
+ {
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+ options.Converters.Add(new RequestConverter());
+ return options;
+ }
+
+ private sealed class WritableRequest : IRequest
+ {
+ public string Name { get; set; }
+ }
+
+ private sealed class UnsupportedRequest : IRequest
+ {
+ private readonly string _name = string.Empty;
+
+ public string Name => _name;
+ }
+ }
+}