How to invoke MassTransit consumer using reflection - c#

I am building a inbox transactional pattern approach for "intercepting" messages using an oberserver and filter to log them to a database. The idea is if a consumer failed to let a background worker periodically check the database for any records where the Integration event has failed, retrieve them and re-execute them.
The data being stored in the DB looks like this:
public class InboxMessage
{
public long Id { get; private set; }
public DateTime CreatedDate { get; private set; }
public string CreatedUser { get; private set; }
public DateTime EditedDate { get; private set; }
public string EditedUser { get; private set; }
public string MessageType { get; private set; }
public string? ConsumerType { get; private set; }
public string Data { get; private set; }
public Guid EventNumber { get; private set; }
public EventStatus Status { get; private set; }
}
The idea is to retrieve all messages with a Status = Failed, use reflection (or perhaps something else?) to Deserialise the "Data" prop to the MessageType. The ConsumerType would then be used to re-execute the consumer.
I use a Filter to log the initial message (before being transfered to the consumer):
public class InboxPatternConsumerFilter<T> : IFilter<ConsumeContext<T>> where T : class
{
private readonly IntegrationEventsContext _context;
private readonly ILogger _logger;
private const string MassTransitDynamicTypeName = "MassTransit.DynamicInternal.";
public InboxPatternConsumerFilter(ILoggerFactory logger, IntegrationEventsContext context)
{
_logger = logger.CreateLogger("InboxPatternConsumerFilter");
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public async Task Send(ConsumeContext<T> context, IPipe<ConsumeContext<T>> next)
{
if (context.Message is IIntegrationEvent #event)
{
try
{
_logger.LogInformation("Integration event is type of - {generic}. Applying inbox pattern.", nameof(IGenericIntegrationEvent));
var message = new InboxMessage(
#event.EditedUser ?? "unknown",
context.Message.GetType().FullName?.Replace(MassTransitDynamicTypeName, string.Empty) ?? string.Empty,
null,
System.Text.Json.JsonSerializer.Serialize(context.Message),
#event.EventId,
EventStatus.Received);
_context.InboxMessages.Add(message);
await _context.SaveChangesAsync(context.CancellationToken);
}
catch (Exception ex)
{
// exception is catched to ensure the consumer can still continue.
_logger.LogError(ex, "Failed to create inbox message");
}
}
await next.Send(context);
}
public void Probe(ProbeContext context) {}
}
My reason for using a filter would be to check the EventNumber to confirm whether this message already exists in the DB, this should allow me to prevent sending this message to the consumer to resolve the idempotent issue in cases where we are using Retry mechanism for failed messaged.
I use a basic ReceiveObserver to update the messages as follows:
public class ReceiveObserver : IReceiveObserver
{
private readonly IntegrationEventsContext _context;
private readonly ILogger<ReceiveObserver> _logger;
public ReceiveObserver(IntegrationEventsContext context, ILogger<ReceiveObserver> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public async Task PostConsume<T>(ConsumeContext<T> context, TimeSpan duration, string consumerType) where T : class
{
try
{
if (context.Message is IIntegrationEvent #event)
{
var message = await _context.InboxMessages.FirstOrDefaultAsync(x => x.EventNumber == #event.EventId);
if (message is not null)
{
message.Update(EventStatus.Completed, "post-consumer", consumerType);
_context.Update(message);
await _context.SaveChangesAsync();
var typeTest = System.Reflection.Assembly
.GetEntryAssembly()?
.GetType(consumerType);
}
else
{
_logger.LogWarning("Inbox Message not found");
}
}
}
catch (Exception ex)
{
_logger.LogError("An error occurred trying to update the Message's complete status", ex);
}
// called when the message was consumed, once for each consumer
}
public async Task ConsumeFault<T>(ConsumeContext<T> context, TimeSpan elapsed, string consumerType, Exception exception) where T : class
{
if (context.Message is IIntegrationEvent #event)
{
var message = await _context.InboxMessages.FirstOrDefaultAsync(x => x.EventNumber == #event.EventId);
if (message is not null)
{
message.Update(EventStatus.Failed, "consumer-fault", consumerType);
_context.Update(message);
await _context.SaveChangesAsync();
}
else
{
_logger.LogWarning("Inbox Message not found");
}
}
// called when the message is consumed but the consumer throws an exception
}
public Task ReceiveFault(ReceiveContext context, Exception exception)
{
// TODO: Get the message id and update the status in db.
// called when an exception occurs early in the message processing, such as deserialization, etc.
return Task.CompletedTask;
}
}
The idea is then to use a background service to check for any failed messages as follows:
public class InboxMessageService : IHostedService
{
private readonly IBusControl _bus;
private readonly IntegrationEventsContext _context;
private readonly ILogger<InboxMessageService> _logger;
private readonly IServiceProvider _serviceProvider;
public InboxMessageService(
IBusControl bus,
IntegrationEventsContext context,
ILogger<InboxMessageService> logger,
IServiceProvider serviceProvider)
{
_bus = bus ?? throw new ArgumentNullException(nameof(context)); ;
_context = context ?? throw new ArgumentNullException(nameof(context));
_logger = logger ?? throw new ArgumentNullException(nameof(context));
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
public async Task StartAsync(CancellationToken cancellationToken)
{
List<InboxMessage> messages = await _context.InboxMessages
.Where(x => x.Status != EventStatus.Completed)
.ToListAsync(cancellationToken);
foreach(InboxMessage message in messages)
{
try
{
if (message.ConsumerType is null)
{
_logger.LogWarning("Unable to find the consumer type to start this message.");
continue;
}
var typeTest = System.Reflection.Assembly
.GetEntryAssembly()?
.GetType(message.ConsumerType);
if (typeTest is null)
{
throw new Exception();
}
var constructor = typeTest.GetConstructors().First(); // We can always assume that the consumer will contain a ctor
var parameters = new List<object?>();
foreach (var param in constructor.GetParameters())
{
var service = _serviceProvider.GetService(param.ParameterType);//get instance of the class
parameters.Add(service);
}
var obj = Activator.CreateInstance(typeTest, parameters.ToArray());
// TODO: fiqure out how to create a ConsumeContext<T> message from the DB data
typeTest.GetMethod("Consume")?.Invoke(obj, System.Reflection.BindingFlags.InvokeMethod, Type.DefaultBinder, null, null);
}
catch (Exception ex)
{
// ...
}
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
The part I am struggling with is the TODO in the background service to figure out how to essentially recreate the Message (using the MessageType and Data props) and invoke the Consumer (using the ConsumerType).

Related

Raise event from activity

I'm building a saga state machine, trimmed down implementation below:
public class DueDiligenceCaseCreateStateMachine : MassTransitStateMachine<DueDiligenceCaseCreateState>
{
public State CreatingCase { get; private set; }
public Event<DueDiligenceCaseCreateCommand> TriggerReceived { get; private set; }
public Event CaseCreationFinished { get; private set; }
public Event CaseCreationFailed { get; private set; }
private readonly ILogger<DueDiligenceCaseCreateStateMachine> _logger;
private readonly IOptions<DueDiligenceCaseCreateSagaOptions> _sagaOptions;
public DueDiligenceCaseCreateStateMachine(
ILogger<DueDiligenceCaseCreateStateMachine> logger,
IOptions<DueDiligenceCaseCreateSagaOptions> sagaOptions)
{
_logger = logger;
_sagaOptions = sagaOptions;
Configure();
BuildProcess();
}
private void Configure()
{
Event(
() => TriggerReceived,
e => e.CorrelateById(x => x.Message.DueDiligenceCaseId));
}
private void BuildProcess()
{
During(
Initial,
When(TriggerReceived)
.TransitionTo(CreatingCase)
.Activity(CreateCase));
}
private EventActivityBinder<DueDiligenceCaseCreateState, DueDiligenceCaseCreateCommand> CreateCase(
IStateMachineActivitySelector<DueDiligenceCaseCreateState, DueDiligenceCaseCreateCommand> sel) =>
sel.OfType<CreateCaseActivity>();
}
And the activity itself is here:
public class CreateCaseActivity : BaseActivity<DueDiligenceCaseCreateState, DueDiligenceCaseCreateCommand>
{
private readonly ICommandHandler<InitializeCaseCommand> _initializeCaseHandler;
private readonly IOptions<ApplicationOptions> _options;
private readonly ILogger<DueDiligenceCaseCreateConsumer> _logger;
public CreateCaseActivity(
ICommandHandler<InitializeCaseCommand> initializeCaseHandler,
IOptions<ApplicationOptions> options,
ILogger<DueDiligenceCaseCreateConsumer> logger)
{
_initializeCaseHandler = initializeCaseHandler;
_options = options;
_logger = logger;
}
public override async Task Execute(
BehaviorContext<DueDiligenceCaseCreateState, DueDiligenceCaseCreateCommand> context,
Behavior<DueDiligenceCaseCreateState, DueDiligenceCaseCreateCommand> next)
{
_logger.LogInformation(
"Consuming {Command} started, case id: {caseid}, creating a case...",
nameof(DueDiligenceCaseCreateCommand),
context.Data.DueDiligenceCaseId);
var initializeCaseCmd = ConvertMessageToCommand(context.Data);
initializeCaseCmd.CanHaveOnlyOneActiveCasePerCustomer = !_options.Value.FeatureToggles.AllowMultipleActiveCasesOnSingleCustomer;
try
{
await _initializeCaseHandler.Handle(initializeCaseCmd);
}
catch
{
}
finally
{
await next.Execute(context);
}
}
private InitializeCaseCommand ConvertMessageToCommand(DueDiligenceCaseCreateCommand message) =>
// returns the command object
}
The state machine has two events for now - CaseCreationFinished and CaseCreationFailed. I'd like to raise the first one in the try clause of the activity and the other one in the catch part. I see the context object passed in as an argument has the Raise method, but the problem is that I can't reach the DueDiligenceCaseCreateStateMachine.CaseCreationFinished from within the activity. Is there a way to do it?
There is a Raise method on BehaviorContext, why not use it?
Updated
You can add a dependency on your activity for the state machine itself, which would give you access to the events.

I get an ObjectDisposedException when reading a stream in the background service

I want to process a Request.Body in a background-service in ASP.Net Core, so I first add it to a ConcurrentQueue.
public class BackgroundJobs
{
public ConcurrentQueue<Stream> Queue { get; set; } = new ConcurrentQueue<Stream>();
}
The concurrentQueue is instantiated with AddSingleton in Startup.cs
[ApiController]
public class NotificationsController : ControllerBase
{
private readonly BackgroundJobs backgroundJobs;
public NotificationsController( BackgroundJobs backgroundJobs)
{
this.backgroundJobs = backgroundJobs;
}
//...........
[HttpPost]
public ActionResult<string> Post([FromQuery] string validationToken = null)
{
if (!string.IsNullOrEmpty(validationToken))
{
return Ok(validationToken);
}
else
{
backgroundJobs.Queue.Enqueue(Request.Body);
return Accepted();
}
}
}
BackgroundServices.cs was instantiated in Startup.cs with AddHostedService.
When the Request.Body is in the queue I want to process it further.
public class BackgroundServices : BackgroundService, IHostedService
{
private readonly BackgroundJobs backgroundJobs;
private IServiceScopeFactory iServiceScopeFactory;
public BackgroundServices(BackgroundJobs backgroundJobs, IServiceScopeFactory iServiceScopeFactory)
{
this.backgroundJobs = backgroundJobs;
this.iServiceScopeFactory = iServiceScopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
Stream stream;
while (!cancellationToken.IsCancellationRequested)
{
if (this.backgroundJobs.Queue.TryDequeue(out stream))
{
StreamReader streamReader = new StreamReader(stream);
string content = await streamReader.ReadToEndAsync();
var list = JsonConvert.DeserializeObject<Notifications>(content)
//......
But get on string content = await streamReader.ReadToEndAsync() the following error message: System.ObjectDisposedException in System.Private.CoreLib.dll

Class MessageAppService cannot have multiple base classes 'Hub' and 'AsyncCrudAppService'

I'm using ASP.NET Boilerplate with .NET Core 3.1.
I'm trying to save SignalR chat history to the database. The problem is when I want to create a subclass of AsyncCrudAppService and Hub, an error occurred with below text:
Class MessageAppService cannot have multiple base classes 'Hub' and 'AsyncCrudAppService'
Here is my code:
namespace MyProject.ChatAppService
{
public class MessageAppService : Hub, AsyncCrudAppService<Message, MessageDto, int, PagedAndSortedResultRequestDto, CreateMessageDto, UpdateMessageDto, ReadMessageDto>
{
private readonly IRepository<Message> _repository;
private readonly IDbContextProvider<MyProjectDbContext> _dbContextProvider;
private MyProjectPanelDbContext db => _dbContextProvider.GetDbContext();
public MessageAppService(
IDbContextProvider<MyProjectDbContext> dbContextProvider,
IRepository<Message> repository)
: base(repository)
{
_repository = repository;
_dbContextProvider = dbContextProvider;
}
public List<Dictionary<long, Tuple<string, string>>> InboxChat()
{
// The result will be List<userid, Tuple<username, latest message>>();
List<Dictionary<long, Tuple<string, string>>> result = new List<Dictionary<long, Tuple<string, string>>>();
List<User> listOfAllUsers = db.Set<User>().ToList();
listOfAllUsers.ForEach((user) =>
{
try
{
var dict = new Dictionary<long, Tuple<string, string>>();
var latestMessage = (from msg in db.Set<Message>() select msg)
.Where(msg => msg.CreatorUserId == user.Id && msg.receiverID == AbpSession.UserId)
.OrderByDescending(x => x.CreationTime)
.FirstOrDefault()
.Text.ToString();
dict.Add(user.Id, Tuple.Create(user.UserName, latestMessage));
result.Add(dict);
}
catch (Exception ex)
{
new UserFriendlyException(ex.Message.ToString());
}
});
return result;
}
public List<Message> getMessageHistory(int senderId)
{
return _repository.GetAll()
.Where(x => x.CreatorUserId == senderId && x.receiverID == AbpSession.UserId )
.ToList();
}
}
}
How could I avoid this error?
Update
Here is MyChatHub code that I wanted to combine with the AsyncCrudAppService subclass to become one class (I don't know if this way is correct but this was what came to my mind!).
public class MyChatHub : Hub, ITransientDependency
{
public IAbpSession AbpSession { get; set; }
public ILogger Logger { get; set; }
public MyChatHub()
{
AbpSession = NullAbpSession.Instance;
Logger = NullLogger.Instance;
}
public async Task SendMessage(string message)
{
await Clients.All.SendAsync("getMessage", string.Format("User {0}: {1}", AbpSession.UserId, "the message that has been sent from client is "+message));
}
public async Task ReceiveMessage(string msg, long userId)
{
if (this.Clients != null)
{
await Clients.User(userId.ToString())
.SendAsync("ReceiveMessage", msg, "From Server by userID ", Context.ConnectionId, Clock.Now);
}
else
{
throw new UserFriendlyException("something wrong");
}
}
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
Logger.Debug("A client connected to MyChatHub: " + Context.ConnectionId);
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await base.OnDisconnectedAsync(exception);
Logger.Debug("A client disconnected from MyChatHub: " + Context.ConnectionId);
}
}
Your AsyncCrudAppService subclass can't and shouldn't inherit Hub.
Instead, inject and use IHubContext<MyChatHub> similar to ABP's SignalRRealTimeNotifier.
public MessageAppService(
IHubContext<MyChatHub> hubContext,
IDbContextProvider<MyProjectDbContext> dbContextProvider,
IRepository<Message> repository)
: base(repository)
{
_dbContextProvider = dbContextProvider;
_hubContext = hubContext;
_repository = repository;
}
To send a message to all clients, call _hubContext.Clients.All.SendAsync(...).
References:
https://learn.microsoft.com/en-us/aspnet/core/signalr/hubcontext?view=aspnetcore-3.1
https://aspnetboilerplate.com/Pages/Documents/SignalR-AspNetCore-Integration

Property injection

I'm trying make a telegram bot with reminder. I'm using Telegram.Bot 14.10.0, Quartz 3.0.7, .net core 2.0. The first version should : get message "reminder" from telegram, create job (using Quartz) and send meaasage back in 5 seconds.
My console app with DI looks like:
Program.cs
static IBot _botClient;
public static void Main(string[] args)
{
// it doesn't matter
var servicesProvider = BuildDi(connecionString, section);
_botClient = servicesProvider.GetRequiredService<IBot>();
_botClient.Start(appModel.BotConfiguration.BotToken, httpProxy);
var reminderJob = servicesProvider.GetRequiredService<IReminderJob>();
reminderJob.Bot = _botClient;
Console.ReadLine();
_botClient.Stop();
// it doesn't matter
}
private static ServiceProvider BuildDi(string connectionString, IConfigurationSection section)
{
var rJob = new ReminderJob();
var sCollection = new ServiceCollection()
.AddSingleton<IBot, Bot>()
.AddSingleton<ReminderJob>(rJob)
.AddSingleton<ISchedulerBot>(s =>
{
var schedBor = new SchedulerBot();
schedBor.StartScheduler();
return schedBor;
});
return sCollection.BuildServiceProvider();
}
Bot.cs
public class Bot : IBot
{
static TelegramBotClient _botClient;
public void Start(string botToken, WebProxy httpProxy)
{
_botClient = new TelegramBotClient(botToken, httpProxy);
_botClient.OnReceiveError += BotOnReceiveError;
_botClient.OnMessage += Bot_OnMessage;
_botClient.StartReceiving();
}
private static async void Bot_OnMessage(object sender, MessageEventArgs e)
{
var me = wait _botClient.GetMeAsync();
if (e.Message.Text == "reminder")
{
var map= new Dictionary<string, object> { { ReminderJobConst.ChatId, e.Message.Chat.Id.ToString() }, { ReminderJobConst.HomeWordId, 1} };
var job = JobBuilder.Create<ReminderJob>().WithIdentity($"{prefix}{rnd.Next()}").UsingJobData(new JobDataMap(map)).Build();
var trigger = TriggerBuilder.Create().WithIdentity($"{prefix}{rnd.Next()}").StartAt(DateTime.Now.AddSeconds(5).ToUniversalTime())
.Build();
await bot.Scheduler.ScheduleJob(job, trigger);
}
}
}
Quartz.net not allow use constructor with DI. That's why I'm trying to create property with DI.
ReminderJob.cs
public class ReminderJob : IJob
{
static IBot _bot;
public IBot Bot { get; set; }
public async Task Execute(IJobExecutionContext context)
{
var parameters = context.JobDetail.JobDataMap;
var userId = parameters.GetLongValue(ReminderJobConst.ChatId);
var homeWorkId = parameters.GetLongValue(ReminderJobConst.HomeWordId);
await System.Console.Out.WriteLineAsync("HelloJob is executing.");
}
}
How can I pass _botClient to reminderJob in Program.cs?
If somebody looks for answer, I have one:
Program.cs (in Main)
var schedBor = servicesProvider.GetRequiredService<ISchedulerBot>();
var logger = servicesProvider.GetRequiredService<ILogger<DIJobFactory>>();
schedBor.StartScheduler();
schedBor.Scheduler.JobFactory = new DIJobFactory(logger, servicesProvider);
DIJobFactory.cs
public class DIJobFactory : IJobFactory
{
static ILogger<DIJobFactory> _logger;
static IServiceProvider _serviceProvider;
public DIJobFactory(ILogger<DIJobFactory> logger, IServiceProvider sp)
{
_logger = logger;
_serviceProvider = sp;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
IJobDetail jobDetail = bundle.JobDetail;
Type jobType = jobDetail.JobType;
try
{
_logger.LogDebug($"Producing instance of Job '{jobDetail.Key}', class={jobType.FullName}");
if (jobType == null)
{
throw new ArgumentNullException(nameof(jobType), "Cannot instantiate null");
}
return (IJob)_serviceProvider.GetRequiredService(jobType);
}
catch (Exception e)
{
SchedulerException se = new SchedulerException($"Problem instantiating class '{jobDetail.JobType.FullName}'", e);
throw se;
}
}
// get from https://github.com/quartznet/quartznet/blob/139aafa23728892b0a5ebf845ce28c3bfdb0bfe8/src/Quartz/Simpl/SimpleJobFactory.cs
public void ReturnJob(IJob job)
{
var disposable = job as IDisposable;
disposable?.Dispose();
}
}
ReminderJob.cs
public interface IReminderJob : IJob
{
}
public class ReminderJob : IReminderJob
{
ILogger<ReminderJob> _logger;
IBot _bot;
public ReminderJob(ILogger<ReminderJob> logger, IBot bot)
{
_logger = logger;
_bot = bot;
}
public async Task Execute(IJobExecutionContext context)
{
var parameters = context.JobDetail.JobDataMap;
var userId = parameters.GetLongValue(ReminderJobConst.ChatId);
var homeWorkId = parameters.GetLongValue(ReminderJobConst.HomeWordId);
await _bot.Send(userId.ToString(), "test");
}
}

using global exception handeling messes up DelegatingHandler

When ovveride the IExceptionHandler, the response does not reach the DelegatingHandler when a unexpected exception occurs. How can I fix this?
In webapi 2, I want to implement a audit logger for request and response messages. I also want to add a global exception handler. However, when I replace the IExceptionHandler with my custom implementation. the response never reaches the DelegatingHandler -on exception - And thus the audit for response is lost.
in WebApiConfig
// add custom audittrail logger
config.MessageHandlers.Add(new AuditLogHandler());
// replace global exception handeling
config.Services.Replace(typeof(IExceptionHandler), new WebAPiExceptionHandler());
Custom Exception Handler
public class WebAPiExceptionHandler : ExceptionHandler
{
//A basic DTO to return back to the caller with data about the error
private class ErrorInformation
{
public string Message { get; set; }
public DateTime ErrorDate { get; set; }
}
public override void Handle(ExceptionHandlerContext context)
{
context.Result = new ResponseMessageResult(context.Request.CreateResponse(HttpStatusCode.InternalServerError,
new ErrorInformation { Message = "Iets is misgegaan", ErrorDate = DateTime.UtcNow }));
}
}
Custom Auditlogger
public class AuditLogHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request.Content != null)
{
var task = await request.Content.ReadAsStringAsync();
// .. code for loggign request
}
var result = await base.SendAsync(request, cancellationToken);
// .. code for logging response
// when I do not replace WebAPiExceptionHandler, code is reachred here
// When I Do use WebAPiExceptionHandler, code is not reached here
return result;
}
}
Code for throwing exception in webapi
public class Values_v2Controller : ApiController
{
public string Get(int id)
{
throw new Exception("haha");
}
}
Dont use ExceptionHandler as base class, implement interface IExceptionHandler
public class WebAPiExceptionHandler : IExceptionHandler
{
public Task HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken)
{
var fout = new ErrorInformation
{
Message = "Iets is misgegaan"
, ErrorDate = DateTime.UtcNow
};
var httpResponse = context.Request.CreateResponse(HttpStatusCode.InternalServerError, fout);
context.Result = new ResponseMessageResult(httpResponse);
return Task.FromResult(0);
}
private class ErrorInformation
{
public string Message { get; set; }
public DateTime ErrorDate { get; set; }
}
}
The problem is that ExceptionHandler only executes Handle(ExceptionHandlerContext context) method if ShouldHandle(ExceptionHandlerContext context) returns true.
Overriding bool ShouldHandle(ExceptionHandlerContext context) to always return true fix the problem for me.

Categories

Resources