Skip to content

Commit 4dcfdea

Browse files
DavidBoikeandreasohlunddanielmarbach
authored
Sagas with custom finders or not found handlers can't be tested (#811) (#813)
* Allows sagas with custom finders to be tested * Simplify * Minor cleanup * Also handle not found handler mappings --------- Co-authored-by: Andreas Öhlund <andreas.ohlund@particular.net> Co-authored-by: Daniel Marbach <danielmarbach@users.noreply.github.com>
1 parent 176642a commit 4dcfdea

File tree

3 files changed

+173
-17
lines changed

3 files changed

+173
-17
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
namespace NServiceBus.Testing.Tests.Sagas;
2+
3+
using System;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Extensibility;
7+
using NServiceBus.Sagas;
8+
using NUnit.Framework;
9+
using Persistence;
10+
11+
[TestFixture]
12+
public class CustomFinder
13+
{
14+
[Test]
15+
public async Task TestSagaWithCustomFinder()
16+
{
17+
var testableSaga = new TestableSaga<SagaWithCustomFinder, CustomFinderSagaData>();
18+
19+
var placeResult = await testableSaga.Handle(new OrderPlaced { OrderId = "abc" });
20+
21+
var exception = Assert.ThrowsAsync<NotSupportedException>(async () => await testableSaga.Handle(new OrderBilled { OrderId = "abc" }));
22+
23+
Assert.Multiple(() =>
24+
{
25+
Assert.That(placeResult.Completed, Is.False);
26+
Assert.That(placeResult.SagaDataSnapshot.Placed, Is.True);
27+
Assert.That(placeResult.SagaDataSnapshot.Billed, Is.False);
28+
Assert.That(exception?.Message, Contains.Substring("custom saga finder"));
29+
});
30+
}
31+
32+
public class SagaWithCustomFinder : Saga<CustomFinderSagaData>,
33+
IAmStartedByMessages<OrderPlaced>,
34+
IAmStartedByMessages<OrderBilled>
35+
{
36+
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<CustomFinderSagaData> mapper)
37+
{
38+
mapper.MapSaga(saga => saga.OrderId)
39+
.ToMessage<OrderPlaced>(msg => msg.OrderId);
40+
41+
mapper.ConfigureFinderMapping<OrderBilled, MyFinder>();
42+
}
43+
44+
public class MyFinder : ISagaFinder<CustomFinderSagaData, OrderBilled>
45+
{
46+
public Task<CustomFinderSagaData> FindBy(OrderBilled message, ISynchronizedStorageSession storageSession, IReadOnlyContextBag context, CancellationToken cancellationToken = new CancellationToken()) => throw new NotImplementedException();
47+
}
48+
49+
public Task Handle(OrderPlaced message, IMessageHandlerContext context) => Task.FromResult(Data.Placed = true);
50+
51+
public Task Handle(OrderBilled message, IMessageHandlerContext context) => Task.FromResult(Data.Billed = true);
52+
}
53+
54+
public class CustomFinderSagaData : ContainSagaData
55+
{
56+
public string OrderId { get; set; }
57+
public bool Placed { get; set; }
58+
public bool Billed { get; set; }
59+
}
60+
61+
public class OrderPlaced : IEvent
62+
{
63+
public string OrderId { get; set; }
64+
}
65+
66+
public class OrderBilled : IEvent
67+
{
68+
public string OrderId { get; set; }
69+
}
70+
71+
public class OrderShipped : IEvent
72+
{
73+
public string OrderId { get; set; }
74+
}
75+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
namespace NServiceBus.Testing.Tests.Sagas;
2+
3+
using System;
4+
using System.Threading.Tasks;
5+
using NUnit.Framework;
6+
7+
[TestFixture]
8+
public class NotFoundHandler
9+
{
10+
[Test]
11+
public async Task TestSagaWithNotFoundHandler()
12+
{
13+
var testableSaga = new TestableSaga<SagaWithCustomFinder, CustomFinderSagaData>();
14+
15+
var placeResult = await testableSaga.Handle(new OrderPlaced { OrderId = "abc" });
16+
17+
var exception = Assert.ThrowsAsync<Exception>(async () => await testableSaga.Handle(new OrderBilled { OrderId = "abc" }));
18+
19+
Assert.Multiple(() =>
20+
{
21+
Assert.That(placeResult.Completed, Is.True);
22+
Assert.That(placeResult.SagaDataSnapshot.Placed, Is.True);
23+
Assert.That(placeResult.SagaDataSnapshot.Billed, Is.False);
24+
Assert.That(exception?.Message, Contains.Substring("Saga not found").And.Contains("not allowed to start the saga"));
25+
});
26+
}
27+
28+
public class SagaWithCustomFinder : Saga<CustomFinderSagaData>,
29+
IAmStartedByMessages<OrderPlaced>,
30+
IHandleMessages<OrderBilled>
31+
{
32+
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<CustomFinderSagaData> mapper)
33+
{
34+
mapper.MapSaga(saga => saga.OrderId)
35+
.ToMessage<OrderPlaced>(msg => msg.OrderId)
36+
.ToMessage<OrderBilled>(msg => msg.OrderId);
37+
38+
mapper.ConfigureNotFoundHandler<NotFoundHandlerClass>();
39+
}
40+
41+
public Task Handle(OrderPlaced message, IMessageHandlerContext context)
42+
{
43+
Data.Placed = true;
44+
MarkAsComplete();
45+
return Task.CompletedTask;
46+
}
47+
48+
public async Task Handle(OrderBilled message, IMessageHandlerContext context)
49+
{
50+
Data.Placed = true;
51+
await context.Send(new OrderBilled { OrderId = message.OrderId });
52+
MarkAsComplete(); // Saga won't be around to get message
53+
}
54+
}
55+
56+
public class NotFoundHandlerClass : ISagaNotFoundHandler
57+
{
58+
public Task Handle(object message, IMessageProcessingContext context) => Task.CompletedTask;
59+
}
60+
61+
public class CustomFinderSagaData : ContainSagaData
62+
{
63+
public string OrderId { get; set; }
64+
public bool Placed { get; set; }
65+
public bool Billed { get; set; }
66+
}
67+
68+
public class OrderPlaced : IEvent
69+
{
70+
public string OrderId { get; set; }
71+
}
72+
73+
public class OrderBilled : IEvent
74+
{
75+
public string OrderId { get; set; }
76+
}
77+
}

src/NServiceBus.Testing/Sagas/SagaMapper.cs

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using System;
44
using System.Collections.Concurrent;
55
using System.Collections.Generic;
6-
using System.Collections.ObjectModel;
76
using System.Linq;
87
using System.Linq.Expressions;
98
using System.Reflection;
@@ -12,10 +11,10 @@
1211

1312
class SagaMapper
1413
{
15-
static readonly ConcurrentDictionary<Type, SagaMapper> sagaMappers = new ConcurrentDictionary<Type, SagaMapper>();
14+
static readonly ConcurrentDictionary<Type, SagaMapper> sagaMappers = new();
1615

1716
readonly SagaMetadata metadata;
18-
readonly IReadOnlyDictionary<Type, Func<QueuedSagaMessage, object>> mappings;
17+
readonly IReadOnlyDictionary<Type, SagaMapping> mappings;
1918
readonly SagaMetadata.CorrelationPropertyMetadata correlationProperty;
2019
readonly PropertyInfo correlationPropertyInfo;
2120
readonly ConcurrentDictionary<(Type messageType, string methodName), MethodInfo> handlerMethods;
@@ -65,50 +64,55 @@ public SagaMessage GetMessageMetadata(Type messageType)
6564

6665
public object GetMessageMappedValue(QueuedSagaMessage message)
6766
{
68-
if (mappings.TryGetValue(message.Type, out var mapping))
67+
if (!mappings.TryGetValue(message.Type, out var mapping))
6968
{
70-
return mapping(message);
69+
throw new Exception("No mapped value found from message, could not look up saga data.");
7170
}
7271

73-
throw new Exception("No mapped value found from message, could not look up saga data.");
72+
return mapping.IsCustomFinder ? throw new NotSupportedException("Testing saga invocations with a custom saga finder is currently not supported") : mapping.Map(message);
7473
}
7574

76-
public void SetCorrelationPropertyValue(IContainSagaData sagaEntity, object value)
77-
=> correlationPropertyInfo.SetValue(sagaEntity, value);
75+
public void SetCorrelationPropertyValue(IContainSagaData sagaEntity, object value) => correlationPropertyInfo.SetValue(sagaEntity, value);
7876

7977
public Task InvokeHandlerMethod<TSaga>(TSaga saga, string methodName, QueuedSagaMessage message, TestableMessageHandlerContext context)
8078
{
8179
var key = (message.Type, methodName);
8280
var handlerMethodInfo = handlerMethods.GetOrAdd(key, newKey =>
8381
{
84-
var handlerTypes = new Type[] { newKey.messageType, typeof(IMessageHandlerContext) };
82+
var handlerTypes = new[] { newKey.messageType, typeof(IMessageHandlerContext) };
8583
return typeof(TSaga).GetMethod(newKey.methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, handlerTypes, null);
8684
});
8785

8886
var invokeTask = handlerMethodInfo.Invoke(saga, [message.Message, context]) as Task;
8987
return invokeTask;
9088
}
9189

92-
class MappingReader : IConfigureHowToFindSagaWithMessage, IConfigureHowToFindSagaWithMessageHeaders
90+
class MappingReader : IConfigureHowToFindSagaWithMessage, IConfigureHowToFindSagaWithMessageHeaders, IConfigureHowToFindSagaWithFinder, IConfigureSagaNotFoundHandler
9391
{
94-
readonly Dictionary<Type, Func<QueuedSagaMessage, object>> mappings = [];
92+
readonly Dictionary<Type, SagaMapping> mappings = [];
9593

9694
void IConfigureHowToFindSagaWithMessage.ConfigureMapping<TSagaEntity, TMessage>(Expression<Func<TSagaEntity, object>> sagaEntityProperty, Expression<Func<TMessage, object>> messageProperty)
9795
{
9896
Func<TMessage, object> compiledExpression = messageProperty.Compile();
9997
object GetValueFromMessage(QueuedSagaMessage message) => compiledExpression((TMessage)message.Message);
100-
mappings.Add(typeof(TMessage), GetValueFromMessage);
98+
mappings.Add(typeof(TMessage), new SagaMapping(GetValueFromMessage, false));
10199
}
102100

103101
void IConfigureHowToFindSagaWithMessageHeaders.ConfigureMapping<TSagaEntity, TMessage>(Expression<Func<TSagaEntity, object>> sagaEntityProperty, string headerName)
104102
{
105-
object GetValueFromMessage(QueuedSagaMessage message)
106-
=> message.Headers.GetValueOrDefault(headerName);
103+
object GetValueFromMessage(QueuedSagaMessage message) => message.Headers.GetValueOrDefault(headerName);
104+
mappings.Add(typeof(TMessage), new SagaMapping(GetValueFromMessage, false));
105+
}
106+
107+
void IConfigureHowToFindSagaWithFinder.ConfigureMapping<TSagaEntity, TMessage, TFinder>() => mappings.Add(typeof(TMessage), new SagaMapping(null, true));
107108

108-
mappings.Add(typeof(TMessage), GetValueFromMessage);
109+
public void ConfigureSagaNotFoundHandler<TNotFoundHandler>() where TNotFoundHandler : ISagaNotFoundHandler
110+
{
111+
// Do nothing, framework doesn't explicitly test not found handlers
109112
}
110113

111-
public IReadOnlyDictionary<Type, Func<QueuedSagaMessage, object>> GetMappings() =>
112-
new ReadOnlyDictionary<Type, Func<QueuedSagaMessage, object>>(mappings);
114+
public IReadOnlyDictionary<Type, SagaMapping> GetMappings() => mappings.AsReadOnly();
113115
}
116+
117+
record SagaMapping(Func<QueuedSagaMessage, object> Map, bool IsCustomFinder);
114118
}

0 commit comments

Comments
 (0)