From 1e982e223ba51a8dc8e4b3f164637087345fda8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Thu, 24 Oct 2024 14:12:52 +0200 Subject: [PATCH 1/6] Fix ref struct scan for sagas (#7187) * Prevent saga conventions from throwing on ref struct * Better test * Tweaks --------- Co-authored-by: Mike Minutillo --- ...ntaining_a_ref_struct_and_sagas_enabled.cs | 53 +++++++++++++++++++ src/NServiceBus.Core/Sagas/Sagas.cs | 39 ++++++++++++++ 2 files changed, 92 insertions(+) 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_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..c58b3dc3da2 --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Core/Conventions/When_scanning_an_assembly_containing_a_ref_struct_and_sagas_enabled.cs @@ -0,0 +1,53 @@ +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() + ); + + // 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; } + } + } + } + + public class SomeMessage : IMessage + { + public Guid BusinessId { get; set; } + } +} diff --git a/src/NServiceBus.Core/Sagas/Sagas.cs b/src/NServiceBus.Core/Sagas/Sagas.cs index 93b379f57f3..4e62aa6f720 100644 --- a/src/NServiceBus.Core/Sagas/Sagas.cs +++ b/src/NServiceBus.Core/Sagas/Sagas.cs @@ -98,6 +98,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); @@ -106,4 +113,36 @@ static bool IsTypeATimeoutHandledByAnySaga(Type type, IEnumerable sagas) Conventions conventions; } + + static bool IsSagaType(Type t) + { + return IsCompatible(t, typeof(Saga)); + } + + static bool IsSagaNotFoundHandler(Type t) + { + return IsCompatible(t, typeof(IHandleSagaNotFound)); + } + + static bool IsCompatible(Type t, Type source) + { + return source.IsAssignableFrom(t) && t != source && !t.IsAbstract && !t.IsInterface && !t.IsGenericType; + } + + 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); + + return sagas.Any(t => timeoutHandler.IsAssignableFrom(t) && !messageHandler.IsAssignableFrom(t)); + } + + Conventions conventions; } \ No newline at end of file From c31d533500a2ad6950ca90ab115b8df657cb9225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Mon, 28 Oct 2024 14:33:26 +0100 Subject: [PATCH 2/6] Handle .net framework --- ...ntaining_a_ref_struct_and_sagas_enabled.cs | 88 ++++++++++--------- src/NServiceBus.Core/Sagas/Sagas.cs | 35 +------- 2 files changed, 48 insertions(+), 75 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 c58b3dc3da2..b4de3291180 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 @@ -1,53 +1,57 @@ -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 +namespace NServiceBus.AcceptanceTests.Core.Conventions { - [Test] - public void It_should_not_throw_an_exception() - => Assert.DoesNotThrowAsync( - () => Scenario.Define() - .WithEndpoint() - .Run() - ); - - // 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 { } + using System; + using System.Threading.Tasks; + using AcceptanceTesting; + using EndpointTemplates; + using NServiceBus.AcceptanceTesting.Customization; + using NUnit.Framework; - class EndpointWithASaga : EndpointConfigurationBuilder + public class When_scanning_an_assembly_containing_a_ref_struct_and_sagas_enabled : NServiceBusAcceptanceTest { - public EndpointWithASaga() => EndpointSetup(cfg => cfg - .ConfigureRouting() - .RouteToEndpoint( - typeof(RefStruct).Assembly, - Conventions.EndpointNamingConvention(typeof(EndpointWithASaga)) - ) - ); + [Test] + public void It_should_not_throw_an_exception() + => Assert.DoesNotThrowAsync( + () => Scenario.Define() + .WithEndpoint() + .Run() + ); - class RealSagaToSetUpConventions : Saga, IAmStartedByMessages + // 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 { - 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 + class EndpointWithASaga : EndpointConfigurationBuilder + { + public EndpointWithASaga() => EndpointSetup(cfg => cfg + .ConfigureTransport() + .Routing() + .RouteToEndpoint( + typeof(RefStruct).Assembly, + Conventions.EndpointNamingConvention(typeof(EndpointWithASaga)) + ) + ); + class RealSagaToSetUpConventions : Saga, IAmStartedByMessages { - public virtual Guid BusinessId { get; set; } + 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; } + } } } - } - public class SomeMessage : IMessage - { - public Guid BusinessId { get; set; } + public class SomeMessage : IMessage + { + public Guid BusinessId { get; set; } + } } -} +} \ No newline at end of file diff --git a/src/NServiceBus.Core/Sagas/Sagas.cs b/src/NServiceBus.Core/Sagas/Sagas.cs index 4e62aa6f720..d3dfaddf514 100644 --- a/src/NServiceBus.Core/Sagas/Sagas.cs +++ b/src/NServiceBus.Core/Sagas/Sagas.cs @@ -100,11 +100,12 @@ static bool IsTypeATimeoutHandledByAnySaga(Type type, IEnumerable sagas) { // MakeGenericType() throws an exception if passed a ref struct type // Messages cannot be ref struct types +#if NET if (type.IsByRefLike) { return false; } - +#endif var timeoutHandler = typeof(IHandleTimeouts<>).MakeGenericType(type); var messageHandler = typeof(IHandleMessages<>).MakeGenericType(type); @@ -113,36 +114,4 @@ static bool IsTypeATimeoutHandledByAnySaga(Type type, IEnumerable sagas) Conventions conventions; } - - static bool IsSagaType(Type t) - { - return IsCompatible(t, typeof(Saga)); - } - - static bool IsSagaNotFoundHandler(Type t) - { - return IsCompatible(t, typeof(IHandleSagaNotFound)); - } - - static bool IsCompatible(Type t, Type source) - { - return source.IsAssignableFrom(t) && t != source && !t.IsAbstract && !t.IsInterface && !t.IsGenericType; - } - - 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); - - return sagas.Any(t => timeoutHandler.IsAssignableFrom(t) && !messageHandler.IsAssignableFrom(t)); - } - - Conventions conventions; } \ No newline at end of file From 6bcfa056cbac0f37c051bbc831052bf6e3f1d308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Mon, 28 Oct 2024 15:23:34 +0100 Subject: [PATCH 3/6] Workaround for missing strongly typed check --- src/NServiceBus.Core/Sagas/Sagas.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/NServiceBus.Core/Sagas/Sagas.cs b/src/NServiceBus.Core/Sagas/Sagas.cs index d3dfaddf514..af1b0db439e 100644 --- a/src/NServiceBus.Core/Sagas/Sagas.cs +++ b/src/NServiceBus.Core/Sagas/Sagas.cs @@ -100,12 +100,11 @@ static bool IsTypeATimeoutHandledByAnySaga(Type type, IEnumerable sagas) { // MakeGenericType() throws an exception if passed a ref struct type // Messages cannot be ref struct types -#if NET - if (type.IsByRefLike) + if (type.CustomAttributes.Any(a => a.AttributeType.Name == "IsByRefLikeAttribute")) { return false; } -#endif + var timeoutHandler = typeof(IHandleTimeouts<>).MakeGenericType(type); var messageHandler = typeof(IHandleMessages<>).MakeGenericType(type); From 216e3b18fc9c7feec1115fbfb467f455785edbbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Tue, 29 Oct 2024 08:11:13 +0100 Subject: [PATCH 4/6] Update src/NServiceBus.Core/Sagas/Sagas.cs Co-authored-by: Daniel Marbach --- src/NServiceBus.Core/Sagas/Sagas.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.Core/Sagas/Sagas.cs b/src/NServiceBus.Core/Sagas/Sagas.cs index af1b0db439e..df107f7a1d3 100644 --- a/src/NServiceBus.Core/Sagas/Sagas.cs +++ b/src/NServiceBus.Core/Sagas/Sagas.cs @@ -100,7 +100,7 @@ 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.CustomAttributes.Any(a => a.AttributeType.Name == "IsByRefLikeAttribute")) + if (type.CustomAttributes.Any(a => a.AttributeType.FullName == "System.Runtime.CompilerServices.IsByRefLikeAttribute")) { return false; } From 01fc331ba457da8ce31dccb96ede7f980cd7b609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Tue, 29 Oct 2024 15:23:20 +0100 Subject: [PATCH 5/6] Use string equals --- src/NServiceBus.Core/Sagas/Sagas.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.Core/Sagas/Sagas.cs b/src/NServiceBus.Core/Sagas/Sagas.cs index df107f7a1d3..efac035aac6 100644 --- a/src/NServiceBus.Core/Sagas/Sagas.cs +++ b/src/NServiceBus.Core/Sagas/Sagas.cs @@ -100,7 +100,7 @@ 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.CustomAttributes.Any(a => a.AttributeType.FullName == "System.Runtime.CompilerServices.IsByRefLikeAttribute")) + if (type.CustomAttributes.Any(a => a.AttributeType.FullName != null && a.AttributeType.FullName.Equals("System.Runtime.CompilerServices.IsByRefLikeAttribute"))) { return false; } From bd4903209d36799e628a33290dd77329e930fc83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=96hlund?= Date: Tue, 29 Oct 2024 15:26:50 +0100 Subject: [PATCH 6/6] Avoid null check --- src/NServiceBus.Core/Sagas/Sagas.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.Core/Sagas/Sagas.cs b/src/NServiceBus.Core/Sagas/Sagas.cs index efac035aac6..1bd51f9e474 100644 --- a/src/NServiceBus.Core/Sagas/Sagas.cs +++ b/src/NServiceBus.Core/Sagas/Sagas.cs @@ -100,7 +100,7 @@ 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.CustomAttributes.Any(a => a.AttributeType.FullName != null && a.AttributeType.FullName.Equals("System.Runtime.CompilerServices.IsByRefLikeAttribute"))) + if (type.CustomAttributes.Any(a => string.Equals(a.AttributeType.FullName, "System.Runtime.CompilerServices.IsByRefLikeAttribute"))) { return false; }