V2: Major architecture upgrade to .NET 10#18
Conversation
…r and project configuration
…on behavior for improved role checks
… instructions; add custom commands to global.json
…s struct and update ISecuredRequest interface ``` BenchmarkDotNet v0.14.0, Arch Linux AMD Ryzen 7 7800X3D, 1 CPU, 16 logical and 8 physical cores .NET SDK 9.0.102 [Host] : .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI Job-BZVZVG : .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI IterationCount=5 WarmupCount=3 Before: | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | |---------------- |-----------:|----------:|----------:|------:|--------:|-------:|----------:|------------:| | AdminRole | 307.0 ns | 16.80 ns | 4.36 ns | 1.00 | 0.02 | 0.0143 | 736 B | 1.00 | | MatchingRole | 327.1 ns | 8.12 ns | 2.11 ns | 1.07 | 0.02 | 0.0143 | 736 B | 1.00 | | NonMatchingRole | 4,976.5 ns | 710.57 ns | 109.96 ns | 16.21 | 0.38 | 0.0305 | 1592 B | 2.16 | | MultipleRoles | 318.0 ns | 26.94 ns | 7.00 ns | 1.04 | 0.02 | 0.0143 | 736 B | 1.00 | After: | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | |---------------- |------------:|----------:|---------:|-------:|--------:|-------:|----------:|------------:| | AdminRole | 15.92 ns | 0.440 ns | 0.114 ns | 1.00 | 0.01 | 0.0014 | 72 B | 1.00 | | MatchingRole | 21.82 ns | 1.253 ns | 0.194 ns | 1.37 | 0.01 | 0.0014 | 72 B | 1.00 | | NonMatchingRole | 2,609.38 ns | 40.857 ns | 6.323 ns | 163.95 | 1.14 | 0.0153 | 904 B | 12.56 | | MultipleRoles | 32.44 ns | 1.570 ns | 0.408 ns | 2.04 | 0.03 | 0.0014 | 72 B | 1.00 | ```
…ents, restructure request interfaces, and update expiration settings
…ling and improved cancellation handling ``` BenchmarkDotNet v0.14.0, Arch Linux AMD Ryzen 7 7800X3D, 1 CPU, 16 logical and 8 physical cores .NET SDK 9.0.102 [Host] : .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI Job-NVPNBL : .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI IterationCount=5 WarmupCount=3 Before: | Method | Mean | Error | StdDev | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | |--------------------------------- |----------:|----------:|----------:|------:|-------:|-------:|----------:|------------:| | 'Baseline - No Cache Operations' | 26.938 us | 0.4809 us | 0.1249 us | 1.00 | 2.5330 | - | 125 KB | 1.00 | | 'Remove Single Key' | 2.062 us | 0.4421 us | 0.1148 us | 0.08 | 0.0273 | 0.0234 | 1.41 KB | 0.01 | | 'Remove Group Keys' | 10.181 us | 0.3096 us | 0.0804 us | 0.38 | 0.1250 | 0.1094 | 7.16 KB | 0.06 | | 'Remove Both Single and Group' | 12.763 us | 1.1912 us | 0.3093 us | 0.47 | 0.1406 | 0.1250 | 7.28 KB | 0.06 | After: | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | |--------------------------------- |----------:|----------:|----------:|------:|--------:|-------:|-------:|----------:|------------:| | 'Baseline - No Cache Operations' | 24.979 us | 1.9945 us | 0.5180 us | 1.00 | 0.03 | 2.5330 | - | 128000 B | 1.000 | | 'Remove Single Key' | 1.053 us | 0.0874 us | 0.0227 us | 0.04 | 0.00 | 0.0137 | 0.0117 | 712 B | 0.006 | | 'Remove Group Keys' | 7.879 us | 0.9904 us | 0.2572 us | 0.32 | 0.01 | 0.0781 | 0.0625 | 4416 B | 0.035 | | 'Remove Both Single and Group' | 8.876 us | 0.3903 us | 0.1014 us | 0.36 | 0.01 | 0.0938 | 0.0781 | 4864 B | 0.038 | ```
…nd improved cancellation handling ``` AMD Ryzen 7 7800X3D, 1 CPU, 16 logical and 8 physical cores .NET SDK 9.0.102 [Host] : .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI Job-SPBUNF : .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI IterationCount=5 WarmupCount=3 Before: | Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | |----------------------------- |--------------:|--------------:|--------------:|-------:|-------:|----------:| | 'Cache Miss - New Entry' | 182,094.96 ns | 50,984.493 ns | 13,240.511 ns | 0.3333 | - | 23324 B | | 'Cache Hit - Existing Entry' | 1,901.39 ns | 121.888 ns | 31.654 ns | 0.0267 | 0.0248 | 1432 B | | 'Cache Bypass' | 24.57 ns | 0.908 ns | 0.236 ns | 0.0029 | - | 144 B | After: | Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | |----------------------------- |--------------:|--------------:|-------------:|-------:|-------:|----------:| | 'Cache Miss - New Entry' | 156,176.51 ns | 36,653.266 ns | 9,518.737 ns | 0.5000 | - | 25864 B | | 'Cache Hit - Existing Entry' | 1,799.25 ns | 217.875 ns | 56.581 ns | 0.0248 | 0.0229 | 1320 B | | 'Cache Bypass' | 24.85 ns | 1.294 ns | 0.200 ns | 0.0029 | - | 144 B | ```
…n handling with various edge cases
… and improve readability
…llel and sequential request handling
…gBehavior for better responsiveness and remove pools ``` Before: | Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | |--------------------------------------- |----------------:|-----------------:|---------------:|-------:|-------:|----------:| | 'Cache Miss - New Entry' | 155,628.72 ns | 44,816.636 ns | 11,638.738 ns | 0.5000 | - | 25936 B | | 'Cache Hit - Existing Entry' | 1,886.46 ns | 217.700 ns | 56.536 ns | 0.0248 | 0.0229 | 1320 B | | 'Cache Bypass' | 25.30 ns | 2.055 ns | 0.534 ns | 0.0029 | - | 144 B | | 'Parallel Cache Miss - 100 Requests' | 1,110,506.80 ns | 1,158,851.560 ns | 300,950.071 ns | 9.7656 | 3.9063 | 608378 B | | 'Sequential Cache Miss - 100 Requests' | 1,048,207.76 ns | 1,058,025.939 ns | 274,765.977 ns | 9.7656 | 3.9063 | 606225 B | | 'Parallel Cache Hit - 100 Requests' | 220,323.14 ns | 30,529.056 ns | 7,928.299 ns | 2.6855 | 2.4414 | 177435 B | | 'Sequential Cache Hit - 100 Requests' | 201,467.41 ns | 21,038.839 ns | 5,463.720 ns | 2.6855 | 2.4414 | 175171 B | After: | Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | | -------------------------------------- | ------------: | ---------------: | -------------: | -----: | -----: | --------: | | 'Cache Miss - New Entry' | 171,978.84 ns | 56,949.025 ns | 14,789.481 ns | 0.3333 | - | 22280 B | | 'Cache Hit - Existing Entry' | 1,692.19 ns | 170.363 ns | 44.243 ns | 0.0248 | 0.0229 | 1320 B | | 'Cache Bypass' | 23.61 ns | 0.438 ns | 0.114 ns | 0.0029 | - | 144 B | | 'Parallel Cache Miss - 100 Requests' | 887,043.03 ns | 1,083,843.609 ns | 281,470.744 ns | 1.9531 | - | 189141 B | | 'Sequential Cache Miss - 100 Requests' | 904,417.10 ns | 1,091,516.239 ns | 283,463.302 ns | 1.9531 | - | 186878 B | | 'Parallel Cache Hit - 100 Requests' | 198,198.37 ns | 24,243.779 ns | 6,296.032 ns | 2.6855 | 2.4414 | 177435 B | | 'Sequential Cache Hit - 100 Requests' | 189,024.62 ns | 23,531.312 ns | 6,111.007 ns | 2.6855 | 2.4414 | 175172 B | ```
…emovingBehavior during group key processing
…and LogExcludeParameter structs
…or enhanced logging and remove http context dependency ``` BenchmarkDotNet v0.14.0, Arch Linux AMD Ryzen 7 7800X3D, 1 CPU, 16 logical and 8 physical cores .NET SDK 9.0.102 [Host] : .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI Job-AXFFZO : .NET 9.0.1 (9.0.124.61010), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI IterationCount=5 WarmupCount=3 Before: | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | | ------------------------------- | -----------: | ---------: | --------: | -----: | ------: | ------: | ------: | ---------: | ----------: | | Handle_BasicRequest | 10.20 us | 0.953 us | 0.247 us | 1.00 | 0.03 | 0.1984 | 0.1831 | 10.98 KB | 1.00 | | Handle_WithLargePayload | 58.25 us | 5.124 us | 0.793 us | 5.71 | 0.14 | 4.1504 | 4.0283 | 206.76 KB | 18.84 | | Handle_WithAsyncResponse | 10,116.91 us | 144.551 us | 22.369 us | 992.52 | 22.01 | - | - | 13.67 KB | 1.25 | | Handle_MultipleSequential | 1,007.93 us | 65.982 us | 17.135 us | 98.88 | 2.66 | 19.5313 | 17.5781 | 1079.88 KB | 98.39 | | Handle_MultipleParallel | 1,017.80 us | 41.982 us | 10.903 us | 99.85 | 2.40 | 19.5313 | 17.5781 | 1083.83 KB | 98.75 | | Handle_MultipleBatched | 1,040.46 us | 33.805 us | 8.779 us | 102.07 | 2.38 | 21.4844 | 19.5313 | 1097.03 KB | 99.95 | | Handle_ConcurrentWithThrottling | 571.82 us | 26.524 us | 6.888 us | 56.10 | 1.38 | 24.4141 | 23.4375 | 1107.36 KB | 100.89 | | Handle_ProducerConsumerPattern | 646.96 us | 38.022 us | 9.874 us | 63.47 | 1.65 | 23.4375 | 21.4844 | 1082.65 KB | 98.64 | After: | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | | ------------------------------- | ------------: | ---------: | ---------: | -------: | ------: | ------: | ------: | --------: | ----------: | | Handle_BasicRequest | 8.089 us | 1.6028 us | 0.4162 us | 1.00 | 0.07 | 0.1831 | 0.1678 | 9.15 KB | 1.00 | | Handle_WithLargePayload | 14.865 us | 0.5664 us | 0.1471 us | 1.84 | 0.09 | 0.9766 | 0.4578 | 48.3 KB | 5.28 | | Handle_WithAsyncResponse | 10,107.998 us | 34.2875 us | 5.3060 us | 1,252.30 | 60.04 | - | - | 9.73 KB | 1.06 | | Handle_MultipleSequential | 797.664 us | 55.4247 us | 14.3936 us | 98.82 | 4.99 | 18.5547 | 17.5781 | 946.85 KB | 103.50 | | Handle_MultipleParallel | 798.220 us | 66.8508 us | 17.3609 us | 98.89 | 5.11 | 18.5547 | 17.5781 | 950.8 KB | 103.93 | | Handle_MultipleBatched | 821.460 us | 38.9047 us | 10.1034 us | 101.77 | 4.99 | 18.5547 | 17.5781 | 963.68 KB | 105.34 | | Handle_ConcurrentWithThrottling | 439.364 us | 46.7406 us | 12.1384 us | 54.43 | 2.94 | 20.5078 | 19.5313 | 978.97 KB | 107.01 | | Handle_ProducerConsumerPattern | 479.659 us | 4.5981 us | 0.7116 us | 59.43 | 2.85 | 19.5313 | 18.5547 | 950.31 KB | 103.88 | ```
…ptions structs for improved clarity and performance
…itoring and update PerformanceBehavior to utilize new interval structure
…s for transaction handling
…s for request validation
|
Warning Gemini encountered an error creating the review. You can try again by commenting |
- Replace Authors: Kodlama.io, Contributors → NArchitecture, Contributors - Remove Company and Copyright fields - Update RepositoryUrl to github.com/NArchitecture/narchitecture-core - Apply consistent branding across all 39 core packages
- Update .NET version from 9.0.x to 10.0.x in CI workflow - Upgrade actions/checkout from v3 to v4 in dotnet.yml - Upgrade actions/setup-dotnet from v3 to v4 in dotnet.yml - Add explicit .NET 10.0.x setup to nuget-publish workflow - Upgrade actions/checkout from v2 to v4 in nuget-publish.yml
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a significant and impressive architectural upgrade to .NET 10, modularizing the solution and improving many aspects like developer workflow, performance, and security abstractions. The new structure is much cleaner and more maintainable.
My review has identified a few issues, including a critical security vulnerability in the authorization logic that needs immediate attention. I've also found a potential security concern with the use of sudo in scripts and a few minor issues in configuration and documentation. Overall, this is a fantastic step forward for the project. Once the identified issues are addressed, this will be a solid foundation to build upon.
| public bool HasAnyRequiredRole() | ||
| { | ||
| if (!IsAuthenticated) | ||
| return false; | ||
| if (_hasAdminRole) | ||
| return true; | ||
| if (_requiredRoles == null || _requiredRoles.Length == 0) | ||
| return true; | ||
|
|
||
| bool hasAnyValidRequiredRole = false; | ||
| foreach (string required in _requiredRoles) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(required)) | ||
| return true; | ||
|
|
||
| hasAnyValidRequiredRole = true; | ||
| ReadOnlySpan<char> requiredSpan = required.AsSpan().Trim(); | ||
|
|
||
| foreach (string identity in _identityRoles!) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(identity)) | ||
| continue; | ||
| if (identity.AsSpan().Trim().Equals(requiredSpan, StringComparison.OrdinalIgnoreCase)) | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return !hasAnyValidRequiredRole; | ||
| } |
There was a problem hiding this comment.
There's a security vulnerability in the HasAnyRequiredRole method. An empty or whitespace string in the requiredRoles array will incorrectly grant access.
Specifically, this line is problematic:
if (string.IsNullOrWhiteSpace(required)) return true;
This should probably be continue; to ignore invalid role entries.
The logic with return !hasAnyValidRequiredRole; is also a bit confusing. A clearer and safer implementation is recommended.
public bool HasAnyRequiredRole()
{
if (!IsAuthenticated)
return false;
if (_hasAdminRole)
return true;
if (_requiredRoles is null || _requiredRoles.Length == 0)
return true;
// If there are no valid roles to check against, access is granted.
if (!_requiredRoles.Any(r => !string.IsNullOrWhiteSpace(r)))
return true;
foreach (string required in _requiredRoles)
{
if (string.IsNullOrWhiteSpace(required))
continue;
ReadOnlySpan<char> requiredSpan = required.AsSpan().Trim();
foreach (string identity in _identityRoles!)
{
if (string.IsNullOrWhiteSpace(identity))
continue;
if (identity.AsSpan().Trim().Equals(requiredSpan, StringComparison.OrdinalIgnoreCase))
return true;
}
}
return false;
}| "benchmark": "sudo dotnet run --project tests/benchmarks/Benchmarks/NArchitecture.Core.Benchmarks.csproj && dotnet restore", | ||
| "benchmark:last": "sudo dotnet run --project tests/benchmarks/Benchmarks/NArchitecture.Core.Benchmarks.csproj -- --last-run && dotnet restore", |
There was a problem hiding this comment.
The benchmark scripts use sudo. This can pose a security risk and may cause issues in CI/CD environments or on developer machines where the user doesn't have sudo privileges. Is sudo strictly necessary? If it's for accessing performance counters, consider if there are alternative ways to run the benchmarks without requiring elevated privileges, or document why it's needed.
| }, | ||
| "roslynator.dotnet.cli": { | ||
| "version": "0.10.0", | ||
| "version": "0.9.3", |
There was a problem hiding this comment.
| <a href="https://github.com/kodlamaio-projects/nArchitecture.Core/issues">Request Feature</a> | ||
| </p> | ||
| </p> | ||
| #  NArchitecture Core Packages |
There was a problem hiding this comment.
The NArchitecture logo image link appears to be broken. The URL points to a GitHub user attachment asset which might not be accessible to everyone or could be temporary. It's recommended to commit the image to the repository (e.g., in a docs/images folder) and use a relative path to ensure it always loads correctly.
| public class DeleteUserCommand : IRequest<bool>, ISecuredRequest | ||
| { | ||
| public string UserId { get; set; } | ||
| public RoleClaims RoleClaims => new(CurrentUser.Roles, ["Admin"]); |
There was a problem hiding this comment.
There seems to be a typo in the example for ISecuredRequest. The type RoleClaims is used, but based on the new AuthorizationBehavior, it should probably be AuthOptions.
| public RoleClaims RoleClaims => new(CurrentUser.Roles, ["Admin"]); | |
| public AuthOptions AuthOptions => new(CurrentUser.Roles, ["Admin"]); |
…ation logic - Fix HasAnyRequiredRole method that incorrectly granted access for empty/whitespace required roles - Empty or whitespace required roles now skip validation instead of granting access - Improve logic clarity and safety by checking for valid required roles first - Maintain existing behavior for admin roles and null/empty role arrays
- Remove sudo from benchmark and benchmark:last scripts for security and compatibility - Benchmarks can run without elevated privileges in most environments - Improves CI/CD compatibility and reduces security risks
- Upgrade roslynator.dotnet.cli from 0.9.3 back to 0.10.0 - Ensures latest features and bug fixes are available - Maintains consistency with modern tooling versions
- Replace temporary GitHub user attachment URL with local logo file - Ensures logo always loads correctly for all users - Improves documentation reliability and accessibility
- Fix RoleClaims type to AuthOptions in ISecuredRequest example - Ensures documentation matches actual implementation - Improves developer experience with accurate examples
- csharpier: 0.30.6 → 1.1.1 - roslynator.dotnet.cli: 0.10.0 → 0.10.2 - dotnet-outdated-tool: 4.6.7 → 4.6.8 - run-script and security-scan remain up to date
|
/gemini review |
There was a problem hiding this comment.
Code Review
This is an extensive and impressive architectural upgrade. The move to a more modular structure, the introduction of a custom high-performance Mediator, and the abstraction of core services like mapping are excellent improvements. The code demonstrates a strong focus on performance and modern C# practices. I've found a few issues, including a critical one in the new Mediator's service registration that would prevent some pipeline behaviors from working, and a couple of other suggestions for improvement.
| // Skip types that have constructor parameters which would prevent auto-registration | ||
| if ( | ||
| implementation | ||
| .GetConstructors() | ||
| .All(ctor => ctor.GetParameters().Length > 0 && !ctor.GetParameters().All(p => p.HasDefaultValue)) | ||
| ) | ||
| { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
The logic to skip behaviors with constructor parameters seems incorrect for dependency injection scenarios. The DI container is responsible for resolving constructor dependencies. This check will prevent any behavior that has dependencies (like ILogger or IHttpContextAccessor) from being registered, which is a common use case. This check should be removed to allow the DI container to correctly instantiate behaviors with their dependencies.
| public static IServiceCollection AddYamlResourceLocalization(this IServiceCollection services) | ||
| { | ||
| _ = services.AddScoped<ILocalizationService, ResourceLocalizationManager>(_ => | ||
| { | ||
| Dictionary<string, Dictionary<string, string>> resources = GetLocalizationResources(); | ||
| return new ResourceLocalizationManager(resources); | ||
| }); | ||
|
|
||
| return services; | ||
| } | ||
|
|
||
| private static Dictionary<string, Dictionary<string, string>> GetLocalizationResources() | ||
| { | ||
| var resources = new Dictionary<string, Dictionary<string, string>>(); | ||
| string? assemblyLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); | ||
| if (assemblyLocation is null) | ||
| return resources; |
There was a problem hiding this comment.
The AddYamlResourceLocalization extension method currently uses Assembly.GetExecutingAssembly() to find localization resources. This forces the resources to be in the output directory of the NArchitecture.Core.Localization.Resource.Yaml.DependencyInjection library, which is not ideal for consumers of this library who would expect to keep resources in their own application projects. A more flexible approach would be to allow the consumer to specify which assemblies to scan, similar to how AddMediator and AddNArchitectureAutoMapper are implemented. I recommend changing the method signature to accept assemblies to scan.
public static IServiceCollection AddYamlResourceLocalization(this IServiceCollection services, params Assembly[] assembliesToScan)
{
_ = services.AddScoped<ILocalizationService, ResourceLocalizationManager>(_ =>
{
Dictionary<string, Dictionary<string, string>> resources = GetLocalizationResources(assembliesToScan);
return new ResourceLocalizationManager(resources);
});
return services;
}
private static Dictionary<string, Dictionary<string, string>> GetLocalizationResources(Assembly[] assembliesToScan)
{| catch (Exception) | ||
| { | ||
| // Ensure transaction is disposed on exception | ||
| transactionScope.Dispose(); |
There was a problem hiding this comment.
The explicit call to transactionScope.Dispose() within the catch block is redundant. The using statement on line 21 already ensures that Dispose() will be called on the transactionScope when the try block is exited, whether normally or due to an exception. If an exception occurs, transactionScope.Complete() is not called, and the transaction is automatically rolled back upon disposal by the using block. Removing this redundant call makes the code cleaner and relies on the standard using pattern for resource management.
…or registration - Remove logic that prevented behaviors with constructor dependencies from being registered - DI container is responsible for resolving constructor dependencies - Allows behaviors with dependencies like ILogger or IHttpContextAccessor to be properly registered - Fixes critical issue that would prevent pipeline behaviors from working correctly
…xecuting assembly - Change AddYamlResourceLocalization to accept assemblies to scan parameter - Remove dependency on Assembly.GetExecutingAssembly() which limited resources to library location - Allow consumers to specify which assemblies contain localization resources - Consistent with other extension methods like AddMediator and AddNArchitectureAutoMapper - Improves flexibility for applications using the localization service
…ScopeBehavior - Remove explicit transactionScope.Dispose() call in catch block - Using statement already ensures proper disposal when leaving scope - Transaction is automatically rolled back when Complete() is not called - Cleaner code that relies on standard using pattern for resource management
|
/gemini review |
There was a problem hiding this comment.
Code Review
This is an impressive and substantial upgrade to the project's architecture. The move to a more modular structure with .NET 10, the introduction of a custom Mediator, and the focus on performance through modern C# features are all excellent changes. The new pipeline behaviors for caching, logging, and authorization are well-designed and robust. My review includes a few suggestions to improve cross-platform compatibility and enhance the flexibility of the new logging features. Overall, this is a very strong pull request that significantly modernizes the codebase.
| "benchmark:last": "dotnet run --project tests/benchmarks/Benchmarks/NArchitecture.Core.Benchmarks.csproj -- --last-run && dotnet restore", | ||
| "build": "dotnet build --configuration Release", | ||
| "test": "dotnet test --configuration Release" | ||
| "clean": "dotnet clean && find . -type d -name bin -exec rm -rf {} + && find . -type d -name obj -exec rm -rf {} + && dotnet restore && dotnet build --configuration Release", |
There was a problem hiding this comment.
The clean script uses Unix-specific commands (find, rm -rf), which will cause it to fail on Windows development environments. For better cross-platform compatibility, consider using a tool that works on all supported platforms. A cross-platform PowerShell equivalent would be:
"Get-ChildItem -Path . -Include bin,obj -Recurse -Directory | ForEach-Object { Remove-Item -Path $_.FullName -Recurse -Force }; dotnet restore; dotnet build --configuration Release"This would require developers to have PowerShell Core installed, which is a reasonable expectation for .NET development.
| int startChars = param.Name == "SensitiveData" ? 4 : param.KeepStartChars; | ||
| int endChars = param.Name == "SensitiveData" ? 5 : param.KeepEndChars; |
There was a problem hiding this comment.
The MaskEmail method contains hardcoded logic that checks for a specific property name, "SensitiveData". This creates a tight coupling between the logging behavior and specific property names in your requests, making it less reusable and harder to maintain. It's better to rely on the configuration provided in the LogExcludeParameter (KeepStartChars, KeepEndChars) for all properties. If a default masking strategy for emails is desired, it should be applied when KeepStartChars and KeepEndChars are not specified, rather than being tied to a property name.
int startChars = param.KeepStartChars;
int endChars = param.KeepEndChars;| const int keepStart = 2, | ||
| keepEnd = 2; |
There was a problem hiding this comment.
The MaskNumeric method uses hardcoded values for keepStart and keepEnd, ignoring any values that might be set in the LogExcludeParameter. This limits flexibility. The method should respect the KeepStartChars and KeepEndChars from the param argument, perhaps with a default fallback if they are not set to allow for configurable masking of numeric values.
int keepStart = param.KeepStartChars;
int keepEnd = param.KeepEndChars;
if (keepStart == 0 && keepEnd == 0)
{
keepStart = 2;
keepEnd = 2;
}| public abstract class SerilogLoggerServiceBase(PackageSerilog.ILogger logger) : ILogger | ||
| { | ||
| /// <summary> | ||
| /// The Serilog logger instance used for logging operations. | ||
| /// </summary> | ||
| protected readonly PackageSerilog.ILogger Logger = logger; | ||
|
|
||
| /// <summary> | ||
| /// Logs a critical message that represents a critical error that requires immediate attention. | ||
| /// </summary> | ||
| /// <param name="message">The message to be logged</param> | ||
| public Task CriticalAsync(string message) | ||
| { | ||
| Logger.Fatal(message); | ||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Logs a debug message that is used for development and troubleshooting. | ||
| /// </summary> | ||
| /// <param name="message">The message to be logged</param> | ||
| public Task DebugAsync(string message) | ||
| { | ||
| Logger.Debug(message); | ||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Logs an error message that represents an error that has occurred. | ||
| /// </summary> | ||
| /// <param name="message">The message to be logged</param> | ||
| public Task ErrorAsync(string message) | ||
| { | ||
| Logger.Error(message); | ||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Logs an informational message that provides general information. | ||
| /// </summary> | ||
| /// <param name="message">The message to be logged</param> | ||
| public Task InformationAsync(string message) | ||
| { | ||
| Logger.Information(message); | ||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Logs a trace message that is used for detailed tracing of the application's execution. | ||
| /// </summary> | ||
| /// <param name="message">The message to be logged</param> | ||
| public Task TraceAsync(string message) | ||
| { | ||
| Logger.Verbose(message); | ||
| return Task.CompletedTask; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Logs a warning message that represents a potential issue or important situation. | ||
| /// </summary> | ||
| /// <param name="message">The message to be logged</param> | ||
| public Task WarningAsync(string message) | ||
| { | ||
| Logger.Warning(message); | ||
| return Task.CompletedTask; | ||
| } | ||
| } |
There was a problem hiding this comment.
The methods in this base class are async but wrap synchronous Serilog calls (e.g., Logger.Fatal(message)), returning Task.CompletedTask. This doesn't provide true asynchrony and can block the calling thread if the underlying sink performs I/O. To achieve non-blocking logging, you should configure Serilog with Serilog.Sinks.Async. This would involve wrapping your sinks in the LoggerConfiguration, for example: .WriteTo.Async(a => a.File(...)). This ensures that log messages are passed to a background thread for processing, preventing your application threads from being blocked by logging operations.
… clean script - Replace find/rm commands with PowerShell Get-ChildItem/Remove-Item for Windows compatibility - Uses pwsh (PowerShell Core) which is available on all platforms - Maintains same functionality while ensuring cross-platform compatibility - Removes dependency on Unix-specific command line tools
…ility in logging behavior - Remove hardcoded SensitiveData property name check in MaskEmail method - Use KeepStartChars and KeepEndChars from LogExcludeParameter configuration - Add fallback defaults for MaskNumeric when parameters are not specified - Improves reusability and reduces tight coupling between behavior and property names - Makes masking configuration more flexible and configurable
- Add remarks section explaining how to achieve true asynchronous logging - Provide examples for configuring async sinks with .WriteTo.Async() - Clarify that current implementation wraps synchronous calls with Task.CompletedTask - Guides developers to use async sinks to prevent thread blocking in I/O operations
|
/gemini review |
There was a problem hiding this comment.
Code Review
This is an impressive and extensive architectural upgrade. The move to a modular structure, the introduction of abstractions for core services like mailing and mapping, and the implementation of a custom Mediator pattern are all excellent changes that will improve maintainability and flexibility. The code shows a strong focus on performance and modern C# features. My feedback focuses on a few areas to further enhance robustness and performance.
| private static Dictionary<string, Dictionary<string, string>> GetLocalizationResources(Assembly[] assembliesToScan) | ||
| { | ||
| var resources = new Dictionary<string, Dictionary<string, string>>(); | ||
|
|
||
| foreach (var assembly in assembliesToScan) | ||
| { | ||
| string? assemblyLocation = Path.GetDirectoryName(assembly.Location); | ||
| if (assemblyLocation is null) | ||
| continue; | ||
|
|
||
| string featuresPath = Path.Combine(assemblyLocation, FeaturesFolder); | ||
| if (!Directory.Exists(featuresPath)) | ||
| continue; | ||
|
|
||
| string[] featureDirectories = Directory.GetDirectories(featuresPath); | ||
|
|
||
| foreach (string featureDir in featureDirectories) | ||
| { | ||
| IEnumerable<(string culture, string filePath)> localeFiles = GetLocaleFiles(featureDir); | ||
| foreach ((string culture, string filePath) in localeFiles) | ||
| { | ||
| if (!resources.ContainsKey(culture)) | ||
| resources[culture] = []; | ||
|
|
||
| resources[culture][Path.GetFileName(featureDir)] = filePath; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return resources; | ||
| } |
There was a problem hiding this comment.
The current implementation for discovering localization resources relies on scanning the file system based on assembly.Location. This approach can be fragile and may fail in certain deployment scenarios, such as single-file executables, containerized environments where file paths might differ, or when assemblies are loaded from memory. A more robust solution would be to use embedded resources. By embedding the YAML files into the assemblies and loading them via assembly.GetManifestResourceStream(), you would decouple the localization system from the physical file structure on disk, making it more reliable across different environments.
| await _logger.InformationAsync( | ||
| $"Performance -> {requestName} took {_stopwatch.Elapsed.TotalSeconds}s, exceeding the threshold of {request.IntervalOptions.Interval}s" | ||
| ); |
There was a problem hiding this comment.
The await on the logging call inside the finally block can introduce latency, which is undesirable in a performance monitoring behavior. The request pipeline will be blocked until the logging operation completes. To avoid this, the logging call should be fire-and-forget.
_ = _logger.InformationAsync(
$"Performance -> {requestName} took {_stopwatch.Elapsed.TotalSeconds}s, exceeding the threshold of {request.IntervalOptions.Interval}s"
);| private async Task<TResponse> CreateRequestPipelineWithResponse<TResponse>( | ||
| IRequest<TResponse> request, | ||
| Type requestType, | ||
| Type responseType, | ||
| CancellationToken cancellationToken | ||
| ) | ||
| { | ||
| // Create a scope to resolve handlers with scoped dependencies | ||
| using var scope = _serviceScopeFactory.CreateScope(); | ||
| var serviceProvider = scope.ServiceProvider; | ||
|
|
||
| Type handlerType = typeof(IRequestHandler<,>).MakeGenericType(requestType, responseType); | ||
| object? handler = serviceProvider.GetService(handlerType); | ||
|
|
||
| if (handler == null) | ||
| { | ||
| throw new InvalidOperationException($"Handler not found for {requestType.Name}"); | ||
| } | ||
|
|
||
| // Get the handle method | ||
| var handleMethod = handlerType.GetMethod("Handle")!; | ||
|
|
||
| // Create the core request handler delegate | ||
| RequestHandlerDelegate<TResponse> coreHandlerDelegate = async () => | ||
| { | ||
| var result = await (Task<TResponse>)handleMethod.Invoke(handler, new object[] { request, cancellationToken })!; | ||
| return result; | ||
| }; | ||
|
|
||
| // Get the closed behavior type for this specific request/response | ||
| var closedBehaviorType = typeof(IPipelineBehavior<,>).MakeGenericType(requestType, responseType); | ||
|
|
||
| // Get behaviors directly registered for this specific request/response type | ||
| var behaviors = serviceProvider.GetServices(closedBehaviorType).ToArray(); | ||
|
|
||
| // If no behaviors, just execute the handler directly | ||
| if (behaviors.Length == 0) | ||
| return await coreHandlerDelegate(); | ||
|
|
||
| // Build the pipeline by wrapping behaviors around the core handler | ||
| var behaviorHandleMethod = closedBehaviorType.GetMethod("Handle")!; | ||
|
|
||
| // Start with the core handler | ||
| RequestHandlerDelegate<TResponse> pipeline = coreHandlerDelegate; | ||
|
|
||
| // Wrap each behavior around the pipeline in reverse order of registration | ||
| // This ensures first registered behavior executes first | ||
| for (int i = behaviors.Length - 1; i >= 0; i--) | ||
| { | ||
| var behavior = behaviors[i]; | ||
| var currentPipeline = pipeline; // Capture the current pipeline | ||
|
|
||
| // Create a new pipeline that wraps the current one with this behavior | ||
| pipeline = async () => | ||
| { | ||
| var result = await (Task<TResponse>) | ||
| behaviorHandleMethod.Invoke(behavior, new object[] { request, currentPipeline, cancellationToken })!; | ||
| return result; | ||
| }; | ||
| } | ||
|
|
||
| // Execute the pipeline and return the result | ||
| return await pipeline(); | ||
| } |
There was a problem hiding this comment.
The current mediator implementation uses reflection on every SendAsync call to find and invoke handlers and behaviors. This can introduce performance overhead, especially for frequently used requests. Caching the constructed handlers and pipeline delegates would significantly improve performance. A ConcurrentDictionary could be used to store delegates created for each request type, so that the reflection work is only done once per request type.
- Add OS detection to use pwsh on systems where available - Fallback to Unix find/rm commands on systems without PowerShell - Ensures clean script works on both Windows (with pwsh) and Unix systems - Adds error handling with 2>/dev/null and || true for robust execution
…ipeline blocking - Remove await from logger call in performance behavior - Use discard operator (_) to make logging fire-and-forget - Prevents performance monitoring from introducing additional latency - Avoids blocking the request pipeline for logging operations
- Add ConcurrentDictionary caching for handler and behavior types and methods - Cache handler types, behavior types, and their Handle methods - Reduce reflection overhead by caching lookups for request, stream, and behavior types - Improves performance for frequently used requests by avoiding repeated reflection calls - Maintain thread safety with ConcurrentDictionary for concurrent access
- Add embedded resource discovery with fallback to file system resources - Embedded resources provide deployment resilience for containers and single-file apps - Maintain backward compatibility with existing file system approach - Improve resource loading performance and deployment flexibility - Update documentation to show embedded resource patterns and usage
…logging behavior - Update authorization tests to reflect secure empty role handling - Fix email masking to use configurable parameters instead of hardcoded length - Update test expectations to match actual masking calculations - Ensure all Application tests pass with new secure behavior
Summary
Test plan