Skip to content

Commit 21e5b5e

Browse files
Fix nullability issues in saga message mapper (#6776) (#6777)
* Compile-only smoke test for API usages under nullable reference types * Change ToSaga expressions to object? * Message mappers too, otherwise messages have to be 100% non-null properties * Approve * Additional approvals and API test coverage --------- Co-authored-by: Daniel Marbach <[email protected]>
1 parent 627c0be commit 21e5b5e

6 files changed

+205
-15
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
#nullable enable
2+
3+
/**
4+
* The code in this file is not meant to be executed. It only shows idiomatic usages of NServiceBus
5+
* APIs, but with nullable reference types enabled, so that we can attempt to make sure that the
6+
* addition of nullable annotations in our public APIs doesn't create nullability warnings on
7+
* our own APIs under normal circumstances. It's sort of a mini-Snippets to provide faster
8+
* feedback than having to release an alpha package and check that Snippets in docs compile.
9+
*/
10+
11+
namespace NServiceBus.Core.Tests.API.NullableApiUsages
12+
{
13+
using System;
14+
using System.Threading;
15+
using System.Threading.Tasks;
16+
using NServiceBus.Extensibility;
17+
using NServiceBus.Logging;
18+
using NServiceBus.MessageMutator;
19+
using NServiceBus.Persistence;
20+
using NServiceBus.Pipeline;
21+
using NServiceBus.Sagas;
22+
23+
public class TopLevelApis
24+
{
25+
public async Task SetupEndpoint(CancellationToken cancellationToken = default)
26+
{
27+
var cfg = new EndpointConfiguration("EndpointName");
28+
29+
cfg.Conventions()
30+
.DefiningCommandsAs(t => t.Namespace?.EndsWith(".Commands") ?? false)
31+
.DefiningEventsAs(t => t.Namespace?.EndsWith(".Events") ?? false)
32+
.DefiningMessagesAs(t => t.Namespace?.EndsWith(".Messages") ?? false);
33+
34+
cfg.SendFailedMessagesTo("error");
35+
36+
var routing = cfg.UseTransport(new LearningTransport());
37+
routing.RouteToEndpoint(typeof(Cmd), "Destination");
38+
39+
var persistence = cfg.UsePersistence<LearningPersistence>();
40+
41+
cfg.UseSerialization<SystemJsonSerializer>()
42+
.Options(new System.Text.Json.JsonSerializerOptions());
43+
44+
// Start directly
45+
await Endpoint.Start(cfg, cancellationToken);
46+
47+
// Or create, then start
48+
var startable = await Endpoint.Create(cfg, cancellationToken);
49+
var ep = await startable.Start(cancellationToken);
50+
51+
await ep.Send(new Cmd(), cancellationToken);
52+
await ep.Publish(new Evt(), cancellationToken);
53+
await ep.Publish<Evt>(cancellationToken);
54+
}
55+
}
56+
57+
public class TestHandler : IHandleMessages<Cmd>
58+
{
59+
ILog logger;
60+
61+
public TestHandler(ILog logger)
62+
{
63+
this.logger = logger;
64+
}
65+
66+
public async Task Handle(Cmd message, IMessageHandlerContext context)
67+
{
68+
logger.Info(message.OrderId);
69+
await context.Send(new Cmd());
70+
await context.Publish(new Evt());
71+
72+
var opts = new SendOptions();
73+
opts.DelayDeliveryWith(TimeSpan.FromSeconds(5));
74+
opts.SetHeader("a", "1");
75+
}
76+
}
77+
78+
public class TestSaga : Saga<TestSagaData>,
79+
IAmStartedByMessages<Cmd>,
80+
IHandleMessages<Evt>,
81+
IHandleTimeouts<TestTimeout>
82+
{
83+
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<TestSagaData> mapper)
84+
{
85+
mapper.MapSaga(saga => saga.OrderId)
86+
.ToMessage<Cmd>(m => m.OrderId)
87+
.ToMessage<Evt>(m => m.OrderId)
88+
.ToMessageHeader<Cmd>("HeaderName");
89+
}
90+
91+
public async Task Handle(Cmd message, IMessageHandlerContext context)
92+
{
93+
await context.Send(new Cmd());
94+
await context.Publish(new Evt());
95+
Console.WriteLine(Data.OrderId);
96+
await RequestTimeout<TestTimeout>(context, TimeSpan.FromMinutes(1));
97+
MarkAsComplete();
98+
}
99+
100+
public async Task Handle(Evt message, IMessageHandlerContext context)
101+
{
102+
await context.Send(new Cmd());
103+
await context.Publish(new Evt());
104+
await context.Publish<Evt>();
105+
}
106+
107+
public Task Timeout(TestTimeout state, IMessageHandlerContext context)
108+
{
109+
Console.WriteLine(state.TimeoutData);
110+
return Task.CompletedTask;
111+
}
112+
}
113+
114+
public class TestSagaOldMapping : Saga<TestSagaData>,
115+
IAmStartedByMessages<Cmd>
116+
{
117+
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<TestSagaData> mapper)
118+
{
119+
mapper.ConfigureMapping<Cmd>(m => m.OrderId).ToSaga(s => s.OrderId);
120+
mapper.ConfigureHeaderMapping<Cmd>("HeaderName");
121+
}
122+
123+
public Task Handle(Cmd message, IMessageHandlerContext context) => throw new NotImplementedException();
124+
}
125+
126+
public class Cmd : ICommand
127+
{
128+
public string? OrderId { get; set; }
129+
}
130+
131+
public class Evt : IEvent
132+
{
133+
public string? OrderId { get; set; }
134+
}
135+
136+
public class TestSagaData : ContainSagaData
137+
{
138+
public string? OrderId { get; set; }
139+
}
140+
141+
public class TestTimeout
142+
{
143+
public string? TimeoutData { get; set; }
144+
}
145+
146+
public class NotUsedSagaFinder : ISagaFinder<TestSagaData, Cmd>
147+
{
148+
public async Task<TestSagaData?> FindBy(Cmd message, ISynchronizedStorageSession storageSession, IReadOnlyContextBag context, CancellationToken cancellationToken = default)
149+
{
150+
// Super-gross, never do this
151+
await Task.Yield();
152+
153+
if (context.TryGet<TestSagaData>(out var result))
154+
{
155+
return result;
156+
}
157+
158+
return null;
159+
}
160+
}
161+
162+
public class TestBehavior : Behavior<IIncomingLogicalMessageContext>
163+
{
164+
public override async Task Invoke(IIncomingLogicalMessageContext context, Func<Task> next)
165+
{
166+
await Task.Delay(10);
167+
await next();
168+
}
169+
}
170+
171+
public class TestIncomingMutator : IMutateIncomingMessages
172+
{
173+
public Task MutateIncoming(MutateIncomingMessageContext context) => Task.CompletedTask;
174+
}
175+
176+
public class TestIncomingTransportMutator : IMutateIncomingTransportMessages
177+
{
178+
public Task MutateIncoming(MutateIncomingTransportMessageContext context) => Task.CompletedTask;
179+
}
180+
181+
public class TestOutgoingMutator : IMutateOutgoingMessages
182+
{
183+
public Task MutateOutgoing(MutateOutgoingMessageContext context) => Task.CompletedTask;
184+
}
185+
186+
public class TestOutgoingTransportMutator : IMutateOutgoingTransportMessages
187+
{
188+
public Task MutateOutgoing(MutateOutgoingTransportMessageContext context) => Task.CompletedTask;
189+
}
190+
}

src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt

+6-6
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ namespace NServiceBus
181181
public class CorrelatedSagaPropertyMapper<TSagaData>
182182
where TSagaData : class, NServiceBus.IContainSagaData
183183
{
184-
public NServiceBus.CorrelatedSagaPropertyMapper<TSagaData> ToMessage<TMessage>(System.Linq.Expressions.Expression<System.Func<TMessage, object>> messageProperty) { }
184+
public NServiceBus.CorrelatedSagaPropertyMapper<TSagaData> ToMessage<TMessage>(System.Linq.Expressions.Expression<System.Func<TMessage, object?>> messageProperty) { }
185185
public NServiceBus.CorrelatedSagaPropertyMapper<TSagaData> ToMessageHeader<TMessage>(string headerName) { }
186186
}
187187
public class CriticalError
@@ -479,7 +479,7 @@ namespace NServiceBus
479479
public interface ICommand : NServiceBus.IMessage { }
480480
public interface IConfigureHowToFindSagaWithMessage
481481
{
482-
void ConfigureMapping<TSagaEntity, TMessage>(System.Linq.Expressions.Expression<System.Func<TSagaEntity, object>> sagaEntityProperty, System.Linq.Expressions.Expression<System.Func<TMessage, object>> messageProperty)
482+
void ConfigureMapping<TSagaEntity, TMessage>(System.Linq.Expressions.Expression<System.Func<TSagaEntity, object?>> sagaEntityProperty, System.Linq.Expressions.Expression<System.Func<TMessage, object?>> messageProperty)
483483
where TSagaEntity : NServiceBus.IContainSagaData
484484
;
485485
}
@@ -1041,8 +1041,8 @@ namespace NServiceBus
10411041
where TSagaData : class, NServiceBus.IContainSagaData
10421042
{
10431043
public NServiceBus.IToSagaExpression<TSagaData> ConfigureHeaderMapping<TMessage>(string headerName) { }
1044-
public NServiceBus.ToSagaExpression<TSagaData, TMessage> ConfigureMapping<TMessage>(System.Linq.Expressions.Expression<System.Func<TMessage, object>> messageProperty) { }
1045-
public NServiceBus.CorrelatedSagaPropertyMapper<TSagaData> MapSaga(System.Linq.Expressions.Expression<System.Func<TSagaData, object>> sagaProperty) { }
1044+
public NServiceBus.ToSagaExpression<TSagaData, TMessage> ConfigureMapping<TMessage>(System.Linq.Expressions.Expression<System.Func<TMessage, object?>> messageProperty) { }
1045+
public NServiceBus.CorrelatedSagaPropertyMapper<TSagaData> MapSaga(System.Linq.Expressions.Expression<System.Func<TSagaData, object?>> sagaProperty) { }
10461046
}
10471047
public abstract class Saga<TSagaData> : NServiceBus.Saga
10481048
where TSagaData : class, NServiceBus.IContainSagaData, new ()
@@ -1219,8 +1219,8 @@ namespace NServiceBus
12191219
public class ToSagaExpression<TSagaData, TMessage>
12201220
where TSagaData : class, NServiceBus.IContainSagaData
12211221
{
1222-
public ToSagaExpression(NServiceBus.IConfigureHowToFindSagaWithMessage sagaMessageFindingConfiguration, System.Linq.Expressions.Expression<System.Func<TMessage, object>> messageProperty) { }
1223-
public void ToSaga(System.Linq.Expressions.Expression<System.Func<TSagaData, object>> sagaEntityProperty) { }
1222+
public ToSagaExpression(NServiceBus.IConfigureHowToFindSagaWithMessage sagaMessageFindingConfiguration, System.Linq.Expressions.Expression<System.Func<TMessage, object?>> messageProperty) { }
1223+
public void ToSaga(System.Linq.Expressions.Expression<System.Func<TSagaData, object?>> sagaEntityProperty) { }
12241224
}
12251225
[System.Obsolete("Configure the transport via the TransportDefinition instance\'s properties. Will b" +
12261226
"e removed in version 9.0.0.", true)]

src/NServiceBus.Core/Sagas/CorrelatedSagaPropertyMapper.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ namespace NServiceBus
1212
public class CorrelatedSagaPropertyMapper<TSagaData> where TSagaData : class, IContainSagaData
1313
{
1414
readonly SagaPropertyMapper<TSagaData> sagaPropertyMapper;
15-
readonly Expression<Func<TSagaData, object>> sagaProperty;
15+
readonly Expression<Func<TSagaData, object?>> sagaProperty;
1616

17-
internal CorrelatedSagaPropertyMapper(SagaPropertyMapper<TSagaData> sagaPropertyMapper, Expression<Func<TSagaData, object>> sagaProperty)
17+
internal CorrelatedSagaPropertyMapper(SagaPropertyMapper<TSagaData> sagaPropertyMapper, Expression<Func<TSagaData, object?>> sagaProperty)
1818
{
1919
Guard.ThrowIfNull(sagaPropertyMapper);
2020
Guard.ThrowIfNull(sagaProperty);
@@ -30,7 +30,7 @@ internal CorrelatedSagaPropertyMapper(SagaPropertyMapper<TSagaData> sagaProperty
3030
/// <returns>
3131
/// The same mapper instance.
3232
/// </returns>
33-
public CorrelatedSagaPropertyMapper<TSagaData> ToMessage<TMessage>(Expression<Func<TMessage, object>> messageProperty)
33+
public CorrelatedSagaPropertyMapper<TSagaData> ToMessage<TMessage>(Expression<Func<TMessage, object?>> messageProperty)
3434
{
3535
sagaPropertyMapper.ConfigureMapping(messageProperty).ToSaga(sagaProperty);
3636
return this;

src/NServiceBus.Core/Sagas/IConfigureHowToFindSagaWithMessage.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@ public interface IConfigureHowToFindSagaWithMessage
1717
/// of the given type, which message property should be matched to
1818
/// which saga entity property in the persistent saga store.
1919
/// </summary>
20-
void ConfigureMapping<TSagaEntity, TMessage>(Expression<Func<TSagaEntity, object>> sagaEntityProperty, Expression<Func<TMessage, object>> messageProperty) where TSagaEntity : IContainSagaData;
20+
void ConfigureMapping<TSagaEntity, TMessage>(Expression<Func<TSagaEntity, object?>> sagaEntityProperty, Expression<Func<TMessage, object?>> messageProperty) where TSagaEntity : IContainSagaData;
2121
}
2222
}

src/NServiceBus.Core/Sagas/SagaPropertyMapper.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ internal SagaPropertyMapper(IConfigureHowToFindSagaWithMessage sagaMessageFindin
2424
/// <see cref="ToSagaExpression{TSagaData,TMessage}.ToSaga" /> to link <paramref name="messageProperty" /> with
2525
/// <typeparamref name="TSagaData" />.
2626
/// </returns>
27-
public ToSagaExpression<TSagaData, TMessage> ConfigureMapping<TMessage>(Expression<Func<TMessage, object>> messageProperty)
27+
public ToSagaExpression<TSagaData, TMessage> ConfigureMapping<TMessage>(Expression<Func<TMessage, object?>> messageProperty)
2828
{
2929
Guard.ThrowIfNull(messageProperty);
3030
return new ToSagaExpression<TSagaData, TMessage>(sagaMessageFindingConfiguration, messageProperty);
@@ -61,7 +61,7 @@ public IToSagaExpression<TSagaData> ConfigureHeaderMapping<TMessage>(string head
6161
/// <see cref="CorrelatedSagaPropertyMapper{TSagaData}.ToMessage{TMessage}"/> to map a message type to
6262
/// the correlation property.
6363
/// </returns>
64-
public CorrelatedSagaPropertyMapper<TSagaData> MapSaga(Expression<Func<TSagaData, object>> sagaProperty)
64+
public CorrelatedSagaPropertyMapper<TSagaData> MapSaga(Expression<Func<TSagaData, object?>> sagaProperty)
6565
{
6666
Guard.ThrowIfNull(sagaProperty);
6767
return new CorrelatedSagaPropertyMapper<TSagaData>(this, sagaProperty);

src/NServiceBus.Core/Sagas/ToSagaExpression.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public class ToSagaExpression<TSagaData, TMessage> where TSagaData : class, ICon
1313
/// <summary>
1414
/// Initializes a new instance of <see cref="ToSagaExpression{TSagaData,TMessage}" />.
1515
/// </summary>
16-
public ToSagaExpression(IConfigureHowToFindSagaWithMessage sagaMessageFindingConfiguration, Expression<Func<TMessage, object>> messageProperty)
16+
public ToSagaExpression(IConfigureHowToFindSagaWithMessage sagaMessageFindingConfiguration, Expression<Func<TMessage, object?>> messageProperty)
1717
{
1818
Guard.ThrowIfNull(sagaMessageFindingConfiguration);
1919
Guard.ThrowIfNull(messageProperty);
@@ -26,13 +26,13 @@ public ToSagaExpression(IConfigureHowToFindSagaWithMessage sagaMessageFindingCon
2626
/// Defines the property on the saga data to which the message property should be mapped.
2727
/// </summary>
2828
/// <param name="sagaEntityProperty">The property to map.</param>
29-
public void ToSaga(Expression<Func<TSagaData, object>> sagaEntityProperty)
29+
public void ToSaga(Expression<Func<TSagaData, object?>> sagaEntityProperty)
3030
{
3131
Guard.ThrowIfNull(sagaEntityProperty);
3232
sagaMessageFindingConfiguration.ConfigureMapping(sagaEntityProperty, messageProperty);
3333
}
3434

35-
readonly Expression<Func<TMessage, object>> messageProperty;
35+
readonly Expression<Func<TMessage, object?>> messageProperty;
3636
readonly IConfigureHowToFindSagaWithMessage sagaMessageFindingConfiguration;
3737
}
3838
}

0 commit comments

Comments
 (0)