1
0
mirror of synced 2024-11-22 01:13:49 +00:00

Implemented nullable reference types support

This commit is contained in:
Mark van Renswoude 2022-11-23 09:13:38 +01:00
parent bcdb376256
commit 97672f4321
96 changed files with 706 additions and 510 deletions

View File

@ -4,6 +4,7 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<RootNamespace>_01_PublishSubscribe</RootNamespace> <RootNamespace>_01_PublishSubscribe</RootNamespace>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -4,6 +4,7 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<RootNamespace>_02_DeclareDurableQueues</RootNamespace> <RootNamespace>_02_DeclareDurableQueues</RootNamespace>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -4,6 +4,7 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<RootNamespace>_03_FlowRequestResponse</RootNamespace> <RootNamespace>_03_FlowRequestResponse</RootNamespace>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -15,9 +15,9 @@ namespace _03_FlowRequestResponse
private readonly IFlowProvider flowProvider; private readonly IFlowProvider flowProvider;
private readonly IExampleState exampleState; private readonly IExampleState exampleState;
public string FirstQuote; public string? FirstQuote;
public string SecondQuote; public string? SecondQuote;
public string ThirdQuote; public string? ThirdQuote;
public ParallelFlowController(IFlowProvider flowProvider, IExampleState exampleState) public ParallelFlowController(IFlowProvider flowProvider, IExampleState exampleState)

View File

@ -4,6 +4,7 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<RootNamespace>_04_Transient</RootNamespace> <RootNamespace>_04_Transient</RootNamespace>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -4,6 +4,7 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<RootNamespace>_05_SpeedTest</RootNamespace> <RootNamespace>_05_SpeedTest</RootNamespace>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -4,6 +4,7 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<RootNamespace>_06_StatelessRequestResponse</RootNamespace> <RootNamespace>_06_StatelessRequestResponse</RootNamespace>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -4,6 +4,7 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<RootNamespace>_07_ParallelizationTest</RootNamespace> <RootNamespace>_07_ParallelizationTest</RootNamespace>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -107,7 +107,7 @@ namespace _07_ParallelizationTest
private int count; private int count;
private readonly object waitLock = new(); private readonly object waitLock = new();
private TaskCompletionSource<bool> batchReachedTask = new(); private TaskCompletionSource<bool> batchReachedTask = new();
private Timer messageExpectedTimer; private Timer? messageExpectedTimer;
private readonly TimeSpan messageExpectedTimeout = TimeSpan.FromMilliseconds(5000); private readonly TimeSpan messageExpectedTimeout = TimeSpan.FromMilliseconds(5000);

View File

@ -4,6 +4,7 @@
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<RootNamespace>_08_MessageHandlerLogging</RootNamespace> <RootNamespace>_08_MessageHandlerLogging</RootNamespace>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -3,6 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using Autofac; using Autofac;
using Autofac.Builder; using Autofac.Builder;
@ -14,21 +15,20 @@ namespace Tapeti.Autofac
/// </summary> /// </summary>
public class AutofacDependencyResolver : IDependencyContainer public class AutofacDependencyResolver : IDependencyContainer
{ {
private ContainerBuilder containerBuilder; private ContainerBuilder? containerBuilder;
private IContainer container; private IContainer? container;
/// <summary> /// <summary>
/// The built container. Either set directly, or use the Build method to built the /// The built container. Either set directly, or use the Build method to build the
/// update this reference. /// update this reference.
/// </summary> /// </summary>
public IContainer Container public IContainer Container
{ {
get => container; get => container ?? throw new ArgumentNullException(nameof(container));
set set
{ {
container = value; container = value;
if (value != null)
containerBuilder = null; containerBuilder = null;
} }
} }
@ -50,7 +50,7 @@ namespace Tapeti.Autofac
CheckContainerBuilder(); CheckContainerBuilder();
Container = containerBuilder.Build(options); Container = containerBuilder.Build(options);
return container; return Container;
} }
@ -141,6 +141,7 @@ namespace Tapeti.Autofac
} }
[MemberNotNull(nameof(containerBuilder))]
private void CheckContainerBuilder() private void CheckContainerBuilder()
{ {
if (containerBuilder == null) if (containerBuilder == null)

View File

@ -12,6 +12,7 @@
<PackageIcon>Tapeti.SimpleInjector.png</PackageIcon> <PackageIcon>Tapeti.SimpleInjector.png</PackageIcon>
<Version>2.0.0</Version> <Version>2.0.0</Version>
<LangVersion>9</LangVersion> <LangVersion>9</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -12,6 +12,7 @@
<PackageIcon>Tapeti.SimpleInjector.png</PackageIcon> <PackageIcon>Tapeti.SimpleInjector.png</PackageIcon>
<Version>2.0.0</Version> <Version>2.0.0</Version>
<LangVersion>9</LangVersion> <LangVersion>9</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -13,6 +13,9 @@ namespace Tapeti.DataAnnotations
/// <inheritdoc /> /// <inheritdoc />
public ValueTask Handle(IMessageContext context, Func<ValueTask> next) public ValueTask Handle(IMessageContext context, Func<ValueTask> next)
{ {
if (context.Message == null)
return next();
var validationContext = new ValidationContext(context.Message); var validationContext = new ValidationContext(context.Message);
Validator.ValidateObject(context.Message, validationContext, true); Validator.ValidateObject(context.Message, validationContext, true);

View File

@ -12,6 +12,7 @@
<PackageIcon>Tapeti.DataAnnotations.png</PackageIcon> <PackageIcon>Tapeti.DataAnnotations.png</PackageIcon>
<Version>2.0.0</Version> <Version>2.0.0</Version>
<LangVersion>9</LangVersion> <LangVersion>9</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Tapeti.Config; using Tapeti.Config;
// ReSharper disable UnusedMember.Global // ReSharper disable UnusedMember.Global
@ -45,7 +46,7 @@ namespace Tapeti.Flow.SQL
public IEnumerable<object> GetMiddleware(IDependencyResolver dependencyResolver) public IEnumerable<object> GetMiddleware(IDependencyResolver dependencyResolver)
{ {
return null; return Enumerable.Empty<object>();
} }
} }
} }

View File

@ -58,6 +58,7 @@ namespace Tapeti.Flow.SQL
var stateJson = flowReader.GetString(2); var stateJson = flowReader.GetString(2);
var state = JsonConvert.DeserializeObject<T>(stateJson); var state = JsonConvert.DeserializeObject<T>(stateJson);
if (state != null)
result.Add(new FlowRecord<T>(flowID, creationTime, state)); result.Add(new FlowRecord<T>(flowID, creationTime, state));
} }

View File

@ -38,11 +38,13 @@ namespace Tapeti.Flow.SQL
/// <summary> /// <summary>
/// Extracts alls SqlExceptions from the main and inner or aggregate exceptions /// Extracts alls SqlExceptions from the main and inner or aggregate exceptions
/// </summary> /// </summary>
public static IEnumerable<SqlException> ExtractSqlExceptions(Exception e) public static IEnumerable<SqlException> ExtractSqlExceptions(Exception exception)
{ {
while (e != null) var exceptionHead = exception;
while (exceptionHead != null)
{ {
switch (e) switch (exceptionHead)
{ {
case AggregateException aggregateException: case AggregateException aggregateException:
foreach (var innerException in aggregateException.InnerExceptions) foreach (var innerException in aggregateException.InnerExceptions)
@ -56,7 +58,8 @@ namespace Tapeti.Flow.SQL
yield return sqlException; yield return sqlException;
break; break;
} }
e = e.InnerException;
exceptionHead = exceptionHead.InnerException;
} }
} }
@ -66,12 +69,14 @@ namespace Tapeti.Flow.SQL
/// </summary> /// </summary>
public static IEnumerable<SqlError> UnwrapSqlErrors(SqlException exception) public static IEnumerable<SqlError> UnwrapSqlErrors(SqlException exception)
{ {
while (exception != null) var exceptionHead = exception;
while (exceptionHead != null)
{ {
foreach (SqlError error in exception.Errors) foreach (SqlError error in exceptionHead.Errors)
yield return error; yield return error;
exception = exception.InnerException as SqlException; exceptionHead = exceptionHead.InnerException as SqlException;
} }
} }

View File

@ -54,7 +54,7 @@ namespace Tapeti.Flow.SQL
returnValue = await callback(); returnValue = await callback();
}); });
return returnValue; return returnValue!;
} }
} }
} }

View File

@ -12,6 +12,7 @@
<PackageIcon>Tapeti.Flow.SQL.png</PackageIcon> <PackageIcon>Tapeti.Flow.SQL.png</PackageIcon>
<Version>2.0.0</Version> <Version>2.0.0</Version>
<LangVersion>9</LangVersion> <LangVersion>9</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@ -15,7 +15,7 @@ namespace Tapeti.Flow
/// <param name="config"></param> /// <param name="config"></param>
/// <param name="flowRepository">An optional IFlowRepository implementation to persist flow state. If not provided, flow state will be lost when the application restarts.</param> /// <param name="flowRepository">An optional IFlowRepository implementation to persist flow state. If not provided, flow state will be lost when the application restarts.</param>
/// <returns></returns> /// <returns></returns>
public static ITapetiConfigBuilder WithFlow(this ITapetiConfigBuilder config, IFlowRepository flowRepository = null) public static ITapetiConfigBuilder WithFlow(this ITapetiConfigBuilder config, IFlowRepository? flowRepository = null)
{ {
config.Use(new FlowExtension(flowRepository)); config.Use(new FlowExtension(flowRepository));
return config; return config;

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -46,6 +47,9 @@ namespace Tapeti.Flow.Default
{ {
context.Result.SetHandler(async (messageContext, value) => context.Result.SetHandler(async (messageContext, value) =>
{ {
if (value == null)
throw new InvalidOperationException("Return value should be a Task, not null");
await (Task)value; await (Task)value;
await HandleParallelResponse(messageContext); await HandleParallelResponse(messageContext);
}); });
@ -54,6 +58,10 @@ namespace Tapeti.Flow.Default
{ {
context.Result.SetHandler(async (messageContext, value) => context.Result.SetHandler(async (messageContext, value) =>
{ {
if (value == null)
// ValueTask is a struct and should never be null
throw new UnreachableException("Return value should be a ValueTask, not null");
await (ValueTask)value; await (ValueTask)value;
await HandleParallelResponse(messageContext); await HandleParallelResponse(messageContext);
}); });
@ -82,14 +90,22 @@ namespace Tapeti.Flow.Default
switch (taskType) switch (taskType)
{ {
case TaskType.None: case TaskType.None:
context.Result.SetHandler((messageContext, value) => HandleYieldPoint(messageContext, (IYieldPoint)value)); context.Result.SetHandler((messageContext, value) =>
{
if (value == null)
throw new InvalidOperationException("Return value should be an IYieldPoint, not null");
return HandleYieldPoint(messageContext, (IYieldPoint)value);
});
break; break;
case TaskType.Task: case TaskType.Task:
context.Result.SetHandler(async (messageContext, value) => context.Result.SetHandler(async (messageContext, value) =>
{ {
if (value == null)
throw new InvalidOperationException("Return value should be a Task<IYieldPoint>, not null");
var yieldPoint = await (Task<IYieldPoint>)value; var yieldPoint = await (Task<IYieldPoint>)value;
if (yieldPoint != null)
await HandleYieldPoint(messageContext, yieldPoint); await HandleYieldPoint(messageContext, yieldPoint);
}); });
break; break;
@ -97,8 +113,11 @@ namespace Tapeti.Flow.Default
case TaskType.ValueTask: case TaskType.ValueTask:
context.Result.SetHandler(async (messageContext, value) => context.Result.SetHandler(async (messageContext, value) =>
{ {
if (value == null)
// ValueTask is a struct and should never be null
throw new UnreachableException("Return value should be a ValueTask<IYieldPoint>, not null");
var yieldPoint = await (ValueTask<IYieldPoint>)value; var yieldPoint = await (ValueTask<IYieldPoint>)value;
if (yieldPoint != null)
await HandleYieldPoint(messageContext, yieldPoint); await HandleYieldPoint(messageContext, yieldPoint);
}); });
break; break;
@ -140,7 +159,7 @@ namespace Tapeti.Flow.Default
} }
private static object ParallelRequestParameterFactory(IMessageContext context) private static object? ParallelRequestParameterFactory(IMessageContext context)
{ {
var flowHandler = context.Config.DependencyResolver.Resolve<IFlowHandler>(); var flowHandler = context.Config.DependencyResolver.Resolve<IFlowHandler>();
return flowHandler.GetParallelRequest(new FlowHandlerContext(context)); return flowHandler.GetParallelRequest(new FlowHandlerContext(context));

View File

@ -6,25 +6,49 @@ namespace Tapeti.Flow.Default
{ {
internal class FlowContext : IDisposable internal class FlowContext : IDisposable
{ {
public IFlowHandlerContext HandlerContext { get; set; } private readonly IFlowHandlerContext? handlerContext;
public IFlowStateLock FlowStateLock { get; set; } private IFlowStateLock? flowStateLock;
public FlowState FlowState { get; set; } private FlowState? flowState;
public IFlowHandlerContext HandlerContext => handlerContext ?? throw new InvalidOperationException("FlowContext does not have a HandlerContext");
public IFlowStateLock FlowStateLock => flowStateLock ?? throw new InvalidOperationException("FlowContext does not have a FlowStateLock");
public FlowState FlowState => flowState ?? throw new InvalidOperationException("FlowContext does not have a FlowState");
public bool HasFlowStateAndLock => flowState != null && flowStateLock != null;
public Guid ContinuationID { get; set; } public Guid ContinuationID { get; set; }
public ContinuationMetadata ContinuationMetadata { get; set; } public ContinuationMetadata? ContinuationMetadata { get; set; }
private int storeCalled; private int storeCalled;
private int deleteCalled; private int deleteCalled;
public FlowContext(IFlowHandlerContext handlerContext, FlowState flowState, IFlowStateLock flowStateLock)
{
this.flowState = flowState;
this.flowStateLock = flowStateLock;
this.handlerContext = handlerContext;
}
public FlowContext(IFlowHandlerContext handlerContext)
{
this.handlerContext = handlerContext;
}
public void SetFlowState(FlowState newFlowState, IFlowStateLock newFlowStateLock)
{
flowState = newFlowState;
flowStateLock = newFlowStateLock;
}
public ValueTask Store(bool persistent) public ValueTask Store(bool persistent)
{ {
storeCalled++; storeCalled++;
if (HandlerContext == null) throw new ArgumentNullException(nameof(HandlerContext));
if (FlowState == null) throw new ArgumentNullException(nameof(FlowState));
if (FlowStateLock == null) throw new ArgumentNullException(nameof(FlowStateLock));
FlowState.Data = Newtonsoft.Json.JsonConvert.SerializeObject(HandlerContext.Controller); FlowState.Data = Newtonsoft.Json.JsonConvert.SerializeObject(HandlerContext.Controller);
return FlowStateLock.StoreFlowState(FlowState, persistent); return FlowStateLock.StoreFlowState(FlowState, persistent);
} }
@ -32,7 +56,7 @@ namespace Tapeti.Flow.Default
public ValueTask Delete() public ValueTask Delete()
{ {
deleteCalled++; deleteCalled++;
return FlowStateLock?.DeleteFlowState() ?? default; return flowStateLock?.DeleteFlowState() ?? default;
} }
public bool IsStoredOrDeleted() public bool IsStoredOrDeleted()
@ -43,7 +67,7 @@ namespace Tapeti.Flow.Default
public void EnsureStoreOrDeleteIsCalled() public void EnsureStoreOrDeleteIsCalled()
{ {
if (!IsStoredOrDeleted()) if (!IsStoredOrDeleted())
throw new InvalidProgramException("Neither Store nor Delete are called for the state of the current flow. FlowID = " + FlowStateLock?.FlowID); throw new InvalidProgramException("Neither Store nor Delete are called for the state of the current flow. FlowID = " + flowStateLock?.FlowID);
Debug.Assert(storeCalled <= 1, "Store called more than once!"); Debug.Assert(storeCalled <= 1, "Store called more than once!");
Debug.Assert(deleteCalled <= 1, "Delete called more than once!"); Debug.Assert(deleteCalled <= 1, "Delete called more than once!");
@ -51,7 +75,7 @@ namespace Tapeti.Flow.Default
public void Dispose() public void Dispose()
{ {
FlowStateLock?.Dispose(); flowStateLock?.Dispose();
} }
} }
} }

View File

@ -34,7 +34,11 @@ namespace Tapeti.Flow.Default
if (context.TryGet<FlowMessageContextPayload>(out var flowPayload)) if (context.TryGet<FlowMessageContextPayload>(out var flowPayload))
{ {
if (controllerPayload.Controller == null)
throw new InvalidOperationException("Controller is not available (method is static?)");
var flowContext = flowPayload.FlowContext; var flowContext = flowPayload.FlowContext;
if (!string.IsNullOrEmpty(flowContext.FlowState.Data))
Newtonsoft.Json.JsonConvert.PopulateObject(flowContext.FlowState.Data, controllerPayload.Controller); Newtonsoft.Json.JsonConvert.PopulateObject(flowContext.FlowState.Data, controllerPayload.Controller);
// Remove Continuation now because the IYieldPoint result handler will store the new state // Remove Continuation now because the IYieldPoint result handler will store the new state
@ -65,11 +69,11 @@ namespace Tapeti.Flow.Default
var flowContext = flowPayload.FlowContext; var flowContext = flowPayload.FlowContext;
if (flowContext.ContinuationMetadata.MethodName != MethodSerializer.Serialize(controllerPayload.Binding.Method)) if (flowContext.ContinuationMetadata == null || flowContext.ContinuationMetadata.MethodName != MethodSerializer.Serialize(controllerPayload.Binding.Method))
// Do not call when the controller method was filtered, if the same message has two methods // Do not call when the controller method was filtered, if the same message has two methods
return; return;
if (flowContext.FlowStateLock != null) if (flowContext.HasFlowStateAndLock)
{ {
if (!flowContext.IsStoredOrDeleted()) if (!flowContext.IsStoredOrDeleted())
// The exception strategy can set the consume result to Success. Instead, check if the yield point // The exception strategy can set the consume result to Success. Instead, check if the yield point
@ -82,7 +86,7 @@ namespace Tapeti.Flow.Default
private static async ValueTask<FlowContext> EnrichWithFlowContext(IMessageContext context) private static async ValueTask<FlowContext?> EnrichWithFlowContext(IMessageContext context)
{ {
if (context.TryGet<FlowMessageContextPayload>(out var flowPayload)) if (context.TryGet<FlowMessageContextPayload>(out var flowPayload))
return flowPayload.FlowContext; return flowPayload.FlowContext;
@ -106,13 +110,8 @@ namespace Tapeti.Flow.Default
if (flowState == null) if (flowState == null)
return null; return null;
var flowContext = new FlowContext var flowContext = new FlowContext(new FlowHandlerContext(context), flowState, flowStateLock)
{ {
HandlerContext = new FlowHandlerContext(context),
FlowStateLock = flowStateLock,
FlowState = flowState,
ContinuationID = continuationID, ContinuationID = continuationID,
ContinuationMetadata = flowState.Continuations.TryGetValue(continuationID, out var continuation) ? continuation : null ContinuationMetadata = flowState.Continuations.TryGetValue(continuationID, out var continuation) ? continuation : null
}; };

View File

@ -10,8 +10,11 @@ namespace Tapeti.Flow.Default
{ {
/// <summary> /// <summary>
/// </summary> /// </summary>
public FlowHandlerContext() public FlowHandlerContext(ITapetiConfig config, object? controller, MethodInfo method)
{ {
Config = config;
Controller = controller;
Method = method;
} }
@ -19,11 +22,7 @@ namespace Tapeti.Flow.Default
/// </summary> /// </summary>
public FlowHandlerContext(IMessageContext source) public FlowHandlerContext(IMessageContext source)
{ {
if (source == null) var controllerPayload = source.Get<ControllerMessageContextPayload>();
return;
if (!source.TryGet<ControllerMessageContextPayload>(out var controllerPayload))
return;
Config = source.Config; Config = source.Config;
Controller = controllerPayload.Controller; Controller = controllerPayload.Controller;
@ -38,15 +37,15 @@ namespace Tapeti.Flow.Default
} }
/// <inheritdoc /> /// <inheritdoc />
public ITapetiConfig Config { get; set; } public ITapetiConfig Config { get; }
/// <inheritdoc /> /// <inheritdoc />
public object Controller { get; set; } public object? Controller { get; }
/// <inheritdoc /> /// <inheritdoc />
public MethodInfo Method { get; set; } public MethodInfo Method { get; }
/// <inheritdoc /> /// <inheritdoc />
public IMessageContext MessageContext { get; set; } public IMessageContext? MessageContext { get; }
} }
} }

View File

@ -32,21 +32,21 @@ namespace Tapeti.Flow.Default
/// <inheritdoc /> /// <inheritdoc />
public IYieldPoint YieldWithRequest<TRequest, TResponse>(TRequest message, Func<TResponse, Task<IYieldPoint>> responseHandler) public IYieldPoint YieldWithRequest<TRequest, TResponse>(TRequest message, Func<TResponse, Task<IYieldPoint>> responseHandler) where TRequest : class where TResponse : class
{ {
var responseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler); var responseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler);
return new DelegateYieldPoint(context => SendRequest(context, message, responseHandlerInfo)); return new DelegateYieldPoint(context => SendRequest(context, message, responseHandlerInfo));
} }
/// <inheritdoc /> /// <inheritdoc />
public IYieldPoint YieldWithRequest<TRequest, TResponse>(TRequest message, Func<TResponse, ValueTask<IYieldPoint>> responseHandler) public IYieldPoint YieldWithRequest<TRequest, TResponse>(TRequest message, Func<TResponse, ValueTask<IYieldPoint>> responseHandler) where TRequest : class where TResponse : class
{ {
var responseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler); var responseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler);
return new DelegateYieldPoint(context => SendRequest(context, message, responseHandlerInfo)); return new DelegateYieldPoint(context => SendRequest(context, message, responseHandlerInfo));
} }
/// <inheritdoc /> /// <inheritdoc />
public IYieldPoint YieldWithRequestSync<TRequest, TResponse>(TRequest message, Func<TResponse, IYieldPoint> responseHandler) public IYieldPoint YieldWithRequestSync<TRequest, TResponse>(TRequest message, Func<TResponse, IYieldPoint> responseHandler) where TRequest : class where TResponse : class
{ {
var responseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler); var responseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler);
return new DelegateYieldPoint(context => SendRequest(context, message, responseHandlerInfo)); return new DelegateYieldPoint(context => SendRequest(context, message, responseHandlerInfo));
@ -59,7 +59,7 @@ namespace Tapeti.Flow.Default
} }
/// <inheritdoc /> /// <inheritdoc />
public IYieldPoint EndWithResponse<TResponse>(TResponse message) public IYieldPoint EndWithResponse<TResponse>(TResponse message) where TResponse : class
{ {
return new DelegateYieldPoint(context => SendResponse(context, message)); return new DelegateYieldPoint(context => SendResponse(context, message));
} }
@ -72,7 +72,7 @@ namespace Tapeti.Flow.Default
internal async Task SendRequest(FlowContext context, object message, ResponseHandlerInfo responseHandlerInfo, internal async Task SendRequest(FlowContext context, object message, ResponseHandlerInfo responseHandlerInfo,
string convergeMethodName = null, bool convergeMethodTaskSync = false, bool store = true) string? convergeMethodName = null, bool convergeMethodTaskSync = false, bool store = true)
{ {
if (context.FlowState == null) if (context.FlowState == null)
{ {
@ -105,9 +105,9 @@ namespace Tapeti.Flow.Default
private async Task SendResponse(FlowContext context, object message) private async Task SendResponse(FlowContext context, object message)
{ {
var reply = context.FlowState == null var reply = context.HasFlowStateAndLock
? GetReply(context.HandlerContext) ? context.FlowState.Metadata.Reply
: context.FlowState.Metadata.Reply; : GetReply(context.HandlerContext);
if (reply == null) if (reply == null)
throw new YieldPointException("No response is required"); throw new YieldPointException("No response is required");
@ -134,7 +134,7 @@ namespace Tapeti.Flow.Default
{ {
await context.Delete(); await context.Delete();
if (context.FlowState?.Metadata.Reply != null) if (context.HasFlowStateAndLock && context.FlowState.Metadata.Reply != null)
throw new YieldPointException($"Flow must end with a response message of type {context.FlowState.Metadata.Reply.ResponseTypeName}"); throw new YieldPointException($"Flow must end with a response message of type {context.FlowState.Metadata.Reply.ResponseTypeName}");
} }
@ -159,16 +159,15 @@ namespace Tapeti.Flow.Default
if (binding.QueueName == null) if (binding.QueueName == null)
throw new ArgumentException("responseHandler is not yet subscribed to a queue, TapetiConnection.Subscribe must be called before starting a flow", nameof(responseHandler)); throw new ArgumentException("responseHandler is not yet subscribed to a queue, TapetiConnection.Subscribe must be called before starting a flow", nameof(responseHandler));
return new ResponseHandlerInfo return new ResponseHandlerInfo(
{ MethodSerializer.Serialize(responseHandler.Method),
MethodName = MethodSerializer.Serialize(responseHandler.Method), binding.QueueName,
ReplyToQueue = binding.QueueName, binding.QueueType == QueueType.Durable
IsDurableQueue = binding.QueueType == QueueType.Durable );
};
} }
private static ReplyMetadata GetReply(IFlowHandlerContext context) private static ReplyMetadata? GetReply(IFlowHandlerContext context)
{ {
var requestAttribute = context.MessageContext?.Message?.GetType().GetCustomAttribute<RequestAttribute>(); var requestAttribute = context.MessageContext?.Message?.GetType().GetCustomAttribute<RequestAttribute>();
if (requestAttribute?.Response == null) if (requestAttribute?.Response == null)
@ -176,7 +175,7 @@ namespace Tapeti.Flow.Default
return new ReplyMetadata return new ReplyMetadata
{ {
CorrelationId = context.MessageContext.Properties.CorrelationId, CorrelationId = context.MessageContext!.Properties.CorrelationId,
ReplyTo = context.MessageContext.Properties.ReplyTo, ReplyTo = context.MessageContext.Properties.ReplyTo,
ResponseTypeName = requestAttribute.Response.FullName, ResponseTypeName = requestAttribute.Response.FullName,
Mandatory = context.MessageContext.Properties.Persistent.GetValueOrDefault(true) Mandatory = context.MessageContext.Properties.Persistent.GetValueOrDefault(true)
@ -188,18 +187,17 @@ namespace Tapeti.Flow.Default
var flowStore = flowContext.HandlerContext.Config.DependencyResolver.Resolve<IFlowStore>(); var flowStore = flowContext.HandlerContext.Config.DependencyResolver.Resolve<IFlowStore>();
var flowID = Guid.NewGuid(); var flowID = Guid.NewGuid();
flowContext.FlowStateLock = await flowStore.LockFlowState(flowID); var flowStateLock = await flowStore.LockFlowState(flowID);
if (flowContext.FlowStateLock == null) if (flowStateLock == null)
throw new InvalidOperationException("Unable to lock a new flow"); throw new InvalidOperationException("Unable to lock a new flow");
flowContext.FlowState = new FlowState var flowState = new FlowState
{ {
Metadata = new FlowMetadata Metadata = new FlowMetadata(GetReply(flowContext.HandlerContext))
{
Reply = GetReply(flowContext.HandlerContext)
}
}; };
flowContext.SetFlowState(flowState, flowStateLock);
} }
@ -207,20 +205,16 @@ namespace Tapeti.Flow.Default
public async ValueTask Execute(IFlowHandlerContext context, IYieldPoint yieldPoint) public async ValueTask Execute(IFlowHandlerContext context, IYieldPoint yieldPoint)
{ {
if (yieldPoint is not DelegateYieldPoint executableYieldPoint) if (yieldPoint is not DelegateYieldPoint executableYieldPoint)
throw new YieldPointException($"Yield point is required in controller {context.Controller.GetType().Name} for method {context.Method.Name}"); throw new YieldPointException($"Yield point is required in controller {context.Controller?.GetType().Name} for method {context.Method.Name}");
FlowContext flowContext = null; FlowContext? flowContext = null;
var disposeFlowContext = false; var disposeFlowContext = false;
try try
{ {
var messageContext = context.MessageContext; if (context.MessageContext == null || !context.MessageContext.TryGet<FlowMessageContextPayload>(out var flowPayload))
if (messageContext == null || !messageContext.TryGet<FlowMessageContextPayload>(out var flowPayload))
{ {
flowContext = new FlowContext flowContext = new FlowContext(context);
{
HandlerContext = context
};
// If we ended up here it is because of a Start. No point in storing the new FlowContext // If we ended up here it is because of a Start. No point in storing the new FlowContext
// in the messageContext as the yield point is the last to execute. // in the messageContext as the yield point is the last to execute.
@ -236,7 +230,7 @@ namespace Tapeti.Flow.Default
catch (YieldPointException e) catch (YieldPointException e)
{ {
// Useful for debugging // Useful for debugging
e.Data["Tapeti.Controller.Name"] = context.Controller.GetType().FullName; e.Data["Tapeti.Controller.Name"] = context.Controller?.GetType().FullName;
e.Data["Tapeti.Controller.Method"] = context.Method.Name; e.Data["Tapeti.Controller.Method"] = context.Method.Name;
throw; throw;
} }
@ -246,15 +240,15 @@ namespace Tapeti.Flow.Default
finally finally
{ {
if (disposeFlowContext) if (disposeFlowContext)
flowContext.Dispose(); flowContext?.Dispose();
} }
} }
/// <inheritdoc /> /// <inheritdoc />
public IFlowParallelRequest GetParallelRequest(IFlowHandlerContext context) public IFlowParallelRequest? GetParallelRequest(IFlowHandlerContext context)
{ {
return context.MessageContext.TryGet<FlowMessageContextPayload>(out var flowPayload) return context.MessageContext != null && context.MessageContext.TryGet<FlowMessageContextPayload>(out var flowPayload)
? new ParallelRequest(config, this, flowPayload.FlowContext) ? new ParallelRequest(config, this, flowPayload.FlowContext)
: null; : null;
} }
@ -263,27 +257,49 @@ namespace Tapeti.Flow.Default
/// <inheritdoc /> /// <inheritdoc />
public ValueTask Converge(IFlowHandlerContext context) public ValueTask Converge(IFlowHandlerContext context)
{ {
return Execute(context, new DelegateYieldPoint(flowContext => return Execute(context, new DelegateYieldPoint(async flowContext =>
Converge(flowContext, flowContext.ContinuationMetadata.ConvergeMethodName, flowContext.ContinuationMetadata.ConvergeMethodSync))); {
if (flowContext.ContinuationMetadata == null)
throw new InvalidOperationException("Missing ContinuationMetadata in FlowContext");
if (flowContext.ContinuationMetadata.ConvergeMethodName == null)
throw new InvalidOperationException("Missing ConvergeMethodName in FlowContext ContinuationMetadata");
await Converge(flowContext, flowContext.ContinuationMetadata.ConvergeMethodName, flowContext.ContinuationMetadata.ConvergeMethodSync);
}));
} }
internal async Task Converge(FlowContext flowContext, string convergeMethodName, bool convergeMethodSync) internal async Task Converge(FlowContext flowContext, string convergeMethodName, bool convergeMethodSync)
{ {
IYieldPoint yieldPoint; IYieldPoint? yieldPoint;
if (flowContext.HandlerContext == null)
throw new InvalidOperationException($"Missing HandleContext in FlowContext for converge method {convergeMethodName}");
if (flowContext.HandlerContext.MessageContext == null)
throw new InvalidOperationException($"Missing MessageContext in FlowContext for converge method {convergeMethodName}");
if (!flowContext.HandlerContext.MessageContext.TryGet<ControllerMessageContextPayload>(out var controllerPayload)) if (!flowContext.HandlerContext.MessageContext.TryGet<ControllerMessageContextPayload>(out var controllerPayload))
throw new ArgumentException("Context does not contain a controller payload", nameof(flowContext)); throw new ArgumentException("Context does not contain a controller payload", nameof(flowContext));
if (controllerPayload.Controller == null)
throw new InvalidOperationException($"Controller is not available for converge method {convergeMethodName} (method is static?)");
var method = controllerPayload.Controller.GetType().GetMethod(convergeMethodName, BindingFlags.NonPublic | BindingFlags.Instance); var method = controllerPayload.Controller.GetType().GetMethod(convergeMethodName, BindingFlags.NonPublic | BindingFlags.Instance);
if (method == null) if (method == null)
throw new ArgumentException($"Unknown converge method in controller {controllerPayload.Controller.GetType().Name}: {convergeMethodName}"); throw new ArgumentException($"Unknown converge method in controller {controllerPayload.Controller.GetType().Name}: {convergeMethodName}");
if (convergeMethodSync) if (convergeMethodSync)
yieldPoint = (IYieldPoint)method.Invoke(controllerPayload.Controller, new object[] { }); yieldPoint = (IYieldPoint?)method.Invoke(controllerPayload.Controller, new object[] { });
else else
yieldPoint = await(Task<IYieldPoint>)method.Invoke(controllerPayload.Controller, new object[] { }); {
var yieldPointTask = method.Invoke(controllerPayload.Controller, new object[] { });
if (yieldPointTask == null)
throw new YieldPointException($"Yield point is required in controller {controllerPayload.Controller.GetType().Name} for converge method {convergeMethodName}");
yieldPoint = await (Task<IYieldPoint>)yieldPointTask;
}
if (yieldPoint == null) if (yieldPoint == null)
throw new YieldPointException($"Yield point is required in controller {controllerPayload.Controller.GetType().Name} for converge method {convergeMethodName}"); throw new YieldPointException($"Yield point is required in controller {controllerPayload.Controller.GetType().Name} for converge method {convergeMethodName}");
@ -297,8 +313,15 @@ namespace Tapeti.Flow.Default
{ {
private class RequestInfo private class RequestInfo
{ {
public object Message { get; init; } public object Message { get; }
public ResponseHandlerInfo ResponseHandlerInfo { get; init; } public ResponseHandlerInfo ResponseHandlerInfo { get; }
public RequestInfo(object message, ResponseHandlerInfo responseHandlerInfo)
{
Message = message;
ResponseHandlerInfo = responseHandlerInfo;
}
} }
@ -314,32 +337,32 @@ namespace Tapeti.Flow.Default
} }
public IFlowParallelRequestBuilder AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, Task> responseHandler) public IFlowParallelRequestBuilder AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, Task> responseHandler) where TRequest : class where TResponse : class
{ {
return InternalAddRequest(message, responseHandler); return InternalAddRequest(message, responseHandler);
} }
public IFlowParallelRequestBuilder AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, ValueTask> responseHandler) public IFlowParallelRequestBuilder AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, ValueTask> responseHandler) where TRequest : class where TResponse : class
{ {
return InternalAddRequest(message, responseHandler); return InternalAddRequest(message, responseHandler);
} }
public IFlowParallelRequestBuilder AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, IFlowParallelRequest, Task> responseHandler) public IFlowParallelRequestBuilder AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, IFlowParallelRequest, Task> responseHandler) where TRequest : class where TResponse : class
{ {
return InternalAddRequest(message, responseHandler); return InternalAddRequest(message, responseHandler);
} }
public IFlowParallelRequestBuilder AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, IFlowParallelRequest, ValueTask> responseHandler) public IFlowParallelRequestBuilder AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, IFlowParallelRequest, ValueTask> responseHandler) where TRequest : class where TResponse : class
{ {
return InternalAddRequest(message, responseHandler); return InternalAddRequest(message, responseHandler);
} }
public IFlowParallelRequestBuilder AddRequestSync<TRequest, TResponse>(TRequest message, Action<TResponse> responseHandler) public IFlowParallelRequestBuilder AddRequestSync<TRequest, TResponse>(TRequest message, Action<TResponse> responseHandler) where TRequest : class where TResponse : class
{ {
return InternalAddRequest(message, responseHandler); return InternalAddRequest(message, responseHandler);
} }
public IFlowParallelRequestBuilder AddRequestSync<TRequest, TResponse>(TRequest message, Action<TResponse, IFlowParallelRequest> responseHandler) public IFlowParallelRequestBuilder AddRequestSync<TRequest, TResponse>(TRequest message, Action<TResponse, IFlowParallelRequest> responseHandler) where TRequest : class where TResponse : class
{ {
return InternalAddRequest(message, responseHandler); return InternalAddRequest(message, responseHandler);
} }
@ -347,12 +370,7 @@ namespace Tapeti.Flow.Default
private IFlowParallelRequestBuilder InternalAddRequest(object message, Delegate responseHandler) private IFlowParallelRequestBuilder InternalAddRequest(object message, Delegate responseHandler)
{ {
requests.Add(new RequestInfo requests.Add(new RequestInfo(message, GetResponseHandlerInfo(config, message, responseHandler)));
{
Message = message,
ResponseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler)
});
return this; return this;
} }
@ -381,12 +399,12 @@ namespace Tapeti.Flow.Default
}; };
} }
if (convergeMethod?.Method == null) if (convergeMethod.Method == null)
throw new ArgumentNullException(nameof(convergeMethod)); throw new ArgumentNullException(nameof(convergeMethod));
return new DelegateYieldPoint(async context => return new DelegateYieldPoint(async context =>
{ {
if (convergeMethod.Method.DeclaringType != context.HandlerContext.Controller.GetType()) if (convergeMethod.Method.DeclaringType != context.HandlerContext.Controller?.GetType())
throw new YieldPointException("Converge method must be in the same controller class"); throw new YieldPointException("Converge method must be in the same controller class");
await Task.WhenAll(requests.Select(requestInfo => await Task.WhenAll(requests.Select(requestInfo =>
@ -419,19 +437,19 @@ namespace Tapeti.Flow.Default
} }
public Task AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, Task> responseHandler) public Task AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, Task> responseHandler) where TRequest : class where TResponse : class
{ {
return InternalAddRequest(message, responseHandler); return InternalAddRequest(message, responseHandler);
} }
public Task AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, IFlowParallelRequest, Task> responseHandler) public Task AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, IFlowParallelRequest, Task> responseHandler) where TRequest : class where TResponse : class
{ {
return InternalAddRequest(message, responseHandler); return InternalAddRequest(message, responseHandler);
} }
public Task AddRequestSync<TRequest, TResponse>(TRequest message, Action<TResponse> responseHandler) public Task AddRequestSync<TRequest, TResponse>(TRequest message, Action<TResponse> responseHandler) where TRequest : class where TResponse : class
{ {
return InternalAddRequest(message, responseHandler); return InternalAddRequest(message, responseHandler);
} }
@ -441,6 +459,9 @@ namespace Tapeti.Flow.Default
{ {
var responseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler); var responseHandlerInfo = GetResponseHandlerInfo(config, message, responseHandler);
if (flowContext.ContinuationMetadata == null)
throw new InvalidOperationException("No ContinuationMetadata in FlowContext");
return flowProvider.SendRequest( return flowProvider.SendRequest(
flowContext, flowContext,
message, message,
@ -454,9 +475,17 @@ namespace Tapeti.Flow.Default
internal class ResponseHandlerInfo internal class ResponseHandlerInfo
{ {
public string MethodName { get; set; } public string MethodName { get; }
public string ReplyToQueue { get; set; } public string ReplyToQueue { get; }
public bool IsDurableQueue { get; set; } public bool IsDurableQueue { get; }
public ResponseHandlerInfo(string methodName, string replyToQueue, bool isDurableQueue)
{
MethodName = methodName;
ReplyToQueue = replyToQueue;
IsDurableQueue = isDurableQueue;
}
} }
} }
} }

View File

@ -25,39 +25,38 @@ namespace Tapeti.Flow.Default
/// <inheritdoc /> /// <inheritdoc />
public async Task Start<TController>(Expression<Func<TController, Func<IYieldPoint>>> methodSelector) where TController : class public async Task Start<TController>(Expression<Func<TController, Func<IYieldPoint>>> methodSelector) where TController : class
{ {
await CallControllerMethod<TController>(GetExpressionMethod(methodSelector), value => Task.FromResult((IYieldPoint)value), new object[] { }); await CallControllerMethod<TController>(GetExpressionMethod(methodSelector), value => Task.FromResult((IYieldPoint)value), Array.Empty<object?>());
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task Start<TController>(Expression<Func<TController, Func<Task<IYieldPoint>>>> methodSelector) where TController : class public async Task Start<TController>(Expression<Func<TController, Func<Task<IYieldPoint>>>> methodSelector) where TController : class
{ {
await CallControllerMethod<TController>(GetExpressionMethod(methodSelector), value => (Task<IYieldPoint>)value, new object[] {}); await CallControllerMethod<TController>(GetExpressionMethod(methodSelector), value => (Task<IYieldPoint>)value, Array.Empty<object?>());
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task Start<TController, TParameter>(Expression<Func<TController, Func<TParameter, IYieldPoint>>> methodSelector, TParameter parameter) where TController : class public async Task Start<TController, TParameter>(Expression<Func<TController, Func<TParameter, IYieldPoint>>> methodSelector, TParameter parameter) where TController : class
{ {
await CallControllerMethod<TController>(GetExpressionMethod(methodSelector), value => Task.FromResult((IYieldPoint)value), new object[] {parameter}); await CallControllerMethod<TController>(GetExpressionMethod(methodSelector), value => Task.FromResult((IYieldPoint)value), new object?[] {parameter});
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task Start<TController, TParameter>(Expression<Func<TController, Func<TParameter, Task<IYieldPoint>>>> methodSelector, TParameter parameter) where TController : class public async Task Start<TController, TParameter>(Expression<Func<TController, Func<TParameter, Task<IYieldPoint>>>> methodSelector, TParameter parameter) where TController : class
{ {
await CallControllerMethod<TController>(GetExpressionMethod(methodSelector), value => (Task<IYieldPoint>)value, new object[] {parameter}); await CallControllerMethod<TController>(GetExpressionMethod(methodSelector), value => (Task<IYieldPoint>)value, new object?[] {parameter});
} }
private async Task CallControllerMethod<TController>(MethodInfo method, Func<object, Task<IYieldPoint>> getYieldPointResult, object[] parameters) where TController : class private async Task CallControllerMethod<TController>(MethodInfo method, Func<object, Task<IYieldPoint>> getYieldPointResult, object?[] parameters) where TController : class
{ {
var controller = config.DependencyResolver.Resolve<TController>(); var controller = config.DependencyResolver.Resolve<TController>();
var yieldPoint = await getYieldPointResult(method.Invoke(controller, parameters)); var result = method.Invoke(controller, parameters);
if (result == null)
throw new InvalidOperationException($"Method {method.Name} must return an IYieldPoint or Task<IYieldPoint>, got null");
var context = new FlowHandlerContext var yieldPoint = await getYieldPointResult(result);
{
Config = config, var context = new FlowHandlerContext(config, controller, method);
Controller = controller,
Method = method
};
var flowHandler = config.DependencyResolver.Resolve<IFlowHandler>(); var flowHandler = config.DependencyResolver.Resolve<IFlowHandler>();
await flowHandler.Execute(context, yieldPoint); await flowHandler.Execute(context, yieldPoint);

View File

@ -9,8 +9,8 @@ namespace Tapeti.Flow.Default
/// </summary> /// </summary>
public class FlowState public class FlowState
{ {
private FlowMetadata metadata; private FlowMetadata? metadata;
private Dictionary<Guid, ContinuationMetadata> continuations; private Dictionary<Guid, ContinuationMetadata>? continuations;
/// <summary> /// <summary>
@ -18,7 +18,7 @@ namespace Tapeti.Flow.Default
/// </summary> /// </summary>
public FlowMetadata Metadata public FlowMetadata Metadata
{ {
get => metadata ??= new FlowMetadata(); get => metadata ??= new FlowMetadata(null);
set => metadata = value; set => metadata = value;
} }
@ -26,7 +26,7 @@ namespace Tapeti.Flow.Default
/// <summary> /// <summary>
/// Contains the serialized state which is restored when a flow continues. /// Contains the serialized state which is restored when a flow continues.
/// </summary> /// </summary>
public string Data { get; set; } public string? Data { get; set; }
/// <summary> /// <summary>
@ -45,7 +45,7 @@ namespace Tapeti.Flow.Default
public FlowState Clone() public FlowState Clone()
{ {
return new FlowState { return new FlowState {
metadata = metadata.Clone(), metadata = metadata?.Clone(),
Data = Data, Data = Data,
continuations = continuations?.ToDictionary(kv => kv.Key, kv => kv.Value.Clone()) continuations = continuations?.ToDictionary(kv => kv.Key, kv => kv.Value.Clone())
}; };
@ -61,7 +61,14 @@ namespace Tapeti.Flow.Default
/// <summary> /// <summary>
/// Contains information about the expected response for this flow. /// Contains information about the expected response for this flow.
/// </summary> /// </summary>
public ReplyMetadata Reply { get; set; } public ReplyMetadata? Reply { get; }
/// <inheritdoc cref="FlowMetadata"/>
public FlowMetadata(ReplyMetadata? reply)
{
Reply = reply;
}
/// <summary> /// <summary>
@ -69,10 +76,7 @@ namespace Tapeti.Flow.Default
/// </summary> /// </summary>
public FlowMetadata Clone() public FlowMetadata Clone()
{ {
return new FlowMetadata return new FlowMetadata(Reply);
{
Reply = Reply?.Clone()
};
} }
} }
@ -85,17 +89,17 @@ namespace Tapeti.Flow.Default
/// <summary> /// <summary>
/// The queue to which the response should be sent. /// The queue to which the response should be sent.
/// </summary> /// </summary>
public string ReplyTo { get; set; } public string? ReplyTo { get; set; }
/// <summary> /// <summary>
/// The correlation ID included in the original request. /// The correlation ID included in the original request.
/// </summary> /// </summary>
public string CorrelationId { get; set; } public string? CorrelationId { get; set; }
/// <summary> /// <summary>
/// The expected response message class. /// The expected response message class.
/// </summary> /// </summary>
public string ResponseTypeName { get; set; } public string? ResponseTypeName { get; set; }
/// <summary> /// <summary>
/// Indicates whether the response should be sent a mandatory. /// Indicates whether the response should be sent a mandatory.
@ -128,12 +132,12 @@ namespace Tapeti.Flow.Default
/// <summary> /// <summary>
/// The name of the method which will handle the response. /// The name of the method which will handle the response.
/// </summary> /// </summary>
public string MethodName { get; set; } public string? MethodName { get; set; }
/// <summary> /// <summary>
/// The name of the method which is called when all responses have been processed. /// The name of the method which is called when all responses have been processed.
/// </summary> /// </summary>
public string ConvergeMethodName { get; set; } public string? ConvergeMethodName { get; set; }
/// <summary> /// <summary>
/// Determines if the converge method is synchronous or asynchronous. /// Determines if the converge method is synchronous or asynchronous.

View File

@ -16,11 +16,11 @@ namespace Tapeti.Flow.Default
{ {
private class CachedFlowState private class CachedFlowState
{ {
public readonly FlowState FlowState; public readonly FlowState? FlowState;
public readonly DateTime CreationTime; public readonly DateTime CreationTime;
public readonly bool IsPersistent; public readonly bool IsPersistent;
public CachedFlowState(FlowState flowState, DateTime creationTime, bool isPersistent) public CachedFlowState(FlowState? flowState, DateTime creationTime, bool isPersistent)
{ {
FlowState = flowState; FlowState = flowState;
CreationTime = creationTime; CreationTime = creationTime;
@ -31,7 +31,7 @@ namespace Tapeti.Flow.Default
private readonly ConcurrentDictionary<Guid, CachedFlowState> flowStates = new(); private readonly ConcurrentDictionary<Guid, CachedFlowState> flowStates = new();
private readonly ConcurrentDictionary<Guid, Guid> continuationLookup = new(); private readonly ConcurrentDictionary<Guid, Guid> continuationLookup = new();
private readonly LockCollection<Guid> locks = new(EqualityComparer<Guid>.Default); private readonly LockCollection<Guid> locks = new(EqualityComparer<Guid>.Default);
private HashSet<string> validatedMethods; private HashSet<string>? validatedMethods;
private readonly IFlowRepository repository; private readonly IFlowRepository repository;
private readonly ITapetiConfig config; private readonly ITapetiConfig config;
@ -85,10 +85,13 @@ namespace Tapeti.Flow.Default
private void ValidateContinuation(Guid flowId, Guid continuationId, ContinuationMetadata metadata) private void ValidateContinuation(Guid flowId, Guid continuationId, ContinuationMetadata metadata)
{ {
if (string.IsNullOrEmpty(metadata.MethodName))
return;
// We could check all the things that are required for a continuation or converge method, but this should suffice // We could check all the things that are required for a continuation or converge method, but this should suffice
// for the common scenario where you change code without realizing that it's signature has been persisted // for the common scenario where you change code without realizing that it's signature has been persisted
// ReSharper disable once InvertIf // ReSharper disable once InvertIf
if (validatedMethods.Add(metadata.MethodName)) if (validatedMethods!.Add(metadata.MethodName))
{ {
var methodInfo = MethodSerializer.Deserialize(metadata.MethodName); var methodInfo = MethodSerializer.Deserialize(metadata.MethodName);
if (methodInfo == null) if (methodInfo == null)
@ -150,8 +153,8 @@ namespace Tapeti.Flow.Default
private class FlowStateLock : IFlowStateLock private class FlowStateLock : IFlowStateLock
{ {
private readonly FlowStore owner; private readonly FlowStore owner;
private volatile IDisposable flowLock; private volatile IDisposable? flowLock;
private CachedFlowState cachedFlowState; private CachedFlowState? cachedFlowState;
public Guid FlowID { get; } public Guid FlowID { get; }
@ -172,12 +175,12 @@ namespace Tapeti.Flow.Default
l?.Dispose(); l?.Dispose();
} }
public ValueTask<FlowState> GetFlowState() public ValueTask<FlowState?> GetFlowState()
{ {
if (flowLock == null) if (flowLock == null)
throw new ObjectDisposedException("FlowStateLock"); throw new ObjectDisposedException("FlowStateLock");
return new ValueTask<FlowState>(cachedFlowState?.FlowState?.Clone()); return new ValueTask<FlowState?>(cachedFlowState?.FlowState?.Clone());
} }
public async ValueTask StoreFlowState(FlowState newFlowState, bool persistent) public async ValueTask StoreFlowState(FlowState newFlowState, bool persistent)
@ -189,13 +192,13 @@ namespace Tapeti.Flow.Default
newFlowState = newFlowState.Clone(); newFlowState = newFlowState.Clone();
// Update the lookup dictionary for the ContinuationIDs // Update the lookup dictionary for the ContinuationIDs
if (cachedFlowState != null) if (cachedFlowState?.FlowState != null)
{ {
foreach (var removedContinuation in cachedFlowState.FlowState.Continuations.Keys.Where(k => !newFlowState.Continuations.ContainsKey(k))) foreach (var removedContinuation in cachedFlowState.FlowState.Continuations.Keys.Where(k => !newFlowState.Continuations.ContainsKey(k)))
owner.continuationLookup.TryRemove(removedContinuation, out _); owner.continuationLookup.TryRemove(removedContinuation, out _);
} }
foreach (var addedContinuation in newFlowState.Continuations.Where(c => cachedFlowState == null || !cachedFlowState.FlowState.Continuations.ContainsKey(c.Key))) foreach (var addedContinuation in newFlowState.Continuations.Where(c => cachedFlowState?.FlowState == null || !cachedFlowState.FlowState.Continuations.ContainsKey(c.Key)))
{ {
owner.continuationLookup.TryAdd(addedContinuation.Key, FlowID); owner.continuationLookup.TryAdd(addedContinuation.Key, FlowID);
} }
@ -203,7 +206,7 @@ namespace Tapeti.Flow.Default
var isNew = cachedFlowState == null; var isNew = cachedFlowState == null;
var wasPersistent = cachedFlowState?.IsPersistent ?? false; var wasPersistent = cachedFlowState?.IsPersistent ?? false;
cachedFlowState = new CachedFlowState(newFlowState, isNew ? DateTime.UtcNow : cachedFlowState.CreationTime, persistent); cachedFlowState = new CachedFlowState(newFlowState, isNew ? DateTime.UtcNow : cachedFlowState!.CreationTime, persistent);
owner.flowStates[FlowID] = cachedFlowState; owner.flowStates[FlowID] = cachedFlowState;
if (persistent) if (persistent)
@ -231,7 +234,7 @@ namespace Tapeti.Flow.Default
if (flowLock == null) if (flowLock == null)
throw new ObjectDisposedException("FlowStateLock"); throw new ObjectDisposedException("FlowStateLock");
if (cachedFlowState != null) if (cachedFlowState?.FlowState != null)
{ {
foreach (var removedContinuation in cachedFlowState.FlowState.Continuations.Keys) foreach (var removedContinuation in cachedFlowState.FlowState.Continuations.Keys)
owner.continuationLookup.TryRemove(removedContinuation, out _); owner.continuationLookup.TryRemove(removedContinuation, out _);
@ -239,7 +242,7 @@ namespace Tapeti.Flow.Default
owner.flowStates.TryRemove(FlowID, out var removedFlowState); owner.flowStates.TryRemove(FlowID, out var removedFlowState);
cachedFlowState = null; cachedFlowState = null;
if (removedFlowState.IsPersistent) if (removedFlowState is { IsPersistent: true })
await owner.repository.DeleteState(FlowID); await owner.repository.DeleteState(FlowID);
} }
} }

View File

@ -9,11 +9,11 @@ namespace Tapeti.Flow
/// </summary> /// </summary>
public class FlowExtension : ITapetiExtension public class FlowExtension : ITapetiExtension
{ {
private readonly IFlowRepository flowRepository; private readonly IFlowRepository? flowRepository;
/// <summary> /// <summary>
/// </summary> /// </summary>
public FlowExtension(IFlowRepository flowRepository) public FlowExtension(IFlowRepository? flowRepository)
{ {
this.flowRepository = flowRepository; this.flowRepository = flowRepository;
} }

View File

@ -7,7 +7,7 @@ namespace Tapeti.Flow.FlowHelpers
/// <summary> /// <summary>
/// Implementation of an asynchronous locking mechanism. /// Implementation of an asynchronous locking mechanism.
/// </summary> /// </summary>
public class LockCollection<T> public class LockCollection<T> where T : notnull
{ {
private readonly Dictionary<T, LockItem> locks; private readonly Dictionary<T, LockItem> locks;
@ -57,7 +57,7 @@ namespace Tapeti.Flow.FlowHelpers
private class LockItem : IDisposable private class LockItem : IDisposable
{ {
internal volatile LockItem Next; internal volatile LockItem? Next;
private readonly Dictionary<T, LockItem> locks; private readonly Dictionary<T, LockItem> locks;
private readonly TaskCompletionSource<IDisposable> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly TaskCompletionSource<IDisposable> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);

View File

@ -25,7 +25,7 @@ namespace Tapeti.Flow.FlowHelpers
/// Deserializes the serialized method representation back into it's MethodInfo, or null if not found. /// Deserializes the serialized method representation back into it's MethodInfo, or null if not found.
/// </summary> /// </summary>
/// <param name="serializedMethod"></param> /// <param name="serializedMethod"></param>
public static MethodInfo Deserialize(string serializedMethod) public static MethodInfo? Deserialize(string serializedMethod)
{ {
var match = DeserializeRegex.Match(serializedMethod); var match = DeserializeRegex.Match(serializedMethod);
if (!match.Success) if (!match.Success)

View File

@ -16,9 +16,8 @@ namespace Tapeti.Flow
/// parallel flow is done and the convergeMethod will be called. /// parallel flow is done and the convergeMethod will be called.
/// Temporarily disables storing the flow state. /// Temporarily disables storing the flow state.
/// </summary> /// </summary>
public bool FlowIsConverging => FlowContext != null && public bool FlowIsConverging => FlowContext.FlowState.Continuations.Count == 0 &&
FlowContext.FlowState.Continuations.Count == 0 && FlowContext.ContinuationMetadata?.ConvergeMethodName != null;
FlowContext.ContinuationMetadata.ConvergeMethodName != null;
public FlowMessageContextPayload(FlowContext flowContext) public FlowMessageContextPayload(FlowContext flowContext)
@ -29,7 +28,7 @@ namespace Tapeti.Flow
public void Dispose() public void Dispose()
{ {
FlowContext?.Dispose(); FlowContext.Dispose();
} }
} }
} }

View File

@ -18,7 +18,7 @@ namespace Tapeti.Flow
/// <summary> /// <summary>
/// An instance of the controller which starts or continues the flow. /// An instance of the controller which starts or continues the flow.
/// </summary> /// </summary>
object Controller { get; } object? Controller { get; }
/// <summary> /// <summary>
@ -31,6 +31,6 @@ namespace Tapeti.Flow
/// Access to the message context if this is a continuated flow. /// Access to the message context if this is a continuated flow.
/// Will be null when in a starting flow. /// Will be null when in a starting flow.
/// </summary> /// </summary>
IMessageContext MessageContext { get; } IMessageContext? MessageContext { get; }
} }
} }

View File

@ -20,7 +20,7 @@ namespace Tapeti.Flow
/// <param name="responseHandler"></param> /// <param name="responseHandler"></param>
/// <typeparam name="TRequest"></typeparam> /// <typeparam name="TRequest"></typeparam>
/// <typeparam name="TResponse"></typeparam> /// <typeparam name="TResponse"></typeparam>
IYieldPoint YieldWithRequest<TRequest, TResponse>(TRequest message, Func<TResponse, Task<IYieldPoint>> responseHandler); IYieldPoint YieldWithRequest<TRequest, TResponse>(TRequest message, Func<TResponse, Task<IYieldPoint>> responseHandler) where TRequest : class where TResponse : class;
/// <summary> /// <summary>
@ -32,7 +32,7 @@ namespace Tapeti.Flow
/// <param name="responseHandler"></param> /// <param name="responseHandler"></param>
/// <typeparam name="TRequest"></typeparam> /// <typeparam name="TRequest"></typeparam>
/// <typeparam name="TResponse"></typeparam> /// <typeparam name="TResponse"></typeparam>
IYieldPoint YieldWithRequest<TRequest, TResponse>(TRequest message, Func<TResponse, ValueTask<IYieldPoint>> responseHandler); IYieldPoint YieldWithRequest<TRequest, TResponse>(TRequest message, Func<TResponse, ValueTask<IYieldPoint>> responseHandler) where TRequest : class where TResponse : class;
/// <summary> /// <summary>
@ -51,7 +51,7 @@ namespace Tapeti.Flow
/// <typeparam name="TRequest"></typeparam> /// <typeparam name="TRequest"></typeparam>
/// <typeparam name="TResponse"></typeparam> /// <typeparam name="TResponse"></typeparam>
/// <returns></returns> /// <returns></returns>
IYieldPoint YieldWithRequestSync<TRequest, TResponse>(TRequest message, Func<TResponse, IYieldPoint> responseHandler); IYieldPoint YieldWithRequestSync<TRequest, TResponse>(TRequest message, Func<TResponse, IYieldPoint> responseHandler) where TRequest : class where TResponse : class;
/// <summary> /// <summary>
@ -67,7 +67,7 @@ namespace Tapeti.Flow
/// </summary> /// </summary>
/// <param name="message"></param> /// <param name="message"></param>
/// <typeparam name="TResponse"></typeparam> /// <typeparam name="TResponse"></typeparam>
IYieldPoint EndWithResponse<TResponse>(TResponse message); IYieldPoint EndWithResponse<TResponse>(TResponse message) where TResponse : class;
/// <summary> /// <summary>
@ -126,7 +126,7 @@ namespace Tapeti.Flow
/// <summary> /// <summary>
/// Returns the parallel request for the given message context. /// Returns the parallel request for the given message context.
/// </summary> /// </summary>
IFlowParallelRequest GetParallelRequest(IFlowHandlerContext context); IFlowParallelRequest? GetParallelRequest(IFlowHandlerContext context);
/// <summary> /// <summary>
@ -174,7 +174,7 @@ namespace Tapeti.Flow
/// </summary> /// </summary>
/// <param name="message"></param> /// <param name="message"></param>
/// <param name="responseHandler"></param> /// <param name="responseHandler"></param>
IFlowParallelRequestBuilder AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, Task> responseHandler); IFlowParallelRequestBuilder AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, Task> responseHandler) where TRequest : class where TResponse : class;
/// <summary> /// <summary>
/// Publish a request message and continue the flow when the response arrives. /// Publish a request message and continue the flow when the response arrives.
@ -184,21 +184,21 @@ namespace Tapeti.Flow
/// </summary> /// </summary>
/// <param name="message"></param> /// <param name="message"></param>
/// <param name="responseHandler"></param> /// <param name="responseHandler"></param>
IFlowParallelRequestBuilder AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, ValueTask> responseHandler); IFlowParallelRequestBuilder AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, ValueTask> responseHandler) where TRequest : class where TResponse : class;
/// <remarks> /// <remarks>
/// This overload allows the response handler access to the IFlowParallelRequest interface, which /// This overload allows the response handler access to the IFlowParallelRequest interface, which
/// can be used to add additional requests to the parallel request before the continuation method passed to the Yield method is called. /// can be used to add additional requests to the parallel request before the continuation method passed to the Yield method is called.
/// </remarks> /// </remarks>
/// <inheritdoc cref="AddRequest{TRequest,TResponse}(TRequest,Func{TResponse,Task})"/> /// <inheritdoc cref="AddRequest{TRequest,TResponse}(TRequest,Func{TResponse,Task})"/>
IFlowParallelRequestBuilder AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, IFlowParallelRequest, Task> responseHandler); IFlowParallelRequestBuilder AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, IFlowParallelRequest, Task> responseHandler) where TRequest : class where TResponse : class;
/// <remarks> /// <remarks>
/// This overload allows the response handler access to the IFlowParallelRequest interface, which /// This overload allows the response handler access to the IFlowParallelRequest interface, which
/// can be used to add additional requests to the parallel request before the continuation method passed to the Yield method is called. /// can be used to add additional requests to the parallel request before the continuation method passed to the Yield method is called.
/// </remarks> /// </remarks>
/// <inheritdoc cref="AddRequest{TRequest,TResponse}(TRequest,Func{TResponse,ValueTask})"/> /// <inheritdoc cref="AddRequest{TRequest,TResponse}(TRequest,Func{TResponse,ValueTask})"/>
IFlowParallelRequestBuilder AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, IFlowParallelRequest, ValueTask> responseHandler); IFlowParallelRequestBuilder AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, IFlowParallelRequest, ValueTask> responseHandler) where TRequest : class where TResponse : class;
/// <summary> /// <summary>
/// Publish a request message and continue the flow when the response arrives. /// Publish a request message and continue the flow when the response arrives.
@ -208,14 +208,14 @@ namespace Tapeti.Flow
/// </summary> /// </summary>
/// <param name="message"></param> /// <param name="message"></param>
/// <param name="responseHandler"></param> /// <param name="responseHandler"></param>
IFlowParallelRequestBuilder AddRequestSync<TRequest, TResponse>(TRequest message, Action<TResponse> responseHandler); IFlowParallelRequestBuilder AddRequestSync<TRequest, TResponse>(TRequest message, Action<TResponse> responseHandler) where TRequest : class where TResponse : class;
/// <remarks> /// <remarks>
/// This overload allows the response handler access to the IFlowParallelRequest interface, which /// This overload allows the response handler access to the IFlowParallelRequest interface, which
/// can be used to add additional requests to the parallel request before the continuation method passed to the Yield method is called. /// can be used to add additional requests to the parallel request before the continuation method passed to the Yield method is called.
/// </remarks> /// </remarks>
/// <inheritdoc cref="AddRequestSync{TRequest,TResponse}(TRequest,Action{TResponse})"/> /// <inheritdoc cref="AddRequestSync{TRequest,TResponse}(TRequest,Action{TResponse})"/>
IFlowParallelRequestBuilder AddRequestSync<TRequest, TResponse>(TRequest message, Action<TResponse, IFlowParallelRequest> responseHandler); IFlowParallelRequestBuilder AddRequestSync<TRequest, TResponse>(TRequest message, Action<TResponse, IFlowParallelRequest> responseHandler) where TRequest : class where TResponse : class;
/// There is no Sync overload with an IFlowParallelRequest parameter, as the AddRequest methods for that are /// There is no Sync overload with an IFlowParallelRequest parameter, as the AddRequest methods for that are
/// async, so you should always await them. /// async, so you should always await them.
@ -260,14 +260,14 @@ namespace Tapeti.Flow
/// </summary> /// </summary>
/// <param name="message"></param> /// <param name="message"></param>
/// <param name="responseHandler"></param> /// <param name="responseHandler"></param>
Task AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, Task> responseHandler); Task AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, Task> responseHandler) where TRequest : class where TResponse : class;
/// <remarks> /// <remarks>
/// This overload allows the response handler access to the IFlowParallelRequest interface, which /// This overload allows the response handler access to the IFlowParallelRequest interface, which
/// can be used to add additional requests to the parallel request before the continuation method passed to the Yield method is called. /// can be used to add additional requests to the parallel request before the continuation method passed to the Yield method is called.
/// </remarks> /// </remarks>
/// <inheritdoc cref="AddRequest{TRequest,TResponse}(TRequest,Func{TResponse,Task})"/> /// <inheritdoc cref="AddRequest{TRequest,TResponse}(TRequest,Func{TResponse,Task})"/>
Task AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, IFlowParallelRequest, Task> responseHandler); Task AddRequest<TRequest, TResponse>(TRequest message, Func<TResponse, IFlowParallelRequest, Task> responseHandler) where TRequest : class where TResponse : class;
/// <summary> /// <summary>
/// Publish a request message and continue the flow when the response arrives. /// Publish a request message and continue the flow when the response arrives.
@ -277,7 +277,7 @@ namespace Tapeti.Flow
/// </summary> /// </summary>
/// <param name="message"></param> /// <param name="message"></param>
/// <param name="responseHandler"></param> /// <param name="responseHandler"></param>
Task AddRequestSync<TRequest, TResponse>(TRequest message, Action<TResponse> responseHandler); Task AddRequestSync<TRequest, TResponse>(TRequest message, Action<TResponse> responseHandler) where TRequest : class where TResponse : class;
} }

View File

@ -55,7 +55,7 @@ namespace Tapeti.Flow
/// <summary> /// <summary>
/// Acquires a copy of the flow state. /// Acquires a copy of the flow state.
/// </summary> /// </summary>
ValueTask<FlowState> GetFlowState(); ValueTask<FlowState?> GetFlowState();
/// <summary> /// <summary>
/// Stores the new flow state. /// Stores the new flow state.

View File

@ -170,11 +170,11 @@ namespace JetBrains.Annotations
{ {
public PublicAPIAttribute() { } public PublicAPIAttribute() { }
public PublicAPIAttribute([NotNull] string comment) public PublicAPIAttribute(string comment)
{ {
Comment = comment; Comment = comment;
} }
[CanBeNull] public string Comment { get; } public string? Comment { get; }
} }
} }

View File

@ -12,6 +12,7 @@
<PackageIcon>Tapeti.Flow.png</PackageIcon> <PackageIcon>Tapeti.Flow.png</PackageIcon>
<Version>2.0.0</Version> <Version>2.0.0</Version>
<LangVersion>9</LangVersion> <LangVersion>9</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@ -12,6 +12,7 @@
<PackageIcon>Tapeti.SimpleInjector.png</PackageIcon> <PackageIcon>Tapeti.SimpleInjector.png</PackageIcon>
<Version>2.0.0</Version> <Version>2.0.0</Version>
<LangVersion>9</LangVersion> <LangVersion>9</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -43,7 +43,7 @@ namespace Tapeti.Serilog.Middleware
} }
private static object DiagnosticContextFactory(IMessageContext context) private static object? DiagnosticContextFactory(IMessageContext context)
{ {
return context.TryGet<DiagnosticContextPayload>(out var diagnosticContextPayload) return context.TryGet<DiagnosticContextPayload>(out var diagnosticContextPayload)
? diagnosticContextPayload.DiagnosticContext ? diagnosticContextPayload.DiagnosticContext

View File

@ -12,6 +12,7 @@
<PackageIcon>Tapeti.Serilog.png</PackageIcon> <PackageIcon>Tapeti.Serilog.png</PackageIcon>
<Version>2.0.0</Version> <Version>2.0.0</Version>
<LangVersion>9</LangVersion> <LangVersion>9</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@ -87,7 +87,7 @@ namespace Tapeti.Serilog
public void ConsumeException(Exception exception, IMessageContext messageContext, ConsumeResult consumeResult) public void ConsumeException(Exception exception, IMessageContext messageContext, ConsumeResult consumeResult)
{ {
var message = new StringBuilder("Tapeti: exception in message handler"); var message = new StringBuilder("Tapeti: exception in message handler");
var messageParams = new List<object>(); var messageParams = new List<object?>();
var contextLogger = seriLogger var contextLogger = seriLogger
.ForContext("consumeResult", consumeResult) .ForContext("consumeResult", consumeResult)
@ -130,7 +130,7 @@ namespace Tapeti.Serilog
} }
/// <inheritdoc /> /// <inheritdoc />
public void QueueExistsWarning(string queueName, IRabbitMQArguments existingArguments, IRabbitMQArguments arguments) public void QueueExistsWarning(string queueName, IRabbitMQArguments? existingArguments, IRabbitMQArguments? arguments)
{ {
seriLogger.Warning("Tapeti: durable queue {queueName} exists with incompatible x-arguments ({existingArguments} vs. {arguments}) and will not be redeclared, queue will be consumed as-is", seriLogger.Warning("Tapeti: durable queue {queueName} exists with incompatible x-arguments ({existingArguments} vs. {arguments}) and will not be redeclared, queue will be consumed as-is",
queueName, queueName,

View File

@ -10,12 +10,12 @@ namespace Tapeti.SimpleInjector
public class SimpleInjectorDependencyResolver : IDependencyContainer public class SimpleInjectorDependencyResolver : IDependencyContainer
{ {
private readonly Container container; private readonly Container container;
private readonly Lifestyle defaultsLifestyle; private readonly Lifestyle? defaultsLifestyle;
private readonly Lifestyle controllersLifestyle; private readonly Lifestyle? controllersLifestyle;
/// <summary> /// <summary>
/// </summary> /// </summary>
public SimpleInjectorDependencyResolver(Container container, Lifestyle defaultsLifestyle = null, Lifestyle controllersLifestyle = null) public SimpleInjectorDependencyResolver(Container container, Lifestyle? defaultsLifestyle = null, Lifestyle? controllersLifestyle = null)
{ {
this.container = container; this.container = container;
this.defaultsLifestyle = defaultsLifestyle; this.defaultsLifestyle = defaultsLifestyle;

View File

@ -12,6 +12,7 @@
<PackageIcon>Tapeti.SimpleInjector.png</PackageIcon> <PackageIcon>Tapeti.SimpleInjector.png</PackageIcon>
<Version>2.0.0</Version> <Version>2.0.0</Version>
<LangVersion>9</LangVersion> <LangVersion>9</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@ -27,7 +27,7 @@ namespace Tapeti.Tests.Client
public ushort RabbitMQManagementPort { get; private set; } public ushort RabbitMQManagementPort { get; private set; }
private TestcontainerMessageBroker testcontainers; private TestcontainerMessageBroker? testcontainers;
private const int DefaultRabbitMQPort = 5672; private const int DefaultRabbitMQPort = 5672;
private const int DefaultRabbitMQManagementPort = 15672; private const int DefaultRabbitMQManagementPort = 15672;

View File

@ -19,7 +19,7 @@ namespace Tapeti.Tests.Client
private readonly RabbitMQFixture fixture; private readonly RabbitMQFixture fixture;
private readonly MockDependencyResolver dependencyResolver = new(); private readonly MockDependencyResolver dependencyResolver = new();
private TapetiClient client; private TapetiClient client = null!;
public TapetiClientTests(RabbitMQFixture fixture, ITestOutputHelper testOutputHelper) public TapetiClientTests(RabbitMQFixture fixture, ITestOutputHelper testOutputHelper)
@ -41,7 +41,6 @@ namespace Tapeti.Tests.Client
public async Task DisposeAsync() public async Task DisposeAsync()
{ {
await client.Close(); await client.Close();
client = null;
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FluentAssertions; using FluentAssertions;
@ -13,6 +14,16 @@ using Xunit;
namespace Tapeti.Tests.Config namespace Tapeti.Tests.Config
{ {
internal static class UTF8StringExtensions
{
public static string AsUTF8String(this object value)
{
value.Should().BeOfType<byte[]>();
return Encoding.UTF8.GetString((byte[])value);
}
}
public class QueueArgumentsTest : BaseControllerTest public class QueueArgumentsTest : BaseControllerTest
{ {
private static readonly MockRepository MoqRepository = new(MockBehavior.Strict); private static readonly MockRepository MoqRepository = new(MockBehavior.Strict);
@ -88,12 +99,12 @@ namespace Tapeti.Tests.Config
declaredQueues.Should().HaveCount(1); declaredQueues.Should().HaveCount(1);
var arguments = declaredQueues["queue-1"]; var arguments = declaredQueues["queue-1"];
arguments.Should().ContainKey("x-custom").WhoseValue.Should().Be("custom value"); arguments.Should().ContainKey("x-custom").WhoseValue.AsUTF8String().Should().Be("custom value");
arguments.Should().ContainKey("x-another").WhoseValue.Should().Be(true); arguments.Should().ContainKey("x-another").WhoseValue.Should().Be(true);
arguments.Should().ContainKey("x-max-length").WhoseValue.Should().Be(100); arguments.Should().ContainKey("x-max-length").WhoseValue.Should().Be(100);
arguments.Should().ContainKey("x-max-length-bytes").WhoseValue.Should().Be(100000); arguments.Should().ContainKey("x-max-length-bytes").WhoseValue.Should().Be(100000);
arguments.Should().ContainKey("x-message-ttl").WhoseValue.Should().Be(4269); arguments.Should().ContainKey("x-message-ttl").WhoseValue.Should().Be(4269);
arguments.Should().ContainKey("x-overflow").WhoseValue.Should().Be("reject-publish"); arguments.Should().ContainKey("x-overflow").WhoseValue.AsUTF8String().Should().Be("reject-publish");
} }

View File

@ -8,7 +8,7 @@ namespace Tapeti.Tests.Mock
private readonly Dictionary<Type, object> container = new(); private readonly Dictionary<Type, object> container = new();
public void Set<TInterface>(TInterface instance) public void Set<TInterface>(TInterface instance) where TInterface : class
{ {
container.Add(typeof(TInterface), instance); container.Add(typeof(TInterface), instance);
} }

View File

@ -49,8 +49,17 @@ namespace Tapeti.Tests.Mock
: $"Declaring {(durable ? "durable" : "dynamic")} queue {queueName}"); : $"Declaring {(durable ? "durable" : "dynamic")} queue {queueName}");
} }
public void QueueExistsWarning(string queueName, IRabbitMQArguments existingArguments, IRabbitMQArguments arguments) public void QueueExistsWarning(string queueName, IRabbitMQArguments? existingArguments, IRabbitMQArguments? arguments)
{ {
testOutputHelper.WriteLine($"[Tapeti] Durable queue {queueName} exists with incompatible x-arguments ({GetArgumentsText(existingArguments)} vs. {GetArgumentsText(arguments)}) and will not be redeclared, queue will be consumed as-is");
}
private static string GetArgumentsText(IRabbitMQArguments? arguments)
{
if (arguments == null || arguments.Count == 0)
return "empty";
var argumentsText = new StringBuilder(); var argumentsText = new StringBuilder();
foreach (var pair in arguments) foreach (var pair in arguments)
{ {
@ -60,9 +69,10 @@ namespace Tapeti.Tests.Mock
argumentsText.Append($"{pair.Key} = {pair.Value}"); argumentsText.Append($"{pair.Key} = {pair.Value}");
} }
testOutputHelper.WriteLine($"Durable queue {queueName} exists with incompatible x-arguments ({argumentsText}) and will not be redeclared, queue will be consumed as-is"); return argumentsText.ToString();
} }
public void QueueBind(string queueName, bool durable, string exchange, string routingKey) public void QueueBind(string queueName, bool durable, string exchange, string routingKey)
{ {
testOutputHelper.WriteLine($"Binding {queueName} to exchange {exchange} with routing key {routingKey}"); testOutputHelper.WriteLine($"Binding {queueName} to exchange {exchange} with routing key {routingKey}");

View File

@ -2,6 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@ -15,6 +15,6 @@ namespace Tapeti.Transient
/// <param name="request"></param> /// <param name="request"></param>
/// <typeparam name="TRequest"></typeparam> /// <typeparam name="TRequest"></typeparam>
/// <typeparam name="TResponse"></typeparam> /// <typeparam name="TResponse"></typeparam>
Task<TResponse> RequestResponse<TRequest, TResponse>(TRequest request); Task<TResponse> RequestResponse<TRequest, TResponse>(TRequest request) where TRequest : class where TResponse : class;
} }
} }

View File

@ -12,6 +12,7 @@
<PackageIcon>Tapeti.Flow.png</PackageIcon> <PackageIcon>Tapeti.Flow.png</PackageIcon>
<Version>2.0.0</Version> <Version>2.0.0</Version>
<LangVersion>9</LangVersion> <LangVersion>9</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Tapeti.Config; using Tapeti.Config;
namespace Tapeti.Transient namespace Tapeti.Transient
@ -31,7 +32,7 @@ namespace Tapeti.Transient
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<object> GetMiddleware(IDependencyResolver dependencyResolver) public IEnumerable<object> GetMiddleware(IDependencyResolver dependencyResolver)
{ {
return null; return Enumerable.Empty<object>();
} }

View File

@ -14,10 +14,10 @@ namespace Tapeti.Transient
private readonly string dynamicQueuePrefix; private readonly string dynamicQueuePrefix;
/// <inheritdoc /> /// <inheritdoc />
public string QueueName { get; private set; } public string? QueueName { get; private set; }
/// <inheritdoc /> /// <inheritdoc />
public QueueType QueueType => QueueType.Dynamic; public QueueType? QueueType => Config.QueueType.Dynamic;
/// <summary> /// <summary>

View File

@ -21,7 +21,7 @@ namespace Tapeti.Transient
/// <inheritdoc /> /// <inheritdoc />
public async Task<TResponse> RequestResponse<TRequest, TResponse>(TRequest request) public async Task<TResponse> RequestResponse<TRequest, TResponse>(TRequest request) where TRequest : class where TResponse : class
{ {
return (TResponse)await router.RequestResponse(publisher, request); return (TResponse)await router.RequestResponse(publisher, request);
} }

View File

@ -18,7 +18,7 @@ namespace Tapeti.Transient
/// <summary> /// <summary>
/// The generated name of the dynamic queue to which responses should be sent. /// The generated name of the dynamic queue to which responses should be sent.
/// </summary> /// </summary>
public string TransientResponseQueueName { get; set; } public string? TransientResponseQueueName { get; set; }
/// <summary> /// <summary>
@ -41,7 +41,12 @@ namespace Tapeti.Transient
if (!Guid.TryParse(context.Properties.CorrelationId, out var continuationID)) if (!Guid.TryParse(context.Properties.CorrelationId, out var continuationID))
return; return;
if (map.TryRemove(continuationID, out var tcs)) if (!map.TryRemove(continuationID, out var tcs))
return;
if (context.Message == null)
throw new InvalidOperationException();
tcs.TrySetResult(context.Message); tcs.TrySetResult(context.Message);
} }
@ -72,7 +77,7 @@ namespace Tapeti.Transient
{ {
// Simple cleanup of the task and map dictionary. // Simple cleanup of the task and map dictionary.
if (map.TryRemove(correlation, out tcs)) if (map.TryRemove(correlation, out tcs))
tcs.TrySetResult(null); tcs.TrySetResult(null!);
throw; throw;
} }
@ -84,8 +89,10 @@ namespace Tapeti.Transient
} }
private void TimeoutResponse(object tcs) private void TimeoutResponse(object? tcs)
{ {
ArgumentNullException.ThrowIfNull(tcs, nameof(tcs));
((TaskCompletionSource<object>)tcs).TrySetException(new TimeoutException("Transient RequestResponse timed out at (ms) " + defaultTimeoutMs)); ((TaskCompletionSource<object>)tcs).TrySetException(new TimeoutException("Transient RequestResponse timed out at (ms) " + defaultTimeoutMs));
} }
} }

View File

@ -8,7 +8,7 @@
/// <summary> /// <summary>
/// An instance of the controller referenced by the binding. Note: can be null during Cleanup or when bound to static methods. /// An instance of the controller referenced by the binding. Note: can be null during Cleanup or when bound to static methods.
/// </summary> /// </summary>
public object Controller { get; } public object? Controller { get; }
/// <remarks> /// <remarks>
@ -22,7 +22,7 @@
/// </summary> /// </summary>
/// <param name="controller">An instance of the controller referenced by the binding</param> /// <param name="controller">An instance of the controller referenced by the binding</param>
/// <param name="binding">The binding which is currently processing the message</param> /// <param name="binding">The binding which is currently processing the message</param>
public ControllerMessageContextPayload(object controller, IControllerMethodBinding binding) public ControllerMessageContextPayload(object? controller, IControllerMethodBinding binding)
{ {
Controller = controller; Controller = controller;
Binding = binding; Binding = binding;

View File

@ -29,13 +29,13 @@ namespace Tapeti.Config
/// <summary> /// <summary>
/// The name of the queue the binding is consuming. May change after a reconnect for dynamic queues. /// The name of the queue the binding is consuming. May change after a reconnect for dynamic queues.
/// </summary> /// </summary>
string QueueName { get; } string? QueueName { get; }
/// <summary> /// <summary>
/// Determines the type of queue the binding registers /// Determines the type of queue the binding registers
/// </summary> /// </summary>
QueueType QueueType { get; } QueueType? QueueType { get; }
/// <summary> /// <summary>
@ -82,7 +82,7 @@ namespace Tapeti.Config
/// <param name="messageClass">The message class to be bound to the queue</param> /// <param name="messageClass">The message class to be bound to the queue</param>
/// <param name="queueName">The name of the durable queue</param> /// <param name="queueName">The name of the durable queue</param>
/// <param name="arguments">Optional arguments</param> /// <param name="arguments">Optional arguments</param>
ValueTask BindDurable(Type messageClass, string queueName, IRabbitMQArguments arguments); ValueTask BindDurable(Type messageClass, string queueName, IRabbitMQArguments? arguments);
/// <summary> /// <summary>
/// Binds the messageClass to a dynamic auto-delete queue. /// Binds the messageClass to a dynamic auto-delete queue.
@ -95,7 +95,7 @@ namespace Tapeti.Config
/// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param> /// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param>
/// <param name="arguments">Optional arguments</param> /// <param name="arguments">Optional arguments</param>
/// <returns>The generated name of the dynamic queue</returns> /// <returns>The generated name of the dynamic queue</returns>
ValueTask<string> BindDynamic(Type messageClass, string queuePrefix, IRabbitMQArguments arguments); ValueTask<string> BindDynamic(Type messageClass, string? queuePrefix, IRabbitMQArguments? arguments);
/// <summary> /// <summary>
/// Declares a durable queue but does not add a binding for a messageClass' routing key. /// Declares a durable queue but does not add a binding for a messageClass' routing key.
@ -103,7 +103,7 @@ namespace Tapeti.Config
/// </summary> /// </summary>
/// <param name="queueName">The name of the durable queue</param> /// <param name="queueName">The name of the durable queue</param>
/// <param name="arguments">Optional arguments</param> /// <param name="arguments">Optional arguments</param>
ValueTask BindDurableDirect(string queueName, IRabbitMQArguments arguments); ValueTask BindDurableDirect(string queueName, IRabbitMQArguments? arguments);
/// <summary> /// <summary>
/// Declares a dynamic queue but does not add a binding for a messageClass' routing key. /// Declares a dynamic queue but does not add a binding for a messageClass' routing key.
@ -113,7 +113,7 @@ namespace Tapeti.Config
/// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param> /// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param>
/// <param name="arguments">Optional arguments</param> /// <param name="arguments">Optional arguments</param>
/// <returns>The generated name of the dynamic queue</returns> /// <returns>The generated name of the dynamic queue</returns>
ValueTask<string> BindDynamicDirect(Type messageClass, string queuePrefix, IRabbitMQArguments arguments); ValueTask<string> BindDynamicDirect(Type messageClass, string? queuePrefix, IRabbitMQArguments? arguments);
/// <summary> /// <summary>
/// Declares a dynamic queue but does not add a binding for a messageClass' routing key. /// Declares a dynamic queue but does not add a binding for a messageClass' routing key.
@ -122,7 +122,7 @@ namespace Tapeti.Config
/// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param> /// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param>
/// <param name="arguments">Optional arguments</param> /// <param name="arguments">Optional arguments</param>
/// <returns>The generated name of the dynamic queue</returns> /// <returns>The generated name of the dynamic queue</returns>
ValueTask<string> BindDynamicDirect(string queuePrefix, IRabbitMQArguments arguments); ValueTask<string> BindDynamicDirect(string? queuePrefix, IRabbitMQArguments? arguments);
/// <summary> /// <summary>
/// Marks the specified durable queue as having an obsolete binding. If after all bindings have subscribed, the queue only contains obsolete /// Marks the specified durable queue as having an obsolete binding. If after all bindings have subscribed, the queue only contains obsolete

View File

@ -11,7 +11,7 @@ namespace Tapeti.Config
/// Injects a value for a controller method parameter. /// Injects a value for a controller method parameter.
/// </summary> /// </summary>
/// <param name="context"></param> /// <param name="context"></param>
public delegate object ValueFactory(IMessageContext context); public delegate object? ValueFactory(IMessageContext context);
/// <summary> /// <summary>
@ -19,7 +19,7 @@ namespace Tapeti.Config
/// </summary> /// </summary>
/// <param name="context"></param> /// <param name="context"></param>
/// <param name="value"></param> /// <param name="value"></param>
public delegate ValueTask ResultHandler(IMessageContext context, object value); public delegate ValueTask ResultHandler(IMessageContext context, object? value);
/// <summary> /// <summary>
@ -48,7 +48,7 @@ namespace Tapeti.Config
/// The message class for this method. Can be null if not yet set by the default MessageBinding or other middleware. /// The message class for this method. Can be null if not yet set by the default MessageBinding or other middleware.
/// If required, call next first to ensure it is available. /// If required, call next first to ensure it is available.
/// </summary> /// </summary>
Type MessageClass { get; } Type? MessageClass { get; }
/// <summary> /// <summary>
/// Determines if SetMessageClass has already been called. /// Determines if SetMessageClass has already been called.

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading; using System.Threading;
// ReSharper disable UnusedMemberInSuper.Global - public API // ReSharper disable UnusedMemberInSuper.Global - public API
@ -34,12 +35,12 @@ namespace Tapeti.Config
/// <summary> /// <summary>
/// Contains the raw body of the message. /// Contains the raw body of the message.
/// </summary> /// </summary>
byte[] RawBody { get; } byte[]? RawBody { get; }
/// <summary> /// <summary>
/// Contains the decoded message instance. /// Contains the decoded message instance.
/// </summary> /// </summary>
object Message { get; } object? Message { get; }
/// <summary> /// <summary>
/// Provides access to the message metadata. /// Provides access to the message metadata.
@ -87,7 +88,7 @@ namespace Tapeti.Config
/// Returns true and the payload value if this message context was previously enriched with the payload T. /// Returns true and the payload value if this message context was previously enriched with the payload T.
/// </summary> /// </summary>
/// <typeparam name="T">The payload type as passed to Enrich</typeparam> /// <typeparam name="T">The payload type as passed to Enrich</typeparam>
bool TryGet<T>(out T payload) where T : IMessageContextPayload; bool TryGet<T>([NotNullWhen(true)] out T? payload) where T : IMessageContextPayload;
/// <summary> /// <summary>
/// Stores a key-value pair in the context for passing information between the various /// Stores a key-value pair in the context for passing information between the various
@ -105,7 +106,7 @@ namespace Tapeti.Config
/// <param name="value"></param> /// <param name="value"></param>
/// <returns>True if the value was found, False otherwise</returns> /// <returns>True if the value was found, False otherwise</returns>
[Obsolete("For backwards compatibility only. Use Get<T> payload overload for typed properties instead")] [Obsolete("For backwards compatibility only. Use Get<T> payload overload for typed properties instead")]
bool Get<T>(string key, out T value) where T : class; bool Get<T>(string key, out T? value) where T : class;
} }

View File

@ -9,13 +9,13 @@ namespace Tapeti.Config
public interface IMessageProperties public interface IMessageProperties
{ {
/// <summary></summary> /// <summary></summary>
string ContentType { get; set; } string? ContentType { get; set; }
/// <summary></summary> /// <summary></summary>
string CorrelationId { get; set; } string? CorrelationId { get; set; }
/// <summary></summary> /// <summary></summary>
string ReplyTo { get; set; } string? ReplyTo { get; set; }
/// <summary></summary> /// <summary></summary>
bool? Persistent { get; set; } bool? Persistent { get; set; }
@ -37,7 +37,7 @@ namespace Tapeti.Config
/// </summary> /// </summary>
/// <param name="name"></param> /// <param name="name"></param>
/// <returns>The value if found, null otherwise</returns> /// <returns>The value if found, null otherwise</returns>
string GetHeader(string name); string? GetHeader(string name);
/// <summary> /// <summary>

View File

@ -16,7 +16,7 @@ namespace Tapeti.Config
/// <summary> /// <summary>
/// The exchange to which the message will be published. /// The exchange to which the message will be published.
/// </summary> /// </summary>
string Exchange { get; set; } string? Exchange { get; set; }
/// <summary> /// <summary>
/// The routing key which will be included with the message. /// The routing key which will be included with the message.
@ -31,6 +31,6 @@ namespace Tapeti.Config
/// <summary> /// <summary>
/// Provides access to the message metadata. /// Provides access to the message metadata.
/// </summary> /// </summary>
IMessageProperties Properties { get; } IMessageProperties? Properties { get; }
} }
} }

View File

@ -87,40 +87,13 @@ namespace Tapeti.Config
/// </summary> /// </summary>
/// <param name="method"></param> /// <param name="method"></param>
/// <returns>The binding if found, null otherwise</returns> /// <returns>The binding if found, null otherwise</returns>
IControllerMethodBinding ForMethod(Delegate method); IControllerMethodBinding? ForMethod(Delegate method);
/// <summary> /// <summary>
/// Searches for a binding linked to the specified method. /// Searches for a binding linked to the specified method.
/// </summary> /// </summary>
/// <param name="method"></param> /// <param name="method"></param>
/// <returns>The binding if found, null otherwise</returns> /// <returns>The binding if found, null otherwise</returns>
IControllerMethodBinding ForMethod(MethodInfo method); IControllerMethodBinding? ForMethod(MethodInfo method);
} }
/*
public interface IBinding
{
Type Controller { get; }
MethodInfo Method { get; }
Type MessageClass { get; }
string QueueName { get; }
QueueBindingMode QueueBindingMode { get; set; }
IReadOnlyList<IMessageFilterMiddleware> MessageFilterMiddleware { get; }
IReadOnlyList<IMessageMiddleware> MessageMiddleware { get; }
bool Accept(Type messageClass);
bool Accept(IMessageContext context, object message);
Task Invoke(IMessageContext context, object message);
}
*/
/*
public interface IBuildBinding : IBinding
{
void SetQueueName(string queueName);
}
*/
} }

View File

@ -37,9 +37,9 @@ namespace Tapeti.Connection
} }
/// <inheritdoc /> /// <inheritdoc />
public override bool Equals(object obj) public override bool Equals(object? obj)
{ {
if (ReferenceEquals(null, obj)) return false; if (obj is null) return false;
return obj is QueueBinding other && Equals(other); return obj is QueueBinding other && Equals(other);
} }
@ -79,7 +79,7 @@ namespace Tapeti.Connection
/// <param name="exchange">The exchange to publish the message to, or empty to send it directly to a queue</param> /// <param name="exchange">The exchange to publish the message to, or empty to send it directly to a queue</param>
/// <param name="routingKey">The routing key for the message, or queue name if exchange is empty</param> /// <param name="routingKey">The routing key for the message, or queue name if exchange is empty</param>
/// <param name="mandatory">If true, an exception will be raised if the message can not be delivered to at least one queue</param> /// <param name="mandatory">If true, an exception will be raised if the message can not be delivered to at least one queue</param>
Task Publish(byte[] body, IMessageProperties properties, string exchange, string routingKey, bool mandatory); Task Publish(byte[] body, IMessageProperties properties, string? exchange, string routingKey, bool mandatory);
/// <summary> /// <summary>
@ -89,7 +89,7 @@ namespace Tapeti.Connection
/// <param name="consumer">The consumer implementation which will receive the messages from the queue</param> /// <param name="consumer">The consumer implementation which will receive the messages from the queue</param>
/// <param name="cancellationToken">Cancelled when the connection is lost</param> /// <param name="cancellationToken">Cancelled when the connection is lost</param>
/// <returns>The consumer tag as returned by BasicConsume.</returns> /// <returns>The consumer tag as returned by BasicConsume.</returns>
Task<TapetiConsumerTag> Consume(string queueName, IConsumer consumer, CancellationToken cancellationToken); Task<TapetiConsumerTag?> Consume(string queueName, IConsumer consumer, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Stops the consumer with the specified tag. /// Stops the consumer with the specified tag.
@ -104,7 +104,7 @@ namespace Tapeti.Connection
/// <param name="bindings">A list of bindings. Any bindings already on the queue which are not in this list will be removed</param> /// <param name="bindings">A list of bindings. Any bindings already on the queue which are not in this list will be removed</param>
/// <param name="arguments">Optional arguments</param> /// <param name="arguments">Optional arguments</param>
/// <param name="cancellationToken">Cancelled when the connection is lost</param> /// <param name="cancellationToken">Cancelled when the connection is lost</param>
Task DurableQueueDeclare(string queueName, IEnumerable<QueueBinding> bindings, IRabbitMQArguments arguments, CancellationToken cancellationToken); Task DurableQueueDeclare(string queueName, IEnumerable<QueueBinding> bindings, IRabbitMQArguments? arguments, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Verifies a durable queue exists. Will raise an exception if it does not. /// Verifies a durable queue exists. Will raise an exception if it does not.
@ -112,7 +112,7 @@ namespace Tapeti.Connection
/// <param name="queueName">The name of the queue to verify</param> /// <param name="queueName">The name of the queue to verify</param>
/// <param name="arguments">Optional arguments</param> /// <param name="arguments">Optional arguments</param>
/// <param name="cancellationToken">Cancelled when the connection is lost</param> /// <param name="cancellationToken">Cancelled when the connection is lost</param>
Task DurableQueueVerify(string queueName, IRabbitMQArguments arguments, CancellationToken cancellationToken); Task DurableQueueVerify(string queueName, IRabbitMQArguments? arguments, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Deletes a durable queue. /// Deletes a durable queue.
@ -128,7 +128,7 @@ namespace Tapeti.Connection
/// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param> /// <param name="queuePrefix">An optional prefix for the dynamic queue's name. If not provided, RabbitMQ's default logic will be used to create an amq.gen queue.</param>
/// <param name="arguments">Optional arguments</param> /// <param name="arguments">Optional arguments</param>
/// <param name="cancellationToken">Cancelled when the connection is lost</param> /// <param name="cancellationToken">Cancelled when the connection is lost</param>
Task<string> DynamicQueueDeclare(string queuePrefix, IRabbitMQArguments arguments, CancellationToken cancellationToken); Task<string> DynamicQueueDeclare(string? queuePrefix, IRabbitMQArguments? arguments, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Add a binding to a dynamic queue. /// Add a binding to a dynamic queue.

View File

@ -15,17 +15,10 @@ namespace Tapeti.Connection
{ {
} }
#if NETSTANDARD2_1_OR_GREATER
public RabbitMQArguments(IReadOnlyDictionary<string, object> values) : base(values) public RabbitMQArguments(IReadOnlyDictionary<string, object> values) : base(values)
{ {
} }
#else
public RabbitMQArguments(IReadOnlyDictionary<string, object> values)
{
foreach (var pair in values)
Add(pair.Key, pair.Value);
}
#endif
public void AddUTF8(string key, string value) public void AddUTF8(string key, string value)

View File

@ -21,7 +21,7 @@ namespace Tapeti.Connection
{ {
private readonly Func<IModel> modelFactory; private readonly Func<IModel> modelFactory;
private readonly object taskQueueLock = new(); private readonly object taskQueueLock = new();
private SingleThreadTaskQueue taskQueue; private SingleThreadTaskQueue? taskQueue;
private readonly ModelProvider modelProvider; private readonly ModelProvider modelProvider;
@ -34,7 +34,7 @@ namespace Tapeti.Connection
public async Task Reset() public async Task Reset()
{ {
SingleThreadTaskQueue capturedTaskQueue; SingleThreadTaskQueue? capturedTaskQueue;
lock (taskQueueLock) lock (taskQueueLock)
{ {

View File

@ -43,7 +43,7 @@ namespace Tapeti.Connection
/// <summary> /// <summary>
/// Receives events when the connection state changes. /// Receives events when the connection state changes.
/// </summary> /// </summary>
public IConnectionEventListener ConnectionEventListener { get; set; } public IConnectionEventListener? ConnectionEventListener { get; set; }
private readonly TapetiChannel consumeChannel; private readonly TapetiChannel consumeChannel;
@ -53,9 +53,9 @@ namespace Tapeti.Connection
// These fields must be locked using connectionLock // These fields must be locked using connectionLock
private readonly object connectionLock = new(); private readonly object connectionLock = new();
private long connectionReference; private long connectionReference;
private RabbitMQ.Client.IConnection connection; private RabbitMQ.Client.IConnection? connection;
private IModel consumeChannelModel; private IModel? consumeChannelModel;
private IModel publishChannelModel; private IModel? publishChannelModel;
private bool isClosing; private bool isClosing;
private bool isReconnect; private bool isReconnect;
private DateTime connectedDateTime; private DateTime connectedDateTime;
@ -72,8 +72,15 @@ namespace Tapeti.Connection
private class ConfirmMessageInfo private class ConfirmMessageInfo
{ {
public string ReturnKey; public string ReturnKey { get; }
public TaskCompletionSource<int> CompletionSource; public TaskCompletionSource<int> CompletionSource { get; }
public ConfirmMessageInfo(string returnKey, TaskCompletionSource<int> completionSource)
{
ReturnKey = returnKey;
CompletionSource = completionSource;
}
} }
@ -110,7 +117,7 @@ namespace Tapeti.Connection
/// <inheritdoc /> /// <inheritdoc />
public async Task Publish(byte[] body, IMessageProperties properties, string exchange, string routingKey, bool mandatory) public async Task Publish(byte[] body, IMessageProperties properties, string? exchange, string routingKey, bool mandatory)
{ {
if (string.IsNullOrEmpty(routingKey)) if (string.IsNullOrEmpty(routingKey))
throw new ArgumentNullException(nameof(routingKey)); throw new ArgumentNullException(nameof(routingKey));
@ -118,16 +125,13 @@ namespace Tapeti.Connection
await GetTapetiChannel(TapetiChannelType.Publish).QueueWithProvider(async channelProvider => await GetTapetiChannel(TapetiChannelType.Publish).QueueWithProvider(async channelProvider =>
{ {
Task<int> publishResultTask = null; Task<int>? publishResultTask = null;
var messageInfo = new ConfirmMessageInfo var messageInfo = new ConfirmMessageInfo(GetReturnKey(exchange ?? string.Empty, routingKey), new TaskCompletionSource<int>());
{
ReturnKey = GetReturnKey(exchange, routingKey),
CompletionSource = new TaskCompletionSource<int>()
};
channelProvider.WithRetryableChannel(channel => channelProvider.WithRetryableChannel(channel =>
{ {
if (exchange != null)
DeclareExchange(channel, exchange); DeclareExchange(channel, exchange);
// The delivery tag is lost after a reconnect, register under the new tag // The delivery tag is lost after a reconnect, register under the new tag
@ -153,7 +157,7 @@ namespace Tapeti.Connection
try try
{ {
var publishProperties = new RabbitMQMessageProperties(channel.CreateBasicProperties(), properties); var publishProperties = new RabbitMQMessageProperties(channel.CreateBasicProperties(), properties);
channel.BasicPublish(exchange ?? "", routingKey, mandatory, publishProperties.BasicProperties, body); channel.BasicPublish(exchange ?? string.Empty, routingKey, mandatory, publishProperties.BasicProperties, body);
} }
catch catch
{ {
@ -202,7 +206,7 @@ namespace Tapeti.Connection
/// <inheritdoc /> /// <inheritdoc />
public async Task<TapetiConsumerTag> Consume(string queueName, IConsumer consumer, CancellationToken cancellationToken) public async Task<TapetiConsumerTag?> Consume(string queueName, IConsumer consumer, CancellationToken cancellationToken)
{ {
if (deletedQueues.Contains(queueName)) if (deletedQueues.Contains(queueName))
return null; return null;
@ -212,7 +216,7 @@ namespace Tapeti.Connection
long capturedConnectionReference = -1; long capturedConnectionReference = -1;
string consumerTag = null; string? consumerTag = null;
await GetTapetiChannel(TapetiChannelType.Consume).QueueRetryable(channel => await GetTapetiChannel(TapetiChannelType.Consume).QueueRetryable(channel =>
{ {
@ -224,7 +228,9 @@ namespace Tapeti.Connection
consumerTag = channel.BasicConsume(queueName, false, basicConsumer); consumerTag = channel.BasicConsume(queueName, false, basicConsumer);
}); });
return new TapetiConsumerTag(capturedConnectionReference, consumerTag); return consumerTag == null
? null
: new TapetiConsumerTag(capturedConnectionReference, consumerTag);
} }
@ -289,7 +295,7 @@ namespace Tapeti.Connection
} }
private async Task<bool> GetDurableQueueDeclareRequired(string queueName, IRabbitMQArguments arguments) private async Task<bool> GetDurableQueueDeclareRequired(string queueName, IRabbitMQArguments? arguments)
{ {
var existingQueue = await GetQueueInfo(queueName); var existingQueue = await GetQueueInfo(queueName);
if (existingQueue == null) if (existingQueue == null)
@ -298,9 +304,6 @@ namespace Tapeti.Connection
if (!existingQueue.Durable || existingQueue.AutoDelete || existingQueue.Exclusive) if (!existingQueue.Durable || existingQueue.AutoDelete || existingQueue.Exclusive)
throw new InvalidOperationException($"Durable queue {queueName} already exists with incompatible parameters, durable = {existingQueue.Durable} (expected True), autoDelete = {existingQueue.AutoDelete} (expected False), exclusive = {existingQueue.Exclusive} (expected False)"); throw new InvalidOperationException($"Durable queue {queueName} already exists with incompatible parameters, durable = {existingQueue.Durable} (expected True), autoDelete = {existingQueue.AutoDelete} (expected False), exclusive = {existingQueue.Exclusive} (expected False)");
if (arguments == null && existingQueue.Arguments.Count == 0)
return true;
var existingArguments = ConvertJsonArguments(existingQueue.Arguments); var existingArguments = ConvertJsonArguments(existingQueue.Arguments);
if (existingArguments.NullSafeSameValues(arguments)) if (existingArguments.NullSafeSameValues(arguments))
return true; return true;
@ -310,7 +313,7 @@ namespace Tapeti.Connection
} }
private static RabbitMQArguments ConvertJsonArguments(IReadOnlyDictionary<string, JObject> arguments) private static RabbitMQArguments? ConvertJsonArguments(IReadOnlyDictionary<string, JObject>? arguments)
{ {
if (arguments == null) if (arguments == null)
return null; return null;
@ -325,7 +328,6 @@ namespace Tapeti.Connection
JTokenType.Float => pair.Value.Value<double>(), JTokenType.Float => pair.Value.Value<double>(),
JTokenType.String => Encoding.UTF8.GetBytes(pair.Value.Value<string>() ?? string.Empty), JTokenType.String => Encoding.UTF8.GetBytes(pair.Value.Value<string>() ?? string.Empty),
JTokenType.Boolean => pair.Value.Value<bool>(), JTokenType.Boolean => pair.Value.Value<bool>(),
JTokenType.Null => null,
_ => throw new ArgumentOutOfRangeException(nameof(arguments)) _ => throw new ArgumentOutOfRangeException(nameof(arguments))
}; };
@ -338,7 +340,7 @@ namespace Tapeti.Connection
/// <inheritdoc /> /// <inheritdoc />
public async Task DurableQueueDeclare(string queueName, IEnumerable<QueueBinding> bindings, IRabbitMQArguments arguments, CancellationToken cancellationToken) public async Task DurableQueueDeclare(string queueName, IEnumerable<QueueBinding> bindings, IRabbitMQArguments? arguments, CancellationToken cancellationToken)
{ {
var declareRequired = await GetDurableQueueDeclareRequired(queueName, arguments); var declareRequired = await GetDurableQueueDeclareRequired(queueName, arguments);
@ -373,7 +375,7 @@ namespace Tapeti.Connection
} }
private static IDictionary<string, object> GetDeclareArguments(IRabbitMQArguments arguments) private static IDictionary<string, object>? GetDeclareArguments(IRabbitMQArguments? arguments)
{ {
return arguments == null || arguments.Count == 0 return arguments == null || arguments.Count == 0
? null ? null
@ -382,7 +384,7 @@ namespace Tapeti.Connection
/// <inheritdoc /> /// <inheritdoc />
public async Task DurableQueueVerify(string queueName, IRabbitMQArguments arguments, CancellationToken cancellationToken) public async Task DurableQueueVerify(string queueName, IRabbitMQArguments? arguments, CancellationToken cancellationToken)
{ {
if (!await GetDurableQueueDeclareRequired(queueName, arguments)) if (!await GetDurableQueueDeclareRequired(queueName, arguments))
return; return;
@ -484,9 +486,9 @@ namespace Tapeti.Connection
/// <inheritdoc /> /// <inheritdoc />
public async Task<string> DynamicQueueDeclare(string queuePrefix, IRabbitMQArguments arguments, CancellationToken cancellationToken) public async Task<string> DynamicQueueDeclare(string? queuePrefix, IRabbitMQArguments? arguments, CancellationToken cancellationToken)
{ {
string queueName = null; string? queueName = null;
var bindingLogger = logger as IBindingLogger; var bindingLogger = logger as IBindingLogger;
await GetTapetiChannel(TapetiChannelType.Consume).Queue(channel => await GetTapetiChannel(TapetiChannelType.Consume).Queue(channel =>
@ -507,6 +509,10 @@ namespace Tapeti.Connection
} }
}); });
cancellationToken.ThrowIfCancellationRequested();
if (queueName == null)
throw new InvalidOperationException("Failed to declare dynamic queue");
return queueName; return queueName;
} }
@ -528,9 +534,9 @@ namespace Tapeti.Connection
/// <inheritdoc /> /// <inheritdoc />
public async Task Close() public async Task Close()
{ {
IModel capturedConsumeModel; IModel? capturedConsumeModel;
IModel capturedPublishModel; IModel? capturedPublishModel;
RabbitMQ.Client.IConnection capturedConnection; RabbitMQ.Client.IConnection? capturedConnection;
lock (connectionLock) lock (connectionLock)
{ {
@ -578,10 +584,10 @@ namespace Tapeti.Connection
private class ManagementQueueInfo private class ManagementQueueInfo
{ {
[JsonProperty("name")] [JsonProperty("name")]
public string Name { get; set; } public string? Name { get; set; }
[JsonProperty("vhost")] [JsonProperty("vhost")]
public string VHost { get; set; } public string? VHost { get; set; }
[JsonProperty("durable")] [JsonProperty("durable")]
public bool Durable { get; set; } public bool Durable { get; set; }
@ -593,7 +599,7 @@ namespace Tapeti.Connection
public bool Exclusive { get; set; } public bool Exclusive { get; set; }
[JsonProperty("arguments")] [JsonProperty("arguments")]
public Dictionary<string, JObject> Arguments { get; set; } public Dictionary<string, JObject>? Arguments { get; set; }
[JsonProperty("messages")] [JsonProperty("messages")]
public uint Messages { get; set; } public uint Messages { get; set; }
@ -601,7 +607,7 @@ namespace Tapeti.Connection
private async Task<ManagementQueueInfo> GetQueueInfo(string queueName) private async Task<ManagementQueueInfo?> GetQueueInfo(string queueName)
{ {
var virtualHostPath = Uri.EscapeDataString(connectionParams.VirtualHost); var virtualHostPath = Uri.EscapeDataString(connectionParams.VirtualHost);
var queuePath = Uri.EscapeDataString(queueName); var queuePath = Uri.EscapeDataString(queueName);
@ -622,25 +628,25 @@ namespace Tapeti.Connection
private class ManagementBinding private class ManagementBinding
{ {
[JsonProperty("source")] [JsonProperty("source")]
public string Source { get; set; } public string? Source { get; set; }
[JsonProperty("vhost")] [JsonProperty("vhost")]
public string Vhost { get; set; } public string? Vhost { get; set; }
[JsonProperty("destination")] [JsonProperty("destination")]
public string Destination { get; set; } public string? Destination { get; set; }
[JsonProperty("destination_type")] [JsonProperty("destination_type")]
public string DestinationType { get; set; } public string? DestinationType { get; set; }
[JsonProperty("routing_key")] [JsonProperty("routing_key")]
public string RoutingKey { get; set; } public string? RoutingKey { get; set; }
[JsonProperty("arguments")] [JsonProperty("arguments")]
public Dictionary<string, string> Arguments { get; set; } public Dictionary<string, string>? Arguments { get; set; }
[JsonProperty("properties_key")] [JsonProperty("properties_key")]
public string PropertiesKey { get; set; } public string? PropertiesKey { get; set; }
} }
@ -658,8 +664,8 @@ namespace Tapeti.Connection
// Filter out the binding to an empty source, which is always present for direct-to-queue routing // Filter out the binding to an empty source, which is always present for direct-to-queue routing
return bindings? return bindings?
.Where(binding => !string.IsNullOrEmpty(binding.Source)) .Where(binding => !string.IsNullOrEmpty(binding.Source) && !string.IsNullOrEmpty(binding.RoutingKey))
.Select(binding => new QueueBinding(binding.Source, binding.RoutingKey)) .Select(binding => new QueueBinding(binding.Source!, binding.RoutingKey!))
?? Enumerable.Empty<QueueBinding>(); ?? Enumerable.Empty<QueueBinding>();
}); });
} }
@ -723,9 +729,6 @@ namespace Tapeti.Connection
private void DeclareExchange(IModel channel, string exchange) private void DeclareExchange(IModel channel, string exchange)
{ {
if (string.IsNullOrEmpty(exchange))
return;
if (declaredExchanges.Contains(exchange)) if (declaredExchanges.Contains(exchange))
return; return;
@ -791,9 +794,9 @@ namespace Tapeti.Connection
{ {
try try
{ {
RabbitMQ.Client.IConnection capturedConnection; RabbitMQ.Client.IConnection? capturedConnection;
IModel capturedConsumeChannelModel; IModel? capturedConsumeChannelModel;
IModel capturedPublishChannelModel; IModel? capturedPublishChannelModel;
lock (connectionLock) lock (connectionLock)
@ -805,7 +808,7 @@ namespace Tapeti.Connection
{ {
try try
{ {
if (connection.IsOpen) if (connection is { IsOpen: true })
connection.Close(); connection.Close();
} }
catch (AlreadyClosedException) catch (AlreadyClosedException)
@ -813,7 +816,7 @@ namespace Tapeti.Connection
} }
finally finally
{ {
connection.Dispose(); connection?.Dispose();
} }
connection = null; connection = null;
@ -873,12 +876,7 @@ namespace Tapeti.Connection
consumeChannelModel = null; consumeChannelModel = null;
} }
ConnectionEventListener?.Disconnected(new DisconnectedEventArgs ConnectionEventListener?.Disconnected(new DisconnectedEventArgs(e.ReplyCode, e.ReplyText));
{
ReplyCode = e.ReplyCode,
ReplyText = e.ReplyText
});
logger.Disconnect(new DisconnectContext(connectionParams, e.ReplyCode, e.ReplyText)); logger.Disconnect(new DisconnectContext(connectionParams, e.ReplyCode, e.ReplyText));
// Reconnect if the disconnect was unexpected // Reconnect if the disconnect was unexpected
@ -906,11 +904,7 @@ namespace Tapeti.Connection
connectedDateTime = DateTime.UtcNow; connectedDateTime = DateTime.UtcNow;
var connectedEventArgs = new ConnectedEventArgs var connectedEventArgs = new ConnectedEventArgs(connectionParams, capturedConnection.LocalPort);
{
ConnectionParams = connectionParams,
LocalPort = capturedConnection.LocalPort
};
if (isReconnect) if (isReconnect)
ConnectionEventListener?.Reconnected(connectedEventArgs); ConnectionEventListener?.Reconnected(connectedEventArgs);
@ -938,7 +932,7 @@ namespace Tapeti.Connection
} }
private void HandleBasicReturn(object sender, BasicReturnEventArgs e) private void HandleBasicReturn(object? sender, BasicReturnEventArgs e)
{ {
/* /*
* "If the message is also published as mandatory, the basic.return is sent to the client before basic.ack." * "If the message is also published as mandatory, the basic.return is sent to the client before basic.ack."
@ -968,7 +962,7 @@ namespace Tapeti.Connection
} }
private void HandleBasicAck(object sender, BasicAckEventArgs e) private void HandleBasicAck(object? sender, BasicAckEventArgs e)
{ {
Monitor.Enter(confirmLock); Monitor.Enter(confirmLock);
try try
@ -999,7 +993,7 @@ namespace Tapeti.Connection
} }
private void HandleBasicNack(object sender, BasicNackEventArgs e) private void HandleBasicNack(object? sender, BasicNackEventArgs e)
{ {
Monitor.Enter(confirmLock); Monitor.Enter(confirmLock);
try try
@ -1048,10 +1042,10 @@ namespace Tapeti.Connection
public TapetiConnectionParams ConnectionParams { get; } public TapetiConnectionParams ConnectionParams { get; }
public bool IsReconnect { get; } public bool IsReconnect { get; }
public int LocalPort { get; } public int LocalPort { get; }
public Exception Exception { get; } public Exception? Exception { get; }
public ConnectContext(TapetiConnectionParams connectionParams, bool isReconnect, int localPort = 0, Exception exception = null) public ConnectContext(TapetiConnectionParams connectionParams, bool isReconnect, int localPort = 0, Exception? exception = null)
{ {
ConnectionParams = connectionParams; ConnectionParams = connectionParams;
IsReconnect = isReconnect; IsReconnect = isReconnect;

View File

@ -40,7 +40,7 @@ namespace Tapeti.Connection
/// <inheritdoc /> /// <inheritdoc />
public async Task<ConsumeResult> Consume(string exchange, string routingKey, IMessageProperties properties, byte[] body) public async Task<ConsumeResult> Consume(string exchange, string routingKey, IMessageProperties properties, byte[] body)
{ {
object message = null; object? message = null;
try try
{ {
try try
@ -73,7 +73,7 @@ namespace Tapeti.Connection
RawBody = body, RawBody = body,
Message = message, Message = message,
Properties = properties, Properties = properties,
Binding = null, Binding = new ExceptionContextBinding(queueName),
ConnectionClosed = CancellationToken.None ConnectionClosed = CancellationToken.None
}; };
@ -184,5 +184,42 @@ namespace Tapeti.Connection
public string RoutingKey; public string RoutingKey;
public IMessageProperties Properties; public IMessageProperties Properties;
} }
private class ExceptionContextBinding : IBinding
{
public string? QueueName { get; }
public QueueType? QueueType => null;
public ExceptionContextBinding(string? queueName)
{
QueueName = queueName;
}
public ValueTask Apply(IBindingTarget target)
{
throw new InvalidOperationException("Apply method should not be called on a binding in an Exception context");
}
public bool Accept(Type messageClass)
{
throw new InvalidOperationException("Accept method should not be called on a binding in an Exception context");
}
public ValueTask Invoke(IMessageContext context)
{
throw new InvalidOperationException("Invoke method should not be called on a binding in an Exception context");
}
public ValueTask Cleanup(IMessageContext context, ConsumeResult consumeResult)
{
throw new InvalidOperationException("Cleanup method should not be called on a binding in an Exception context");
}
}
} }
} }

View File

@ -38,14 +38,14 @@ namespace Tapeti.Connection
/// <inheritdoc /> /// <inheritdoc />
public async Task PublishRequest<TController, TRequest, TResponse>(TRequest message, Expression<Func<TController, Action<TResponse>>> responseMethodSelector) where TController : class public async Task PublishRequest<TController, TRequest, TResponse>(TRequest message, Expression<Func<TController, Action<TResponse>>> responseMethodSelector) where TController : class where TRequest : class where TResponse : class
{ {
await PublishRequest(message, responseMethodSelector.Body); await PublishRequest(message, responseMethodSelector.Body);
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task PublishRequest<TController, TRequest, TResponse>(TRequest message, Expression<Func<TController, Func<TResponse, Task>>> responseMethodSelector) where TController : class public async Task PublishRequest<TController, TRequest, TResponse>(TRequest message, Expression<Func<TController, Func<TResponse, Task>>> responseMethodSelector) where TController : class where TRequest : class where TResponse : class
{ {
await PublishRequest(message, responseMethodSelector.Body); await PublishRequest(message, responseMethodSelector.Body);
} }
@ -97,7 +97,7 @@ namespace Tapeti.Connection
/// <inheritdoc /> /// <inheritdoc />
public async Task Publish(object message, IMessageProperties properties, bool mandatory) public async Task Publish(object message, IMessageProperties? properties, bool mandatory)
{ {
var messageClass = message.GetType(); var messageClass = message.GetType();
var exchange = exchangeStrategy.GetExchange(messageClass); var exchange = exchangeStrategy.GetExchange(messageClass);
@ -108,13 +108,13 @@ namespace Tapeti.Connection
/// <inheritdoc /> /// <inheritdoc />
public async Task PublishDirect(object message, string queueName, IMessageProperties properties, bool mandatory) public async Task PublishDirect(object message, string queueName, IMessageProperties? properties, bool mandatory)
{ {
await Publish(message, properties, null, queueName, mandatory); await Publish(message, properties, null, queueName, mandatory);
} }
private async Task Publish(object message, IMessageProperties properties, string exchange, string routingKey, bool mandatory) private async Task Publish(object message, IMessageProperties? properties, string? exchange, string routingKey, bool mandatory)
{ {
var writableProperties = new MessageProperties(properties); var writableProperties = new MessageProperties(properties);
@ -151,11 +151,11 @@ namespace Tapeti.Connection
private class PublishContext : IPublishContext private class PublishContext : IPublishContext
{ {
public ITapetiConfig Config { get; init; } public ITapetiConfig Config { get; init; } = null!;
public string Exchange { get; set; } public string? Exchange { get; set; }
public string RoutingKey { get; init; } public string RoutingKey { get; init; } = null!;
public object Message { get; init; } public object Message { get; init; } = null!;
public IMessageProperties Properties { get; init; } public IMessageProperties? Properties { get; init; }
} }
} }
} }

View File

@ -16,7 +16,7 @@ namespace Tapeti.Connection
private bool consuming; private bool consuming;
private readonly List<TapetiConsumerTag> consumerTags = new(); private readonly List<TapetiConsumerTag> consumerTags = new();
private CancellationTokenSource initializeCancellationTokenSource; private CancellationTokenSource? initializeCancellationTokenSource;
public TapetiSubscriber(Func<ITapetiClient> clientFactory, ITapetiConfig config) public TapetiSubscriber(Func<ITapetiClient> clientFactory, ITapetiConfig config)
@ -141,15 +141,24 @@ namespace Tapeti.Connection
private async Task ConsumeQueues(CancellationToken cancellationToken) private async Task ConsumeQueues(CancellationToken cancellationToken)
{ {
var queues = config.Bindings.GroupBy(binding => binding.QueueName); var queues = config.Bindings.GroupBy(binding =>
{
if (string.IsNullOrEmpty(binding.QueueName))
throw new InvalidOperationException("QueueName must not be empty");
consumerTags.AddRange((await Task.WhenAll(queues.Select(async group => return binding.QueueName;
});
consumerTags.AddRange(
(await Task.WhenAll(queues.Select(async group =>
{ {
var queueName = group.Key; var queueName = group.Key;
var consumer = new TapetiConsumer(cancellationToken, config, queueName, group); var consumer = new TapetiConsumer(cancellationToken, config, queueName, group);
return await clientFactory().Consume(queueName, consumer, cancellationToken); return await clientFactory().Consume(queueName, consumer, cancellationToken);
}))).Where(t => t != null)); })))
.Where(t => t?.ConsumerTag != null)
.Cast<TapetiConsumerTag>());
} }
@ -164,7 +173,7 @@ namespace Tapeti.Connection
{ {
public string QueueName; public string QueueName;
public List<Type> MessageClasses; public List<Type> MessageClasses;
public IRabbitMQArguments Arguments; public IRabbitMQArguments? Arguments;
} }
private readonly Dictionary<string, List<DynamicQueueInfo>> dynamicQueues = new(); private readonly Dictionary<string, List<DynamicQueueInfo>> dynamicQueues = new();
@ -185,12 +194,12 @@ namespace Tapeti.Connection
} }
public abstract ValueTask BindDurable(Type messageClass, string queueName, IRabbitMQArguments arguments); public abstract ValueTask BindDurable(Type messageClass, string queueName, IRabbitMQArguments? arguments);
public abstract ValueTask BindDurableDirect(string queueName, IRabbitMQArguments arguments); public abstract ValueTask BindDurableDirect(string queueName, IRabbitMQArguments? arguments);
public abstract ValueTask BindDurableObsolete(string queueName); public abstract ValueTask BindDurableObsolete(string queueName);
public async ValueTask<string> BindDynamic(Type messageClass, string queuePrefix, IRabbitMQArguments arguments) public async ValueTask<string> BindDynamic(Type messageClass, string? queuePrefix, IRabbitMQArguments? arguments)
{ {
var result = await DeclareDynamicQueue(messageClass, queuePrefix, arguments); var result = await DeclareDynamicQueue(messageClass, queuePrefix, arguments);
if (!result.IsNewMessageClass) if (!result.IsNewMessageClass)
@ -205,14 +214,14 @@ namespace Tapeti.Connection
} }
public async ValueTask<string> BindDynamicDirect(Type messageClass, string queuePrefix, IRabbitMQArguments arguments) public async ValueTask<string> BindDynamicDirect(Type messageClass, string? queuePrefix, IRabbitMQArguments? arguments)
{ {
var result = await DeclareDynamicQueue(messageClass, queuePrefix, arguments); var result = await DeclareDynamicQueue(messageClass, queuePrefix, arguments);
return result.QueueName; return result.QueueName;
} }
public async ValueTask<string> BindDynamicDirect(string queuePrefix, IRabbitMQArguments arguments) public async ValueTask<string> BindDynamicDirect(string? queuePrefix, IRabbitMQArguments? arguments)
{ {
// If we don't know the routing key, always create a new queue to ensure there is no overlap. // If we don't know the routing key, always create a new queue to ensure there is no overlap.
// Keep it out of the dynamicQueues dictionary, so it can't be re-used later on either. // Keep it out of the dynamicQueues dictionary, so it can't be re-used later on either.
@ -226,7 +235,7 @@ namespace Tapeti.Connection
public bool IsNewMessageClass; public bool IsNewMessageClass;
} }
private async Task<DeclareDynamicQueueResult> DeclareDynamicQueue(Type messageClass, string queuePrefix, IRabbitMQArguments arguments) private async Task<DeclareDynamicQueueResult> DeclareDynamicQueue(Type messageClass, string? queuePrefix, IRabbitMQArguments? arguments)
{ {
// Group by prefix // Group by prefix
var key = queuePrefix ?? ""; var key = queuePrefix ?? "";
@ -282,7 +291,7 @@ namespace Tapeti.Connection
private struct DurableQueueInfo private struct DurableQueueInfo
{ {
public List<Type> MessageClasses; public List<Type> MessageClasses;
public IRabbitMQArguments Arguments; public IRabbitMQArguments? Arguments;
} }
@ -295,7 +304,7 @@ namespace Tapeti.Connection
} }
public override ValueTask BindDurable(Type messageClass, string queueName, IRabbitMQArguments arguments) public override ValueTask BindDurable(Type messageClass, string queueName, IRabbitMQArguments? arguments)
{ {
// Collect the message classes per queue so we can determine afterwards // Collect the message classes per queue so we can determine afterwards
// if any of the bindings currently set on the durable queue are no // if any of the bindings currently set on the durable queue are no
@ -324,7 +333,7 @@ namespace Tapeti.Connection
} }
public override ValueTask BindDurableDirect(string queueName, IRabbitMQArguments arguments) public override ValueTask BindDurableDirect(string queueName, IRabbitMQArguments? arguments)
{ {
if (!durableQueues.TryGetValue(queueName, out var durableQueueInfo)) if (!durableQueues.TryGetValue(queueName, out var durableQueueInfo))
{ {
@ -396,12 +405,12 @@ namespace Tapeti.Connection
} }
public override async ValueTask BindDurable(Type messageClass, string queueName, IRabbitMQArguments arguments) public override async ValueTask BindDurable(Type messageClass, string queueName, IRabbitMQArguments? arguments)
{ {
await VerifyDurableQueue(queueName, arguments); await VerifyDurableQueue(queueName, arguments);
} }
public override async ValueTask BindDurableDirect(string queueName, IRabbitMQArguments arguments) public override async ValueTask BindDurableDirect(string queueName, IRabbitMQArguments? arguments)
{ {
await VerifyDurableQueue(queueName, arguments); await VerifyDurableQueue(queueName, arguments);
} }
@ -412,7 +421,7 @@ namespace Tapeti.Connection
} }
private async Task VerifyDurableQueue(string queueName, IRabbitMQArguments arguments) private async Task VerifyDurableQueue(string queueName, IRabbitMQArguments? arguments)
{ {
if (!durableQueues.Add(queueName)) if (!durableQueues.Add(queueName))
return; return;
@ -429,12 +438,12 @@ namespace Tapeti.Connection
} }
public override ValueTask BindDurable(Type messageClass, string queueName, IRabbitMQArguments arguments) public override ValueTask BindDurable(Type messageClass, string queueName, IRabbitMQArguments? arguments)
{ {
return default; return default;
} }
public override ValueTask BindDurableDirect(string queueName, IRabbitMQArguments arguments) public override ValueTask BindDurableDirect(string queueName, IRabbitMQArguments? arguments)
{ {
return default; return default;
} }

View File

@ -80,14 +80,17 @@ namespace Tapeti.Default
} }
/// <inheritdoc /> /// <inheritdoc />
public void QueueExistsWarning(string queueName, IRabbitMQArguments existingArguments, IRabbitMQArguments arguments) public void QueueExistsWarning(string queueName, IRabbitMQArguments? existingArguments, IRabbitMQArguments? arguments)
{ {
Console.WriteLine($"[Tapeti] Durable queue {queueName} exists with incompatible x-arguments ({GetArgumentsText(existingArguments)} vs. {GetArgumentsText(arguments)}) and will not be redeclared, queue will be consumed as-is"); Console.WriteLine($"[Tapeti] Durable queue {queueName} exists with incompatible x-arguments ({GetArgumentsText(existingArguments)} vs. {GetArgumentsText(arguments)}) and will not be redeclared, queue will be consumed as-is");
} }
private static string GetArgumentsText(IRabbitMQArguments arguments) private static string GetArgumentsText(IRabbitMQArguments? arguments)
{ {
if (arguments == null || arguments.Count == 0)
return "empty";
var argumentsText = new StringBuilder(); var argumentsText = new StringBuilder();
foreach (var pair in arguments) foreach (var pair in arguments)
{ {

View File

@ -26,7 +26,7 @@ namespace Tapeti.Default
/// <inheritdoc /> /// <inheritdoc />
public Type MessageClass { get; set; } public Type? MessageClass { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public bool HasMessageClass => MessageClass != null; public bool HasMessageClass => MessageClass != null;
@ -44,10 +44,12 @@ namespace Tapeti.Default
public IBindingResult Result => result; public IBindingResult Result => result;
public ControllerBindingContext(IEnumerable<ParameterInfo> parameters, ParameterInfo result) public ControllerBindingContext(Type controller, MethodInfo method, IEnumerable<ParameterInfo> parameters, ParameterInfo result)
{ {
this.parameters = parameters.Select(parameter => new ControllerBindingParameter(parameter)).ToList(); Controller = controller;
Method = method;
this.parameters = parameters.Select(parameter => new ControllerBindingParameter(parameter)).ToList();
this.result = new ControllerBindingResult(result); this.result = new ControllerBindingResult(result);
} }
@ -84,7 +86,13 @@ namespace Tapeti.Default
/// </summary> /// </summary>
public IEnumerable<ValueFactory> GetParameterHandlers() public IEnumerable<ValueFactory> GetParameterHandlers()
{ {
return parameters.Select(p => p.Binding); return parameters.Select(p =>
{
if (p.Binding == null)
throw new TopologyConfigurationException($"No Binding for parameter {p.Info.Name}");
return p.Binding;
});
} }
@ -92,7 +100,7 @@ namespace Tapeti.Default
/// Returns the configured result handler. /// Returns the configured result handler.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public ResultHandler GetResultHandler() public ResultHandler? GetResultHandler()
{ {
return result.Handler; return result.Handler;
} }
@ -107,7 +115,7 @@ namespace Tapeti.Default
/// <summary> /// <summary>
/// Provides access to the configured binding. /// Provides access to the configured binding.
/// </summary> /// </summary>
public ValueFactory Binding { get; set; } public ValueFactory? Binding { get; set; }
/// <inheritdoc /> /// <inheritdoc />
@ -146,7 +154,7 @@ namespace Tapeti.Default
/// <summary> /// <summary>
/// Provides access to the configured handler. /// Provides access to the configured handler.
/// </summary> /// </summary>
public ResultHandler Handler { get; set; } public ResultHandler? Handler { get; set; }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -60,7 +60,7 @@ namespace Tapeti.Default
/// <summary> /// <summary>
/// The return value handler. /// The return value handler.
/// </summary> /// </summary>
public ResultHandler ResultHandler; public ResultHandler? ResultHandler;
/// <summary> /// <summary>
@ -87,10 +87,10 @@ namespace Tapeti.Default
/// <inheritdoc /> /// <inheritdoc />
public string QueueName { get; private set; } public string? QueueName { get; private set; }
/// <inheritdoc /> /// <inheritdoc />
public QueueType QueueType => bindingInfo.QueueInfo.QueueType; public QueueType? QueueType => bindingInfo.QueueInfo.QueueType;
/// <inheritdoc /> /// <inheritdoc />
public Type Controller => bindingInfo.ControllerType; public Type Controller => bindingInfo.ControllerType;
@ -116,7 +116,7 @@ namespace Tapeti.Default
switch (bindingInfo.BindingTargetMode) switch (bindingInfo.BindingTargetMode)
{ {
case BindingTargetMode.Default: case BindingTargetMode.Default:
if (bindingInfo.QueueInfo.QueueType == QueueType.Dynamic) if (bindingInfo.QueueInfo.QueueType == Config.QueueType.Dynamic)
QueueName = await target.BindDynamic(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name, bindingInfo.QueueInfo.QueueArguments); QueueName = await target.BindDynamic(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name, bindingInfo.QueueInfo.QueueArguments);
else else
{ {
@ -127,7 +127,7 @@ namespace Tapeti.Default
break; break;
case BindingTargetMode.Direct: case BindingTargetMode.Direct:
if (bindingInfo.QueueInfo.QueueType == QueueType.Dynamic) if (bindingInfo.QueueInfo.QueueType == Config.QueueType.Dynamic)
QueueName = await target.BindDynamicDirect(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name, bindingInfo.QueueInfo.QueueArguments); QueueName = await target.BindDynamicDirect(bindingInfo.MessageClass, bindingInfo.QueueInfo.Name, bindingInfo.QueueInfo.QueueArguments);
else else
{ {
@ -141,7 +141,7 @@ namespace Tapeti.Default
throw new ArgumentOutOfRangeException(nameof(bindingInfo.BindingTargetMode), bindingInfo.BindingTargetMode, "Invalid BindingTargetMode"); throw new ArgumentOutOfRangeException(nameof(bindingInfo.BindingTargetMode), bindingInfo.BindingTargetMode, "Invalid BindingTargetMode");
} }
} }
else if (bindingInfo.QueueInfo.QueueType == QueueType.Durable) else if (bindingInfo.QueueInfo.QueueType == Config.QueueType.Durable)
{ {
await target.BindDurableObsolete(bindingInfo.QueueInfo.Name); await target.BindDurableObsolete(bindingInfo.QueueInfo.Name);
QueueName = bindingInfo.QueueInfo.Name; QueueName = bindingInfo.QueueInfo.Name;
@ -159,8 +159,11 @@ namespace Tapeti.Default
/// <inheritdoc /> /// <inheritdoc />
public async ValueTask Invoke(IMessageContext context) public async ValueTask Invoke(IMessageContext context)
{ {
if (context.Binding == null)
throw new InvalidOperationException("Invoke should not be called on a context without a binding");
var controller = Method.IsStatic ? null : dependencyResolver.Resolve(bindingInfo.ControllerType); var controller = Method.IsStatic ? null : dependencyResolver.Resolve(bindingInfo.ControllerType);
context.Store(new ControllerMessageContextPayload(controller, context.Binding as IControllerMethodBinding)); context.Store(new ControllerMessageContextPayload(controller, (IControllerMethodBinding)context.Binding));
if (!await FilterAllowed(context)) if (!await FilterAllowed(context))
return; return;
@ -202,7 +205,7 @@ namespace Tapeti.Default
private delegate ValueTask MessageHandlerFunc(IMessageContext context); private delegate ValueTask MessageHandlerFunc(IMessageContext context);
private MessageHandlerFunc WrapMethod(MethodInfo method, IEnumerable<ValueFactory> parameterFactories, ResultHandler resultHandler) private MessageHandlerFunc WrapMethod(MethodInfo method, IEnumerable<ValueFactory> parameterFactories, ResultHandler? resultHandler)
{ {
if (resultHandler != null) if (resultHandler != null)
return WrapResultHandlerMethod(method.CreateExpressionInvoke(), parameterFactories, resultHandler); return WrapResultHandlerMethod(method.CreateExpressionInvoke(), parameterFactories, resultHandler);
@ -296,8 +299,8 @@ namespace Tapeti.Default
private void AddExceptionData(Exception exception) private void AddExceptionData(Exception exception)
{ {
exception.Data["Tapeti.Controller.Name"] = bindingInfo.ControllerType?.FullName; exception.Data["Tapeti.Controller.Name"] = bindingInfo.ControllerType.FullName;
exception.Data["Tapeti.Controller.Method"] = bindingInfo.Method?.Name; exception.Data["Tapeti.Controller.Method"] = bindingInfo.Method.Name;
} }
@ -319,12 +322,19 @@ namespace Tapeti.Default
/// <summary> /// <summary>
/// Optional arguments (x-arguments) passed when declaring the queue. /// Optional arguments (x-arguments) passed when declaring the queue.
/// </summary> /// </summary>
public IRabbitMQArguments QueueArguments { get; set; } public IRabbitMQArguments? QueueArguments { get; set; }
/// <summary> /// <summary>
/// Determines if the QueueInfo properties contain a valid combination. /// Determines if the QueueInfo properties contain a valid combination.
/// </summary> /// </summary>
public bool IsValid => QueueType == QueueType.Dynamic || !string.IsNullOrEmpty(Name); public bool IsValid => QueueType == QueueType.Dynamic || !string.IsNullOrEmpty(Name);
public QueueInfo(QueueType queueType, string name)
{
QueueType = queueType;
Name = name;
}
} }
} }
} }

View File

@ -24,7 +24,7 @@ namespace Tapeti.Default
/// <inheritdoc /> /// <inheritdoc />
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{ {
if (value == null) if (value == null)
{ {
@ -41,7 +41,7 @@ namespace Tapeti.Default
/// <inheritdoc /> /// <inheritdoc />
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{ {
var isNullable = IsNullableType(objectType); var isNullable = IsNullableType(objectType);

View File

@ -46,7 +46,7 @@ namespace Tapeti.Default
/// <inheritdoc /> /// <inheritdoc />
public object Deserialize(byte[] body, IMessageProperties properties) public object? Deserialize(byte[] body, IMessageProperties properties)
{ {
if (properties.ContentType is not ContentType) if (properties.ContentType is not ContentType)
throw new ArgumentException($"content_type must be {ContentType}"); throw new ArgumentException($"content_type must be {ContentType}");

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Tapeti.Config; using Tapeti.Config;
@ -12,28 +13,28 @@ namespace Tapeti.Default
/// <inheritdoc /> /// <inheritdoc />
public ITapetiConfig Config { get; set; } public ITapetiConfig Config { get; set; } = null!;
/// <inheritdoc /> /// <inheritdoc />
public string Queue { get; set; } public string Queue { get; set; } = null!;
/// <inheritdoc /> /// <inheritdoc />
public string Exchange { get; set; } public string Exchange { get; set; } = null!;
/// <inheritdoc /> /// <inheritdoc />
public string RoutingKey { get; set; } public string RoutingKey { get; set; } = null!;
/// <inheritdoc /> /// <inheritdoc />
public byte[] RawBody { get; set; } public byte[] RawBody { get; set; } = null!;
/// <inheritdoc /> /// <inheritdoc />
public object Message { get; set; } public object? Message { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public IMessageProperties Properties { get; set; } public IMessageProperties Properties { get; set; } = null!;
/// <inheritdoc /> /// <inheritdoc />
public IBinding Binding { get; set; } public IBinding Binding { get; set; } = null!;
/// <inheritdoc /> /// <inheritdoc />
public CancellationToken ConnectionClosed { get; set; } public CancellationToken ConnectionClosed { get; set; }
@ -57,7 +58,7 @@ namespace Tapeti.Default
return (T)payloads[typeof(T)]; return (T)payloads[typeof(T)];
} }
public bool TryGet<T>(out T payload) where T : IMessageContextPayload public bool TryGet<T>([NotNullWhen(true)] out T? payload) where T : IMessageContextPayload
{ {
if (payloads.TryGetValue(typeof(T), out var payloadValue)) if (payloads.TryGetValue(typeof(T), out var payloadValue))
{ {
@ -100,7 +101,7 @@ namespace Tapeti.Default
/// <inheritdoc /> /// <inheritdoc />
public bool Get<T>(string key, out T value) where T : class public bool Get<T>(string key, out T? value) where T : class
{ {
if (!TryGet<KeyValuePayload>(out var payload) || if (!TryGet<KeyValuePayload>(out var payload) ||
!payload.TryGetValue(key, out var objectValue)) !payload.TryGetValue(key, out var objectValue))
@ -109,7 +110,7 @@ namespace Tapeti.Default
return false; return false;
} }
value = (T)objectValue; value = (T?)objectValue;
return true; return true;
} }
@ -132,7 +133,7 @@ namespace Tapeti.Default
} }
public bool TryGetValue(string key, out object value) public bool TryGetValue(string key, out object? value)
{ {
return items.TryGetValue(key, out value); return items.TryGetValue(key, out value);
} }

View File

@ -13,13 +13,13 @@ namespace Tapeti.Default
/// <inheritdoc /> /// <inheritdoc />
public string ContentType { get; set; } public string? ContentType { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public string CorrelationId { get; set; } public string? CorrelationId { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public string ReplyTo { get; set; } public string? ReplyTo { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public bool? Persistent { get; set; } public bool? Persistent { get; set; }
@ -37,7 +37,7 @@ namespace Tapeti.Default
/// <summary> /// <summary>
/// </summary> /// </summary>
public MessageProperties(IMessageProperties source) public MessageProperties(IMessageProperties? source)
{ {
if (source == null) if (source == null)
return; return;
@ -64,7 +64,7 @@ namespace Tapeti.Default
} }
/// <inheritdoc /> /// <inheritdoc />
public string GetHeader(string name) public string? GetHeader(string name)
{ {
return headers.TryGetValue(name, out var value) ? value : null; return headers.TryGetValue(name, out var value) ? value : null;
} }

View File

@ -31,7 +31,7 @@ namespace Tapeti.Default
// Tapeti 1.2: if you just want to publish another message as a result of the incoming message, explicitly call IPublisher.Publish. // Tapeti 1.2: if you just want to publish another message as a result of the incoming message, explicitly call IPublisher.Publish.
// ReSharper disable once ConvertIfStatementToSwitchStatement // ReSharper disable once ConvertIfStatementToSwitchStatement
if (!hasClassResult && expectedClassResult != null || hasClassResult && expectedClassResult != actualType) if (!hasClassResult && expectedClassResult != null || hasClassResult && expectedClassResult != actualType)
throw new ArgumentException($"Message handler for non-request message type {context.MessageClass?.FullName} must return type {expectedClassResult?.FullName ?? "void"} in controller {context.Method.DeclaringType?.FullName}, method {context.Method.Name}, found: {actualType?.FullName ?? "void"}"); throw new ArgumentException($"Message handler for non-request message type {context.MessageClass?.FullName} must return type {expectedClassResult?.FullName ?? "void"} in controller {context.Method.DeclaringType?.FullName}, method {context.Method.Name}, found: {actualType.FullName ?? "void"}");
if (!hasClassResult) if (!hasClassResult)
return; return;
@ -48,14 +48,22 @@ namespace Tapeti.Default
var handler = GetType().GetMethod(nameof(PublishGenericTaskResult), BindingFlags.NonPublic | BindingFlags.Static)?.MakeGenericMethod(actualType); var handler = GetType().GetMethod(nameof(PublishGenericTaskResult), BindingFlags.NonPublic | BindingFlags.Static)?.MakeGenericMethod(actualType);
Debug.Assert(handler != null, nameof(handler) + " != null"); Debug.Assert(handler != null, nameof(handler) + " != null");
context.Result.SetHandler((messageContext, value) => (ValueTask)handler.Invoke(null, new[] { messageContext, value })); context.Result.SetHandler((messageContext, value) =>
{
var result = handler.Invoke(null, new[] { messageContext, value });
return result != null ? (ValueTask)result : ValueTask.CompletedTask;
});
break; break;
case TaskType.ValueTask: case TaskType.ValueTask:
var valueTaskHandler = GetType().GetMethod(nameof(PublishGenericValueTaskResult), BindingFlags.NonPublic | BindingFlags.Static)?.MakeGenericMethod(actualType); var valueTaskHandler = GetType().GetMethod(nameof(PublishGenericValueTaskResult), BindingFlags.NonPublic | BindingFlags.Static)?.MakeGenericMethod(actualType);
Debug.Assert(valueTaskHandler != null, nameof(handler) + " != null"); Debug.Assert(valueTaskHandler != null, nameof(handler) + " != null");
context.Result.SetHandler((messageContext, value) => (ValueTask)valueTaskHandler.Invoke(null, new[] { messageContext, value })); context.Result.SetHandler((messageContext, value) =>
{
var result = valueTaskHandler.Invoke(null, new[] { messageContext, value });
return result != null ? (ValueTask)result : ValueTask.CompletedTask;
});
break; break;
default: default:
@ -79,7 +87,7 @@ namespace Tapeti.Default
} }
private static async ValueTask Reply(object message, IMessageContext messageContext) private static async ValueTask Reply(object? message, IMessageContext messageContext)
{ {
if (message == null) if (message == null)
throw new ArgumentException("Return value of a request message handler must not be null"); throw new ArgumentException("Return value of a request message handler must not be null");

View File

@ -15,21 +15,21 @@ namespace Tapeti.Default
/// <inheritdoc /> /// <inheritdoc />
public string ContentType public string? ContentType
{ {
get => BasicProperties.IsContentTypePresent() ? BasicProperties.ContentType : null; get => BasicProperties.IsContentTypePresent() ? BasicProperties.ContentType : null;
set { if (!string.IsNullOrEmpty(value)) BasicProperties.ContentType = value; else BasicProperties.ClearContentType(); } set { if (!string.IsNullOrEmpty(value)) BasicProperties.ContentType = value; else BasicProperties.ClearContentType(); }
} }
/// <inheritdoc /> /// <inheritdoc />
public string CorrelationId public string? CorrelationId
{ {
get => BasicProperties.IsCorrelationIdPresent() ? BasicProperties.CorrelationId : null; get => BasicProperties.IsCorrelationIdPresent() ? BasicProperties.CorrelationId : null;
set { if (!string.IsNullOrEmpty(value)) BasicProperties.CorrelationId = value; else BasicProperties.ClearCorrelationId(); } set { if (!string.IsNullOrEmpty(value)) BasicProperties.CorrelationId = value; else BasicProperties.ClearCorrelationId(); }
} }
/// <inheritdoc /> /// <inheritdoc />
public string ReplyTo public string? ReplyTo
{ {
get => BasicProperties.IsReplyToPresent() ? BasicProperties.ReplyTo : null; get => BasicProperties.IsReplyToPresent() ? BasicProperties.ReplyTo : null;
set { if (!string.IsNullOrEmpty(value)) BasicProperties.ReplyTo = value; else BasicProperties.ClearReplyTo(); } set { if (!string.IsNullOrEmpty(value)) BasicProperties.ReplyTo = value; else BasicProperties.ClearReplyTo(); }
@ -66,7 +66,7 @@ namespace Tapeti.Default
/// <summary> /// <summary>
/// </summary> /// </summary>
public RabbitMQMessageProperties(IBasicProperties basicProperties, IMessageProperties source) public RabbitMQMessageProperties(IBasicProperties basicProperties, IMessageProperties? source)
{ {
BasicProperties = basicProperties; BasicProperties = basicProperties;
if (source == null) if (source == null)
@ -97,7 +97,7 @@ namespace Tapeti.Default
/// <inheritdoc /> /// <inheritdoc />
public string GetHeader(string name) public string? GetHeader(string name)
{ {
if (BasicProperties.Headers == null) if (BasicProperties.Headers == null)
return null; return null;

View File

@ -60,7 +60,7 @@ namespace Tapeti.Default
} }
private static List<string> SplitPascalCase(string value) private static List<string>? SplitPascalCase(string value)
{ {
var split = SeparatorRegex.Split(value); var split = SeparatorRegex.Split(value);
if (split.Length == 0) if (split.Length == 0)

View File

@ -10,7 +10,7 @@ namespace Tapeti.Helpers
/// </summary> /// </summary>
public class ConnectionStringParser public class ConnectionStringParser
{ {
private readonly TapetiConnectionParams result = new TapetiAppSettingsConnectionParams(); private readonly TapetiConnectionParams result = new();
private readonly string connectionstring; private readonly string connectionstring;
private int pos = -1; private int pos = -1;

View File

@ -10,7 +10,7 @@ namespace Tapeti.Helpers
/// <summary> /// <summary>
/// Checks if two dictionaries are considered compatible. If either is null they are considered empty. /// Checks if two dictionaries are considered compatible. If either is null they are considered empty.
/// </summary> /// </summary>
public static bool NullSafeSameValues(this IReadOnlyDictionary<string, object> arguments1, IReadOnlyDictionary<string, object> arguments2) public static bool NullSafeSameValues(this IReadOnlyDictionary<string, object>? arguments1, IReadOnlyDictionary<string, object>? arguments2)
{ {
if (arguments1 == null || arguments2 == null) if (arguments1 == null || arguments2 == null)
return (arguments1 == null || arguments1.Count == 0) && (arguments2 == null || arguments2.Count == 0); return (arguments1 == null || arguments1.Count == 0) && (arguments2 == null || arguments2.Count == 0);

View File

@ -16,7 +16,7 @@ namespace Tapeti.Helpers
/// </summary> /// </summary>
/// <param name="target">The instance on which the method should be called.</param> /// <param name="target">The instance on which the method should be called.</param>
/// <param name="args">The arguments passed to the method.</param> /// <param name="args">The arguments passed to the method.</param>
public delegate object ExpressionInvoke(object target, params object[] args); public delegate object ExpressionInvoke(object? target, params object?[] args);
/// <summary> /// <summary>

View File

@ -16,7 +16,7 @@ namespace Tapeti.Helpers
/// <param name="handle">Receives the middleware which should be called and a reference to the action which will call the next. Pass this on to the middleware.</param> /// <param name="handle">Receives the middleware which should be called and a reference to the action which will call the next. Pass this on to the middleware.</param>
/// <param name="lastHandler">The action to execute when the innermost middleware calls next.</param> /// <param name="lastHandler">The action to execute when the innermost middleware calls next.</param>
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
public static void Go<T>(IReadOnlyList<T> middleware, Action<T, Action> handle, Action lastHandler) public static void Go<T>(IReadOnlyList<T>? middleware, Action<T, Action> handle, Action lastHandler)
{ {
var handlerIndex = middleware?.Count - 1 ?? -1; var handlerIndex = middleware?.Count - 1 ?? -1;
if (middleware == null || handlerIndex == -1) if (middleware == null || handlerIndex == -1)
@ -45,7 +45,7 @@ namespace Tapeti.Helpers
/// <param name="handle">Receives the middleware which should be called and a reference to the action which will call the next. Pass this on to the middleware.</param> /// <param name="handle">Receives the middleware which should be called and a reference to the action which will call the next. Pass this on to the middleware.</param>
/// <param name="lastHandler">The action to execute when the innermost middleware calls next.</param> /// <param name="lastHandler">The action to execute when the innermost middleware calls next.</param>
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
public static async ValueTask GoAsync<T>(IReadOnlyList<T> middleware, Func<T, Func<ValueTask>, ValueTask> handle, Func<ValueTask> lastHandler) public static async ValueTask GoAsync<T>(IReadOnlyList<T>? middleware, Func<T, Func<ValueTask>, ValueTask> handle, Func<ValueTask> lastHandler)
{ {
var handlerIndex = middleware?.Count - 1 ?? -1; var handlerIndex = middleware?.Count - 1 ?? -1;
if (middleware == null || handlerIndex == -1) if (middleware == null || handlerIndex == -1)

View File

@ -7,19 +7,27 @@ using System.Threading.Tasks;
namespace Tapeti namespace Tapeti
{ {
/// <summary> /// <summary>
/// /// Contains information about the established connection.
/// </summary> /// </summary>
public class ConnectedEventArgs public class ConnectedEventArgs
{ {
/// <summary> /// <summary>
/// The connection parameters used to establish the connection. /// The connection parameters used to establish the connection.
/// </summary> /// </summary>
public TapetiConnectionParams ConnectionParams; public TapetiConnectionParams ConnectionParams { get; }
/// <summary> /// <summary>
/// The local port for the connection. Useful for identifying the connection in the management interface. /// The local port for the connection. Useful for identifying the connection in the management interface.
/// </summary> /// </summary>
public int LocalPort; public int LocalPort { get; }
/// <summary></summary>
public ConnectedEventArgs(TapetiConnectionParams connectionParams, int localPort)
{
ConnectionParams = connectionParams;
LocalPort = localPort;
}
} }
@ -31,12 +39,20 @@ namespace Tapeti
/// <summary> /// <summary>
/// The ReplyCode as indicated by the client library /// The ReplyCode as indicated by the client library
/// </summary> /// </summary>
public ushort ReplyCode; public ushort ReplyCode { get; }
/// <summary> /// <summary>
/// The ReplyText as indicated by the client library /// The ReplyText as indicated by the client library
/// </summary> /// </summary>
public string ReplyText; public string ReplyText { get; }
/// <summary></summary>
public DisconnectedEventArgs(ushort replyCode, string replyText)
{
ReplyCode = replyCode;
ReplyText = replyText;
}
} }

View File

@ -32,7 +32,7 @@ namespace Tapeti
/// <summary> /// <summary>
/// The exception that caused the connection to fail. /// The exception that caused the connection to fail.
/// </summary> /// </summary>
Exception Exception { get; } Exception? Exception { get; }
} }
@ -138,7 +138,7 @@ namespace Tapeti
/// <param name="queueName">The name of the queue that is declared</param> /// <param name="queueName">The name of the queue that is declared</param>
/// <param name="existingArguments">The x-arguments of the existing queue</param> /// <param name="existingArguments">The x-arguments of the existing queue</param>
/// <param name="arguments">The x-arguments of the queue that would be declared</param> /// <param name="arguments">The x-arguments of the queue that would be declared</param>
void QueueExistsWarning(string queueName, IRabbitMQArguments existingArguments, IRabbitMQArguments arguments); void QueueExistsWarning(string queueName, IRabbitMQArguments? existingArguments, IRabbitMQArguments? arguments);
/// <summary> /// <summary>
/// Called before a binding is added to a queue. /// Called before a binding is added to a queue.

View File

@ -21,6 +21,6 @@ namespace Tapeti
/// <param name="body">The encoded message</param> /// <param name="body">The encoded message</param>
/// <param name="properties">The properties as sent along with the message</param> /// <param name="properties">The properties as sent along with the message</param>
/// <returns>A decoded instance of the message</returns> /// <returns>A decoded instance of the message</returns>
object Deserialize(byte[] body, IMessageProperties properties); object? Deserialize(byte[] body, IMessageProperties properties);
} }
} }

View File

@ -29,7 +29,7 @@ namespace Tapeti
/// </remarks> /// </remarks>
/// <param name="responseMethodSelector">An expression defining the method which handles the response. Example: c => c.HandleResponse</param> /// <param name="responseMethodSelector">An expression defining the method which handles the response. Example: c => c.HandleResponse</param>
/// <param name="message">The message to send</param> /// <param name="message">The message to send</param>
Task PublishRequest<TController, TRequest, TResponse>(TRequest message, Expression<Func<TController, Action<TResponse>>> responseMethodSelector) where TController : class; Task PublishRequest<TController, TRequest, TResponse>(TRequest message, Expression<Func<TController, Action<TResponse>>> responseMethodSelector) where TController : class where TRequest : class where TResponse : class;
/// <summary> /// <summary>
@ -42,7 +42,7 @@ namespace Tapeti
/// </remarks> /// </remarks>
/// <param name="responseMethodSelector">An expression defining the method which handles the response. Example: c => c.HandleResponse</param> /// <param name="responseMethodSelector">An expression defining the method which handles the response. Example: c => c.HandleResponse</param>
/// <param name="message">The message to send</param> /// <param name="message">The message to send</param>
Task PublishRequest<TController, TRequest, TResponse>(TRequest message, Expression<Func<TController, Func<TResponse, Task>>> responseMethodSelector) where TController : class; Task PublishRequest<TController, TRequest, TResponse>(TRequest message, Expression<Func<TController, Func<TResponse, Task>>> responseMethodSelector) where TController : class where TRequest : class where TResponse : class;
/// <summary> /// <summary>
@ -69,7 +69,7 @@ namespace Tapeti
/// <param name="message">An instance of a message class</param> /// <param name="message">An instance of a message class</param>
/// <param name="properties">Metadata to include in the message</param> /// <param name="properties">Metadata to include in the message</param>
/// <param name="mandatory">If true, an exception will be raised if the message can not be delivered to at least one queue</param> /// <param name="mandatory">If true, an exception will be raised if the message can not be delivered to at least one queue</param>
Task Publish(object message, IMessageProperties properties, bool mandatory); Task Publish(object message, IMessageProperties? properties, bool mandatory);
/// <summary> /// <summary>
@ -80,6 +80,6 @@ namespace Tapeti
/// <param name="properties">Metadata to include in the message</param> /// <param name="properties">Metadata to include in the message</param>
/// <param name="mandatory">If true, an exception will be raised if the message can not be delivered to the queue</param> /// <param name="mandatory">If true, an exception will be raised if the message can not be delivered to the queue</param>
/// <returns></returns> /// <returns></returns>
Task PublishDirect(object message, string queueName, IMessageProperties properties, bool mandatory); Task PublishDirect(object message, string queueName, IMessageProperties? properties, bool mandatory);
} }
} }

View File

@ -12,6 +12,7 @@
<PackageProjectUrl>https://github.com/MvRens/Tapeti</PackageProjectUrl> <PackageProjectUrl>https://github.com/MvRens/Tapeti</PackageProjectUrl>
<PackageIcon>Tapeti.png</PackageIcon> <PackageIcon>Tapeti.png</PackageIcon>
<LangVersion>9</LangVersion> <LangVersion>9</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@ -17,7 +17,7 @@ namespace Tapeti
/// </summary> /// </summary>
public class TapetiConfig : ITapetiConfigBuilderAccess public class TapetiConfig : ITapetiConfigBuilderAccess
{ {
private Config config; private Config? config;
private readonly List<IControllerBindingMiddleware> bindingMiddleware = new(); private readonly List<IControllerBindingMiddleware> bindingMiddleware = new();
@ -92,10 +92,7 @@ namespace Tapeti
var configInstance = GetConfig(); var configInstance = GetConfig();
var middlewareBundle = extension.GetMiddleware(DependencyResolver); foreach (var middleware in extension.GetMiddleware(DependencyResolver))
if (middlewareBundle != null)
{
foreach (var middleware in middlewareBundle)
{ {
switch (middleware) switch (middleware)
{ {
@ -113,8 +110,7 @@ namespace Tapeti
default: default:
throw new ArgumentException( throw new ArgumentException(
$"Unsupported middleware implementation: {middleware?.GetType().Name ?? "null"}"); $"Unsupported middleware implementation: {middleware.GetType().Name}");
}
} }
} }
@ -123,7 +119,7 @@ namespace Tapeti
return this; return this;
foreach (var binding in bindingBundle) foreach (var binding in bindingBundle)
config.RegisterBinding(binding); GetConfig().RegisterBinding(binding);
return this; return this;
} }
@ -313,17 +309,23 @@ namespace Tapeti
internal class ConfigBindings : List<IBinding>, ITapetiConfigBindings internal class ConfigBindings : List<IBinding>, ITapetiConfigBindings
{ {
private Dictionary<MethodInfo, IControllerMethodBinding> methodLookup; private Dictionary<MethodInfo, IControllerMethodBinding>? methodLookup;
public IControllerMethodBinding ForMethod(Delegate method) public IControllerMethodBinding? ForMethod(Delegate method)
{ {
if (methodLookup == null)
throw new InvalidOperationException("Lock must be called first");
return methodLookup.TryGetValue(method.Method, out var binding) ? binding : null; return methodLookup.TryGetValue(method.Method, out var binding) ? binding : null;
} }
public IControllerMethodBinding ForMethod(MethodInfo method) public IControllerMethodBinding? ForMethod(MethodInfo method)
{ {
if (methodLookup == null)
throw new InvalidOperationException("Lock must be called first");
return methodLookup.TryGetValue(method, out var binding) ? binding : null; return methodLookup.TryGetValue(method, out var binding) ? binding : null;
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text;
using Tapeti.Annotations; using Tapeti.Annotations;
using Tapeti.Config; using Tapeti.Config;
using Tapeti.Connection; using Tapeti.Connection;
@ -49,12 +50,7 @@ namespace Tapeti
{ {
var methodIsObsolete = controllerIsObsolete || method.GetCustomAttribute<ObsoleteAttribute>() != null; var methodIsObsolete = controllerIsObsolete || method.GetCustomAttribute<ObsoleteAttribute>() != null;
var context = new ControllerBindingContext(method.GetParameters(), method.ReturnParameter) var context = new ControllerBindingContext(controller, method, method.GetParameters(), method.ReturnParameter);
{
Controller = controller,
Method = method
};
if (method.GetCustomAttribute<ResponseHandlerAttribute>() != null) if (method.GetCustomAttribute<ResponseHandlerAttribute>() != null)
context.SetBindingTargetMode(BindingTargetMode.Direct); context.SetBindingTargetMode(BindingTargetMode.Direct);
@ -124,11 +120,15 @@ namespace Tapeti
/// <param name="builder"></param> /// <param name="builder"></param>
public static ITapetiConfigBuilder RegisterAllControllers(this ITapetiConfigBuilder builder) public static ITapetiConfigBuilder RegisterAllControllers(this ITapetiConfigBuilder builder)
{ {
return RegisterAllControllers(builder, Assembly.GetEntryAssembly()); var assembly = Assembly.GetEntryAssembly();
if (assembly == null)
throw new InvalidOperationException("No EntryAssembly");
return RegisterAllControllers(builder, assembly);
} }
private static ControllerMethodBinding.QueueInfo GetQueueInfo(MemberInfo member, ControllerMethodBinding.QueueInfo fallbackQueueInfo) private static ControllerMethodBinding.QueueInfo? GetQueueInfo(MemberInfo member, ControllerMethodBinding.QueueInfo? fallbackQueueInfo)
{ {
var dynamicQueueAttribute = member.GetCustomAttribute<DynamicQueueAttribute>(); var dynamicQueueAttribute = member.GetCustomAttribute<DynamicQueueAttribute>();
var durableQueueAttribute = member.GetCustomAttribute<DurableQueueAttribute>(); var durableQueueAttribute = member.GetCustomAttribute<DurableQueueAttribute>();
@ -157,26 +157,33 @@ namespace Tapeti
} }
else else
{ {
queueType = fallbackQueueInfo.QueueType; queueType = fallbackQueueInfo!.QueueType;
name = fallbackQueueInfo.Name; name = fallbackQueueInfo.Name;
} }
return new ControllerMethodBinding.QueueInfo return new ControllerMethodBinding.QueueInfo(queueType, name)
{ {
QueueType = queueType,
Name = name,
QueueArguments = GetQueueArguments(queueArgumentsAttribute) ?? fallbackQueueInfo?.QueueArguments QueueArguments = GetQueueArguments(queueArgumentsAttribute) ?? fallbackQueueInfo?.QueueArguments
}; };
} }
private static IRabbitMQArguments GetQueueArguments(QueueArgumentsAttribute queueArgumentsAttribute) private static IRabbitMQArguments? GetQueueArguments(QueueArgumentsAttribute? queueArgumentsAttribute)
{ {
if (queueArgumentsAttribute == null) if (queueArgumentsAttribute == null)
return null; return null;
var arguments = new RabbitMQArguments(queueArgumentsAttribute.CustomArguments); var arguments = new RabbitMQArguments(queueArgumentsAttribute.CustomArguments.ToDictionary(
p => p.Key,
p => p.Value switch
{
string stringValue => Encoding.UTF8.GetBytes(stringValue),
_ => p.Value
}
))
{
};
if (queueArgumentsAttribute.MaxLength > 0) if (queueArgumentsAttribute.MaxLength > 0)
arguments.Add(@"x-max-length", queueArgumentsAttribute.MaxLength); arguments.Add(@"x-max-length", queueArgumentsAttribute.MaxLength);

View File

@ -24,10 +24,10 @@ namespace Tapeti
/// This property must be set before first subscribing or publishing, otherwise it /// This property must be set before first subscribing or publishing, otherwise it
/// will use the default connection parameters. /// will use the default connection parameters.
/// </remarks> /// </remarks>
public TapetiConnectionParams Params { get; set; } public TapetiConnectionParams? Params { get; set; }
private readonly Lazy<ITapetiClient> client; private readonly Lazy<ITapetiClient> client;
private TapetiSubscriber subscriber; private TapetiSubscriber? subscriber;
private bool disposed; private bool disposed;
@ -48,13 +48,13 @@ namespace Tapeti
} }
/// <inheritdoc /> /// <inheritdoc />
public event ConnectedEventHandler Connected; public event ConnectedEventHandler? Connected;
/// <inheritdoc /> /// <inheritdoc />
public event DisconnectedEventHandler Disconnected; public event DisconnectedEventHandler? Disconnected;
/// <inheritdoc /> /// <inheritdoc />
public event ConnectedEventHandler Reconnected; public event ConnectedEventHandler? Reconnected;
/// <inheritdoc /> /// <inheritdoc />

View File

@ -10,7 +10,7 @@ namespace Tapeti
/// </summary> /// </summary>
public class TapetiConnectionParams public class TapetiConnectionParams
{ {
private IDictionary<string, string> clientProperties; private IDictionary<string, string>? clientProperties;
/// <summary> /// <summary>
@ -59,7 +59,7 @@ namespace Tapeti
/// If any of the default keys used by the RabbitMQ Client library (product, version) are specified their value /// If any of the default keys used by the RabbitMQ Client library (product, version) are specified their value
/// will be overwritten. See DefaultClientProperties in Connection.cs in the RabbitMQ .NET client source for the default values. /// will be overwritten. See DefaultClientProperties in Connection.cs in the RabbitMQ .NET client source for the default values.
/// </remarks> /// </remarks>
public IDictionary<string, string> ClientProperties { public IDictionary<string, string>? ClientProperties {
get => clientProperties ??= new Dictionary<string, string>(); get => clientProperties ??= new Dictionary<string, string>();
set => clientProperties = value; set => clientProperties = value;
} }

View File

@ -120,7 +120,7 @@ namespace Tapeti.Tasks
{ {
while (true) while (true)
{ {
Task task; Task? task;
lock (scheduledTasks) lock (scheduledTasks)
{ {
task = WaitAndDequeueTask(); task = WaitAndDequeueTask();
@ -133,7 +133,7 @@ namespace Tapeti.Tasks
} }
} }
private Task WaitAndDequeueTask() private Task? WaitAndDequeueTask()
{ {
while (!scheduledTasks.Any() && !disposed) while (!scheduledTasks.Any() && !disposed)
Monitor.Wait(scheduledTasks); Monitor.Wait(scheduledTasks);