From f908fc888366e10be3d4d42aa7d64e94a85eff4e Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Wed, 23 Oct 2024 11:05:28 +0800 Subject: [PATCH 1/3] Prevent saga conventions from throwing on ref struct --- ..._conventions_in_an_endpoint_with_a_Saga.cs | 55 +++++++++++++++++++ src/NServiceBus.Core/Sagas/Sagas.cs | 7 +++ 2 files changed, 62 insertions(+) create mode 100644 src/NServiceBus.AcceptanceTests/Core/Conventions/When_a_ref_struct_gets_checked_by_conventions_in_an_endpoint_with_a_Saga.cs diff --git a/src/NServiceBus.AcceptanceTests/Core/Conventions/When_a_ref_struct_gets_checked_by_conventions_in_an_endpoint_with_a_Saga.cs b/src/NServiceBus.AcceptanceTests/Core/Conventions/When_a_ref_struct_gets_checked_by_conventions_in_an_endpoint_with_a_Saga.cs new file mode 100644 index 00000000000..bcf124030fc --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Core/Conventions/When_a_ref_struct_gets_checked_by_conventions_in_an_endpoint_with_a_Saga.cs @@ -0,0 +1,55 @@ +namespace NServiceBus.AcceptanceTests.Core.Conventions; + +using System; +using System.Threading.Tasks; +using AcceptanceTesting; +using EndpointTemplates; +using NUnit.Framework; + +public class When_a_ref_struct_gets_checked_by_conventions_in_an_endpoint_with_a_Saga : NServiceBusAcceptanceTest +{ + // NOTE: AssemblyRouteSource and NamespaceRouteSource do not filter out ref structs encountered in a message assembly. + // The conventions added by the Sagas feature were throwing an exception when passed a ref struct. + // See https://github.com/Particular/NServiceBus/issues/7179 for details. + // This test simulates what the RouteSource objects do so that a separate message assembly is not needed for the test. + [Test] + public void It_should_not_throw_an_exception() + => Assert.DoesNotThrowAsync( + () => Scenario.Define() + .WithEndpoint( + // Capture conventions + b => b.CustomConfig((endpoint, ctx) => ctx.Conventions = endpoint.Conventions().Conventions) + ) + .Done(ctx => !ctx.Conventions.IsMessageType(typeof(RefStruct))) + .Run() + ); + + class Context : ScenarioContext + { + public NServiceBus.Conventions Conventions { get; set; } + } + + class SomeMessage : IMessage + { + public Guid BusinessId { get; set; } + } + + public ref struct RefStruct { } + + class EndpointWithASaga : EndpointConfigurationBuilder + { + public EndpointWithASaga() => EndpointSetup(); + + class MySagaData : ContainSagaData + { + public Guid BusinessId { get; set; } + } + + class MySaga : Saga, IAmStartedByMessages + { + public Task Handle(SomeMessage message, IMessageHandlerContext context) => Task.CompletedTask; + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + => mapper.MapSaga(saga => saga.BusinessId).ToMessage(msg => msg.BusinessId); + } + } +} diff --git a/src/NServiceBus.Core/Sagas/Sagas.cs b/src/NServiceBus.Core/Sagas/Sagas.cs index aa7dfa2a048..1ff272dc7da 100644 --- a/src/NServiceBus.Core/Sagas/Sagas.cs +++ b/src/NServiceBus.Core/Sagas/Sagas.cs @@ -104,6 +104,13 @@ static bool IsCompatible(Type t, Type source) static bool IsTypeATimeoutHandledByAnySaga(Type type, IEnumerable sagas) { + // MakeGenericType() throws an exception if passed a ref struct type + // Messages cannot be ref struct types + if (type.IsByRefLike) + { + return false; + } + var timeoutHandler = typeof(IHandleTimeouts<>).MakeGenericType(type); var messageHandler = typeof(IHandleMessages<>).MakeGenericType(type); From 633381bfc2d4eeb94ff1d19ef7827ca66eea03bc Mon Sep 17 00:00:00 2001 From: Mike Minutillo Date: Wed, 23 Oct 2024 11:34:07 +0800 Subject: [PATCH 2/3] Better test --- ..._conventions_in_an_endpoint_with_a_Saga.cs | 55 ------------------- ...ntaining_a_ref_struct_and_sagas_enabled.cs | 54 ++++++++++++++++++ 2 files changed, 54 insertions(+), 55 deletions(-) delete mode 100644 src/NServiceBus.AcceptanceTests/Core/Conventions/When_a_ref_struct_gets_checked_by_conventions_in_an_endpoint_with_a_Saga.cs create mode 100644 src/NServiceBus.AcceptanceTests/Core/Conventions/When_scanning_an_assembly_containing_a_ref_struct_and_sagas_enabled.cs diff --git a/src/NServiceBus.AcceptanceTests/Core/Conventions/When_a_ref_struct_gets_checked_by_conventions_in_an_endpoint_with_a_Saga.cs b/src/NServiceBus.AcceptanceTests/Core/Conventions/When_a_ref_struct_gets_checked_by_conventions_in_an_endpoint_with_a_Saga.cs deleted file mode 100644 index bcf124030fc..00000000000 --- a/src/NServiceBus.AcceptanceTests/Core/Conventions/When_a_ref_struct_gets_checked_by_conventions_in_an_endpoint_with_a_Saga.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace NServiceBus.AcceptanceTests.Core.Conventions; - -using System; -using System.Threading.Tasks; -using AcceptanceTesting; -using EndpointTemplates; -using NUnit.Framework; - -public class When_a_ref_struct_gets_checked_by_conventions_in_an_endpoint_with_a_Saga : NServiceBusAcceptanceTest -{ - // NOTE: AssemblyRouteSource and NamespaceRouteSource do not filter out ref structs encountered in a message assembly. - // The conventions added by the Sagas feature were throwing an exception when passed a ref struct. - // See https://github.com/Particular/NServiceBus/issues/7179 for details. - // This test simulates what the RouteSource objects do so that a separate message assembly is not needed for the test. - [Test] - public void It_should_not_throw_an_exception() - => Assert.DoesNotThrowAsync( - () => Scenario.Define() - .WithEndpoint( - // Capture conventions - b => b.CustomConfig((endpoint, ctx) => ctx.Conventions = endpoint.Conventions().Conventions) - ) - .Done(ctx => !ctx.Conventions.IsMessageType(typeof(RefStruct))) - .Run() - ); - - class Context : ScenarioContext - { - public NServiceBus.Conventions Conventions { get; set; } - } - - class SomeMessage : IMessage - { - public Guid BusinessId { get; set; } - } - - public ref struct RefStruct { } - - class EndpointWithASaga : EndpointConfigurationBuilder - { - public EndpointWithASaga() => EndpointSetup(); - - class MySagaData : ContainSagaData - { - public Guid BusinessId { get; set; } - } - - class MySaga : Saga, IAmStartedByMessages - { - public Task Handle(SomeMessage message, IMessageHandlerContext context) => Task.CompletedTask; - protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) - => mapper.MapSaga(saga => saga.BusinessId).ToMessage(msg => msg.BusinessId); - } - } -} diff --git a/src/NServiceBus.AcceptanceTests/Core/Conventions/When_scanning_an_assembly_containing_a_ref_struct_and_sagas_enabled.cs b/src/NServiceBus.AcceptanceTests/Core/Conventions/When_scanning_an_assembly_containing_a_ref_struct_and_sagas_enabled.cs new file mode 100644 index 00000000000..59c2042db78 --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Core/Conventions/When_scanning_an_assembly_containing_a_ref_struct_and_sagas_enabled.cs @@ -0,0 +1,54 @@ +namespace NServiceBus.AcceptanceTests.Core.Conventions; + +using System; +using System.Threading.Tasks; +using AcceptanceTesting; +using EndpointTemplates; +using NServiceBus.AcceptanceTesting.Customization; +using NUnit.Framework; + +public class When_scanning_an_assembly_containing_a_ref_struct_and_sagas_enabled : NServiceBusAcceptanceTest +{ + [Test] + public void It_should_not_throw_an_exception() + => Assert.DoesNotThrowAsync( + () => Scenario.Define() + .WithEndpoint() + .Run() + ); + + public class SomeMessage : IMessage + { + public Guid BusinessId { get; set; } + } + + // HINT: This will get picked up by the AssemblyRouteSource created by the routing call below + // Even though it is not a message type, it is still checked by passing it to conventions. + // The conventions added by Sagas were throwing an exception when passed a ref struct. + // See https://github.com/Particular/NServiceBus/issues/7179 for details. + ref struct RefStruct { } + + class EndpointWithASaga : EndpointConfigurationBuilder + { + public EndpointWithASaga() => EndpointSetup(cfg => cfg + .ConfigureRouting() + .RouteToEndpoint( + typeof(RefStruct).Assembly, + Conventions.EndpointNamingConvention(typeof(EndpointWithASaga)) + ) + ); + + + class RealSagaToSetUpConventions : Saga, IAmStartedByMessages + { + public Task Handle(SomeMessage message, IMessageHandlerContext context) => Task.CompletedTask; + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + => mapper.MapSaga(saga => saga.BusinessId).ToMessage(msg => msg.BusinessId); + + public class RealSagaToSetUpConventionsSagaData : ContainSagaData + { + public virtual Guid BusinessId { get; set; } + } + } + } +} From 4093c3a5c5f6ffba1c962383d6a3f438307f8f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Wed, 23 Oct 2024 08:46:30 +0200 Subject: [PATCH 3/3] Tweaks --- ...embly_containing_a_ref_struct_and_sagas_enabled.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/NServiceBus.AcceptanceTests/Core/Conventions/When_scanning_an_assembly_containing_a_ref_struct_and_sagas_enabled.cs b/src/NServiceBus.AcceptanceTests/Core/Conventions/When_scanning_an_assembly_containing_a_ref_struct_and_sagas_enabled.cs index 59c2042db78..c58b3dc3da2 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Conventions/When_scanning_an_assembly_containing_a_ref_struct_and_sagas_enabled.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Conventions/When_scanning_an_assembly_containing_a_ref_struct_and_sagas_enabled.cs @@ -17,11 +17,6 @@ public void It_should_not_throw_an_exception() .Run() ); - public class SomeMessage : IMessage - { - public Guid BusinessId { get; set; } - } - // HINT: This will get picked up by the AssemblyRouteSource created by the routing call below // Even though it is not a message type, it is still checked by passing it to conventions. // The conventions added by Sagas were throwing an exception when passed a ref struct. @@ -38,7 +33,6 @@ public EndpointWithASaga() => EndpointSetup(cfg => cfg ) ); - class RealSagaToSetUpConventions : Saga, IAmStartedByMessages { public Task Handle(SomeMessage message, IMessageHandlerContext context) => Task.CompletedTask; @@ -51,4 +45,9 @@ public class RealSagaToSetUpConventionsSagaData : ContainSagaData } } } + + public class SomeMessage : IMessage + { + public Guid BusinessId { get; set; } + } }