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 Values => _dictionary.Values; @@ -137,7 +137,7 @@ public void Clear() /// /// Copies the elements of the to an array of type , starting at the specified array index. /// - /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. + /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. /// The zero-based index in at which copying begins. public void CopyTo(KeyValuePair[] array, int arrayIndex) { @@ -207,7 +207,7 @@ public IEnumerator> GetEnumerator() /// /// Returns an enumerator that iterates through a collection. /// - /// An object that can be used to iterate through the collection. + /// An object that can be used to iterate through the collection. IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_dictionary).GetEnumerator(); diff --git a/src/Savvyio.Core/Reflection/AssemblyContext.cs b/src/Savvyio.Core/Reflection/AssemblyContext.cs index ee95d3b1..4c898375 100644 --- a/src/Savvyio.Core/Reflection/AssemblyContext.cs +++ b/src/Savvyio.Core/Reflection/AssemblyContext.cs @@ -1 +1,10 @@ -using Cuemon.Extensions.Collections.Generic; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Cuemon; namespace Savvyio.Reflection { /// /// Provides a set of static methods and properties to manage and filter assemblies in the current application domain. /// public static class AssemblyContext { private static readonly Lazy> AssemblyLoadFactory = new(() => AppDomain .CurrentDomain .GetAssemblies() .Where(AssemblyFilterCallback) .SelectMany(AssemblyDependenciesCallback) .Distinct() .Except(typeof(AssemblyContext).Assembly.Yield()) .ToList() .AsReadOnly()); private static Func _assemblyFilterCallback = DefaultAssemblyFilter; private static Func> _assemblyDependenciesCallback = DefaultAssemblyDependencies; private static Func _assemblyDependenciesFilterCallback = DefaultAssemblyDependenciesFilter; /// /// Gets or sets the function delegate that filters assemblies from the current application domain. /// /// The function delegate that filters assemblies from the current application domain. /// The default implementation filters away assemblies that suggest being part of the .NET runtime themselves. /// /// cannot be null. /// public static Func AssemblyFilterCallback { get => _assemblyFilterCallback; set => _assemblyFilterCallback = value ?? throw new ArgumentNullException(nameof(value)); } /// /// Gets or sets the function delegate that recursively discover dependencies for an assembly in the current application domain. /// /// The function delegate that recursively discover dependencies for an assembly in the current application domain. /// /// cannot be null. /// public static Func> AssemblyDependenciesCallback { get => _assemblyDependenciesCallback; set => _assemblyDependenciesCallback = value ?? throw new ArgumentNullException(nameof(value)); } /// /// Gets or sets the function delegate that filters assembly dependencies from the current application domain. /// /// The function delegate that filters assembly dependencies from the current application domain. /// The default implementation filters away assembly dependencies that suggest being part of the .NET runtime themselves. /// /// cannot be null. /// public static Func AssemblyDependenciesFilterCallback { get => _assemblyDependenciesFilterCallback; set => _assemblyDependenciesFilterCallback = value ?? throw new ArgumentNullException(nameof(value)); } /// /// Gets the qualified assemblies from the current application domain. /// /// The qualified assemblies from the current application domain. /// This result of this property is coupled with and . public static IReadOnlyList CurrentDomainAssemblies => AssemblyLoadFactory.Value; private static bool DefaultAssemblyFilter(Assembly assembly) { return assembly.FullName != null && !assembly.FullName.StartsWith(nameof(System)) && !assembly.FullName.StartsWith(nameof(Microsoft)); } private static bool DefaultAssemblyDependenciesFilter(AssemblyName assemblyName) { return !assemblyName.FullName.StartsWith(nameof(System)) && !assemblyName.FullName.StartsWith(nameof(Microsoft)); } private static IEnumerable DefaultAssemblyDependencies(Assembly assembly) { var stack = new Stack(); var guard = new HashSet(); yield return assembly; stack.Push(assembly); guard.Add(assembly.FullName); while (stack.TryPop(out var assemblyToTraverse)) { foreach (var assemblyName in assemblyToTraverse.GetReferencedAssemblies().Where(AssemblyDependenciesFilterCallback)) { if (!guard.Add(assemblyName.FullName)) { continue; } if (Patterns.TryInvoke(() => Assembly.Load(assemblyName), out var referencedAssembly) && referencedAssembly != null) { stack.Push(referencedAssembly); yield return referencedAssembly; } } } } } } \ No newline at end of file +using Cuemon.Extensions.Collections.Generic; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using Cuemon; namespace Savvyio.Reflection { /// /// Provides a set of static methods and properties to manage and filter assemblies in the current application domain. /// public static class AssemblyContext { private static readonly Lazy> AssemblyLoadFactory = new(() => AppDomain .CurrentDomain .GetAssemblies() .Where(AssemblyFilterCallback) .SelectMany(AssemblyDependenciesCallback) .Distinct() .Except(typeof(AssemblyContext).Assembly.Yield()) .ToList() .AsReadOnly()); private static Func _assemblyFilterCallback = DefaultAssemblyFilter; private static Func> _assemblyDependenciesCallback = DefaultAssemblyDependencies; private static Func _assemblyDependenciesFilterCallback = DefaultAssemblyDependenciesFilter; /// /// Gets or sets the function delegate that filters assemblies from the current application domain. /// /// The function delegate that filters assemblies from the current application domain. /// The default implementation filters away assemblies that suggest being part of the .NET runtime themselves. /// /// cannot be null. /// public static Func AssemblyFilterCallback { get => _assemblyFilterCallback; set => _assemblyFilterCallback = value ?? throw new ArgumentNullException(nameof(value)); } /// /// Gets or sets the function delegate that recursively discover dependencies for an assembly in the current application domain. /// /// The function delegate that recursively discover dependencies for an assembly in the current application domain. /// /// cannot be null. /// public static Func> AssemblyDependenciesCallback { get => _assemblyDependenciesCallback; set => _assemblyDependenciesCallback = value ?? throw new ArgumentNullException(nameof(value)); } /// /// Gets or sets the function delegate that filters assembly dependencies from the current application domain. /// /// The function delegate that filters assembly dependencies from the current application domain. /// The default implementation filters away assembly dependencies that suggest being part of the .NET runtime themselves. /// /// cannot be null. /// public static Func AssemblyDependenciesFilterCallback { get => _assemblyDependenciesFilterCallback; set => _assemblyDependenciesFilterCallback = value ?? throw new ArgumentNullException(nameof(value)); } /// /// Gets the qualified assemblies from the current application domain. /// /// The qualified assemblies from the current application domain. /// This result of this property is coupled with and . public static IReadOnlyList CurrentDomainAssemblies => AssemblyLoadFactory.Value; private static bool DefaultAssemblyFilter(Assembly assembly) { return assembly.FullName != null && + !assembly.FullName.StartsWith(nameof(System), StringComparison.Ordinal) && + !assembly.FullName.StartsWith(nameof(Microsoft), StringComparison.Ordinal); + } + + private static bool DefaultAssemblyDependenciesFilter(AssemblyName assemblyName) + { + return !assemblyName.FullName.StartsWith(nameof(System), StringComparison.Ordinal) && + !assemblyName.FullName.StartsWith(nameof(Microsoft), StringComparison.Ordinal); + } private static IEnumerable DefaultAssemblyDependencies(Assembly assembly) { var stack = new Stack(); var guard = new HashSet(); yield return assembly; stack.Push(assembly); guard.Add(assembly.FullName); while (stack.TryPop(out var assemblyToTraverse)) { foreach (var assemblyName in assemblyToTraverse.GetReferencedAssemblies().Where(AssemblyDependenciesFilterCallback)) { if (!guard.Add(assemblyName.FullName)) { continue; } if (Patterns.TryInvoke(() => Assembly.Load(assemblyName), out var referencedAssembly) && referencedAssembly != null) { stack.Push(referencedAssembly); yield return referencedAssembly; } } } } } } \ No newline at end of file diff --git a/src/Savvyio.Domain.EventSourcing/TracedAggregateRoot.cs b/src/Savvyio.Domain.EventSourcing/TracedAggregateRoot.cs index 5bd2abe8..4475f1e8 100644 --- a/src/Savvyio.Domain.EventSourcing/TracedAggregateRoot.cs +++ b/src/Savvyio.Domain.EventSourcing/TracedAggregateRoot.cs @@ -63,7 +63,7 @@ private void ReplayEvents(IEnumerable events) /// /// Adds an event to the Aggregate. /// - /// The event to be added to the end of . + /// The event to be added to the end of . protected sealed override void AddEvent(ITracedDomainEvent e) { ApplyChange(e); diff --git a/src/Savvyio.Domain/ValueObject.cs b/src/Savvyio.Domain/ValueObject.cs index fc17e5cd..a2ef8dcd 100644 --- a/src/Savvyio.Domain/ValueObject.cs +++ b/src/Savvyio.Domain/ValueObject.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using System.Threading; using Cuemon; using Cuemon.Extensions; using Cuemon.Reflection; @@ -9,11 +10,11 @@ namespace Savvyio.Domain /// /// Represents an object whose equality is based on the value rather than identity as specified in Domain Driven Design. /// - /// + /// public abstract record ValueObject { private const int NullHashCode = 472074819; - private readonly object _locker = new(); + private readonly Lock _locker = new(); private IEnumerable _equalityComponents; /// diff --git a/src/Savvyio.EventDriven.Messaging/CloudEvents/Cryptography/SignedCloudEventExtensions.cs b/src/Savvyio.EventDriven.Messaging/CloudEvents/Cryptography/SignedCloudEventExtensions.cs index 49a62213..cb18d358 100644 --- a/src/Savvyio.EventDriven.Messaging/CloudEvents/Cryptography/SignedCloudEventExtensions.cs +++ b/src/Savvyio.EventDriven.Messaging/CloudEvents/Cryptography/SignedCloudEventExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using Cuemon; using Savvyio.Messaging; using Savvyio.Messaging.Cryptography; @@ -35,7 +35,7 @@ public static void CheckCloudEventSignature(this ISignedCloudEvent cloudEv Validator.ThrowIfNull(marshaller); Validator.ThrowIfInvalidConfigurator(setup, out _); var baseCloudEvent = (cloudEvent.Clone(() => new CloudEvent(cloudEvent, cloudEvent.Specversion)) as ICloudEvent).SignCloudEvent(marshaller, setup); - if (!cloudEvent.Signature.Equals(baseCloudEvent.Signature)) + if (!cloudEvent.Signature.Equals(baseCloudEvent.Signature, StringComparison.Ordinal)) { throw new ArgumentOutOfRangeException(nameof(cloudEvent), cloudEvent.Signature, "The signature of the cloud event 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.Extensions.Dapper/DapperDataSource.cs b/src/Savvyio.Extensions.Dapper/DapperDataSource.cs index 365d8b9d..0ed92766 100644 --- a/src/Savvyio.Extensions.Dapper/DapperDataSource.cs +++ b/src/Savvyio.Extensions.Dapper/DapperDataSource.cs @@ -31,7 +31,7 @@ public DapperDataSource(DapperDataSourceOptions options) } /// - /// Called when this object is being disposed by either or having disposing set to true and is false. + /// Called when this object is being disposed by either or having disposing set to true and is false. /// protected override void OnDisposeManagedResources() { diff --git a/src/Savvyio.Extensions.Dapper/DapperDataStore.cs b/src/Savvyio.Extensions.Dapper/DapperDataStore.cs index a3453e97..353130d3 100644 --- a/src/Savvyio.Extensions.Dapper/DapperDataStore.cs +++ b/src/Savvyio.Extensions.Dapper/DapperDataStore.cs @@ -73,7 +73,7 @@ protected DapperDataStore(IDapperDataSource source) public abstract Task DeleteAsync(T dto, Action setup = null); /// - /// Called when this object is being disposed by either or having disposing set to true and is false. + /// Called when this object is being disposed by either or having disposing set to true and is false. /// protected override void OnDisposeManagedResources() { diff --git a/src/Savvyio.Extensions.EFCore.Domain.EventSourcing/TracedDomainEventExtensions.cs b/src/Savvyio.Extensions.EFCore.Domain.EventSourcing/TracedDomainEventExtensions.cs index 0795163a..521c58f2 100644 --- a/src/Savvyio.Extensions.EFCore.Domain.EventSourcing/TracedDomainEventExtensions.cs +++ b/src/Savvyio.Extensions.EFCore.Domain.EventSourcing/TracedDomainEventExtensions.cs @@ -13,7 +13,7 @@ public static class TracedDomainEventExtensions /// /// The domain event to convert. /// The that is used when converting into an array of bytes. - /// A that is equivalent to . + /// A byte[] that is equivalent to . public static byte[] ToByteArray(this ITracedDomainEvent domainEvent, IMarshaller marshaller) { var bytes = marshaller.Serialize(domainEvent, typeof(ITracedDomainEvent)).ToByteArray(); diff --git a/src/Savvyio.Extensions.EFCore/EfCoreDbContext.cs b/src/Savvyio.Extensions.EFCore/EfCoreDbContext.cs index bf5b146d..32201fab 100644 --- a/src/Savvyio.Extensions.EFCore/EfCoreDbContext.cs +++ b/src/Savvyio.Extensions.EFCore/EfCoreDbContext.cs @@ -26,10 +26,10 @@ public EfCoreDbContext(EfCoreDataSourceOptions options) /// The base implementation does nothing. /// /// - /// In situations where an instance of may or may not have been passed - /// to the constructor, you can use to determine if + /// In situations where an instance of may or may not have been passed + /// to the constructor, you can use to determine if /// the options have already been set, and skip some or all of the logic in - /// . + /// . /// /// /// 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; + } + } +}