[ci skip] Reimplemented FlowStarter
This commit is contained in:
parent
d211d33108
commit
8ec85ac99f
@ -12,12 +12,12 @@ namespace Tapeti.DataAnnotations
|
|||||||
internal class DataAnnotationsMessageMiddleware : IMessageMiddleware
|
internal class DataAnnotationsMessageMiddleware : IMessageMiddleware
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task Handle(IMessageContext context, Func<Task> next)
|
public async Task Handle(IMessageContext context, Func<Task> 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);
|
||||||
|
|
||||||
return next();
|
await next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,12 @@ namespace Tapeti.DataAnnotations
|
|||||||
internal class DataAnnotationsPublishMiddleware : IPublishMiddleware
|
internal class DataAnnotationsPublishMiddleware : IPublishMiddleware
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task Handle(IPublishContext context, Func<Task> next)
|
public async Task Handle(IPublishContext context, Func<Task> 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);
|
||||||
|
|
||||||
return next();
|
await next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,9 @@ namespace Tapeti.Flow.Default
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public Task Execute(FlowContext context)
|
public async Task Execute(FlowContext context)
|
||||||
{
|
{
|
||||||
return onExecute(context);
|
await onExecute(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,14 +77,14 @@ namespace Tapeti.Flow.Default
|
|||||||
private static Task HandleYieldPoint(IControllerMessageContext context, IYieldPoint yieldPoint)
|
private static Task HandleYieldPoint(IControllerMessageContext context, IYieldPoint yieldPoint)
|
||||||
{
|
{
|
||||||
var flowHandler = context.Config.DependencyResolver.Resolve<IFlowHandler>();
|
var flowHandler = context.Config.DependencyResolver.Resolve<IFlowHandler>();
|
||||||
return flowHandler.Execute(context, yieldPoint);
|
return flowHandler.Execute(new FlowHandlerContext(context), yieldPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static Task HandleParallelResponse(IControllerMessageContext context)
|
private static Task HandleParallelResponse(IControllerMessageContext context)
|
||||||
{
|
{
|
||||||
var flowHandler = context.Config.DependencyResolver.Resolve<IFlowHandler>();
|
var flowHandler = context.Config.DependencyResolver.Resolve<IFlowHandler>();
|
||||||
return flowHandler.Execute(context, new DelegateYieldPoint(async flowContext =>
|
return flowHandler.Execute(new FlowHandlerContext(context), new DelegateYieldPoint(async flowContext =>
|
||||||
{
|
{
|
||||||
await flowContext.Store();
|
await flowContext.Store();
|
||||||
}));
|
}));
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Tapeti.Config;
|
|
||||||
|
|
||||||
namespace Tapeti.Flow.Default
|
namespace Tapeti.Flow.Default
|
||||||
{
|
{
|
||||||
internal class FlowContext : IDisposable
|
internal class FlowContext : IDisposable
|
||||||
{
|
{
|
||||||
public IControllerMessageContext MessageContext { get; set; }
|
public IFlowHandlerContext HandlerContext { get; set; }
|
||||||
public IFlowStateLock FlowStateLock { get; set; }
|
public IFlowStateLock FlowStateLock { get; set; }
|
||||||
public FlowState FlowState { get; set; }
|
public FlowState FlowState { get; set; }
|
||||||
|
|
||||||
@ -21,11 +20,11 @@ namespace Tapeti.Flow.Default
|
|||||||
{
|
{
|
||||||
storeCalled = true;
|
storeCalled = true;
|
||||||
|
|
||||||
if (MessageContext == null) throw new ArgumentNullException(nameof(MessageContext));
|
if (HandlerContext == null) throw new ArgumentNullException(nameof(HandlerContext));
|
||||||
if (FlowState == null) throw new ArgumentNullException(nameof(FlowState));
|
if (FlowState == null) throw new ArgumentNullException(nameof(FlowState));
|
||||||
if (FlowStateLock == null) throw new ArgumentNullException(nameof(FlowStateLock));
|
if (FlowStateLock == null) throw new ArgumentNullException(nameof(FlowStateLock));
|
||||||
|
|
||||||
FlowState.Data = Newtonsoft.Json.JsonConvert.SerializeObject(MessageContext.Controller);
|
FlowState.Data = Newtonsoft.Json.JsonConvert.SerializeObject(HandlerContext.Controller);
|
||||||
await FlowStateLock.StoreFlowState(FlowState);
|
await FlowStateLock.StoreFlowState(FlowState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ namespace Tapeti.Flow.Default
|
|||||||
|
|
||||||
flowContext = new FlowContext
|
flowContext = new FlowContext
|
||||||
{
|
{
|
||||||
MessageContext = context,
|
HandlerContext = new FlowHandlerContext(context),
|
||||||
|
|
||||||
FlowStateLock = flowStateLock,
|
FlowStateLock = flowStateLock,
|
||||||
FlowState = flowState,
|
FlowState = flowState,
|
||||||
@ -124,7 +124,7 @@ namespace Tapeti.Flow.Default
|
|||||||
throw new YieldPointException($"Yield point is required in controller {context.Controller.GetType().Name} for converge method {methodName}");
|
throw new YieldPointException($"Yield point is required in controller {context.Controller.GetType().Name} for converge method {methodName}");
|
||||||
|
|
||||||
var flowHandler = context.Config.DependencyResolver.Resolve<IFlowHandler>();
|
var flowHandler = context.Config.DependencyResolver.Resolve<IFlowHandler>();
|
||||||
await flowHandler.Execute(context, yieldPoint);
|
await flowHandler.Execute(new FlowHandlerContext(context), yieldPoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
49
Tapeti.Flow/Default/FlowHandlerContext.cs
Normal file
49
Tapeti.Flow/Default/FlowHandlerContext.cs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using Tapeti.Config;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow.Default
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Default implementation for IFlowHandlerContext
|
||||||
|
/// </summary>
|
||||||
|
internal class FlowHandlerContext : IFlowHandlerContext
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public FlowHandlerContext()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public FlowHandlerContext(IControllerMessageContext source)
|
||||||
|
{
|
||||||
|
if (source == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Config = source.Config;
|
||||||
|
Controller = source.Controller;
|
||||||
|
Method = source.Binding.Method;
|
||||||
|
ControllerMessageContext = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ITapetiConfig Config { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public object Controller { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public MethodInfo Method { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IControllerMessageContext ControllerMessageContext { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -97,7 +97,7 @@ 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.FlowState == null
|
||||||
? GetReply(context.MessageContext)
|
? GetReply(context.HandlerContext)
|
||||||
: context.FlowState.Metadata.Reply;
|
: context.FlowState.Metadata.Reply;
|
||||||
|
|
||||||
if (reply == null)
|
if (reply == null)
|
||||||
@ -155,24 +155,24 @@ namespace Tapeti.Flow.Default
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static ReplyMetadata GetReply(IMessageContext context)
|
private static ReplyMetadata GetReply(IFlowHandlerContext context)
|
||||||
{
|
{
|
||||||
var requestAttribute = context.Message?.GetType().GetCustomAttribute<RequestAttribute>();
|
var requestAttribute = context.ControllerMessageContext?.Message?.GetType().GetCustomAttribute<RequestAttribute>();
|
||||||
if (requestAttribute?.Response == null)
|
if (requestAttribute?.Response == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return new ReplyMetadata
|
return new ReplyMetadata
|
||||||
{
|
{
|
||||||
CorrelationId = context.Properties.CorrelationId,
|
CorrelationId = context.ControllerMessageContext.Properties.CorrelationId,
|
||||||
ReplyTo = context.Properties.ReplyTo,
|
ReplyTo = context.ControllerMessageContext.Properties.ReplyTo,
|
||||||
ResponseTypeName = requestAttribute.Response.FullName,
|
ResponseTypeName = requestAttribute.Response.FullName,
|
||||||
Mandatory = context.Properties.Persistent.GetValueOrDefault(true)
|
Mandatory = context.ControllerMessageContext.Properties.Persistent.GetValueOrDefault(true)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task CreateNewFlowState(FlowContext flowContext)
|
private static async Task CreateNewFlowState(FlowContext flowContext)
|
||||||
{
|
{
|
||||||
var flowStore = flowContext.MessageContext.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);
|
flowContext.FlowStateLock = await flowStore.LockFlowState(flowID);
|
||||||
@ -184,25 +184,27 @@ namespace Tapeti.Flow.Default
|
|||||||
{
|
{
|
||||||
Metadata = new FlowMetadata
|
Metadata = new FlowMetadata
|
||||||
{
|
{
|
||||||
Reply = GetReply(flowContext.MessageContext)
|
Reply = GetReply(flowContext.HandlerContext)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task Execute(IControllerMessageContext context, IYieldPoint yieldPoint)
|
public async Task Execute(IFlowHandlerContext context, IYieldPoint yieldPoint)
|
||||||
{
|
{
|
||||||
if (!(yieldPoint is DelegateYieldPoint executableYieldPoint))
|
if (!(yieldPoint is DelegateYieldPoint executableYieldPoint))
|
||||||
throw new YieldPointException($"Yield point is required in controller {context.Controller.GetType().Name} for method {context.Binding.Method.Name}");
|
throw new YieldPointException($"Yield point is required in controller {context.Controller.GetType().Name} for method {context.Method.Name}");
|
||||||
|
|
||||||
if (!context.Get(ContextItems.FlowContext, out FlowContext flowContext))
|
var messageContext = context.ControllerMessageContext;
|
||||||
|
if (messageContext == null || !messageContext.Get(ContextItems.FlowContext, out FlowContext flowContext))
|
||||||
{
|
{
|
||||||
flowContext = new FlowContext
|
flowContext = new FlowContext
|
||||||
{
|
{
|
||||||
MessageContext = context
|
HandlerContext = context
|
||||||
};
|
};
|
||||||
|
|
||||||
context.Store(ContextItems.FlowContext, flowContext);
|
messageContext?.Store(ContextItems.FlowContext, flowContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -213,7 +215,7 @@ namespace Tapeti.Flow.Default
|
|||||||
{
|
{
|
||||||
// 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.Binding.Method.Name;
|
e.Data["Tapeti.Controller.Method"] = context.Method.Name;
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,7 +295,7 @@ namespace Tapeti.Flow.Default
|
|||||||
|
|
||||||
return new DelegateYieldPoint(context =>
|
return new DelegateYieldPoint(context =>
|
||||||
{
|
{
|
||||||
if (convergeMethod.Method.DeclaringType != context.MessageContext.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");
|
||||||
|
|
||||||
return Task.WhenAll(requests.Select(requestInfo =>
|
return Task.WhenAll(requests.Select(requestInfo =>
|
||||||
|
@ -13,84 +13,55 @@ namespace Tapeti.Flow.Default
|
|||||||
internal class FlowStarter : IFlowStarter
|
internal class FlowStarter : IFlowStarter
|
||||||
{
|
{
|
||||||
private readonly ITapetiConfig config;
|
private readonly ITapetiConfig config;
|
||||||
private readonly ILogger logger;
|
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public FlowStarter(ITapetiConfig config, ILogger logger)
|
public FlowStarter(ITapetiConfig config)
|
||||||
{
|
{
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.logger = logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public 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
|
||||||
{
|
{
|
||||||
return CallControllerMethod<TController>(GetExpressionMethod(methodSelector), value => Task.FromResult((IYieldPoint)value), new object[] { });
|
await CallControllerMethod<TController>(GetExpressionMethod(methodSelector), value => Task.FromResult((IYieldPoint)value), new object[] { });
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public 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
|
||||||
{
|
{
|
||||||
return CallControllerMethod<TController>(GetExpressionMethod(methodSelector), value => (Task<IYieldPoint>)value, new object[] {});
|
await CallControllerMethod<TController>(GetExpressionMethod(methodSelector), value => (Task<IYieldPoint>)value, new object[] {});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public 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
|
||||||
{
|
{
|
||||||
return 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 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
|
||||||
{
|
{
|
||||||
return 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>(MethodBase 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 yieldPoint = await getYieldPointResult(method.Invoke(controller, parameters));
|
||||||
|
|
||||||
/*
|
var context = new FlowHandlerContext
|
||||||
var context = new ControllerMessageContext()
|
|
||||||
{
|
{
|
||||||
Config = config,
|
Config = config,
|
||||||
Controller = controller
|
Controller = controller,
|
||||||
|
Method = method
|
||||||
};
|
};
|
||||||
*/
|
|
||||||
|
|
||||||
var flowHandler = config.DependencyResolver.Resolve<IFlowHandler>();
|
var flowHandler = config.DependencyResolver.Resolve<IFlowHandler>();
|
||||||
|
await flowHandler.Execute(context, yieldPoint);
|
||||||
try
|
|
||||||
{
|
|
||||||
//await flowHandler.Execute(context, yieldPoint);
|
|
||||||
//handlingResult.ConsumeResponse = ConsumeResponse.Ack;
|
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
//await RunCleanup(context, handlingResult.ToHandlingResult());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
private async Task RunCleanup(MessageContext context, HandlingResult handlingResult)
|
|
||||||
{
|
|
||||||
foreach (var handler in config.CleanupMiddleware)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await handler.Handle(context, handlingResult);
|
|
||||||
}
|
|
||||||
catch (Exception eCleanup)
|
|
||||||
{
|
|
||||||
logger.HandlerException(eCleanup);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
private static MethodInfo GetExpressionMethod<TController, TResult>(Expression<Func<TController, Func<TResult>>> methodSelector)
|
private static MethodInfo GetExpressionMethod<TController, TResult>(Expression<Func<TController, Func<TResult>>> methodSelector)
|
||||||
@ -105,6 +76,7 @@ namespace Tapeti.Flow.Default
|
|||||||
return method;
|
return method;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static MethodInfo GetExpressionMethod<TController, TResult, TParameter>(Expression<Func<TController, Func<TParameter, TResult>>> methodSelector)
|
private static MethodInfo GetExpressionMethod<TController, TResult, TParameter>(Expression<Func<TController, Func<TParameter, TResult>>> methodSelector)
|
||||||
{
|
{
|
||||||
var callExpression = (methodSelector.Body as UnaryExpression)?.Operand as MethodCallExpression;
|
var callExpression = (methodSelector.Body as UnaryExpression)?.Operand as MethodCallExpression;
|
||||||
|
37
Tapeti.Flow/IFlowHandlerContext.cs
Normal file
37
Tapeti.Flow/IFlowHandlerContext.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reflection;
|
||||||
|
using Tapeti.Config;
|
||||||
|
|
||||||
|
namespace Tapeti.Flow
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Provides information about the handler for the flow.
|
||||||
|
/// </summary>
|
||||||
|
public interface IFlowHandlerContext : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides access to the Tapeti config.
|
||||||
|
/// </summary>
|
||||||
|
ITapetiConfig Config { get; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An instance of the controller which starts or continues the flow.
|
||||||
|
/// </summary>
|
||||||
|
object Controller { get; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Information about the method which starts or continues the flow.
|
||||||
|
/// </summary>
|
||||||
|
MethodInfo Method { get; }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access to the controller message context if this is a continuated flow.
|
||||||
|
/// Will be null when in a starting flow.
|
||||||
|
/// </summary>
|
||||||
|
IControllerMessageContext ControllerMessageContext { get; }
|
||||||
|
}
|
||||||
|
}
|
@ -109,7 +109,7 @@ namespace Tapeti.Flow
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context"></param>
|
/// <param name="context"></param>
|
||||||
/// <param name="yieldPoint"></param>
|
/// <param name="yieldPoint"></param>
|
||||||
Task Execute(IControllerMessageContext context, IYieldPoint yieldPoint);
|
Task Execute(IFlowHandlerContext context, IYieldPoint yieldPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user