diff --git a/Source/DotNET/Arc.Core.CodeAnalysis.Specs/Arc.Core.CodeAnalysis.Specs.csproj b/Source/DotNET/Arc.Core.CodeAnalysis.Specs/Arc.Core.CodeAnalysis.Specs.csproj index 295b7c3bf..d9c5f5fb0 100644 --- a/Source/DotNET/Arc.Core.CodeAnalysis.Specs/Arc.Core.CodeAnalysis.Specs.csproj +++ b/Source/DotNET/Arc.Core.CodeAnalysis.Specs/Arc.Core.CodeAnalysis.Specs.csproj @@ -5,6 +5,7 @@ Cratis.Arc.CodeAnalysis false true + false $(NoWarn);MA0101;RCS1266 @@ -21,4 +22,3 @@ - diff --git a/Source/DotNET/Arc.Core.CodeAnalysis/Arc.Core.CodeAnalysis.csproj b/Source/DotNET/Arc.Core.CodeAnalysis/Arc.Core.CodeAnalysis.csproj index 9d2dd8719..2c2aecb0f 100644 --- a/Source/DotNET/Arc.Core.CodeAnalysis/Arc.Core.CodeAnalysis.csproj +++ b/Source/DotNET/Arc.Core.CodeAnalysis/Arc.Core.CodeAnalysis.csproj @@ -11,6 +11,7 @@ netstandard2.0 latest + false $(NoWarn);NU5128;NU1507 diff --git a/Source/DotNET/Arc.Core.Generators.Integration/Specs/Specs.csproj b/Source/DotNET/Arc.Core.Generators.Integration/Specs/Specs.csproj index 4f2192d9e..b6680ae60 100644 --- a/Source/DotNET/Arc.Core.Generators.Integration/Specs/Specs.csproj +++ b/Source/DotNET/Arc.Core.Generators.Integration/Specs/Specs.csproj @@ -4,10 +4,11 @@ Cratis.Arc.Core.Generators.Integration.Specs false true + false $(NoWarn);MA0101;RCS1266;MA0136 - \ No newline at end of file + diff --git a/Source/DotNET/Arc.Core.Generators.Specs/Arc.Core.Generators.Specs.csproj b/Source/DotNET/Arc.Core.Generators.Specs/Arc.Core.Generators.Specs.csproj index 6f5a75a4c..99551ae87 100644 --- a/Source/DotNET/Arc.Core.Generators.Specs/Arc.Core.Generators.Specs.csproj +++ b/Source/DotNET/Arc.Core.Generators.Specs/Arc.Core.Generators.Specs.csproj @@ -5,6 +5,7 @@ Cratis.Arc.Generators false true + false $(NoWarn);MA0101;RCS1266;MA0136 diff --git a/Source/DotNET/Arc.Core.Generators/Arc.Core.Generators.csproj b/Source/DotNET/Arc.Core.Generators/Arc.Core.Generators.csproj index 4e3a8f51f..abd6bb9bd 100644 --- a/Source/DotNET/Arc.Core.Generators/Arc.Core.Generators.csproj +++ b/Source/DotNET/Arc.Core.Generators/Arc.Core.Generators.csproj @@ -11,6 +11,7 @@ netstandard2.0 latest + false $(NoWarn);NU5128;NU1507 @@ -21,4 +22,4 @@ - \ No newline at end of file + diff --git a/Source/DotNET/Arc.Core.Specs/Arc.Core.Specs.csproj b/Source/DotNET/Arc.Core.Specs/Arc.Core.Specs.csproj index c5ea1e267..bcc729074 100644 --- a/Source/DotNET/Arc.Core.Specs/Arc.Core.Specs.csproj +++ b/Source/DotNET/Arc.Core.Specs/Arc.Core.Specs.csproj @@ -5,9 +5,10 @@ Cratis.Arc false true + false - \ No newline at end of file + diff --git a/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_executing_command/with_complex_object_and_nested_validation_fails.cs b/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_executing_command/with_complex_object_and_nested_validation_fails.cs index 6f37e27e6..5cf7f2fb0 100644 --- a/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_executing_command/with_complex_object_and_nested_validation_fails.cs +++ b/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_executing_command/with_complex_object_and_nested_validation_fails.cs @@ -22,10 +22,10 @@ void Establish() _command = new ComplexCommand("ValidName", nestedObject); _context = new CommandContext(_correlationId, typeof(ComplexCommand), _command, [], new()); - _commandValidator = Substitute.For(); + _commandValidator = Substitute.For(); _commandValidationResult = new FluentValidation.Results.ValidationResult(); - _nestedValidator = Substitute.For(); + _nestedValidator = Substitute.For(); _nestedValidationResult = new FluentValidation.Results.ValidationResult([ new ValidationFailure("Value", "Nested value is invalid") ]); @@ -44,8 +44,8 @@ void Establish() return true; }); - _commandValidator.ValidateAsync(Arg.Any(), Arg.Any()).Returns(_commandValidationResult); - _nestedValidator.ValidateAsync(Arg.Any(), Arg.Any()).Returns(_nestedValidationResult); + ((IObjectValidator)_commandValidator).ValidateObjectAsync(Arg.Any(), Arg.Any()).Returns(_commandValidationResult); + ((IObjectValidator)_nestedValidator).ValidateObjectAsync(Arg.Any(), Arg.Any()).Returns(_nestedValidationResult); } async Task Because() => _result = await _filter.OnExecution(_context); @@ -59,8 +59,8 @@ void Establish() [Fact] void should_have_validation_result_with_error_severity() => _result.ValidationResults.First().Severity.ShouldEqual(ValidationResultSeverity.Error); [Fact] void should_have_validation_result_with_correct_message() => _result.ValidationResults.First().Message.ShouldEqual("Nested value is invalid"); [Fact] void should_have_validation_result_with_correct_member() => _result.ValidationResults.First().Members.ShouldContain("Value"); - [Fact] void should_call_command_validator() => _commandValidator.Received(1).ValidateAsync(Arg.Any(), Arg.Any()); - [Fact] void should_call_nested_validator() => _nestedValidator.Received(1).ValidateAsync(Arg.Any(), Arg.Any()); + [Fact] void should_call_command_validator() => ((IObjectValidator)_commandValidator).Received(1).ValidateObjectAsync(Arg.Any(), Arg.Any()); + [Fact] void should_call_nested_validator() => ((IObjectValidator)_nestedValidator).Received(1).ValidateObjectAsync(Arg.Any(), Arg.Any()); record ComplexCommand(string Name, NestedObject Nested); record NestedObject(string Value); diff --git a/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_executing_command/with_primitive_command.cs b/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_executing_command/with_primitive_command.cs index c00ae86d2..e815b695d 100644 --- a/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_executing_command/with_primitive_command.cs +++ b/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_executing_command/with_primitive_command.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Cratis.Arc.Validation; using FluentValidation; using FluentValidationResult = FluentValidation.Results.ValidationResult; @@ -17,7 +18,7 @@ void Establish() const int command = 42; _context = new CommandContext(_correlationId, typeof(int), command, [], new()); - _validator = Substitute.For(); + _validator = Substitute.For(); _validationResult = new FluentValidationResult(); _discoverableValidators.TryGet(typeof(int), out Arg.Any()) @@ -27,7 +28,7 @@ void Establish() return true; }); - _validator.ValidateAsync(Arg.Any(), Arg.Any()).Returns(_validationResult); + ((IObjectValidator)_validator).ValidateObjectAsync(Arg.Any(), Arg.Any()).Returns(_validationResult); } async Task Because() => _result = await _filter.OnExecution(_context); @@ -38,5 +39,5 @@ void Establish() [Fact] void should_be_valid() => _result.IsValid.ShouldBeTrue(); [Fact] void should_not_have_exceptions() => _result.HasExceptions.ShouldBeFalse(); [Fact] void should_not_have_validation_results() => _result.ValidationResults.ShouldBeEmpty(); - [Fact] void should_call_validator_once() => _validator.Received(1).ValidateAsync(Arg.Any(), Arg.Any()); + [Fact] void should_call_validator_once() => ((IObjectValidator)_validator).Received(1).ValidateObjectAsync(Arg.Any(), Arg.Any()); } \ No newline at end of file diff --git a/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_executing_command/with_validator_found_and_validation_fails.cs b/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_executing_command/with_validator_found_and_validation_fails.cs index 4d1cc3bad..adf569aba 100644 --- a/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_executing_command/with_validator_found_and_validation_fails.cs +++ b/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_executing_command/with_validator_found_and_validation_fails.cs @@ -20,7 +20,7 @@ void Establish() _command = new TestCommand("InvalidName"); _context = new CommandContext(_correlationId, typeof(TestCommand), _command, [], new()); - _validator = Substitute.For(); + _validator = Substitute.For(); _validationResult = new FluentValidationResult([ new ValidationFailure("Name", "Name is required") ]); @@ -32,7 +32,7 @@ void Establish() return true; }); - _validator.ValidateAsync(Arg.Any(), Arg.Any()).Returns(_validationResult); + ((IObjectValidator)_validator).ValidateObjectAsync(Arg.Any(), Arg.Any()).Returns(_validationResult); } async Task Because() => _result = await _filter.OnExecution(_context); @@ -46,7 +46,7 @@ void Establish() [Fact] void should_have_validation_result_with_error_severity() => _result.ValidationResults.First().Severity.ShouldEqual(ValidationResultSeverity.Error); [Fact] void should_have_validation_result_with_correct_message() => _result.ValidationResults.First().Message.ShouldEqual("Name is required"); [Fact] void should_have_validation_result_with_correct_member() => _result.ValidationResults.First().Members.ShouldContain("Name"); - [Fact] void should_call_validator() => _validator.Received(1).ValidateAsync(Arg.Any(), Arg.Any()); + [Fact] void should_call_validator() => ((IObjectValidator)_validator).Received(1).ValidateObjectAsync(Arg.Any(), Arg.Any()); record TestCommand(string Name); } \ No newline at end of file diff --git a/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_executing_command/with_validator_found_and_validation_succeeds.cs b/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_executing_command/with_validator_found_and_validation_succeeds.cs index 05c0975db..ff490b440 100644 --- a/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_executing_command/with_validator_found_and_validation_succeeds.cs +++ b/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_executing_command/with_validator_found_and_validation_succeeds.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Cratis.Arc.Validation; using FluentValidation; using FluentValidationResult = FluentValidation.Results.ValidationResult; @@ -18,7 +19,7 @@ void Establish() _command = new SimpleCommand(); _context = new CommandContext(_correlationId, typeof(SimpleCommand), _command, [], new()); - _validator = Substitute.For(); + _validator = Substitute.For(); _validationResult = new FluentValidationResult(); _discoverableValidators.TryGet(typeof(SimpleCommand), out Arg.Any()) @@ -28,7 +29,7 @@ void Establish() return true; }); - _validator.ValidateAsync(Arg.Any(), Arg.Any()).Returns(_validationResult); + ((IObjectValidator)_validator).ValidateObjectAsync(Arg.Any(), Arg.Any()).Returns(_validationResult); } async Task Because() => _result = await _filter.OnExecution(_context); @@ -39,7 +40,7 @@ void Establish() [Fact] void should_be_valid() => _result.IsValid.ShouldBeTrue(); [Fact] void should_not_have_exceptions() => _result.HasExceptions.ShouldBeFalse(); [Fact] void should_not_have_validation_results() => _result.ValidationResults.ShouldBeEmpty(); - [Fact] void should_call_validator() => _validator.Received(1).ValidateAsync(Arg.Any(), Arg.Any()); + [Fact] void should_call_validator() => ((IObjectValidator)_validator).Received(1).ValidateObjectAsync(Arg.Any(), Arg.Any()); public class SimpleCommand; } \ No newline at end of file diff --git a/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_validating/with_multiple_validation_errors_at_different_levels.cs b/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_validating/with_multiple_validation_errors_at_different_levels.cs index ee5cf5de2..969633165 100644 --- a/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_validating/with_multiple_validation_errors_at_different_levels.cs +++ b/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_validating/with_multiple_validation_errors_at_different_levels.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Cratis.Arc.Validation; using FluentValidation; using FluentValidation.Results; using ValidationResult = FluentValidation.Results.ValidationResult; @@ -22,12 +23,12 @@ void Establish() _command = new ComplexCommand("", nestedObject); _context = new CommandContext(_correlationId, typeof(ComplexCommand), _command, [], new()); - _commandValidator = Substitute.For(); + _commandValidator = Substitute.For(); _commandValidationResult = new ValidationResult([ new ValidationFailure("Name", "Name is required") ]); - _nestedValidator = Substitute.For(); + _nestedValidator = Substitute.For(); _nestedValidationResult = new ValidationResult([ new ValidationFailure("Value", "Nested value is invalid") ]); @@ -46,8 +47,8 @@ void Establish() return true; }); - _commandValidator.ValidateAsync(Arg.Any(), Arg.Any()).Returns(_commandValidationResult); - _nestedValidator.ValidateAsync(Arg.Any(), Arg.Any()).Returns(_nestedValidationResult); + ((IObjectValidator)_commandValidator).ValidateObjectAsync(Arg.Any(), Arg.Any()).Returns(_commandValidationResult); + ((IObjectValidator)_nestedValidator).ValidateObjectAsync(Arg.Any(), Arg.Any()).Returns(_nestedValidationResult); } async Task Because() => _result = await _filter.OnExecution(_context); @@ -60,8 +61,8 @@ void Establish() [Fact] void should_have_two_validation_results() => _result.ValidationResults.Count().ShouldEqual(2); [Fact] void should_have_command_validation_error() => _result.ValidationResults.Any(vr => vr.Message == "Name is required").ShouldBeTrue(); [Fact] void should_have_nested_validation_error() => _result.ValidationResults.Any(vr => vr.Message == "Nested value is invalid").ShouldBeTrue(); - [Fact] void should_call_command_validator() => _commandValidator.Received(1).ValidateAsync(Arg.Any(), Arg.Any()); - [Fact] void should_call_nested_validator() => _nestedValidator.Received(1).ValidateAsync(Arg.Any(), Arg.Any()); + [Fact] void should_call_command_validator() => ((IObjectValidator)_commandValidator).Received(1).ValidateObjectAsync(Arg.Any(), Arg.Any()); + [Fact] void should_call_nested_validator() => ((IObjectValidator)_nestedValidator).Received(1).ValidateObjectAsync(Arg.Any(), Arg.Any()); record ComplexCommand(string Name, NestedObject Nested); record NestedObject(string Value); diff --git a/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_validating/with_object_containing_null_property.cs b/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_validating/with_object_containing_null_property.cs index 1cc798b15..053ee18df 100644 --- a/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_validating/with_object_containing_null_property.cs +++ b/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_validating/with_object_containing_null_property.cs @@ -19,7 +19,7 @@ void Establish() _command = new CommandWithNullProperty("ValidName", null); _context = new CommandContext(_correlationId, typeof(CommandWithNullProperty), _command, [], new()); - _validator = Substitute.For(); + _validator = Substitute.For(); _validationResult = new FluentValidation.Results.ValidationResult([ new ValidationFailure("NullProperty", "Property cannot be null") ]); @@ -31,7 +31,7 @@ void Establish() return true; }); - _validator.ValidateAsync(Arg.Any(), Arg.Any()).Returns(_validationResult); + ((IObjectValidator)_validator).ValidateObjectAsync(Arg.Any(), Arg.Any()).Returns(_validationResult); } async Task Because() => _result = await _filter.OnExecution(_context); @@ -45,7 +45,7 @@ void Establish() [Fact] void should_have_validation_result_with_error_severity() => _result.ValidationResults.First().Severity.ShouldEqual(ValidationResultSeverity.Error); [Fact] void should_have_validation_result_with_correct_message() => _result.ValidationResults.First().Message.ShouldEqual("Property cannot be null"); [Fact] void should_have_validation_result_with_correct_member() => _result.ValidationResults.First().Members.ShouldContain("NullProperty"); - [Fact] void should_call_validator() => _validator.Received(1).ValidateAsync(Arg.Any(), Arg.Any()); + [Fact] void should_call_validator() => ((IObjectValidator)_validator).Received(1).ValidateObjectAsync(Arg.Any(), Arg.Any()); [Fact] void should_not_attempt_to_validate_null_property() => _discoverableValidators.DidNotReceive().TryGet(typeof(NestedObject), out Arg.Any()); record CommandWithNullProperty(string Name, NestedObject? NullProperty); diff --git a/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_validating/with_validation_context_setup.cs b/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_validating/with_validation_context_setup.cs index d632cad06..17a0b2584 100644 --- a/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_validating/with_validation_context_setup.cs +++ b/Source/DotNET/Arc.Core.Specs/Commands/Filters/for_FluentValidationFilter/when_validating/with_validation_context_setup.cs @@ -1,8 +1,8 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Cratis.Arc.Validation; using FluentValidation; -using FluentValidation.Results; namespace Cratis.Arc.Commands.Filters.for_FluentValidationFilter.when_validating; @@ -10,17 +10,17 @@ public class with_validation_context_setup : given.a_fluent_validation_filter { CommandResult _result; IValidator _validator; - ValidationResult _validationResult; + FluentValidation.Results.ValidationResult _validationResult; TestCommand _command; - IValidationContext _capturedContext; + object _capturedInstance; void Establish() { _command = new TestCommand("TestName"); _context = new CommandContext(_correlationId, typeof(TestCommand), _command, [], new()); - _validator = Substitute.For(); - _validationResult = new ValidationResult(); + _validator = Substitute.For(); + _validationResult = new FluentValidation.Results.ValidationResult(); _discoverableValidators.TryGet(typeof(TestCommand), out Arg.Any()) .Returns(x => @@ -29,10 +29,10 @@ void Establish() return true; }); - _validator.ValidateAsync(Arg.Any(), Arg.Any()) + ((IObjectValidator)_validator).ValidateObjectAsync(Arg.Any(), Arg.Any()) .Returns(call => { - _capturedContext = call.Arg(); + _capturedInstance = call.Arg(); return _validationResult; }); } @@ -40,8 +40,8 @@ void Establish() async Task Because() => _result = await _filter.OnExecution(_context); [Fact] void should_return_successful_result() => _result.IsSuccess.ShouldBeTrue(); - [Fact] void should_call_validator_with_validation_context() => _validator.Received(1).ValidateAsync(Arg.Any(), Arg.Any()); - [Fact] void should_create_validation_context_with_correct_instance() => _capturedContext.InstanceToValidate.ShouldEqual(_command); + [Fact] void should_call_validator_with_instance() => ((IObjectValidator)_validator).Received(1).ValidateObjectAsync(Arg.Any(), Arg.Any()); + [Fact] void should_call_validator_with_correct_instance() => _capturedInstance.ShouldEqual(_command); record TestCommand(string Name); } \ No newline at end of file diff --git a/Source/DotNET/Arc.Core/ArcApplicationBuilderExtensions.cs b/Source/DotNET/Arc.Core/ArcApplicationBuilderExtensions.cs index aaf7f66c5..5873c8164 100644 --- a/Source/DotNET/Arc.Core/ArcApplicationBuilderExtensions.cs +++ b/Source/DotNET/Arc.Core/ArcApplicationBuilderExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using Cratis.Arc.Queries; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -20,6 +21,9 @@ public static class ArcApplicationBuilderExtensions /// Optional callback for configuring the . /// The optional configuration section path. /// The for continuation. + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "IServiceCollection.Configure uses reflection-based configuration binding. Source-generated configuration binders are the long-term fix (tracked in GitHub issue #2204).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "IServiceCollection.Configure uses reflection-based configuration binding. Source-generated configuration binders are the long-term fix (tracked in GitHub issue #2204).")] + [UnconditionalSuppressMessage("AOT", "IL2111", Justification = "ArcOptions.IdentityDetailsProvider setter has DynamicallyAccessedMembers but is accessed via reflection in Configure. Source-generated configuration binders are the long-term fix (tracked in GitHub issue #2204).")] public static ArcApplicationBuilder AddCratisArc( this ArcApplicationBuilder builder, Action? configureOptions = null, diff --git a/Source/DotNET/Arc.Core/ArcOptions.cs b/Source/DotNET/Arc.Core/ArcOptions.cs index 96d5640d1..23566f3dd 100644 --- a/Source/DotNET/Arc.Core/ArcOptions.cs +++ b/Source/DotNET/Arc.Core/ArcOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Text.Json; using Cratis.Arc.Execution; using Cratis.Arc.Queries; @@ -39,6 +40,7 @@ public ArcOptions() /// /// Gets or sets what type of identity details provider to use. If none is specified it will use type discovery to try to find one. /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] public Type? IdentityDetailsProvider { get; set; } /// diff --git a/Source/DotNET/Arc.Core/Commands/CommandPipeline.cs b/Source/DotNET/Arc.Core/Commands/CommandPipeline.cs index 606434386..0922b7f34 100644 --- a/Source/DotNET/Arc.Core/Commands/CommandPipeline.cs +++ b/Source/DotNET/Arc.Core/Commands/CommandPipeline.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Cratis.Arc.Validation; using Cratis.DependencyInjection; @@ -381,6 +382,7 @@ CommandResult FilterValidationResults(CommandResult result, ValidationResultSeve return result; } + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "MakeGenericType for CommandResult where T is the command handler response type. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3c).")] CommandResult CreateCommandResultWithResponse(CorrelationId correlationId, object response) { var commandResultType = typeof(CommandResult<>).MakeGenericType(response.GetType()); diff --git a/Source/DotNET/Arc.Core/Commands/Filters/DataAnnotationValidationFilter.cs b/Source/DotNET/Arc.Core/Commands/Filters/DataAnnotationValidationFilter.cs index ec2375095..08d07eeff 100644 --- a/Source/DotNET/Arc.Core/Commands/Filters/DataAnnotationValidationFilter.cs +++ b/Source/DotNET/Arc.Core/Commands/Filters/DataAnnotationValidationFilter.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using Cratis.Arc.Validation; namespace Cratis.Arc.Commands.Filters; @@ -11,6 +12,7 @@ namespace Cratis.Arc.Commands.Filters; public class DataAnnotationValidationFilter : ICommandFilter { /// + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "ValidationContext and Validator.TryValidateObject use reflection on the command type. Source-generated validation attributes are the long-term fix (tracked in GitHub issue #2204).")] public Task OnExecution(CommandContext context) { var validationResults = new List(); diff --git a/Source/DotNET/Arc.Core/Commands/Filters/FluentValidationFilter.cs b/Source/DotNET/Arc.Core/Commands/Filters/FluentValidationFilter.cs index c6d7f18d1..2e90d1a30 100644 --- a/Source/DotNET/Arc.Core/Commands/Filters/FluentValidationFilter.cs +++ b/Source/DotNET/Arc.Core/Commands/Filters/FluentValidationFilter.cs @@ -1,9 +1,9 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Cratis.Arc.Validation; -using FluentValidation; namespace Cratis.Arc.Commands.Filters; @@ -21,16 +21,15 @@ public async Task OnExecution(CommandContext context) return commandResult; } + [UnconditionalSuppressMessage("AOT", "IL2075", Justification = "instance.GetType() properties are preserved by the type system for command types. Source-generated validation dispatch is the long-term fix (tracked in GitHub issue #2204).")] async Task Validate(CommandContext context, object instance) { var commandResult = CommandResult.Success(context.CorrelationId); var instanceType = instance.GetType(); - if (discoverableValidators.TryGet(instanceType, out var validator)) + if (discoverableValidators.TryGet(instanceType, out var validator) && validator is IObjectValidator objectValidator) { - var validationContextType = typeof(ValidationContext<>).MakeGenericType(instance.GetType()); - var validationContext = Activator.CreateInstance(validationContextType, instance) as IValidationContext; - var validationResult = await validator.ValidateAsync(validationContext, CancellationToken.None); + var validationResult = await objectValidator.ValidateObjectAsync(instance, CancellationToken.None); if (!validationResult.IsValid) { commandResult.MergeWith(new CommandResult diff --git a/Source/DotNET/Arc.Core/Commands/ModelBound/CommandAttributeExtensions.cs b/Source/DotNET/Arc.Core/Commands/ModelBound/CommandAttributeExtensions.cs index fc25054fb..8dfe5d090 100644 --- a/Source/DotNET/Arc.Core/Commands/ModelBound/CommandAttributeExtensions.cs +++ b/Source/DotNET/Arc.Core/Commands/ModelBound/CommandAttributeExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Cratis.Reflection; @@ -16,14 +17,14 @@ public static class CommandAttributeExtensions /// /// Type to check. /// True if the type is a command; otherwise, false. - public static bool IsCommand(this Type type) => type.HasAttribute() && type.HasHandleMethod(); + public static bool IsCommand([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] this Type type) => type.HasAttribute() && type.HasHandleMethod(); /// /// Check if a type has a Handle method that takes a single argument of type object. /// /// Type to check. /// True if the type has a Handle method; otherwise, false. - public static bool HasHandleMethod(this Type type) => + public static bool HasHandleMethod([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] this Type type) => type.GetMethod("Handle", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) is not null; /// @@ -31,6 +32,6 @@ public static bool HasHandleMethod(this Type type) => /// /// Type to get the Handle method from. /// The Handle method. - public static MethodInfo GetHandleMethod(this Type type) => + public static MethodInfo GetHandleMethod([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] this Type type) => type.GetMethod("Handle", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)!; } diff --git a/Source/DotNET/Arc.Core/Commands/ModelBound/CommandHandlerProvider.cs b/Source/DotNET/Arc.Core/Commands/ModelBound/CommandHandlerProvider.cs index 0d1f7e28e..e474a5667 100644 --- a/Source/DotNET/Arc.Core/Commands/ModelBound/CommandHandlerProvider.cs +++ b/Source/DotNET/Arc.Core/Commands/ModelBound/CommandHandlerProvider.cs @@ -19,6 +19,7 @@ public class CommandHandlerProvider : ICommandHandlerProvider /// Initializes a new instance of the class. /// /// The types available in the application. + [UnconditionalSuppressMessage("AOT", "IL2067", Justification = "types.All contains command types whose members are preserved by application assemblies. Source-generated command type registration is the long-term fix (tracked in GitHub issue #2204).")] public CommandHandlerProvider(ITypes types) { _commandTypes = types.All.Where(t => t.IsCommand()).ToDictionary(t => t, t => t.GetHandleMethod()); diff --git a/Source/DotNET/Arc.Core/ConverterExtensions.cs b/Source/DotNET/Arc.Core/ConverterExtensions.cs index 87cb34ae1..b5fec1ca4 100644 --- a/Source/DotNET/Arc.Core/ConverterExtensions.cs +++ b/Source/DotNET/Arc.Core/ConverterExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; namespace Cratis.Arc; @@ -19,6 +20,7 @@ public static class ConverterExtensions /// /// Supports converting primitives to their counterparts. /// + [UnconditionalSuppressMessage("AOT", "IL2067", Justification = "Activator.CreateInstance(targetType) on value-type fallback path; targetType.IsValueType ensures it has a default constructor. Source-generated type converters are the long-term fix (tracked in GitHub issue #2204).")] public static object? ConvertTo(this object value, Type targetType) { if (value is null) @@ -47,6 +49,9 @@ public static class ConverterExtensions return ConvertToUnderlyingType(value, targetType); } + [UnconditionalSuppressMessage("AOT", "IL2067", Justification = "Activator.CreateInstance(targetType) on value-type fallback path; targetType.IsValueType ensures it has a default constructor. Source-generated type converters are the long-term fix (tracked in GitHub issue #2204).")] + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "TypeDescriptor.GetConverter uses reflection. Source-generated TypeConverter registration is the long-term fix (tracked in GitHub issue #2204 item 7).")] + [UnconditionalSuppressMessage("AOT", "IL2067", Justification = "TypeDescriptor.GetConverter requires All members on the type. The targetType/underlyingType values come from user code at runtime; source-generated registration is the long-term fix (tracked in GitHub issue #2204 item 7).")] static object? ConvertToUnderlyingType(object value, Type targetType) { if (value is null) diff --git a/Source/DotNET/Arc.Core/GeneratedMetadataRegistration.cs b/Source/DotNET/Arc.Core/GeneratedMetadataRegistration.cs index bb975cfe3..b7c74990b 100644 --- a/Source/DotNET/Arc.Core/GeneratedMetadataRegistration.cs +++ b/Source/DotNET/Arc.Core/GeneratedMetadataRegistration.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Microsoft.Extensions.DependencyModel; @@ -21,6 +22,7 @@ public static void EnsureGeneratedMetadataRegistered() EnsureProjectAssembliesLoaded(); } + [UnconditionalSuppressMessage("SingleFile", "IL3002", Justification = "DependencyContext.Default is used only for project-assembly discovery at startup in non-single-file deployments; returns null in single-file mode so the branch is safely skipped (tracked in GitHub issue #2204 item 2).")] static void EnsureProjectAssembliesLoaded() { if (Interlocked.Exchange(ref _hasLoadedProjectAssemblies, 1) == 1) diff --git a/Source/DotNET/Arc.Core/HostBuilderExtensions.cs b/Source/DotNET/Arc.Core/HostBuilderExtensions.cs index 75f0e6376..9141ffd36 100644 --- a/Source/DotNET/Arc.Core/HostBuilderExtensions.cs +++ b/Source/DotNET/Arc.Core/HostBuilderExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Metrics; using Cratis.Arc.Tenancy; using Cratis.Conversion; @@ -33,6 +34,9 @@ public static class HostBuilderExtensions /// Callback for configuring the . /// The optional configuration section path. /// for building continuation. + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "IServiceCollection.Configure uses reflection-based configuration binding. Source-generated configuration binders are the long-term fix (tracked in GitHub issue #2204).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "IServiceCollection.Configure uses reflection-based configuration binding. Source-generated configuration binders are the long-term fix (tracked in GitHub issue #2204).")] + [UnconditionalSuppressMessage("AOT", "IL2111", Justification = "ArcOptions.IdentityDetailsProvider setter has DynamicallyAccessedMembers but is accessed via reflection in Configure. Source-generated configuration binders are the long-term fix (tracked in GitHub issue #2204).")] public static IHostBuilder AddCratisArcCore( this IHostBuilder builder, Action? configureOptions = default, diff --git a/Source/DotNET/Arc.Core/Http/HttpListenerRequestContext.cs b/Source/DotNET/Arc.Core/Http/HttpListenerRequestContext.cs index 5fdda8d89..09a12e663 100644 --- a/Source/DotNET/Arc.Core/Http/HttpListenerRequestContext.cs +++ b/Source/DotNET/Arc.Core/Http/HttpListenerRequestContext.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Security.Claims; using System.Text; @@ -79,6 +80,8 @@ public int StatusCode JsonSerializerOptions JsonSerializerOptions => _jsonOptions ??= RequestServices.GetRequiredService>().Value.JsonSerializerOptions; /// + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] public async Task ReadBodyAsJson(Type type, CancellationToken cancellationToken = default) { using var reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding); @@ -103,6 +106,8 @@ public void SetResponseHeader(string name, string value) } /// + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] public async Task WriteResponseAsJson(object? value, Type type, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/Source/DotNET/Arc.Core/Identity/IdentityProvider.cs b/Source/DotNET/Arc.Core/Identity/IdentityProvider.cs index 6c4ecd735..616545816 100644 --- a/Source/DotNET/Arc.Core/Identity/IdentityProvider.cs +++ b/Source/DotNET/Arc.Core/Identity/IdentityProvider.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Security.Claims; using System.Text.Encodings.Web; using System.Text.Json; @@ -75,6 +76,8 @@ public async Task> Get() } /// + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] public async Task SetCookieForHttpResponse(IdentityProviderResult result) { var context = httpRequestContextAccessor.Current; @@ -123,6 +126,8 @@ public async Task ModifyDetails(Func details) } } + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] bool TryGetFromCookie(IHttpRequestContext context, out IdentityProviderResult result) { result = IdentityProviderResult.Anonymous; @@ -145,6 +150,8 @@ bool TryGetFromCookie(IHttpRequestContext context, out IdentityProviderResult re return true; } + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] bool TryGetFromCookie(IHttpRequestContext context, out IdentityProviderResult result) { result = new IdentityProviderResult(IdentityId.Empty, IdentityName.Empty, false, false, [], default!); @@ -193,6 +200,8 @@ async Task CreateFromCurrentContext(IHttpRequestContext return IdentityProviderResult.Unauthorized; } + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] TDetails ConvertDetails(object details) { if (details is TDetails typedDetails) diff --git a/Source/DotNET/Arc.Core/Identity/IdentityProviderServiceCollectionExtensions.cs b/Source/DotNET/Arc.Core/Identity/IdentityProviderServiceCollectionExtensions.cs index f3f9504cc..7373872e1 100644 --- a/Source/DotNET/Arc.Core/Identity/IdentityProviderServiceCollectionExtensions.cs +++ b/Source/DotNET/Arc.Core/Identity/IdentityProviderServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using Cratis.Arc; using Cratis.Arc.Identity; using Cratis.Types; @@ -50,7 +51,7 @@ public static IServiceCollection AddIdentityProvider(this IServiceCollection ser /// The of the implementation to add. /// to add to. /// for continuation. - public static IServiceCollection AddIdentityProvider(this IServiceCollection services) + public static IServiceCollection AddIdentityProvider<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TProvider>(this IServiceCollection services) where TProvider : class, IProvideIdentityDetails => services.AddIdentityProvider(typeof(TProvider)); @@ -60,12 +61,14 @@ public static IServiceCollection AddIdentityProvider(this IServiceCol /// to add to. /// The of the implementation to add. /// for continuation. - public static IServiceCollection AddIdentityProvider(this IServiceCollection services, Type type) + public static IServiceCollection AddIdentityProvider(this IServiceCollection services, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type) { TypeIsNotAnIdentityDetailsProvider.ThrowIfNotAnIdentityDetailsProvider(type); return services.AddScoped(typeof(IProvideIdentityDetails), type); } + [UnconditionalSuppressMessage("AOT", "IL2063", Justification = "providerTypes[0] comes from ITypes.FindMultiple which preserves the types discovered; the [return: DynamicallyAccessedMembers] ensures callers preserve constructors. Source-generated discovery is the long-term fix (tracked in GitHub issue #2204).")] + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] static Type ResolveIdentityDetailsProviderType(ITypes types) { var defaultImplementationType = typeof(DefaultIdentityDetailsProvider); diff --git a/Source/DotNET/Arc.Core/Identity/MicrosoftIdentityPlatformAuthenticationHandler.cs b/Source/DotNET/Arc.Core/Identity/MicrosoftIdentityPlatformAuthenticationHandler.cs index 10099f291..7e4bebe24 100644 --- a/Source/DotNET/Arc.Core/Identity/MicrosoftIdentityPlatformAuthenticationHandler.cs +++ b/Source/DotNET/Arc.Core/Identity/MicrosoftIdentityPlatformAuthenticationHandler.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Security.Claims; using System.Text.Json; using Cratis.Arc.Authentication; @@ -22,6 +23,8 @@ public class MicrosoftIdentityPlatformAuthenticationHandler( readonly ILogger _logger = loggerFactory.CreateLogger(); /// + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] public Task HandleAuthentication(IHttpRequestContext context) { var headers = context.Headers; diff --git a/Source/DotNET/Arc.Core/Introspection/IntrospectionService.cs b/Source/DotNET/Arc.Core/Introspection/IntrospectionService.cs index c3fc6ffb6..4bfbf9e62 100644 --- a/Source/DotNET/Arc.Core/Introspection/IntrospectionService.cs +++ b/Source/DotNET/Arc.Core/Introspection/IntrospectionService.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Xml.Linq; using Cratis.Arc.Commands; using Cratis.Arc.Http; @@ -86,6 +87,7 @@ static List BuildQueries(IQueryPerformerProviders pr }).ToList(); } + [UnconditionalSuppressMessage("SingleFile", "IL3000", Justification = "Assembly.Location is used only for XML documentation discovery; returns empty string in single-file mode, handled gracefully by File.Exists check (tracked in GitHub issue #2204).")] static string GetDocumentationSummary(Type type) { var xmlFile = Path.ChangeExtension(type.Assembly.Location, ".xml"); diff --git a/Source/DotNET/Arc.Core/OpenApi/OpenApiExtensions.cs b/Source/DotNET/Arc.Core/OpenApi/OpenApiExtensions.cs index f1c2ec589..88d9a2561 100644 --- a/Source/DotNET/Arc.Core/OpenApi/OpenApiExtensions.cs +++ b/Source/DotNET/Arc.Core/OpenApi/OpenApiExtensions.cs @@ -42,7 +42,9 @@ public static ArcApplication MapOpenApi( async context => { var document = GenerateOpenApiDocument(httpListenerMapper, title, version); +#pragma warning disable IL2026, IL3050 // JsonSerializer without source-generated context. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5) var json = JsonSerializer.Serialize(document, _jsonOptions); +#pragma warning restore IL2026, IL3050 context.ContentType = "application/json"; await context.Write(json); diff --git a/Source/DotNET/Arc.Core/Queries/ChangeSetComputor.cs b/Source/DotNET/Arc.Core/Queries/ChangeSetComputor.cs index 67c4fe4ae..696bd2257 100644 --- a/Source/DotNET/Arc.Core/Queries/ChangeSetComputor.cs +++ b/Source/DotNET/Arc.Core/Queries/ChangeSetComputor.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json; @@ -33,6 +34,7 @@ public class ChangeSetComputor(JsonSerializerOptions serializerOptions) /// /// The item type to inspect. /// The identity , or if not found. + [UnconditionalSuppressMessage("AOT", "IL2070", Justification = "type.GetProperties() on a runtime item type; the item type's public properties are preserved since these are user-defined domain model types. Source-generated change-set computation is the long-term fix (tracked in GitHub issue #2204).")] public static PropertyInfo? FindIdentityProperty(Type type) => type.GetProperties() .FirstOrDefault(p => string.Equals(p.Name, "Id", StringComparison.OrdinalIgnoreCase)); @@ -83,6 +85,8 @@ public ChangeSet Compute(IEnumerable? previous, IEnumerable curr /// The current snapshot items. /// The property used to extract the item identity key. /// A describing what changed. + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "JsonSerializer.Serialize on object items uses the configured ArcOptions.JsonSerializerOptions. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "JsonSerializer.Serialize on object items uses the configured ArcOptions.JsonSerializerOptions. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] public ChangeSet ComputeWithIdentity(object[] previousItems, object[] currentItems, PropertyInfo idProperty) { var previousById = new Dictionary(previousItems.Length); @@ -147,6 +151,8 @@ public ChangeSet ComputeWithIdentity(object[] previousItems, object[] currentIte /// The previous snapshot items. /// The current snapshot items. /// A describing what changed. + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "JsonSerializer.Serialize on object items uses the configured ArcOptions.JsonSerializerOptions. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "JsonSerializer.Serialize on object items uses the configured ArcOptions.JsonSerializerOptions. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] public ChangeSet ComputeByJson(object[] previousItems, object[] currentItems) { var previousHashes = previousItems diff --git a/Source/DotNET/Arc.Core/Queries/ClientEnumerableObservableSSE.cs b/Source/DotNET/Arc.Core/Queries/ClientEnumerableObservableSSE.cs index c6cdf424e..994ff898f 100644 --- a/Source/DotNET/Arc.Core/Queries/ClientEnumerableObservableSSE.cs +++ b/Source/DotNET/Arc.Core/Queries/ClientEnumerableObservableSSE.cs @@ -51,7 +51,9 @@ public async Task HandleConnection(IHttpRequestContext context) var intercepted = await readModelInterceptors.Intercept(typeof(T), [item], serviceProvider); queryResult.Data = intercepted.First(); +#pragma warning disable IL2026, IL3050 // JsonSerializer with custom options requires source-generated JsonSerializerContext (tracked in GitHub issue #2204 item 5) var json = JsonSerializer.Serialize(queryResult, arcOptions.Value.JsonSerializerOptions); +#pragma warning restore IL2026, IL3050 var sseMessage = $"data: {json}\n\n"; try diff --git a/Source/DotNET/Arc.Core/Queries/ClientObservableSSE.cs b/Source/DotNET/Arc.Core/Queries/ClientObservableSSE.cs index 7e4a290b8..8e7a8bff4 100644 --- a/Source/DotNET/Arc.Core/Queries/ClientObservableSSE.cs +++ b/Source/DotNET/Arc.Core/Queries/ClientObservableSSE.cs @@ -83,7 +83,9 @@ async void Next(T data) queryResult.Paging = new(queryContext.Paging.Page, queryContext.Paging.Size, queryContext.TotalItems); queryResult.Data = intercepted.First(); +#pragma warning disable IL2026, IL3050 // JsonSerializer with custom options requires source-generated JsonSerializerContext (tracked in GitHub issue #2204 item 5) var json = JsonSerializer.Serialize(queryResult, arcOptions.Value.JsonSerializerOptions); +#pragma warning restore IL2026, IL3050 var sseMessage = $"data: {json}\n\n"; try diff --git a/Source/DotNET/Arc.Core/Queries/Filters/DataAnnotationValidationFilter.cs b/Source/DotNET/Arc.Core/Queries/Filters/DataAnnotationValidationFilter.cs index 29057700c..899d87c24 100644 --- a/Source/DotNET/Arc.Core/Queries/Filters/DataAnnotationValidationFilter.cs +++ b/Source/DotNET/Arc.Core/Queries/Filters/DataAnnotationValidationFilter.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Cratis.Arc.Validation; using CratisValidationResult = Cratis.Arc.Validation.ValidationResult; @@ -48,6 +49,8 @@ public Task OnPerform(QueryContext context) return Task.FromResult(queryResult); } + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "ValidationContext uses reflection on the value type. Source-generated validation is the long-term fix (tracked in GitHub issue #2204).")] + [UnconditionalSuppressMessage("AOT", "IL2075", Justification = "parameter.Type comes from the query performer and reflects user-registered parameter types. GetCustomAttributes is AOT-unsafe for runtime types; source-generated validation is the long-term fix (tracked in GitHub issue #2204).")] static void ValidateParameter(QueryParameter parameter, object? value, List validationResults) { if (value is null && !IsNullable(parameter.Type)) diff --git a/Source/DotNET/Arc.Core/Queries/Filters/FluentValidationFilter.cs b/Source/DotNET/Arc.Core/Queries/Filters/FluentValidationFilter.cs index 355c25d1e..f19f9d2a4 100644 --- a/Source/DotNET/Arc.Core/Queries/Filters/FluentValidationFilter.cs +++ b/Source/DotNET/Arc.Core/Queries/Filters/FluentValidationFilter.cs @@ -1,9 +1,9 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Cratis.Arc.Validation; -using FluentValidation; namespace Cratis.Arc.Queries.Filters; @@ -38,6 +38,7 @@ public async Task OnPerform(QueryContext context) return queryResult; } + [UnconditionalSuppressMessage("AOT", "IL2075", Justification = "parameter.Type comes from the query performer and reflects user-registered parameter types. GetProperties is AOT-unsafe for runtime types; source-generated validation is the long-term fix (tracked in GitHub issue #2204).")] async Task ValidateParameter(QueryParameter parameter, object? value) { var queryResult = QueryResult.Success(Cratis.Execution.CorrelationId.NotSet); @@ -48,11 +49,9 @@ async Task ValidateParameter(QueryParameter parameter, object? valu } var parameterType = parameter.Type; - if (discoverableValidators.TryGet(parameterType, out var validator)) + if (discoverableValidators.TryGet(parameterType, out var validator) && validator is IObjectValidator objectValidator) { - var validationContextType = typeof(ValidationContext<>).MakeGenericType(parameterType); - var validationContext = Activator.CreateInstance(validationContextType, value) as IValidationContext; - var validationResult = await validator.ValidateAsync(validationContext); + var validationResult = await objectValidator.ValidateObjectAsync(value); if (!validationResult.IsValid) { queryResult.MergeWith(new QueryResult diff --git a/Source/DotNET/Arc.Core/Queries/ModelBound/QueryPerformerProvider.cs b/Source/DotNET/Arc.Core/Queries/ModelBound/QueryPerformerProvider.cs index 79696302f..3c6390936 100644 --- a/Source/DotNET/Arc.Core/Queries/ModelBound/QueryPerformerProvider.cs +++ b/Source/DotNET/Arc.Core/Queries/ModelBound/QueryPerformerProvider.cs @@ -35,9 +35,7 @@ public QueryPerformerProvider(ITypes types, IQueryMetadataRegistry queryMetadata var readModelTypes = types.All.Where(t => t.IsReadModel()); _performers = readModelTypes - .SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static) - .Where(m => m.IsValidQueryFor(t)) - .Select(m => new ModelBoundQueryPerformer(t, t.FullName ?? t.Name, m, serviceProviderIsService, authorizationEvaluator))) + .SelectMany(GetQueryPerformers(serviceProviderIsService, authorizationEvaluator)) .ToDictionary(p => p.FullyQualifiedName, p => (IQueryPerformer)p); } @@ -48,6 +46,12 @@ public QueryPerformerProvider(ITypes types, IQueryMetadataRegistry queryMetadata public bool TryGetPerformerFor(FullyQualifiedQueryName query, [NotNullWhen(true)] out IQueryPerformer? performer) => _performers.TryGetValue(query, out performer); + [UnconditionalSuppressMessage("AOT", "IL2070", Justification = "Read model types are discovered at startup via ITypes and their methods are preserved by the type system. Source-generated discovery is the long-term fix (tracked in GitHub issue #2204).")] + static Func> GetQueryPerformers(IServiceProviderIsService serviceProviderIsService, IAuthorizationEvaluator authorizationEvaluator) => + t => t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static) + .Where(m => m.IsValidQueryFor(t)) + .Select(m => new ModelBoundQueryPerformer(t, t.FullName ?? t.Name, m, serviceProviderIsService, authorizationEvaluator)); + static IEnumerable CreatePerformersFromGeneratedMetadata( IDictionary generatedMetadata, IServiceProviderIsService serviceProviderIsService, diff --git a/Source/DotNET/Arc.Core/Queries/ModelBound/ReadModelExtensions.cs b/Source/DotNET/Arc.Core/Queries/ModelBound/ReadModelExtensions.cs index bba4abb31..cc30630d5 100644 --- a/Source/DotNET/Arc.Core/Queries/ModelBound/ReadModelExtensions.cs +++ b/Source/DotNET/Arc.Core/Queries/ModelBound/ReadModelExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Reactive.Subjects; using System.Reflection; using Cratis.Reflection; @@ -68,6 +69,7 @@ public static bool IsValidQueryFor(this MethodInfo method, Type readModelType) return false; } + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "MakeGenericType for IEnumerable at startup during query registration. Source-generated type checking is the long-term fix (tracked in GitHub issue #2204).")] static bool IsCollectionOfType(Type type, Type elementType) { if (type.IsArray && type.GetElementType() == elementType) diff --git a/Source/DotNET/Arc.Core/Queries/ObservableQueryDemultiplexer.cs b/Source/DotNET/Arc.Core/Queries/ObservableQueryDemultiplexer.cs index 6af3e4906..707178f86 100644 --- a/Source/DotNET/Arc.Core/Queries/ObservableQueryDemultiplexer.cs +++ b/Source/DotNET/Arc.Core/Queries/ObservableQueryDemultiplexer.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.WebSockets; using System.Reactive.Subjects; @@ -298,7 +299,9 @@ async Task ReadWebSocketMessages( try { var json = System.Text.Encoding.UTF8.GetString(buffer, 0, received.Count); +#pragma warning disable IL2026, IL3050 // JsonSerializer with custom options requires source-generated JsonSerializerContext (tracked in GitHub issue #2204 item 5) var message = JsonSerializer.Deserialize(json, arcOptions.Value.JsonSerializerOptions); +#pragma warning restore IL2026, IL3050 if (message is null) { @@ -503,6 +506,8 @@ void HandleWebSocketUnsubscribe( return null; } + [UnconditionalSuppressMessage("AOT", "IL2070", Justification = "subjectType implements ISubject; its interfaces are preserved by the type system. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3d).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Runtime MakeGenericMethod for SubscribeToSubjectOfType. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3d).")] IDisposable SubscribeToSubject( object subject, Type subjectType, @@ -610,6 +615,8 @@ IDisposable SubscribeToSubjectOfType( }); } + [UnconditionalSuppressMessage("AOT", "IL2070", Justification = "enumerableType implements IAsyncEnumerable; its interfaces are preserved by the type system. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3d).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Runtime MakeGenericMethod for StreamAsyncEnumerableOfType. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3d).")] async Task StreamAsyncEnumerable( object enumerable, Type enumerableType, @@ -678,6 +685,8 @@ async Task StreamAsyncEnumerableOfType( } } + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] async Task SendWebSocketMessage(IWebSocket webSocket, ObservableQueryHubMessage message, KeepAliveTracker keepAliveTracker, SemaphoreSlim writeLock, CancellationToken token) { var lockHeld = false; @@ -720,6 +729,8 @@ async Task SendWebSocketMessage(IWebSocket webSocket, ObservableQueryHubMessage } } + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] async Task SendSseMessage( IHttpRequestContext context, ObservableQueryHubMessage message, @@ -873,6 +884,8 @@ static QueryArguments BuildQueryArguments(IDictionary? argument return result; } + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] ObservableQuerySubscriptionRequest? DeserializeSubscriptionRequest(object? payload) { if (payload is null) diff --git a/Source/DotNET/Arc.Core/Queries/ObservableQueryHandler.cs b/Source/DotNET/Arc.Core/Queries/ObservableQueryHandler.cs index 4abfe5d9c..766f4ec10 100644 --- a/Source/DotNET/Arc.Core/Queries/ObservableQueryHandler.cs +++ b/Source/DotNET/Arc.Core/Queries/ObservableQueryHandler.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Reactive.Subjects; using Cratis.Arc.Http; @@ -111,6 +112,8 @@ async Task HandleAsyncEnumerableResult( } } + [UnconditionalSuppressMessage("AOT", "IL2075", Justification = "The observable query return types implement ISubject/IAsyncEnumerable; their interfaces are preserved by the type system. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3d).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Runtime MakeGenericType for ClientObservable/ClientEnumerableObservable wrappers. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3d).")] async Task HandleSubjectViaWebSocket(IHttpRequestContext context, object streamingData) { var type = streamingData.GetType(); @@ -131,6 +134,8 @@ async Task HandleSubjectViaWebSocket(IHttpRequestContext context, object streami await clientObservable!.HandleConnection(context); } + [UnconditionalSuppressMessage("AOT", "IL2075", Justification = "The observable query return types implement ISubject/IAsyncEnumerable; their interfaces are preserved by the type system. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3d).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Runtime MakeGenericType for ClientObservable/ClientEnumerableObservable wrappers. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3d).")] async Task HandleSubjectViaSSE(IHttpRequestContext context, object streamingData) { var type = streamingData.GetType(); @@ -161,6 +166,8 @@ async Task HandleSubjectViaHttp(IHttpRequestContext context, object streamingDat await context.WriteResponseAsJson(response.Result, typeof(QueryResult), context.RequestAborted); } + [UnconditionalSuppressMessage("AOT", "IL2075", Justification = "The observable query return types implement ISubject/IAsyncEnumerable; their interfaces are preserved by the type system. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3d).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Runtime MakeGenericType for ClientObservable/ClientEnumerableObservable wrappers. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3d).")] async Task HandleAsyncEnumerableViaWebSocket(IHttpRequestContext context, object streamingData) { var type = streamingData.GetType(); @@ -181,6 +188,8 @@ async Task HandleAsyncEnumerableViaWebSocket(IHttpRequestContext context, object await clientEnumerableObservable!.HandleConnection(context); } + [UnconditionalSuppressMessage("AOT", "IL2075", Justification = "The observable query return types implement ISubject/IAsyncEnumerable; their interfaces are preserved by the type system. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3d).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Runtime MakeGenericType for ClientObservable/ClientEnumerableObservable wrappers. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3d).")] async Task HandleAsyncEnumerableViaSSE(IHttpRequestContext context, object streamingData) { var type = streamingData.GetType(); diff --git a/Source/DotNET/Arc.Core/Queries/ObservableQueryHttp.cs b/Source/DotNET/Arc.Core/Queries/ObservableQueryHttp.cs index 3766e935a..fc131b7e1 100644 --- a/Source/DotNET/Arc.Core/Queries/ObservableQueryHttp.cs +++ b/Source/DotNET/Arc.Core/Queries/ObservableQueryHttp.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Net; @@ -58,6 +59,8 @@ public static ObservableQueryHttpOptions GetOptions(IReadOnlyDictionaryThe . /// The for the request. /// The to send. + [UnconditionalSuppressMessage("AOT", "IL2070", Justification = "streamingData.GetType() interfaces are preserved by the type system since query methods return strongly-typed observables. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3d).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Runtime MakeGenericMethod for CreateResponseForObservable. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3d).")] public static async Task CreateResponse( QueryContext queryContext, object streamingData, @@ -159,11 +162,13 @@ static QueryResult CreateSuccessResult(QueryContext queryContext, object? data) Paging = new(queryContext.Paging.Page, queryContext.Paging.Size, queryContext.TotalItems) }; + [UnconditionalSuppressMessage("AOT", "IL2070", Justification = "type.GetInterfaces() to find IObservable implementation; these interfaces are preserved by the framework. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3d).")] static Type? GetObservableInterfaceFor(Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IObservable<>) ? type : type.GetInterfaces().FirstOrDefault(_ => _.IsGenericType && _.GetGenericTypeDefinition() == typeof(IObservable<>)); + [UnconditionalSuppressMessage("AOT", "IL2075", Justification = "streamingData.GetType() properties are preserved by the type system since the Value property is a known pattern. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3d).")] static bool TryGetCurrentValue(object streamingData, out object? currentValue) { var valueProperty = streamingData.GetType().GetProperty("Value"); diff --git a/Source/DotNET/Arc.Core/Queries/QueryRenderers.cs b/Source/DotNET/Arc.Core/Queries/QueryRenderers.cs index ce3b6c53f..f095a166f 100644 --- a/Source/DotNET/Arc.Core/Queries/QueryRenderers.cs +++ b/Source/DotNET/Arc.Core/Queries/QueryRenderers.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using Cratis.Execution; using Cratis.Types; @@ -20,6 +21,8 @@ public class QueryRenderers( readonly IEnumerable _queryProviders = types.FindMultiple(typeof(IQueryRendererFor<>)); /// + [UnconditionalSuppressMessage("AOT", "IL2070", Justification = "Query renderer types are discovered via ITypes.FindMultiple which preserves interfaces. Source-generated type discovery is the long-term fix (tracked in GitHub issue #2204).")] + [UnconditionalSuppressMessage("AOT", "IL2075", Justification = "queryProviderType.GetMethod returns a method from a framework-defined interface preserved by the type system. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3d).")] public QueryRendererResult Render(FullyQualifiedQueryName queryName, object query, IServiceProvider serviceProvider) { var queryType = query.GetType(); diff --git a/Source/DotNET/Arc.Core/Queries/QueryableExtensions.cs b/Source/DotNET/Arc.Core/Queries/QueryableExtensions.cs index e299afc71..2fc397c5c 100644 --- a/Source/DotNET/Arc.Core/Queries/QueryableExtensions.cs +++ b/Source/DotNET/Arc.Core/Queries/QueryableExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Reflection; @@ -17,6 +18,7 @@ public static class QueryableExtensions static readonly MethodInfo _orderByMethod; static readonly MethodInfo _orderByDescendingMethod; + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "Queryable static methods are framework types preserved by the runtime. Source-generated LINQ dispatch is the long-term fix (tracked in GitHub issue #2204).")] static QueryableExtensions() { var queryableMethods = typeof(Queryable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static); @@ -39,6 +41,8 @@ static QueryableExtensions() /// /// The to adorn. /// The number of elements in the input sequence. + [UnconditionalSuppressMessage("AOT", "IL2060", Justification = "queryable.ElementType is preserved by the type system since queries return strongly-typed IQueryable. Source-generated LINQ dispatch is the long-term fix (tracked in GitHub issue #2204).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Runtime MakeGenericMethod for Queryable.Count. Source-generated LINQ dispatch is the long-term fix (tracked in GitHub issue #2204).")] public static int Count(this IQueryable queryable) { var genericMethod = _countMethod.MakeGenericMethod([queryable.ElementType]); @@ -52,6 +56,8 @@ public static int Count(this IQueryable queryable) /// An to adorn. /// The number of elements to skip before returning the remaining elements. /// An for continuation. + [UnconditionalSuppressMessage("AOT", "IL2060", Justification = "queryable.ElementType is preserved by the type system since queries return strongly-typed IQueryable. Source-generated LINQ dispatch is the long-term fix (tracked in GitHub issue #2204).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Runtime MakeGenericMethod for Queryable.Skip. Source-generated LINQ dispatch is the long-term fix (tracked in GitHub issue #2204).")] public static IQueryable Skip(this IQueryable queryable, int count) { var genericMethod = _skipMethod.MakeGenericMethod([queryable.ElementType]); @@ -64,6 +70,8 @@ public static IQueryable Skip(this IQueryable queryable, int count) /// The to adorn. /// The number of elements to return. /// An for continuation. + [UnconditionalSuppressMessage("AOT", "IL2060", Justification = "queryable.ElementType is preserved by the type system since queries return strongly-typed IQueryable. Source-generated LINQ dispatch is the long-term fix (tracked in GitHub issue #2204).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Runtime MakeGenericMethod for Queryable.Take. Source-generated LINQ dispatch is the long-term fix (tracked in GitHub issue #2204).")] public static IQueryable Take(this IQueryable queryable, int count) { var genericMethod = _takeMethod.MakeGenericMethod([queryable.ElementType]); @@ -77,6 +85,10 @@ public static IQueryable Take(this IQueryable queryable, int count) /// The name of the field to order on. /// Optional direction of sort. Defaults to ascending. /// An for continuation. + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "Expression.Property(string) is used with a runtime field name from the query sorting API. Source-generated LINQ dispatch is the long-term fix (tracked in GitHub issue #2204).")] + [UnconditionalSuppressMessage("AOT", "IL2060", Justification = "queryable.ElementType is preserved by the type system since queries return strongly-typed IQueryable. Source-generated LINQ dispatch is the long-term fix (tracked in GitHub issue #2204).")] + [UnconditionalSuppressMessage("AOT", "IL2075", Justification = "queryable.ElementType is preserved by the type system since queries return strongly-typed IQueryable. Source-generated LINQ dispatch is the long-term fix (tracked in GitHub issue #2204).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Runtime MakeGenericMethod for Queryable.OrderBy/OrderByDescending. Source-generated LINQ dispatch is the long-term fix (tracked in GitHub issue #2204).")] public static IQueryable OrderBy(this IQueryable queryable, string field, SortDirection direction = SortDirection.Ascending) { var orderMethod = direction == SortDirection.Ascending ? _orderByMethod : _orderByDescendingMethod; diff --git a/Source/DotNET/Arc.Core/Queries/ReadModelInterceptors.cs b/Source/DotNET/Arc.Core/Queries/ReadModelInterceptors.cs index a7cd10450..aca7f0bba 100644 --- a/Source/DotNET/Arc.Core/Queries/ReadModelInterceptors.cs +++ b/Source/DotNET/Arc.Core/Queries/ReadModelInterceptors.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Cratis.DependencyInjection; using Cratis.Types; @@ -33,6 +34,8 @@ public async Task> Intercept(Type readModelType, IEnumerable return await Task.WhenAll(items.Select(item => InterceptItem(item, entry, serviceProvider))); } + [UnconditionalSuppressMessage("AOT", "IL2070", Justification = "Interceptor types are discovered via ITypes.FindMultiple which preserves interfaces. Source-generated type discovery is the long-term fix (tracked in GitHub issue #2204).")] + [UnconditionalSuppressMessage("AOT", "IL2075", Justification = "The returned MethodInfo/PropertyInfo are for interface members which are preserved by the type system. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3e).")] static Dictionary BuildCache(ITypes types) { var interceptorTypes = types.FindMultiple(typeof(IInterceptReadModel<>)); @@ -66,6 +69,7 @@ static Dictionary BuildCache(ITypes types) kvp => new InterceptorEntry(kvp.Value.Types, kvp.Value.Method!, kvp.Value.ResultProperty!)); } + [UnconditionalSuppressMessage("AOT", "IL2072", Justification = "Interceptor types from ITypes.FindMultiple have their constructors preserved by the type system. Source-generated registration is the long-term fix (tracked in GitHub issue #2204).")] async Task InterceptItem(object item, InterceptorEntry entry, IServiceProvider serviceProvider) { var current = item; diff --git a/Source/DotNET/Arc.Core/Queries/WebSocketConnectionHandler.cs b/Source/DotNET/Arc.Core/Queries/WebSocketConnectionHandler.cs index dd7c13707..bac86ae17 100644 --- a/Source/DotNET/Arc.Core/Queries/WebSocketConnectionHandler.cs +++ b/Source/DotNET/Arc.Core/Queries/WebSocketConnectionHandler.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Net.WebSockets; using System.Text.Json; using Cratis.Arc.Http; @@ -81,6 +82,8 @@ public async Task HandleIncomingMessages(IWebSocket webSocket, SemaphoreSlim wri } /// + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] public async Task SendMessage( IWebSocket webSocket, QueryResult queryResult, @@ -130,6 +133,8 @@ public async Task HandleIncomingMessages(IWebSocket webSocket, SemaphoreSlim wri } } + [UnconditionalSuppressMessage("AOT", "IL2026", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "JsonSerializer with custom options. Source-generated JsonSerializerContext is the long-term fix (tracked in GitHub issue #2204 item 5).")] async Task HandlePotentialPingMessage(IWebSocket webSocket, byte[] buffer, int count, SemaphoreSlim writeLock, CancellationToken token) { try diff --git a/Source/DotNET/Arc.Core/Validation/BaseValidator.cs b/Source/DotNET/Arc.Core/Validation/BaseValidator.cs index 8367e993a..99ce9aa27 100644 --- a/Source/DotNET/Arc.Core/Validation/BaseValidator.cs +++ b/Source/DotNET/Arc.Core/Validation/BaseValidator.cs @@ -7,15 +7,19 @@ namespace Cratis.Arc.Validation; -#pragma warning disable CS0618 // Type or member is obsolete (Related to FluentValidation and the Transform method) #pragma warning disable IDE0004 // Remove unnecessary cast (We need to do this to access the correct RuleFor()) +#pragma warning disable CA1033 // IObjectValidator.ValidateObjectAsync is intentionally explicit to keep the public API of BaseValidator clean /// /// Represents a base validator that we use for discovery. /// /// Type of object the validator is for. -public class BaseValidator : AbstractValidator +public class BaseValidator : AbstractValidator, IObjectValidator { + /// + Task IObjectValidator.ValidateObjectAsync(object instance, CancellationToken cancellationToken) => + ValidateAsync((T)instance, cancellationToken); + /// /// Define a condition for when the context is a command. /// @@ -47,15 +51,14 @@ public class BaseValidator : AbstractValidator public IRuleBuilderInitial RuleFor(Expression>> expression) where TValue : IComparable { - if (expression.Body is ParameterExpression) + var valueExpression = CreateValueExpression(expression); + if (expression.Body is not ParameterExpression) { - var transformExpression = CreateTransformExpression(expression); - return ((AbstractValidator)this).RuleFor(transformExpression); + var propertyName = GetPropertyName(expression); + return ((AbstractValidator)this).RuleFor(valueExpression).OverridePropertyName(propertyName); } - var propertyName = GetPropertyName(expression); - var valueExpression = CreateValueExpression(expression); - return ((AbstractValidator)this).RuleFor(valueExpression).OverridePropertyName(propertyName); + return ((AbstractValidator)this).RuleFor(valueExpression); } static Expression> CreateValueExpression(Expression>> expression) @@ -67,27 +70,22 @@ static Expression> CreateValueExpression(Expressio // Check if concept is null, and if so return default (null for reference types) // This prevents NullReferenceException when accessing .Value on a null concept var nullCheck = Expression.NotEqual(body, Expression.Constant(null, body.Type)); - var valueProperty = Expression.Property(body, nameof(ConceptAs.Value)); + var valueProperty = Expression.Property(body, GetConceptValuePropertyInfo()); var defaultValue = Expression.Default(typeof(TProperty)); var conditional = Expression.Condition(nullCheck, valueProperty, defaultValue); return Expression.Lambda>(conditional, parameter); } - static Expression> CreateTransformExpression(Expression>> expression) + /// Obtain PropertyInfo from a compile-time-known expression tree to avoid the IL2026-flagged string-based Expression.Property(Expression, string) overload. + /// The property type. + /// The for the Value property. + static System.Reflection.PropertyInfo GetConceptValuePropertyInfo() where TProperty : IComparable { - var parameter = expression.Parameters[0]; - - // Create: arg => expression.Compile().Invoke(arg) != null ? expression.Compile().Invoke(arg).Value : default - var compiled = expression.Compile(); - var invokeExpression = Expression.Invoke(Expression.Constant(compiled), parameter); - var nullCheck = Expression.NotEqual(invokeExpression, Expression.Constant(null, invokeExpression.Type)); - var valueProperty = Expression.Property(invokeExpression, nameof(ConceptAs.Value)); - var defaultValue = Expression.Default(typeof(TProperty)); - var conditional = Expression.Condition(nullCheck, valueProperty, defaultValue); - - return Expression.Lambda>(conditional, parameter); + var expr = (Expression, TProperty>>)(x => x.Value); + var body = (System.Linq.Expressions.MemberExpression)expr.Body; + return (System.Reflection.PropertyInfo)body.Member; } static string GetPropertyName(Expression>> expression) @@ -102,5 +100,5 @@ static string GetPropertyName(Expression } } -#pragma warning restore CS0618 // Type or member is obsolete #pragma warning restore IDE0004 // Remove unnecessary cast (We need to do this to access the correct RuleFor()) +#pragma warning restore CA1033 // IObjectValidator.ValidateObjectAsync is intentionally explicit diff --git a/Source/DotNET/Arc.Core/Validation/DiscoverableValidators.cs b/Source/DotNET/Arc.Core/Validation/DiscoverableValidators.cs index c93c6473d..4578463a8 100644 --- a/Source/DotNET/Arc.Core/Validation/DiscoverableValidators.cs +++ b/Source/DotNET/Arc.Core/Validation/DiscoverableValidators.cs @@ -23,13 +23,7 @@ public class DiscoverableValidators : IDiscoverableValidators public DiscoverableValidators(ITypes types) { var candidates = types.FindMultiple(typeof(IDiscoverableValidator<>)); - var invalidValidators = candidates.Where(_ => - { - var interfaces = _.GetInterfaces(); - var validatorType = interfaces.Single(_ => _.IsGenericType && _.GetGenericTypeDefinition() == typeof(IDiscoverableValidator<>)); - var modelType = validatorType.GetGenericArguments()[0]; - return !_.IsAssignableTo(typeof(AbstractValidator<>).MakeGenericType(modelType)); - }).ToArray(); + var invalidValidators = candidates.Where(IsInvalidDiscoverableValidator).ToArray(); if (invalidValidators.Length > 0) { @@ -37,17 +31,7 @@ public DiscoverableValidators(ITypes types) } _validatorTypesByModelType = candidates - .ToDictionary( - _ => - { - var current = _.BaseType!; - while (!current.IsDerivedFromOpenGeneric(typeof(AbstractValidator<>))) - { - current = current.BaseType!; - } - return current.GetGenericArguments()[0]; - }, - _ => _); + .ToDictionary(GetModelTypeFromValidator, _ => _); } /// @@ -62,4 +46,26 @@ public bool TryGet(Type modelType, [MaybeNullWhen(false)] out IValidator validat validator = null; return false; } + + [UnconditionalSuppressMessage("AOT", "IL2070", Justification = "Candidate types are discovered via ITypes.FindMultiple which preserves interfaces. Source-generated type discovery is the long-term fix (tracked in GitHub issue #2204).")] + static bool IsInvalidDiscoverableValidator(Type candidateType) + { + var interfaces = candidateType.GetInterfaces(); + var validatorType = interfaces.Single(_ => _.IsGenericType && _.GetGenericTypeDefinition() == typeof(IDiscoverableValidator<>)); + var modelType = validatorType.GetGenericArguments()[0]; + return !interfaces.Any(i => + i.IsGenericType && + i.GetGenericTypeDefinition() == typeof(IValidator<>) && + i.GetGenericArguments()[0] == modelType); + } + + static Type GetModelTypeFromValidator(Type candidateType) + { + var current = candidateType.BaseType!; + while (!current.IsDerivedFromOpenGeneric(typeof(AbstractValidator<>))) + { + current = current.BaseType!; + } + return current.GetGenericArguments()[0]; + } } diff --git a/Source/DotNET/Arc.Core/Validation/IObjectValidator.cs b/Source/DotNET/Arc.Core/Validation/IObjectValidator.cs new file mode 100644 index 000000000..5fa2d1f78 --- /dev/null +++ b/Source/DotNET/Arc.Core/Validation/IObjectValidator.cs @@ -0,0 +1,22 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.Arc.Validation; + +/// +/// Defines a non-generic interface for validating an object instance without requiring generic type parameters at the call site. +/// +/// +/// This interface enables AOT-safe validation by pushing generic instantiation into the concrete validator, +/// avoiding MakeGenericType + Activator.CreateInstance on ValidationContext<T>. +/// +public interface IObjectValidator +{ + /// + /// Validates the given object instance asynchronously. + /// + /// The object to validate. + /// Optional . + /// A describing any validation failures. + Task ValidateObjectAsync(object instance, CancellationToken cancellationToken = default); +} diff --git a/Source/DotNET/Arc.Core/Validation/RuleBuilderInitialExtensions.cs b/Source/DotNET/Arc.Core/Validation/RuleBuilderInitialExtensions.cs index 874a7414b..3d567ea00 100644 --- a/Source/DotNET/Arc.Core/Validation/RuleBuilderInitialExtensions.cs +++ b/Source/DotNET/Arc.Core/Validation/RuleBuilderInitialExtensions.cs @@ -1,7 +1,6 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.Reflection; using FluentValidation; namespace Cratis.Arc.Validation; @@ -21,23 +20,6 @@ public static class RuleBuilderInitialExtensions /// The same rule builder for chaining. public static IRuleBuilderInitial OverridePropertyName( this IRuleBuilderInitial ruleBuilder, - string propertyName) - { - // Use reflection to access the internal Rule property - var ruleProperty = ruleBuilder.GetType().GetProperty("Rule", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - if (ruleProperty != null) - { - var rule = ruleProperty.GetValue(ruleBuilder); - if (rule != null) - { - var propertyNameProperty = rule.GetType().GetProperty("PropertyName", BindingFlags.Public | BindingFlags.Instance); - if (propertyNameProperty?.CanWrite == true) - { - propertyNameProperty.SetValue(rule, propertyName); - } - } - } - - return ruleBuilder; - } + string propertyName) => + (IRuleBuilderInitial)((IRuleBuilderOptions)ruleBuilder).OverridePropertyName(propertyName); } \ No newline at end of file diff --git a/Source/DotNET/Arc.Specs/Arc.Specs.csproj b/Source/DotNET/Arc.Specs/Arc.Specs.csproj index b0494cb9a..44e7eb5cb 100644 --- a/Source/DotNET/Arc.Specs/Arc.Specs.csproj +++ b/Source/DotNET/Arc.Specs/Arc.Specs.csproj @@ -5,6 +5,7 @@ Cratis.Arc false true + false diff --git a/Source/DotNET/Chronicle.CodeAnalysis.Specs/Chronicle.CodeAnalysis.Specs.csproj b/Source/DotNET/Chronicle.CodeAnalysis.Specs/Chronicle.CodeAnalysis.Specs.csproj index 47345dc77..a1392cce7 100644 --- a/Source/DotNET/Chronicle.CodeAnalysis.Specs/Chronicle.CodeAnalysis.Specs.csproj +++ b/Source/DotNET/Chronicle.CodeAnalysis.Specs/Chronicle.CodeAnalysis.Specs.csproj @@ -5,6 +5,7 @@ Cratis.Arc.Chronicle.CodeAnalysis false true + false $(NoWarn);MA0101;RCS1266 @@ -22,4 +23,3 @@ - diff --git a/Source/DotNET/Chronicle.CodeAnalysis/Chronicle.CodeAnalysis.csproj b/Source/DotNET/Chronicle.CodeAnalysis/Chronicle.CodeAnalysis.csproj index b8772e6d4..099e536c3 100644 --- a/Source/DotNET/Chronicle.CodeAnalysis/Chronicle.CodeAnalysis.csproj +++ b/Source/DotNET/Chronicle.CodeAnalysis/Chronicle.CodeAnalysis.csproj @@ -12,6 +12,7 @@ netstandard2.0 latest + false $(NoWarn);NU5128;NU1507 diff --git a/Source/DotNET/Chronicle.Specs/Chronicle.Specs.csproj b/Source/DotNET/Chronicle.Specs/Chronicle.Specs.csproj index 84c3390aa..9750d10bd 100644 --- a/Source/DotNET/Chronicle.Specs/Chronicle.Specs.csproj +++ b/Source/DotNET/Chronicle.Specs/Chronicle.Specs.csproj @@ -5,6 +5,7 @@ Cratis.Arc.Chronicle true false + false diff --git a/Source/DotNET/Chronicle.Testing/Chronicle.Testing.csproj b/Source/DotNET/Chronicle.Testing/Chronicle.Testing.csproj index 1472af23d..801de2ee7 100644 --- a/Source/DotNET/Chronicle.Testing/Chronicle.Testing.csproj +++ b/Source/DotNET/Chronicle.Testing/Chronicle.Testing.csproj @@ -4,6 +4,7 @@ Cratis.Arc.Chronicle.Testing Cratis.Arc.Chronicle.Testing net10.0 + false Testing utilities for Cratis Arc Chronicle applications, providing in-process event log infrastructure and Chronicle-specific assertion helpers for command scenarios. cratis;arc;chronicle;testing;commands;event-sourcing diff --git a/Source/DotNET/Chronicle/Aggregates/AggregateRootEventHandlers.cs b/Source/DotNET/Chronicle/Aggregates/AggregateRootEventHandlers.cs index 604b2dddb..fef4a1f8f 100644 --- a/Source/DotNET/Chronicle/Aggregates/AggregateRootEventHandlers.cs +++ b/Source/DotNET/Chronicle/Aggregates/AggregateRootEventHandlers.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Cratis.Chronicle.Events; using Cratis.Chronicle.Reactors; @@ -20,7 +21,7 @@ public class AggregateRootEventHandlers : IAggregateRootEventHandlers /// /// for mapping types. /// Type of . - public AggregateRootEventHandlers(IEventTypes eventTypes, Type aggregateRootType) + public AggregateRootEventHandlers(IEventTypes eventTypes, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] Type aggregateRootType) { _methodsByEventType = aggregateRootType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .Where(_ => _.IsEventHandlerMethod(eventTypes.AllClrTypes)) diff --git a/Source/DotNET/Chronicle/Aggregates/AggregateRootFactory.cs b/Source/DotNET/Chronicle/Aggregates/AggregateRootFactory.cs index 9e13e850f..ce5297948 100644 --- a/Source/DotNET/Chronicle/Aggregates/AggregateRootFactory.cs +++ b/Source/DotNET/Chronicle/Aggregates/AggregateRootFactory.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using Cratis.Chronicle; using Cratis.Chronicle.Events; using Cratis.Chronicle.EventSequences; @@ -27,7 +28,7 @@ public class AggregateRootFactory( IServiceProvider serviceProvider) : IAggregateRootFactory { /// - public async Task Get(EventSourceId id, EventStreamId? streamId = default, EventSourceType? eventSourceType = default) + public async Task Get<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TAggregateRoot>(EventSourceId id, EventStreamId? streamId = default, EventSourceType? eventSourceType = default) where TAggregateRoot : IAggregateRoot { // TODO: Create Issue: Must dispose of unit of work in some way or else it's a memory leak. diff --git a/Source/DotNET/Chronicle/Aggregates/AggregateRootServiceCollectionExtensions.cs b/Source/DotNET/Chronicle/Aggregates/AggregateRootServiceCollectionExtensions.cs index 046a92817..35f832722 100644 --- a/Source/DotNET/Chronicle/Aggregates/AggregateRootServiceCollectionExtensions.cs +++ b/Source/DotNET/Chronicle/Aggregates/AggregateRootServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using Cratis.Arc.Chronicle.Aggregates; using Cratis.Arc.Chronicle.Commands; using Cratis.Arc.Commands; @@ -27,26 +28,30 @@ public static IServiceCollection AddAggregateRoots(this IServiceCollection servi foreach (var aggregateRootType in types.All.Where(_ => _.HasInterface()).ToArray()) { services.RemoveAll(aggregateRootType); - services.AddScoped(aggregateRootType, serviceProvider => - { - var commandContext = serviceProvider.GetRequiredService(); - var aggregateRootFactory = serviceProvider.GetRequiredService(); - - var eventSourceId = commandContext.GetEventSourceId(); - if (eventSourceId == EventSourceId.Unspecified) - { - throw new UnableToResolveAggregateRootFromCommandContext(aggregateRootType); - } - - var getMethod = typeof(IAggregateRootFactory) - .GetMethods() - .First(m => m.Name == nameof(IAggregateRootFactory.Get) && m.IsGenericMethod); - - var genericGetMethod = getMethod.MakeGenericMethod(aggregateRootType); - return genericGetMethod.Invoke(aggregateRootFactory, [eventSourceId, null, null])!; - }); + services.AddScoped(aggregateRootType, serviceProvider => ResolveAggregateRoot(aggregateRootType, serviceProvider)); } return services; } + + [UnconditionalSuppressMessage("AOT", "IL2060", Justification = "The aggregate root types discovered at startup are preserved by the application's type system. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3e).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "The aggregate root types discovered at startup are preserved by the application's type system. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3e).")] + static object ResolveAggregateRoot(Type aggregateRootType, IServiceProvider serviceProvider) + { + var commandContext = serviceProvider.GetRequiredService(); + var aggregateRootFactory = serviceProvider.GetRequiredService(); + + var eventSourceId = commandContext.GetEventSourceId(); + if (eventSourceId == EventSourceId.Unspecified) + { + throw new UnableToResolveAggregateRootFromCommandContext(aggregateRootType); + } + + var getMethod = typeof(IAggregateRootFactory) + .GetMethods() + .First(m => m.Name == nameof(IAggregateRootFactory.Get) && m.IsGenericMethod); + + var genericGetMethod = getMethod.MakeGenericMethod(aggregateRootType); + return genericGetMethod.Invoke(aggregateRootFactory, [eventSourceId, null, null])!; + } } diff --git a/Source/DotNET/Chronicle/Aggregates/IAggregateRootFactory.cs b/Source/DotNET/Chronicle/Aggregates/IAggregateRootFactory.cs index 63a5e65eb..ffc38c1bc 100644 --- a/Source/DotNET/Chronicle/Aggregates/IAggregateRootFactory.cs +++ b/Source/DotNET/Chronicle/Aggregates/IAggregateRootFactory.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using Cratis.Chronicle.Events; namespace Cratis.Arc.Chronicle.Aggregates; @@ -22,6 +23,6 @@ public interface IAggregateRootFactory /// If the aggregate has event handler methods, the events for the specified /// will be retrieved and the event handler methods will be invoked. /// - Task Get(EventSourceId id, EventStreamId? streamId = default, EventSourceType? eventSourceType = default) + Task Get<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TAggregateRoot>(EventSourceId id, EventStreamId? streamId = default, EventSourceType? eventSourceType = default) where TAggregateRoot : IAggregateRoot; } diff --git a/Source/DotNET/Chronicle/Chronicle.csproj b/Source/DotNET/Chronicle/Chronicle.csproj index 8042c53f6..c02e083b1 100644 --- a/Source/DotNET/Chronicle/Chronicle.csproj +++ b/Source/DotNET/Chronicle/Chronicle.csproj @@ -20,4 +20,4 @@ OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> - \ No newline at end of file + diff --git a/Source/DotNET/Chronicle/Commands/EventSourceExtensions.cs b/Source/DotNET/Chronicle/Commands/EventSourceExtensions.cs index 3b402a609..3049747c2 100644 --- a/Source/DotNET/Chronicle/Commands/EventSourceExtensions.cs +++ b/Source/DotNET/Chronicle/Commands/EventSourceExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; using Cratis.Chronicle.Events; @@ -21,6 +22,7 @@ public static class EventSourceExtensions /// /// The command to check for an event source ID. /// True if the command has an event source ID; otherwise, false. + [UnconditionalSuppressMessage("AOT", "IL2075", Justification = "command.GetType().GetProperties() on runtime command objects; their public properties are preserved. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204).")] public static bool HasEventSourceId(this object command) => command.GetType().GetProperties().Any(p => p.PropertyType.IsAssignableTo(typeof(EventSourceId)) || @@ -49,6 +51,7 @@ public static bool HasEventSourceId(this ITuple tuple) /// /// The command to get the event source ID from. /// The event source ID. + [UnconditionalSuppressMessage("AOT", "IL2075", Justification = "command.GetType().GetProperties() on runtime command objects; command types are user-defined and their public properties are preserved. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204).")] public static EventSourceId GetEventSourceId(this object command) { var eventSourceId = EventSourceId.Unspecified; @@ -101,6 +104,7 @@ internal static bool IsEventSourceIdValue(object? value) => /// /// The value to convert. /// The corresponding . + [UnconditionalSuppressMessage("AOT", "IL2075", Justification = "value.GetType().GetMethods() on runtime EventSourceId subtypes; these types are user-defined and their public static methods are preserved. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204).")] internal static EventSourceId ToEventSourceId(object value) { if (value is EventSourceId esId) @@ -110,24 +114,35 @@ internal static EventSourceId ToEventSourceId(object value) if (IsGenericEventSourceIdType(value.GetType())) { - var type = value.GetType(); - while (type is not null) + var op = FindImplicitEventSourceIdOperator(value.GetType()); + if (op is not null) { - if (type.IsGenericType && type.GetGenericTypeDefinition() == _genericEventSourceIdDefinition) + return (EventSourceId)op.Invoke(null, [value])!; + } + } + + return value.ToString()!; + } + + [UnconditionalSuppressMessage("AOT", "IL2070", Justification = "GetMethods on EventSourceId subtypes discovered at runtime; these types are framework-defined and their public methods are preserved. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204).")] + static MethodInfo? FindImplicitEventSourceIdOperator(Type type) + { + while (type is not null) + { + if (type.IsGenericType && type.GetGenericTypeDefinition() == _genericEventSourceIdDefinition) + { + var op = type.GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(m => m.Name == "op_Implicit" && m.ReturnType == typeof(EventSourceId)); + if (op is not null) { - var op = type.GetMethods(BindingFlags.Public | BindingFlags.Static) - .FirstOrDefault(m => m.Name == "op_Implicit" && m.ReturnType == typeof(EventSourceId)); - if (op is not null) - { - return (EventSourceId)op.Invoke(null, [value])!; - } + return op; } - - type = type.BaseType; } + + type = type.BaseType!; } - return value.ToString()!; + return null; } static bool IsGenericEventSourceIdType(Type? type) diff --git a/Source/DotNET/Chronicle/Commands/SubjectExtensions.cs b/Source/DotNET/Chronicle/Commands/SubjectExtensions.cs index 48a4e0a0e..a8fc3477f 100644 --- a/Source/DotNET/Chronicle/Commands/SubjectExtensions.cs +++ b/Source/DotNET/Chronicle/Commands/SubjectExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; using Cratis.Chronicle; @@ -78,6 +79,7 @@ static bool IsSubjectProperty(PropertyInfo property) => property.PropertyType == typeof(Subject) || Attribute.IsDefined(property, typeof(SubjectAttribute)); + [UnconditionalSuppressMessage("AOT", "IL2070", Justification = "GetConstructors/GetProperty on user command types; their public members are preserved. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204).")] static PropertyInfo? ResolvePropertyFromConstructorParameter(Type type) { var constructor = type.GetConstructors().FirstOrDefault(); diff --git a/Source/DotNET/Chronicle/ReadModels/ReadModelServiceCollectionExtensions.cs b/Source/DotNET/Chronicle/ReadModels/ReadModelServiceCollectionExtensions.cs index 7df14c332..0fa9a8160 100644 --- a/Source/DotNET/Chronicle/ReadModels/ReadModelServiceCollectionExtensions.cs +++ b/Source/DotNET/Chronicle/ReadModels/ReadModelServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Cratis.Arc.Chronicle.Commands; @@ -47,12 +48,7 @@ public static IServiceCollection AddReadModels(this IServiceCollection services, _initialized = true; var readModelTypesFromProjections = clientArtifactsProvider.Projections - .Select(projectionType => - { - var projectionInterface = projectionType.GetInterfaces() - .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IProjectionFor<>)); - return projectionInterface?.GetGenericArguments()[0]; - }) + .Select(GetReadModelTypeFromProjection) .Where(type => type?.IsClass == true && !type.IsAbstract) .Cast(); @@ -98,6 +94,8 @@ internal static object ResolveReadModel(Type readModelType, CommandContext comma : ReleaseReadModel(readModels, readModelType, subject, readModel); } + [UnconditionalSuppressMessage("AOT", "IL2060", Justification = "IReadModels.Release has no non-generic overload. The read model types are preserved by the application's type system. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3e).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "IReadModels.Release has no non-generic overload. The read model types are preserved by the application's type system. Source-generated dispatch is the long-term fix (tracked in GitHub issue #2204 item 3e).")] static object ReleaseReadModel(IReadModels readModels, Type readModelType, Subject subject, object readModel) { try @@ -115,4 +113,12 @@ static object ReleaseReadModel(IReadModels readModels, Type readModelType, Subje throw new InvalidOperationException($"Failed to release read model '{readModelType.FullName}' with subject '{subject.Value}'.", exception); } } + + [UnconditionalSuppressMessage("AOT", "IL2070", Justification = "The projection types from IClientArtifactsProvider are discovered at startup and their interfaces are preserved. Source-generated type discovery is the long-term fix (tracked in GitHub issue #2204 item 3e).")] + static Type? GetReadModelTypeFromProjection(Type projectionType) + { + var projectionInterface = projectionType.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IProjectionFor<>)); + return projectionInterface?.GetGenericArguments()[0]; + } } diff --git a/Source/DotNET/Cratis.Testing/Cratis.Testing.csproj b/Source/DotNET/Cratis.Testing/Cratis.Testing.csproj index d3f1de9ea..9a2f4ba7a 100644 --- a/Source/DotNET/Cratis.Testing/Cratis.Testing.csproj +++ b/Source/DotNET/Cratis.Testing/Cratis.Testing.csproj @@ -4,6 +4,7 @@ Cratis.Testing Cratis.Testing net10.0 + false Unified testing package for Cratis Arc applications with Chronicle event sourcing. Combines Cratis.Arc.Testing and Cratis.Arc.Chronicle.Testing into a single dependency. cratis;arc;chronicle;testing;commands;event-sourcing;cqrs $(NoWarn);IDE0005 diff --git a/Source/DotNET/Directory.Build.props b/Source/DotNET/Directory.Build.props index 96e91ba3a..45011c106 100644 --- a/Source/DotNET/Directory.Build.props +++ b/Source/DotNET/Directory.Build.props @@ -72,4 +72,15 @@ $(NoWarn);IDE1006;IDE0005;MA0101;RCS1266;xUnit1031 + + + + true + $(NoWarn); + diff --git a/Source/DotNET/Directory.Build.targets b/Source/DotNET/Directory.Build.targets new file mode 100644 index 000000000..61a064d38 --- /dev/null +++ b/Source/DotNET/Directory.Build.targets @@ -0,0 +1,4 @@ + + + + diff --git a/Source/DotNET/EntityFrameworkCore.PostgreSql.Specs/EntityFrameworkCore.PostgreSql.Specs.csproj b/Source/DotNET/EntityFrameworkCore.PostgreSql.Specs/EntityFrameworkCore.PostgreSql.Specs.csproj index 470b14961..bebc75211 100644 --- a/Source/DotNET/EntityFrameworkCore.PostgreSql.Specs/EntityFrameworkCore.PostgreSql.Specs.csproj +++ b/Source/DotNET/EntityFrameworkCore.PostgreSql.Specs/EntityFrameworkCore.PostgreSql.Specs.csproj @@ -4,6 +4,7 @@ Cratis.Arc.EntityFrameworkCore.PostgreSql false true + false diff --git a/Source/DotNET/EntityFrameworkCore.Specs/EntityFrameworkCore.Specs.csproj b/Source/DotNET/EntityFrameworkCore.Specs/EntityFrameworkCore.Specs.csproj index 85e371e77..5e0c7910f 100644 --- a/Source/DotNET/EntityFrameworkCore.Specs/EntityFrameworkCore.Specs.csproj +++ b/Source/DotNET/EntityFrameworkCore.Specs/EntityFrameworkCore.Specs.csproj @@ -4,6 +4,7 @@ Cratis.Arc.EntityFrameworkCore false true + false @@ -11,4 +12,4 @@ - \ No newline at end of file + diff --git a/Source/DotNET/EntityFrameworkCore.SqlServer.Specs/EntityFrameworkCore.SqlServer.Specs.csproj b/Source/DotNET/EntityFrameworkCore.SqlServer.Specs/EntityFrameworkCore.SqlServer.Specs.csproj index 7d41f2801..2635ec379 100644 --- a/Source/DotNET/EntityFrameworkCore.SqlServer.Specs/EntityFrameworkCore.SqlServer.Specs.csproj +++ b/Source/DotNET/EntityFrameworkCore.SqlServer.Specs/EntityFrameworkCore.SqlServer.Specs.csproj @@ -4,6 +4,7 @@ Cratis.Arc.EntityFrameworkCore.SqlServer false true + false diff --git a/Source/DotNET/MongoDB.Specs/MongoDB.Specs.csproj b/Source/DotNET/MongoDB.Specs/MongoDB.Specs.csproj index 2001b81ab..d363a8361 100644 --- a/Source/DotNET/MongoDB.Specs/MongoDB.Specs.csproj +++ b/Source/DotNET/MongoDB.Specs/MongoDB.Specs.csproj @@ -4,6 +4,7 @@ Cratis.Arc.MongoDB false true + false diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/InvocationTarget.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/InvocationTarget.cs deleted file mode 100644 index 81ce427a4..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/InvocationTarget.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptor; - -public class InvocationTarget -{ - public const string ErrorMessage = "Something went wrong"; - - public Task SuccessfulMethod() => Task.CompletedTask; - public Task CancelledMethod() => Task.FromCanceled(new CancellationToken(true)); - -#pragma warning disable AS0008, CA2201 - public Task FaultedMethod() => Task.Run(() => throw new(ErrorMessage)); - - public Task FaultedMethodWithoutTask() => throw new(ErrorMessage); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/InvocationTargetWithCollectionNotFound.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/InvocationTargetWithCollectionNotFound.cs deleted file mode 100644 index 866543c7a..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/InvocationTargetWithCollectionNotFound.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using MongoDB.Bson; -using MongoDB.Driver; -using MongoDB.Driver.Core.Clusters; -using MongoDB.Driver.Core.Connections; -using MongoDB.Driver.Core.Servers; - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptor; - -public class InvocationTargetWithCollectionNotFound -{ - static MongoCommandException CreateCollectionNotFoundException() - { - var endPoint = new System.Net.DnsEndPoint("localhost", 27017); - var serverId = new ServerId(new ClusterId(0), endPoint); - var connectionId = new ConnectionId(serverId); - return new(connectionId, WellKnownErrorMessages.CollectionNotFound, new BsonDocument()); - } - - public Task DeleteAsyncCollectionNotFound() => - Task.FromException(CreateCollectionNotFoundException()); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/given/an_interceptor.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/given/an_interceptor.cs deleted file mode 100644 index 9bf0f951d..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/given/an_interceptor.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using MongoDB.Driver; -using Polly; - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptor.given; - -public abstract class an_interceptor : Specification -{ - protected const int PoolSize = 10; - protected ResiliencePipeline _resiliencePipeline; - protected MongoClientSettings _settings; - protected MongoCollectionInterceptor _interceptor; - protected Castle.DynamicProxy.IInvocation _invocation; - protected Task _returnValue; - protected for_MongoCollectionInterceptorForReturnValue.InvocationTarget _target; - protected SemaphoreSlim _semaphore; - - protected abstract string GetInvocationTargetMethod(); - - void Establish() - { - _resiliencePipeline = new ResiliencePipelineBuilder().Build(); - - _semaphore = new SemaphoreSlim(PoolSize, PoolSize); - - _interceptor = new(_resiliencePipeline, _semaphore); - - _invocation = Substitute.For(); - _invocation.Method.Returns(typeof(for_MongoCollectionInterceptorForReturnValue.InvocationTarget).GetMethod(GetInvocationTargetMethod())); - _target = new(); - _invocation.InvocationTarget.Returns(_target); - _invocation.When(_ => _.ReturnValue = Arg.Any()).Do((_) => _returnValue = _.Arg()); - } -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/when_intercepting/cancelled_method.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/when_intercepting/cancelled_method.cs deleted file mode 100644 index 3c6bfaaec..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/when_intercepting/cancelled_method.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptor.when_intercepting; - -public class cancelled_method : given.an_interceptor -{ - protected override string GetInvocationTargetMethod() => nameof(for_MongoCollectionInterceptorForReturnValue.InvocationTarget.CancelledMethod); - Exception _exception; - - async Task Because() - { - _interceptor.Intercept(_invocation); - _exception = await Catch.Exception(async () => await _returnValue); - } - - [Fact] void should_bubble_up_cancelled_exception() => _exception.ShouldBeOfExactType(); - [Fact] void should_have_cancelled_task() => _returnValue.IsCanceled.ShouldBeTrue(); - [Fact] void should_have_freed_up_semaphore() => _semaphore.CurrentCount.ShouldEqual(PoolSize); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/when_intercepting/collection_not_found_with_void_task.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/when_intercepting/collection_not_found_with_void_task.cs deleted file mode 100644 index 107990076..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/when_intercepting/collection_not_found_with_void_task.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptor.when_intercepting; - -public class collection_not_found_with_void_task : Specification -{ - const int PoolSize = 10; - MongoCollectionInterceptor _interceptor; - Castle.DynamicProxy.IInvocation _invocation; - Task _returnValue; - InvocationTargetWithCollectionNotFound _target; - SemaphoreSlim _semaphore; - - void Establish() - { - var resiliencePipeline = new Polly.ResiliencePipelineBuilder().Build(); - _semaphore = new SemaphoreSlim(PoolSize, PoolSize); - _interceptor = new(resiliencePipeline, _semaphore); - - _invocation = Substitute.For(); - _invocation.Method.Returns(typeof(InvocationTargetWithCollectionNotFound).GetMethod(nameof(InvocationTargetWithCollectionNotFound.DeleteAsyncCollectionNotFound))); - _target = new InvocationTargetWithCollectionNotFound(); - _invocation.InvocationTarget.Returns(_target); - _invocation.When(_ => _.ReturnValue = Arg.Any()).Do((_) => _returnValue = _.Arg()); - } - - async Task Because() - { - _interceptor.Intercept(_invocation); - await _returnValue; - } - - [Fact] void should_not_be_faulted() => _returnValue.IsFaulted.ShouldBeFalse(); - [Fact] void should_complete_successfully() => _returnValue.IsCompletedSuccessfully.ShouldBeTrue(); - [Fact] void should_have_freed_up_semaphore() => _semaphore.CurrentCount.ShouldEqual(PoolSize); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/when_intercepting/faulted_method.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/when_intercepting/faulted_method.cs deleted file mode 100644 index 39933eed2..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/when_intercepting/faulted_method.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptor.when_intercepting; - -public class faulted_method : given.an_interceptor -{ - Exception _exception; - - protected override string GetInvocationTargetMethod() => nameof(for_MongoCollectionInterceptorForReturnValue.InvocationTarget.FaultedMethod); - - async Task Because() - { - _interceptor.Intercept(_invocation); - _exception = await Catch.Exception(async () => await _returnValue); - } - - [Fact] void should_be_faulted() => _returnValue.IsFaulted.ShouldBeTrue(); - [Fact] void should_bubble_up_exception() => _exception.Message.ShouldEqual(for_MongoCollectionInterceptorForReturnValue.InvocationTarget.ErrorMessage); - [Fact] void should_have_freed_up_semaphore() => _semaphore.CurrentCount.ShouldEqual(PoolSize); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/when_intercepting/faulted_method_without_task.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/when_intercepting/faulted_method_without_task.cs deleted file mode 100644 index b1023f22b..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/when_intercepting/faulted_method_without_task.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptor.when_intercepting; - -public class faulted_method_without_task : given.an_interceptor -{ - Exception _exception; - - protected override string GetInvocationTargetMethod() => nameof(for_MongoCollectionInterceptorForReturnValue.InvocationTarget.FaultedMethodWithoutTask); - - void Because() - { - _interceptor.Intercept(_invocation); - _exception = _returnValue.Exception.InnerExceptions.Single().InnerException; - } - - [Fact] void should_be_faulted() => _returnValue.IsFaulted.ShouldBeTrue(); - [Fact] void should_bubble_up_exception() => _exception.Message.ShouldEqual(for_MongoCollectionInterceptorForReturnValue.InvocationTarget.ErrorMessage); - [Fact] void should_have_freed_up_semaphore() => _semaphore.CurrentCount.ShouldEqual(PoolSize); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/when_intercepting/successful_method.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/when_intercepting/successful_method.cs deleted file mode 100644 index 04a2c46e9..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptor/when_intercepting/successful_method.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptor.when_intercepting; - -public class successful_method : given.an_interceptor -{ - protected override string GetInvocationTargetMethod() => nameof(for_MongoCollectionInterceptorForReturnValue.InvocationTarget.SuccessfulMethod); - - void Because() => _interceptor.Intercept(_invocation); - - [Fact] void should_return_successful_task() => _returnValue.IsCompletedSuccessfully.ShouldBeTrue(); - [Fact] void should_have_freed_up_semaphore() => _semaphore.CurrentCount.ShouldEqual(PoolSize); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/InvocationTarget.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/InvocationTarget.cs deleted file mode 100644 index e9a6a5c98..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/InvocationTarget.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptorForReturnValue; - -public class InvocationTarget -{ - public const string ErrorMessage = "Something went wrong"; - - public Task SuccessfulMethod() => Task.FromResult("Hello"); - public Task CancelledMethod() => Task.FromCanceled(new CancellationToken(true)); - public Task FaultedMethod() => Task.Run(() => - { - #pragma warning disable AS0008, CA2201 - throw new(ErrorMessage); -#pragma warning disable CS0162 // Unreachable code detected - return "Hello"; -#pragma warning restore CS0162 // Unreachable code detected - }); - - public Task FaultedMethodWithoutTask() => throw new(ErrorMessage); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/InvocationTargetWithChangeStreamCursor.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/InvocationTargetWithChangeStreamCursor.cs deleted file mode 100644 index e0cbde206..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/InvocationTargetWithChangeStreamCursor.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using MongoDB.Bson; -using MongoDB.Driver; -using MongoDB.Driver.Core.Clusters; -using MongoDB.Driver.Core.Connections; -using MongoDB.Driver.Core.Servers; - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptorForReturnValue; - -public class InvocationTargetWithChangeStreamCursor(int failCount = 0, IChangeStreamCursor? successCursor = null) -{ - int _callCount; - - public int FailCount { get; } = failCount; - public int CallCount => _callCount; - - static MongoCommandException CreateCollectionNotFoundException() - { - var endPoint = new System.Net.DnsEndPoint("localhost", 27017); - var serverId = new ServerId(new ClusterId(0), endPoint); - var connectionId = new ConnectionId(serverId); - return new(connectionId, InvocationTargetWithCollectionNotFound.ErrorMessage, new BsonDocument()); - } - - public Task> WatchAsyncCollectionNotFound() - { - _callCount++; - if (_callCount <= FailCount) - { - return Task.FromException>(CreateCollectionNotFoundException()); - } - - return Task.FromResult(successCursor ?? Substitute.For>()); - } -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/InvocationTargetWithCollectionNotFound.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/InvocationTargetWithCollectionNotFound.cs deleted file mode 100644 index debf6860e..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/InvocationTargetWithCollectionNotFound.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using MongoDB.Bson; -using MongoDB.Driver; -using MongoDB.Driver.Core.Clusters; -using MongoDB.Driver.Core.Connections; -using MongoDB.Driver.Core.Servers; - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptorForReturnValue; - -public class InvocationTargetWithCollectionNotFound -{ - public const string ErrorMessage = "Collection not found"; - - static MongoCommandException CreateCollectionNotFoundException() - { - var endPoint = new System.Net.DnsEndPoint("localhost", 27017); - var serverId = new ServerId(new ClusterId(0), endPoint); - var connectionId = new ConnectionId(serverId); - return new(connectionId, ErrorMessage, new BsonDocument()); - } - - public Task> FindAsyncCollectionNotFound() => - Task.FromException>(CreateCollectionNotFoundException()); - - public Task CountAsyncCollectionNotFound() => - Task.FromException(CreateCollectionNotFoundException()); - - public Task FindOneCollectionNotFound() => - Task.FromException(CreateCollectionNotFoundException()); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/given/an_interceptor.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/given/an_interceptor.cs deleted file mode 100644 index ec63e231b..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/given/an_interceptor.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using MongoDB.Driver; -using Polly; - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptorForReturnValue.given; - -public abstract class an_interceptor : Specification -{ - protected const int PoolSize = 10; - protected ResiliencePipeline _resiliencePipeline; - protected MongoClientSettings _settings; - protected MongoCollectionInterceptorForReturnValues _interceptor; - protected Castle.DynamicProxy.IInvocation _invocation; - protected Task _returnValue; - protected InvocationTarget _target; - protected SemaphoreSlim _semaphore; - - protected abstract string GetInvocationTargetMethod(); - - void Establish() - { - _resiliencePipeline = new ResiliencePipelineBuilder().Build(); - _semaphore = new SemaphoreSlim(PoolSize, PoolSize); - - _interceptor = new(_resiliencePipeline, _semaphore); - - _invocation = Substitute.For(); - _invocation.Method.Returns(typeof(InvocationTarget).GetMethod(GetInvocationTargetMethod())); - _target = new(); - _invocation.InvocationTarget.Returns(_target); - _invocation.When(_ => _.ReturnValue = Arg.Any>()).Do((_) => _returnValue = _.Arg>()); - } -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/cancelled_method.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/cancelled_method.cs deleted file mode 100644 index 6c6ec5e6b..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/cancelled_method.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptorForReturnValue.when_intercepting; - -public class cancelled_method : given.an_interceptor -{ - protected override string GetInvocationTargetMethod() => nameof(for_MongoCollectionInterceptor.InvocationTarget.CancelledMethod); - Exception _exception; - - async Task Because() - { - _interceptor.Intercept(_invocation); - _exception = await Catch.Exception(async () => await _returnValue); - } - - [Fact] void should_bubble_up_cancelled_exception() => _exception.ShouldBeOfExactType(); - [Fact] void should_have_cancelled_task() => _returnValue.IsCanceled.ShouldBeTrue(); - [Fact] void should_have_freed_up_semaphore() => _semaphore.CurrentCount.ShouldEqual(PoolSize); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/collection_not_found_with_async_cursor_return_type.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/collection_not_found_with_async_cursor_return_type.cs deleted file mode 100644 index 8544e4653..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/collection_not_found_with_async_cursor_return_type.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using MongoDB.Driver; - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptorForReturnValue.when_intercepting; - -public class collection_not_found_with_async_cursor_return_type : Specification -{ - const int PoolSize = 10; - MongoCollectionInterceptorForReturnValues _interceptor; - Castle.DynamicProxy.IInvocation _invocation; - Task> _returnValue; - InvocationTargetWithCollectionNotFound _target; - SemaphoreSlim _semaphore; - - void Establish() - { - var resiliencePipeline = new Polly.ResiliencePipelineBuilder().Build(); - _semaphore = new SemaphoreSlim(PoolSize, PoolSize); - _interceptor = new(resiliencePipeline, _semaphore); - - _invocation = Substitute.For(); - _invocation.Method.Returns(typeof(InvocationTargetWithCollectionNotFound).GetMethod(nameof(InvocationTargetWithCollectionNotFound.FindAsyncCollectionNotFound))); - _target = new InvocationTargetWithCollectionNotFound(); - _invocation.InvocationTarget.Returns(_target); - _invocation.When(_ => _.ReturnValue = Arg.Any>>()).Do((_) => _returnValue = _.Arg>>()); - } - - async Task Because() - { - _interceptor.Intercept(_invocation); - await _returnValue; - } - - [Fact] void should_not_be_faulted() => _returnValue.IsFaulted.ShouldBeFalse(); - [Fact] async Task should_return_empty_cursor() => (await _returnValue).ShouldNotBeNull(); - [Fact] async Task should_have_no_documents() => (await (await _returnValue).MoveNextAsync()).ShouldBeFalse(); - [Fact] void should_have_freed_up_semaphore() => _semaphore.CurrentCount.ShouldEqual(PoolSize); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/collection_not_found_with_change_stream_cursor_return_type.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/collection_not_found_with_change_stream_cursor_return_type.cs deleted file mode 100644 index f9eb5d1e1..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/collection_not_found_with_change_stream_cursor_return_type.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using MongoDB.Driver; - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptorForReturnValue.when_intercepting; - -public class collection_not_found_with_change_stream_cursor_return_type : Specification -{ - const int PoolSize = 10; - MongoCollectionInterceptorForReturnValues _interceptor; - Castle.DynamicProxy.IInvocation _invocation; - Task> _returnValue; - InvocationTargetWithChangeStreamCursor _target; - SemaphoreSlim _semaphore; - - void Establish() - { - var resiliencePipeline = new Polly.ResiliencePipelineBuilder().Build(); - _semaphore = new SemaphoreSlim(PoolSize, PoolSize); - _interceptor = new(resiliencePipeline, _semaphore); - - _target = new InvocationTargetWithChangeStreamCursor(failCount: 1); - _invocation = Substitute.For(); - _invocation.Method.Returns(typeof(InvocationTargetWithChangeStreamCursor).GetMethod(nameof(InvocationTargetWithChangeStreamCursor.WatchAsyncCollectionNotFound))); - _invocation.InvocationTarget.Returns(_target); - _invocation.Arguments.Returns([]); - _invocation.When(_ => _.ReturnValue = Arg.Any>>()).Do((_) => _returnValue = _.Arg>>()); - } - - async Task Because() - { - _interceptor.Intercept(_invocation); - await _returnValue; - } - - [Fact] void should_not_be_faulted() => _returnValue.IsFaulted.ShouldBeFalse(); - [Fact] async Task should_return_retrying_cursor() => (await _returnValue).ShouldBeOfExactType>(); - [Fact] void should_have_freed_up_semaphore() => _semaphore.CurrentCount.ShouldEqual(PoolSize); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/collection_not_found_with_long_return_type.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/collection_not_found_with_long_return_type.cs deleted file mode 100644 index 7b1375c9c..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/collection_not_found_with_long_return_type.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptorForReturnValue.when_intercepting; - -public class collection_not_found_with_long_return_type : Specification -{ - const int PoolSize = 10; - MongoCollectionInterceptorForReturnValues _interceptor; - Castle.DynamicProxy.IInvocation _invocation; - Task _returnValue; - InvocationTargetWithCollectionNotFound _target; - SemaphoreSlim _semaphore; - - void Establish() - { - var resiliencePipeline = new Polly.ResiliencePipelineBuilder().Build(); - _semaphore = new SemaphoreSlim(PoolSize, PoolSize); - _interceptor = new(resiliencePipeline, _semaphore); - - _invocation = Substitute.For(); - _invocation.Method.Returns(typeof(InvocationTargetWithCollectionNotFound).GetMethod(nameof(InvocationTargetWithCollectionNotFound.CountAsyncCollectionNotFound))); - _target = new InvocationTargetWithCollectionNotFound(); - _invocation.InvocationTarget.Returns(_target); - _invocation.When(_ => _.ReturnValue = Arg.Any>()).Do((_) => _returnValue = _.Arg>()); - } - - async Task Because() - { - _interceptor.Intercept(_invocation); - await _returnValue; - } - - [Fact] void should_not_be_faulted() => _returnValue.IsFaulted.ShouldBeFalse(); - [Fact] async Task should_return_zero() => (await _returnValue).ShouldEqual(0L); - [Fact] void should_have_freed_up_semaphore() => _semaphore.CurrentCount.ShouldEqual(PoolSize); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/collection_not_found_with_nullable_reference_return_type.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/collection_not_found_with_nullable_reference_return_type.cs deleted file mode 100644 index cebb42fb8..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/collection_not_found_with_nullable_reference_return_type.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptorForReturnValue.when_intercepting; - -public class collection_not_found_with_nullable_reference_return_type : Specification -{ - const int PoolSize = 10; - MongoCollectionInterceptorForReturnValues _interceptor; - Castle.DynamicProxy.IInvocation _invocation; - Task _returnValue; - InvocationTargetWithCollectionNotFound _target; - SemaphoreSlim _semaphore; - - void Establish() - { - var resiliencePipeline = new Polly.ResiliencePipelineBuilder().Build(); - _semaphore = new SemaphoreSlim(PoolSize, PoolSize); - _interceptor = new(resiliencePipeline, _semaphore); - - _invocation = Substitute.For(); - _invocation.Method.Returns(typeof(InvocationTargetWithCollectionNotFound).GetMethod(nameof(InvocationTargetWithCollectionNotFound.FindOneCollectionNotFound))); - _target = new InvocationTargetWithCollectionNotFound(); - _invocation.InvocationTarget.Returns(_target); - _invocation.When(_ => _.ReturnValue = Arg.Any>()).Do((_) => _returnValue = _.Arg>()); - } - - async Task Because() - { - _interceptor.Intercept(_invocation); - await _returnValue; - } - - [Fact] void should_not_be_faulted() => _returnValue.IsFaulted.ShouldBeFalse(); - [Fact] async Task should_return_null() => (await _returnValue).ShouldBeNull(); - [Fact] void should_have_freed_up_semaphore() => _semaphore.CurrentCount.ShouldEqual(PoolSize); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/faulted_method.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/faulted_method.cs deleted file mode 100644 index 28b10c1d0..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/faulted_method.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptorForReturnValue.when_intercepting; - -public class faulted_method : given.an_interceptor -{ - Exception _exception; - - protected override string GetInvocationTargetMethod() => nameof(for_MongoCollectionInterceptor.InvocationTarget.FaultedMethod); - - async Task Because() - { - _interceptor.Intercept(_invocation); - _exception = await Catch.Exception(async () => await _returnValue); - } - - [Fact] void should_be_faulted() => _returnValue.IsFaulted.ShouldBeTrue(); - [Fact] void should_bubble_up_exception() => _exception.Message.ShouldEqual(for_MongoCollectionInterceptor.InvocationTarget.ErrorMessage); - [Fact] void should_have_freed_up_semaphore() => _semaphore.CurrentCount.ShouldEqual(PoolSize); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/faulted_method_without_task.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/faulted_method_without_task.cs deleted file mode 100644 index dba83e21b..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/faulted_method_without_task.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptorForReturnValue.when_intercepting; - -public class faulted_method_without_task : given.an_interceptor -{ - Exception _exception; - - protected override string GetInvocationTargetMethod() => nameof(InvocationTarget.FaultedMethodWithoutTask); - - void Because() - { - _interceptor.Intercept(_invocation); - _exception = _returnValue.Exception.InnerExceptions.Single().InnerException; - } - - [Fact] void should_be_faulted() => _returnValue.IsFaulted.ShouldBeTrue(); - [Fact] void should_bubble_up_exception() => _exception.Message.ShouldEqual(for_MongoCollectionInterceptor.InvocationTarget.ErrorMessage); - [Fact] void should_have_freed_up_semaphore() => _semaphore.CurrentCount.ShouldEqual(PoolSize); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/successful_method.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/successful_method.cs deleted file mode 100644 index 200e52fb0..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorForReturnValue/when_intercepting/successful_method.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptorForReturnValue.when_intercepting; - -public class successful_method : given.an_interceptor -{ - string _result; - protected override string GetInvocationTargetMethod() => nameof(for_MongoCollectionInterceptor.InvocationTarget.SuccessfulMethod); - - async Task Because() - { - _interceptor.Intercept(_invocation); - _result = await _returnValue; - } - - [Fact] void should_return_value_from_invocation() => _result.ShouldEqual("Hello"); - [Fact] void should_return_successful_task() => _returnValue.IsCompletedSuccessfully.ShouldBeTrue(); - [Fact] void should_have_freed_up_semaphore() => _semaphore.CurrentCount.ShouldEqual(PoolSize); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorSelector/given/an_interceptor_selector.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorSelector/given/an_interceptor_selector.cs deleted file mode 100644 index 6cc1b40c3..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorSelector/given/an_interceptor_selector.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using MongoDB.Driver; -using Polly; - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptorSelector.given; - -public class an_interceptor_selector : Specification -{ - protected MongoCollectionInterceptorSelector selector; - protected ResiliencePipeline resilience_pipeline; - protected IMongoClient mongo_client; - protected MongoClientSettings settings; - - void Establish() - { - resilience_pipeline = new ResiliencePipelineBuilder().Build(); - mongo_client = Substitute.For(); - settings = new MongoClientSettings(); - mongo_client.Settings.Returns(settings); - selector = new MongoCollectionInterceptorSelector(resilience_pipeline, mongo_client); - } -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorSelector/when_selecting_interceptors/for_async_methods.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorSelector/when_selecting_interceptors/for_async_methods.cs deleted file mode 100644 index c081c1e9a..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorSelector/when_selecting_interceptors/for_async_methods.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Reflection; -using MongoDB.Bson; -using MongoDB.Driver; - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptorSelector.when_selecting_interceptors; - -public class for_async_methods : given.an_interceptor_selector -{ - protected IEnumerable _asyncMethods; - int _interceptedMethods; - - void Establish() - { - _asyncMethods = [.. typeof(IMongoCollection).GetMethods().Where(m => m.ReturnType == typeof(Task))]; - } - - void Because() => _interceptedMethods = _asyncMethods.Count(methodInfo => - { - var interceptors = selector.SelectInterceptors(typeof(IMongoCollection), methodInfo, []); - return interceptors.Length == 1 && interceptors[0] is MongoCollectionInterceptor; - }); - - [Fact] void should_have_the_mongo_collection_interceptor_for_all() => _interceptedMethods.ShouldEqual(_asyncMethods.Count()); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorSelector/when_selecting_interceptors/for_async_methods_with_return_value.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorSelector/when_selecting_interceptors/for_async_methods_with_return_value.cs deleted file mode 100644 index 63eb0c4d2..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorSelector/when_selecting_interceptors/for_async_methods_with_return_value.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Reflection; -using MongoDB.Bson; -using MongoDB.Driver; - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptorSelector.when_selecting_interceptors; - -public class for_async_methods_with_return_value : given.an_interceptor_selector -{ - protected IEnumerable _asyncMethods; - int _interceptedMethods; - - void Establish() - { - _asyncMethods = [.. typeof(IMongoCollection).GetMethods().Where(m => m.ReturnType.IsAssignableTo(typeof(Task)) && m.ReturnType.IsGenericType)]; - } - - void Because() => _interceptedMethods = _asyncMethods.Count(methodInfo => - { - var interceptors = selector.SelectInterceptors(typeof(IMongoCollection), methodInfo, []); - return interceptors.Length == 1 && interceptors[0] is MongoCollectionInterceptorForReturnValues; - }); - - [Fact] void should_have_the_mongo_collection_interceptor_for_all() => _interceptedMethods.ShouldEqual(_asyncMethods.Count()); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorSelector/when_selecting_interceptors/for_sync_methods.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorSelector/when_selecting_interceptors/for_sync_methods.cs deleted file mode 100644 index 7a9e1f400..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_MongoCollectionInterceptorSelector/when_selecting_interceptors/for_sync_methods.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Reflection; -using MongoDB.Bson; -using MongoDB.Driver; - -namespace Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptorSelector.when_selecting_interceptors; - -public class for_sync_methods : given.an_interceptor_selector -{ - IEnumerable _syncMethods; - int _interceptedMethods; - - void Establish() - { - _syncMethods = [.. typeof(IMongoCollection).GetMethods().Where(m => !m.ReturnType.IsAssignableTo(typeof(Task)))]; - } - - void Because() => _interceptedMethods = _syncMethods.Count(methodInfo => - { - var interceptors = selector.SelectInterceptors(typeof(IMongoCollection), methodInfo, []); - return interceptors.Length == 0; - }); - - [Fact] void should_have_no_mongo_collection_interceptor_for_all() => _interceptedMethods.ShouldEqual(_syncMethods.Count()); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_RetryingChangeStreamCursor/given/a_retrying_change_stream_cursor.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_RetryingChangeStreamCursor/given/a_retrying_change_stream_cursor.cs deleted file mode 100644 index be27d1c2b..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_RetryingChangeStreamCursor/given/a_retrying_change_stream_cursor.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Castle.DynamicProxy; -using Cratis.Arc.MongoDB.Resilience.for_MongoCollectionInterceptorForReturnValue; -using MongoDB.Driver; - -namespace Cratis.Arc.MongoDB.Resilience.for_RetryingChangeStreamCursor.given; - -public class a_retrying_change_stream_cursor : Specification -{ - protected RetryingChangeStreamCursor _cursor; - protected IInvocation _invocation; - protected InvocationTargetWithChangeStreamCursor _target; - protected IChangeStreamCursor _actualCursor; - - void Establish() - { - _actualCursor = Substitute.For>(); - _target = new InvocationTargetWithChangeStreamCursor(failCount: 2, successCursor: _actualCursor); - - _invocation = Substitute.For(); - _invocation.Method.Returns(typeof(InvocationTargetWithChangeStreamCursor).GetMethod(nameof(InvocationTargetWithChangeStreamCursor.WatchAsyncCollectionNotFound))); - _invocation.InvocationTarget.Returns(_target); - _invocation.Arguments.Returns([]); - - _cursor = new RetryingChangeStreamCursor(_invocation, TimeSpan.FromMilliseconds(50)); - } -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_RetryingChangeStreamCursor/when_disposing_cursor.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_RetryingChangeStreamCursor/when_disposing_cursor.cs deleted file mode 100644 index 33f0ef5a7..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_RetryingChangeStreamCursor/when_disposing_cursor.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Cratis.Arc.MongoDB.Resilience.for_RetryingChangeStreamCursor; - -public class when_disposing_cursor : given.a_retrying_change_stream_cursor -{ - void Establish() - { - _actualCursor.MoveNextAsync(Arg.Any()).Returns(Task.FromResult(true)); - } - - async Task Because() - { - _ = await _cursor.MoveNextAsync(); - _cursor.Dispose(); - } - - [Fact] void should_dispose_actual_cursor() => _actualCursor.Received(1).Dispose(); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_RetryingChangeStreamCursor/when_getting_current_before_actual_cursor_exists.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_RetryingChangeStreamCursor/when_getting_current_before_actual_cursor_exists.cs deleted file mode 100644 index dc825f885..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_RetryingChangeStreamCursor/when_getting_current_before_actual_cursor_exists.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Cratis.Arc.MongoDB.Resilience.for_RetryingChangeStreamCursor; - -public class when_getting_current_before_actual_cursor_exists : given.a_retrying_change_stream_cursor -{ - IEnumerable _result; - - void Because() => _result = _cursor.Current; - - [Fact] void should_return_empty_collection() => _result.ShouldBeEmpty(); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_RetryingChangeStreamCursor/when_getting_resume_token_before_actual_cursor_exists.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_RetryingChangeStreamCursor/when_getting_resume_token_before_actual_cursor_exists.cs deleted file mode 100644 index 25c06f1f4..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_RetryingChangeStreamCursor/when_getting_resume_token_before_actual_cursor_exists.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using MongoDB.Bson; - -namespace Cratis.Arc.MongoDB.Resilience.for_RetryingChangeStreamCursor; - -public class when_getting_resume_token_before_actual_cursor_exists : given.a_retrying_change_stream_cursor -{ - BsonDocument? _result; - - void Because() => _result = _cursor.GetResumeToken(); - - [Fact] void should_return_null() => _result.ShouldBeNull(); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_RetryingChangeStreamCursor/when_moving_next_and_collection_exists_after_retries.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_RetryingChangeStreamCursor/when_moving_next_and_collection_exists_after_retries.cs deleted file mode 100644 index 62e58129b..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_RetryingChangeStreamCursor/when_moving_next_and_collection_exists_after_retries.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Cratis.Arc.MongoDB.Resilience.for_RetryingChangeStreamCursor; - -public class when_moving_next_and_collection_exists_after_retries : given.a_retrying_change_stream_cursor -{ - bool _result; - - void Establish() - { - _actualCursor.MoveNextAsync(Arg.Any()).Returns(Task.FromResult(true)); - } - - async Task Because() => _result = await _cursor.MoveNextAsync(); - - [Fact] void should_return_true() => _result.ShouldBeTrue(); - [Fact] void should_have_retried_multiple_times() => _target.CallCount.ShouldEqual(3); - [Fact] void should_delegate_to_actual_cursor() => _actualCursor.Received(1).MoveNextAsync(Arg.Any()); -} diff --git a/Source/DotNET/MongoDB.Specs/Resilience/for_RetryingChangeStreamCursor/when_moving_next_with_cancellation.cs b/Source/DotNET/MongoDB.Specs/Resilience/for_RetryingChangeStreamCursor/when_moving_next_with_cancellation.cs deleted file mode 100644 index 9f5a5d73b..000000000 --- a/Source/DotNET/MongoDB.Specs/Resilience/for_RetryingChangeStreamCursor/when_moving_next_with_cancellation.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Cratis.Arc.MongoDB.Resilience.for_RetryingChangeStreamCursor; - -public class when_moving_next_with_cancellation : given.a_retrying_change_stream_cursor -{ - CancellationTokenSource _cancellationTokenSource; - Exception? _exception; - - void Establish() - { - _cancellationTokenSource = new CancellationTokenSource(); - _cancellationTokenSource.Cancel(); - } - - async Task Because() - { - _exception = await Catch.Exception(async () => await _cursor.MoveNextAsync(_cancellationTokenSource.Token)); - } - - [Fact] void should_throw_operation_canceled_exception() => _exception.ShouldBeOfExactType(); -} diff --git a/Source/DotNET/MongoDB/MongoCollectionExtensions.cs b/Source/DotNET/MongoDB/MongoCollectionExtensions.cs index e7a1223c5..d2d2ba965 100644 --- a/Source/DotNET/MongoDB/MongoCollectionExtensions.cs +++ b/Source/DotNET/MongoDB/MongoCollectionExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Reactive.Subjects; using System.Reflection; @@ -163,6 +164,7 @@ static ISubject ObserveSingle( }); } + [UnconditionalSuppressMessage("AOT", "IL2090", Justification = "typeof(TDocument).GetProperty uses the generic type parameter; TDocument is a MongoDB document type with preserved public properties. Source-generated Id mapping is the long-term fix (tracked in GitHub issue #2204).")] static ISubject Observe( this IMongoCollection collection, Func> findCall, diff --git a/Source/DotNET/MongoDB/MongoDB.csproj b/Source/DotNET/MongoDB/MongoDB.csproj index de1f9d48e..f591d21e1 100644 --- a/Source/DotNET/MongoDB/MongoDB.csproj +++ b/Source/DotNET/MongoDB/MongoDB.csproj @@ -10,8 +10,6 @@ - - diff --git a/Source/DotNET/MongoDB/MongoDBClientFactory.cs b/Source/DotNET/MongoDB/MongoDBClientFactory.cs index 3f3e81f78..9d9dc467d 100644 --- a/Source/DotNET/MongoDB/MongoDBClientFactory.cs +++ b/Source/DotNET/MongoDB/MongoDBClientFactory.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Concurrent; -using Castle.DynamicProxy; -using Cratis.Arc.MongoDB.Resilience; using Cratis.DependencyInjection; using Cratis.Metrics; using Microsoft.Extensions.DependencyInjection; @@ -13,8 +11,6 @@ using MongoDB.Driver; using MongoDB.Driver.Core.Configuration; using MongoDB.Driver.Core.Events; -using Polly; -using Polly.Retry; namespace Cratis.Arc.MongoDB; @@ -55,32 +51,17 @@ public class MongoDBClientFactory( /// public IMongoClient Create(string connectionString) => Create(MongoClientSettings.FromConnectionString(connectionString)); - IMongoClient CreateImplementation(MongoClientSettings settings) + MongoClient CreateImplementation(MongoClientSettings settings) { settings.DirectConnection = options.Value.DirectConnection; + settings.RetryReads = true; + settings.RetryWrites = true; settings.ClusterConfigurator = builder => ClusterConfigurator(settings, builder); logger.CreateClient(settings.Server.ToString()); #pragma warning disable CA2000 // Dispose objects before losing scope - we're returning the client - var client = new MongoClient(settings); + return new MongoClient(settings); #pragma warning restore CA2000 // Dispose objects before losing scope - - var resiliencePipeline = new ResiliencePipelineBuilder() - .AddRetry(new RetryStrategyOptions - { - ShouldHandle = args => args.Outcome.Exception is not null ? PredicateResult.True() : PredicateResult.False(), - UseJitter = true, - MaxRetryAttempts = 5, - Delay = TimeSpan.FromMilliseconds(500) - }).Build(); - - var proxyGenerator = new ProxyGenerator(); - var proxyGeneratorOptions = new ProxyGenerationOptions - { - Selector = new MongoClientInterceptorSelector(proxyGenerator, resiliencePipeline, client) - }; - - return proxyGenerator.CreateInterfaceProxyWithTarget(client, proxyGeneratorOptions); } void ClusterConfigurator(MongoClientSettings settings, ClusterBuilder builder) diff --git a/Source/DotNET/MongoDB/MongoDBDefaults.cs b/Source/DotNET/MongoDB/MongoDBDefaults.cs index 11e2b26cf..f63f56a48 100644 --- a/Source/DotNET/MongoDB/MongoDBDefaults.cs +++ b/Source/DotNET/MongoDB/MongoDBDefaults.cs @@ -1,6 +1,7 @@ // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Diagnostics.CodeAnalysis; using System.Reflection; using MongoDB.Bson; using MongoDB.Bson.Serialization; @@ -102,6 +103,9 @@ static void RegisterConventionPacks(IMongoDBBuilder builder, IEnumerable GetEnumerator() /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + [UnconditionalSuppressMessage("AOT", "IL2090", Justification = "typeof(TDocument).GetProperty uses the generic type parameter; TDocument is a MongoDB document type with preserved public properties. Source-generated query sorting is the long-term fix (tracked in GitHub issue #2204).")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Comparer.MakeGenericType at startup for document property types. Source-generated comparers are the long-term fix (tracked in GitHub issue #2204).")] void Initialize(QueryContext newQueryContext) { var oldQueryContext = _queryContext; @@ -277,4 +280,4 @@ bool ShouldAddBeforeNode(LinkedListNode<(object Id, TDocument Doucment)> node, ( } bool SortingIsEnabled() => _queryContext?.Sorting != Sorting.None; -} \ No newline at end of file +} diff --git a/Source/DotNET/MongoDB/Resilience/EmptyAsyncCursor.cs b/Source/DotNET/MongoDB/Resilience/EmptyAsyncCursor.cs deleted file mode 100644 index 4544fee2b..000000000 --- a/Source/DotNET/MongoDB/Resilience/EmptyAsyncCursor.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using MongoDB.Driver; - -namespace Cratis.Arc.MongoDB.Resilience; - -/// -/// Represents an empty that returns no documents. -/// -/// Type of document. -/// -/// Used to mimic regular MongoDB behavior when a collection doesn't exist in Azure Cosmos DB. -/// -public class EmptyAsyncCursor : IAsyncCursor -{ - /// - public IEnumerable Current => []; - - /// - public bool MoveNext(CancellationToken cancellationToken = default) => false; - - /// - public Task MoveNextAsync(CancellationToken cancellationToken = default) => Task.FromResult(false); - - /// - public void Dispose() - { - } -} diff --git a/Source/DotNET/MongoDB/Resilience/MongoClientDisposeInterceptor.cs b/Source/DotNET/MongoDB/Resilience/MongoClientDisposeInterceptor.cs deleted file mode 100644 index 246b7912b..000000000 --- a/Source/DotNET/MongoDB/Resilience/MongoClientDisposeInterceptor.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Castle.DynamicProxy; -using MongoDB.Driver; - -namespace Cratis.Arc.MongoDB.Resilience; - -/// -/// Represents an interceptor for Dispose method. -/// -public class MongoClientDisposeInterceptor : IInterceptor -{ - /// - public void Intercept(IInvocation invocation) - { - // Do nothing - we don't want to dispose the client as we want to keep a single instance across a running process - } -} \ No newline at end of file diff --git a/Source/DotNET/MongoDB/Resilience/MongoClientInterceptor.cs b/Source/DotNET/MongoDB/Resilience/MongoClientInterceptor.cs deleted file mode 100644 index a906167c0..000000000 --- a/Source/DotNET/MongoDB/Resilience/MongoClientInterceptor.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Castle.DynamicProxy; -using MongoDB.Driver; -using Polly; - -namespace Cratis.Arc.MongoDB.Resilience; - -/// -/// Represents an interceptor for . -/// -/// -/// Initializes a new instance of the class. -/// -/// for creating further proxies. -/// The to use. -/// to intercept. -public class MongoClientInterceptor( - ProxyGenerator proxyGenerator, - ResiliencePipeline resiliencePipeline, - IMongoClient mongoClient) : IInterceptor -{ - /// - public void Intercept(IInvocation invocation) - { - invocation.Proceed(); - - invocation.ReturnValue = proxyGenerator.CreateInterfaceProxyWithTarget( - (invocation.ReturnValue as IMongoDatabase)!, - new ProxyGenerationOptions - { - Selector = new MongoDatabaseInterceptorSelector(proxyGenerator, resiliencePipeline, mongoClient) - }); - } -} diff --git a/Source/DotNET/MongoDB/Resilience/MongoClientInterceptorSelector.cs b/Source/DotNET/MongoDB/Resilience/MongoClientInterceptorSelector.cs deleted file mode 100644 index 3edd8eae9..000000000 --- a/Source/DotNET/MongoDB/Resilience/MongoClientInterceptorSelector.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Reflection; -using Castle.DynamicProxy; -using MongoDB.Driver; -using Polly; - -namespace Cratis.Arc.MongoDB.Resilience; - -/// -/// Represents a selector for . -/// -/// -/// Initializes a new instance of the class. -/// -/// for creating further proxies. -/// The to use. -/// to intercept. -public class MongoClientInterceptorSelector( - ProxyGenerator proxyGenerator, - ResiliencePipeline resiliencePipeline, - IMongoClient mongoClient) : IInterceptorSelector -{ - /// - public IInterceptor[] SelectInterceptors(Type type, MethodInfo method, IInterceptor[] interceptors) - { - if (method.Name == nameof(IDisposable.Dispose)) - { - return [new MongoClientDisposeInterceptor()]; - } - - if (method.Name == nameof(IMongoClient.GetDatabase)) - { - return [new MongoClientInterceptor(proxyGenerator, resiliencePipeline, mongoClient)]; - } - return []; - } -} diff --git a/Source/DotNET/MongoDB/Resilience/MongoCollectionInterceptor.cs b/Source/DotNET/MongoDB/Resilience/MongoCollectionInterceptor.cs deleted file mode 100644 index 2e14a485f..000000000 --- a/Source/DotNET/MongoDB/Resilience/MongoCollectionInterceptor.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Castle.DynamicProxy; -using MongoDB.Driver; -using Polly; - -namespace Cratis.Arc.MongoDB.Resilience; - -/// -/// Represents an interceptor for . -/// -/// -/// Initializes a new instance of the class. -/// -/// The to use. -/// The for keeping track of open connections. -public class MongoCollectionInterceptor( - ResiliencePipeline resiliencePipeline, - SemaphoreSlim openConnectionSemaphore) : IInterceptor -{ - /// - public void Intercept(IInvocation invocation) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - invocation.ReturnValue = tcs.Task; - - var cancellationToken = ExtractCancellationToken(invocation); - -#pragma warning disable CA2012 // Use ValueTasks correctly - resiliencePipeline.ExecuteAsync( - async _ => - { - if (!await TryAcquireSemaphore(tcs, cancellationToken)) - { - return ValueTask.CompletedTask; - } - - try - { - await ExecuteMongoOperation(invocation, tcs); - } - catch (OperationCanceledException) - { - tcs.SetCanceled(); - } - catch (MongoCommandException ex) when (ex.Message.Contains(WellKnownErrorMessages.CollectionNotFound, StringComparison.OrdinalIgnoreCase)) - { - tcs.SetResult(); - } - catch (Exception ex) - { - tcs.SetException(ex); - } - finally - { - openConnectionSemaphore.Release(); - } - - return ValueTask.CompletedTask; - }, - cancellationToken); -#pragma warning restore CA2012 // Use ValueTasks correctly - } - - static CancellationToken ExtractCancellationToken(IInvocation invocation) => - invocation.Arguments.FirstOrDefault(argument => argument is CancellationToken) as CancellationToken? ?? CancellationToken.None; - - static async Task ExecuteMongoOperation(IInvocation invocation, TaskCompletionSource tcs) - { - var result = (invocation.Method.Invoke(invocation.InvocationTarget, invocation.Arguments) as Task)!; - await result.ConfigureAwait(false); - - if (result.IsCanceled) - { - tcs.SetCanceled(); - } - else - { - tcs.SetResult(); - } - } - - async Task TryAcquireSemaphore(TaskCompletionSource tcs, CancellationToken cancellationToken) - { - if (!await openConnectionSemaphore.WaitAsync(1000, cancellationToken)) - { - tcs.SetException(new TimeoutException("Failed to acquire semaphore.")); - return false; - } - return true; - } -} diff --git a/Source/DotNET/MongoDB/Resilience/MongoCollectionInterceptorForReturnValues.cs b/Source/DotNET/MongoDB/Resilience/MongoCollectionInterceptorForReturnValues.cs deleted file mode 100644 index a3a6182b7..000000000 --- a/Source/DotNET/MongoDB/Resilience/MongoCollectionInterceptorForReturnValues.cs +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Castle.DynamicProxy; -using MongoDB.Driver; -using Polly; - -namespace Cratis.Arc.MongoDB.Resilience; - -/// -/// Represents an interceptor for for methods that returns a . -/// -/// -/// Initializes a new instance of the class. -/// -/// The to use. -/// The for keeping track of open connections. -public class MongoCollectionInterceptorForReturnValues( - ResiliencePipeline resiliencePipeline, - SemaphoreSlim openConnectionSemaphore) : IInterceptor -{ - /// - public void Intercept(IInvocation invocation) - { - var returnType = invocation.Method.ReturnType.GetGenericArguments()[0]; - var taskCompletionSource = CreateTaskCompletionSource(returnType); - - invocation.ReturnValue = GetTaskFromCompletionSource(taskCompletionSource); - var cancellationToken = ExtractCancellationToken(invocation); - -#pragma warning disable CA2012 // Use ValueTasks correctly - resiliencePipeline.ExecuteAsync( - async (_) => - { - if (!await TryAcquireSemaphore(taskCompletionSource, cancellationToken)) - { - return ValueTask.CompletedTask; - } - - try - { - await ExecuteMongoOperation(invocation, taskCompletionSource); - } - catch (OperationCanceledException) - { - SetCanceled(taskCompletionSource); - } - catch (MongoCommandException ex) when (ex.Message.Contains(WellKnownErrorMessages.CollectionNotFound, StringComparison.OrdinalIgnoreCase)) - { - SetDefaultValueForCollectionNotFound(taskCompletionSource, returnType, invocation); - } - catch (Exception ex) - { - SetException(taskCompletionSource, ex); - } - finally - { - openConnectionSemaphore.Release(); - } - - return ValueTask.CompletedTask; - }, - cancellationToken); -#pragma warning restore CA2012 // Use ValueTasks correctly - } - - static object CreateTaskCompletionSource(Type returnType) - { - var taskType = typeof(TaskCompletionSource<>).MakeGenericType(returnType); - return Activator.CreateInstance(taskType, TaskCreationOptions.RunContinuationsAsynchronously)!; - } - - static Task GetTaskFromCompletionSource(object taskCompletionSource) - { - var tcsType = taskCompletionSource.GetType(); - return (tcsType.GetProperty(nameof(TaskCompletionSource.Task))!.GetValue(taskCompletionSource) as Task)!; - } - - static CancellationToken ExtractCancellationToken(IInvocation invocation) => - invocation.Arguments.FirstOrDefault(argument => argument is CancellationToken) as CancellationToken? ?? CancellationToken.None; - - static async Task ExecuteMongoOperation(IInvocation invocation, object taskCompletionSource) - { - var result = (invocation.Method.Invoke(invocation.InvocationTarget, invocation.Arguments) as Task)!; - await result.ConfigureAwait(false); - - if (result.IsCanceled) - { - SetCanceled(taskCompletionSource); - } - else - { - SetResult(taskCompletionSource, result); - } - } - - static void SetResult(object taskCompletionSource, Task result) - { - var tcsType = taskCompletionSource.GetType(); - var setResultMethod = tcsType.GetMethod(nameof(TaskCompletionSource.SetResult))!; - -#pragma warning disable CA1849 // Synchronous blocks - var taskResult = result.GetType().GetProperty(nameof(Task.Result))!.GetValue(result); - setResultMethod.Invoke(taskCompletionSource, [taskResult]); -#pragma warning restore CA1849 // Synchronous blocks - } - - static void SetException(object taskCompletionSource, Exception exception) - { - var tcsType = taskCompletionSource.GetType(); - var setExceptionMethod = tcsType.GetMethod(nameof(TaskCompletionSource.SetException), [typeof(Exception)])!; - setExceptionMethod.Invoke(taskCompletionSource, [exception]); - } - - static void SetCanceled(object taskCompletionSource) - { - var tcsType = taskCompletionSource.GetType(); - var setCanceledMethod = tcsType.GetMethod(nameof(TaskCompletionSource.SetCanceled), [])!; - setCanceledMethod.Invoke(taskCompletionSource, []); - } - - static void SetDefaultValueForCollectionNotFound(object taskCompletionSource, Type returnType, IInvocation invocation) - { - var defaultValue = CreateDefaultValueForType(returnType, invocation); - var tcsType = taskCompletionSource.GetType(); - var setResultMethod = tcsType.GetMethod(nameof(TaskCompletionSource.SetResult))!; - setResultMethod.Invoke(taskCompletionSource, [defaultValue]); - } - - static object? CreateDefaultValueForType(Type returnType, IInvocation invocation) - { - if (IsAsyncCursorType(returnType)) - { - return CreateEmptyAsyncCursor(returnType); - } - - if (IsChangeStreamCursorType(returnType)) - { - return CreateRetryingChangeStreamCursor(returnType, invocation); - } - - if (returnType.IsValueType) - { - return Activator.CreateInstance(returnType); - } - - return null; - } - - static bool IsAsyncCursorType(Type type) => - type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IAsyncCursor<>); - - static bool IsChangeStreamCursorType(Type type) => - type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IChangeStreamCursor<>); - - static object CreateEmptyAsyncCursor(Type asyncCursorType) - { - var elementType = asyncCursorType.GetGenericArguments()[0]; - var emptyAsyncCursorType = typeof(EmptyAsyncCursor<>).MakeGenericType(elementType); - return Activator.CreateInstance(emptyAsyncCursorType)!; - } - - static object CreateRetryingChangeStreamCursor(Type changeStreamCursorType, IInvocation invocation) - { - var elementType = changeStreamCursorType.GetGenericArguments()[0]; - var retryingChangeStreamCursorType = typeof(RetryingChangeStreamCursor<>).MakeGenericType(elementType); - return Activator.CreateInstance(retryingChangeStreamCursorType, invocation, TimeSpan.FromSeconds(1))!; - } - - async Task TryAcquireSemaphore(object taskCompletionSource, CancellationToken cancellationToken) - { - if (!await openConnectionSemaphore.WaitAsync(1000, cancellationToken)) - { - SetException(taskCompletionSource, new TimeoutException("Failed to acquire semaphore.")); - return false; - } - return true; - } -} diff --git a/Source/DotNET/MongoDB/Resilience/MongoCollectionInterceptorSelector.cs b/Source/DotNET/MongoDB/Resilience/MongoCollectionInterceptorSelector.cs deleted file mode 100644 index 1226852e9..000000000 --- a/Source/DotNET/MongoDB/Resilience/MongoCollectionInterceptorSelector.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Reflection; -using Castle.DynamicProxy; -using MongoDB.Driver; -using Polly; - -namespace Cratis.Arc.MongoDB.Resilience; - -/// -/// Represents a selector for . -/// -/// -/// Initializes a new instance of the class. -/// -/// The to use. -/// to intercept. -public class MongoCollectionInterceptorSelector( - ResiliencePipeline resiliencePipeline, - IMongoClient mongoClient) : IInterceptorSelector -{ - /// - public IInterceptor[] SelectInterceptors(Type type, MethodInfo method, IInterceptor[] interceptors) - { -#pragma warning disable CA2000 // Dispose objects before losing scope. - var semaphore = new SemaphoreSlim(mongoClient.Settings.MaxConnectionPoolSize / 2, mongoClient.Settings.MaxConnectionPoolSize / 2); -#pragma warning restore CA2000 // Dispose objects before losing scope. - if (method.ReturnType.IsAssignableTo(typeof(Task))) - { - if (method.ReturnType.IsGenericType) - { - return [new MongoCollectionInterceptorForReturnValues(resiliencePipeline, semaphore)]; - } - - return [new MongoCollectionInterceptor(resiliencePipeline, semaphore)]; - } - return []; - } -} diff --git a/Source/DotNET/MongoDB/Resilience/MongoDatabaseInterceptor.cs b/Source/DotNET/MongoDB/Resilience/MongoDatabaseInterceptor.cs deleted file mode 100644 index abc054213..000000000 --- a/Source/DotNET/MongoDB/Resilience/MongoDatabaseInterceptor.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Castle.DynamicProxy; -using MongoDB.Driver; -using Polly; - -namespace Cratis.Arc.MongoDB.Resilience; - -/// -/// Represents an interceptor for . -/// -/// -/// Initializes a new instance of the class. -/// -/// for creating further proxies. -/// The to use. -/// the interceptor is for. -public class MongoDatabaseInterceptor(ProxyGenerator proxyGenerator, ResiliencePipeline resiliencePipeline, IMongoClient mongoClient) : IInterceptor -{ - /// - public void Intercept(IInvocation invocation) - { - invocation.Proceed(); - - invocation.ReturnValue = proxyGenerator.CreateInterfaceProxyWithTarget( - invocation.Method.ReturnType, - invocation.ReturnValue!, - new ProxyGenerationOptions - { - Selector = new MongoCollectionInterceptorSelector(resiliencePipeline, mongoClient) - }); - } -} diff --git a/Source/DotNET/MongoDB/Resilience/MongoDatabaseInterceptorSelector.cs b/Source/DotNET/MongoDB/Resilience/MongoDatabaseInterceptorSelector.cs deleted file mode 100644 index 22c9b919b..000000000 --- a/Source/DotNET/MongoDB/Resilience/MongoDatabaseInterceptorSelector.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Reflection; -using Castle.DynamicProxy; -using MongoDB.Driver; -using Polly; - -namespace Cratis.Arc.MongoDB.Resilience; - -/// -/// Represents a selector for . -/// -/// -/// Initializes a new instance of the class. -/// -/// for creating further proxies. -/// The to use. -/// to intercept. -public class MongoDatabaseInterceptorSelector( - ProxyGenerator proxyGenerator, - ResiliencePipeline resiliencePipeline, - IMongoClient mongoClient) : IInterceptorSelector -{ - /// - public IInterceptor[] SelectInterceptors(Type type, MethodInfo method, IInterceptor[] interceptors) - { - if (method.Name == nameof(IMongoDatabase.GetCollection)) - { - return [new MongoDatabaseInterceptor(proxyGenerator, resiliencePipeline, mongoClient)]; - } - return []; - } -} diff --git a/Source/DotNET/MongoDB/Resilience/RetryingChangeStreamCursor.cs b/Source/DotNET/MongoDB/Resilience/RetryingChangeStreamCursor.cs deleted file mode 100644 index 72d9f181f..000000000 --- a/Source/DotNET/MongoDB/Resilience/RetryingChangeStreamCursor.cs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using Castle.DynamicProxy; -using MongoDB.Bson; -using MongoDB.Driver; - -namespace Cratis.Arc.MongoDB.Resilience; - -/// -/// Represents a that retries creating the actual cursor until the collection exists. -/// -/// Type of document. -/// The original invocation to retry. -/// The interval between retry attempts. -/// -/// Used to handle Azure Cosmos DB's "Collection not found" errors by periodically retrying the watch operation -/// until the collection is created, maintaining the observation alive. -/// -public class RetryingChangeStreamCursor(IInvocation invocation, TimeSpan retryInterval) : IChangeStreamCursor -{ - IChangeStreamCursor? _actualCursor; - bool _disposed; - - /// - public IEnumerable Current => _actualCursor?.Current ?? []; - - /// - public BsonDocument? GetResumeToken() => _actualCursor?.GetResumeToken(); - - /// - public bool MoveNext(CancellationToken cancellationToken = default) - { - try - { - return MoveNextAsync(cancellationToken).GetAwaiter().GetResult(); - } - catch (OperationCanceledException) - { - return false; - } - } - - /// - public async Task MoveNextAsync(CancellationToken cancellationToken = default) - { - while (!_disposed) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (_actualCursor is null) - { - if (await TryCreateActualCursor()) - { - return await _actualCursor!.MoveNextAsync(cancellationToken); - } - - await Task.Delay(retryInterval, cancellationToken); - } - else - { - return await _actualCursor.MoveNextAsync(cancellationToken); - } - } - - return false; - } - - /// - public void Dispose() - { - if (_disposed) - { - return; - } - - _disposed = true; - _actualCursor?.Dispose(); - } - - async Task TryCreateActualCursor() - { - try - { - var result = (Task>)invocation.Method.Invoke(invocation.InvocationTarget, invocation.Arguments)!; - _actualCursor = await result.ConfigureAwait(false); - return true; - } - catch (MongoCommandException ex) when (ex.Message.Contains(WellKnownErrorMessages.CollectionNotFound, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - catch (OperationCanceledException) - { - throw; - } - } -} diff --git a/Source/DotNET/MongoDB/Resilience/WellKnownErrorMessages.cs b/Source/DotNET/MongoDB/Resilience/WellKnownErrorMessages.cs deleted file mode 100644 index af6c2b2fd..000000000 --- a/Source/DotNET/MongoDB/Resilience/WellKnownErrorMessages.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Cratis. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Cratis.Arc.MongoDB.Resilience; - -/// -/// Contains known error messages used for resilience handling in MongoDB operations. -/// -public static class WellKnownErrorMessages -{ - /// - /// The error message indicating that a MongoDB collection was not found given by at least the Azure Cosmos DB MongoDB API. - /// - public const string CollectionNotFound = "Collection not found"; -} diff --git a/Source/DotNET/OpenApi.Specs/OpenApi.Specs.csproj b/Source/DotNET/OpenApi.Specs/OpenApi.Specs.csproj index bfdf91fae..adbe96010 100644 --- a/Source/DotNET/OpenApi.Specs/OpenApi.Specs.csproj +++ b/Source/DotNET/OpenApi.Specs/OpenApi.Specs.csproj @@ -6,6 +6,7 @@ false true net10.0 + false $(InterceptorsNamespaces);Microsoft.AspNetCore.OpenApi.Generated diff --git a/Source/DotNET/Swagger.Specs/Swagger.Specs.csproj b/Source/DotNET/Swagger.Specs/Swagger.Specs.csproj index 1fc9539a0..7442a7233 100644 --- a/Source/DotNET/Swagger.Specs/Swagger.Specs.csproj +++ b/Source/DotNET/Swagger.Specs/Swagger.Specs.csproj @@ -5,6 +5,7 @@ Cratis.Arc.Swagger false true + false diff --git a/Source/DotNET/Testing/Testing.csproj b/Source/DotNET/Testing/Testing.csproj index bdb95f348..f91320eb0 100644 --- a/Source/DotNET/Testing/Testing.csproj +++ b/Source/DotNET/Testing/Testing.csproj @@ -3,6 +3,7 @@ Cratis.Arc.Testing Cratis.Arc.Testing + false Testing utilities for Cratis Arc applications, providing command scenario infrastructure and assertion helpers. cratis;arc;testing;commands;cqrs diff --git a/Source/DotNET/Tools/ProxyGenerator.Build/ProxyGenerator.Build.csproj b/Source/DotNET/Tools/ProxyGenerator.Build/ProxyGenerator.Build.csproj index 16e91c637..7cf582b96 100644 --- a/Source/DotNET/Tools/ProxyGenerator.Build/ProxyGenerator.Build.csproj +++ b/Source/DotNET/Tools/ProxyGenerator.Build/ProxyGenerator.Build.csproj @@ -5,6 +5,7 @@ true tasks $(NoWarn);NU5118 + false false diff --git a/Source/DotNET/Tools/ProxyGenerator.Specs/ProxyGenerator.Specs.csproj b/Source/DotNET/Tools/ProxyGenerator.Specs/ProxyGenerator.Specs.csproj index ecbbb1ccc..13a0d7fdc 100644 --- a/Source/DotNET/Tools/ProxyGenerator.Specs/ProxyGenerator.Specs.csproj +++ b/Source/DotNET/Tools/ProxyGenerator.Specs/ProxyGenerator.Specs.csproj @@ -5,6 +5,7 @@ Cratis.Arc.ProxyGenerator false true + false true $(NoWarn);SA1649;SA1402 diff --git a/Source/DotNET/Tools/ProxyGenerator/ProxyGenerator.csproj b/Source/DotNET/Tools/ProxyGenerator/ProxyGenerator.csproj index 2f98972d2..1bb892abf 100644 --- a/Source/DotNET/Tools/ProxyGenerator/ProxyGenerator.csproj +++ b/Source/DotNET/Tools/ProxyGenerator/ProxyGenerator.csproj @@ -7,6 +7,7 @@ enable true proxygenerator + false