MassTransit Automatonymous - State not changing when a message is Sent - c#

I am trying to figure why "Sending" a message does not invoke state machine, but if I "Publish" a message, it works and I can see the state changing.
Following is my code, it is similar to the documentation, except that I am trying to "Send" a message.
Components
State Machine:
public class OrderState: SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public int CurrentState { get; set; }
public DateTime? OrderDate { get; set; }
}
public class OrderStateMachine: MassTransitStateMachine<OrderState>
{
public State Submitted { get; private set; }
public State Accepted { get; private set; }
public State Completed { get; private set; }
public Event<SubmitOrder> SubmitOrder { get; private set; }
public Event<OrderAccepted> OrderAccepted { get; private set; }
public Event<OrderCompleted> OrderCompleted { get; private set; }
public OrderStateMachine()
{
InstanceState(x => x.CurrentState, Submitted, Accepted, Completed);
Event(() => SubmitOrder, x => x.CorrelateById(context => context.Message.OrderId));
Event(() => OrderAccepted, x => x.CorrelateById(context => context.Message.OrderId));
Event(() => OrderCompleted, x => x.CorrelateById(context => context.Message.OrderId));
Initially(
When(SubmitOrder)
.Then(context => context.Instance.OrderDate = context.Data.OrderDate)
.TransitionTo(Submitted));
During(Submitted,
When(OrderAccepted)
.TransitionTo(Accepted));
During(Accepted,
Ignore(SubmitOrder));
DuringAny(
When(OrderCompleted)
.TransitionTo(Completed));
SetCompleted(async instance =>
{
var currentState = await this.GetState(instance);
return Completed.Equals(currentState);
});
}
}
Contracts:
public record SubmitOrder(Guid OrderId, DateTime? OrderDate);
public record OrderAccepted(Guid OrderId);
public record OrderCompleted(Guid OrderId);
Consumers:
public class SubmitOrderConsumer: IConsumer<SubmitOrder>
{
public async Task Consume(ConsumeContext<SubmitOrder> context)
{
await Task.Delay(2000);
}
}
public class SubmitOrderConsumerDefinition : ConsumerDefinition<SubmitOrderConsumer>
{
public SubmitOrderConsumerDefinition()
{
EndpointName = "submit-order";
}
protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator<SubmitOrderConsumer> consumerConfigurator)
{
endpointConfigurator.ConfigureConsumeTopology = false;
}
}
Web API
Program.cs (snippet)
// Add services to the container.
builder.Services.AddMassTransit(cfg =>
{
cfg.SetKebabCaseEndpointNameFormatter();
cfg.UsingRabbitMq((context, configurator) =>
{
configurator.Host("localhost", "/", hostConfigurator =>
{
hostConfigurator.Username("guest");
hostConfigurator.Password("guest");
});
});
});
builder.Services.AddMassTransitHostedService();
builder.Services.AddControllers();
OrderController
[Route("order")]
public class OrderController : ControllerBase
{
private readonly ISendEndpointProvider _sendEndpointProvider;
public OrderController(ISendEndpointProvider sendEndpointProvider)
{
_sendEndpointProvider = sendEndpointProvider;
}
[HttpPost]
public async Task<IActionResult> SendOrder()
{
var endpoint = await _sendEndpointProvider.GetSendEndpoint(new Uri("exchange:submit-order"));
await endpoint.Send(new SubmitOrder(Guid.NewGuid(), DateTime.Now));
return Ok();
}
}
Worker Service
Program.cs
using IHost = Microsoft.Extensions.Hosting.IHost;
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddMassTransit(cfg =>
{
cfg.AddConsumer<SubmitOrderConsumer>(typeof(SubmitOrderConsumerDefinition));
cfg.AddSagaStateMachine<OrderStateMachine, OrderState>().InMemoryRepository();
cfg.UsingRabbitMq((context, rabbitMqConfigurator) =>
{
rabbitMqConfigurator.Host("localhost", "/", hostConfigurator =>
{
hostConfigurator.Username("guest");
hostConfigurator.Password("guest");
});
rabbitMqConfigurator.ReceiveEndpoint("saga-order", endpointConfigurator =>
{
endpointConfigurator.ConfigureSaga<OrderState>(context);
});
rabbitMqConfigurator.ConfigureEndpoints(context);
});
});
services.AddMassTransitHostedService();
services.AddHostedService<Worker>();
})
.Build();
await host.RunAsync();
Then I do a POST via Postman to: http://localhost:5000/order
It does call the SubmitOrderConsumer, but for some reason, the State machine does not get invoked (it won't hit breakpoint inside the Then handler that sets the Order Date inside Initially state.). I think I am missing something that connects the two together.
Any feedback is greatly appreciated. Thank you.

In your example, you'd want to use Publish, especially in this scenario where you have two consumers (the consumer, and the state machine) on separate endpoints (queue) that would be consuming the message. Sending directly to the exchange would only get the message to one of the endpoints.

Related

Why Not returning value after AddDomainEvent in CQRS pattern

I use CQRS pattern with MediatR in my project.In 2 parts, it doesn't end the way I expect.
1: When I want to return a value after changes with a command,I save the changes by using (for example: usermanager), that's why the created view model is no longer returned and it is sent to the event push path.
BaseEntity :
public abstract class BaseEntity : IBaseId<string>, IEntityWithDomainEvent
{
public string Id { get; set; }
private readonly List<BaseEvent> _domainEvents = new();
[NotMapped]
public IReadOnlyCollection<BaseEvent> DomainEvents => _domainEvents.AsReadOnly();
public void AddDomainEvent(BaseEvent domainEvent)
{
_domainEvents.Add(domainEvent);
}
public void RemoveDomainEvent(BaseEvent domainEvent)
{
_domainEvents.Remove(domainEvent);
}
public void ClearDomainEvents()
{
_domainEvents.Clear();
}
}
public class LoginUserCommand : IRequest<ResponseLoginViewModel>
{
public string PhoneNumber { get; set; }
public string VerifyCode { get; set; }
public string ReturnUrl { get; set; }
}
public class LoginUserCommandHandler : IRequestHandler<LoginUserCommand, ResponseLoginViewModel>
{
private readonly IIdentityService _identityService;
public LoginUserCommandHandler(IIdentityService identityService)
{
_identityService = identityService;
}
public async Task<ResponseLoginViewModel> Handle(LoginUserCommand request, CancellationToken cancellationToken)
{
var user = await _identityService.GetUserWithMobileAsync(request.PhoneNumber);
if (user.VerifyCode == request.VerifyCode)
{
var res = await _identityService.LoginUser(user);
user.RefreshToken = res.RefreshToken;
user.RefreshTokenExpiryTime = res.Expiration;
user.AddDomainEvent(new DriverLoggedInEvent(user));
user.VerifyCode = await _identityService.GenerateRandomCode();
//TODO: Here If I use _usermanager.updateuser it no longer returns to (return res) and
goes to the DriverLoggedInEventHandler class
if (string.IsNullOrEmpty(res.Token))
{
return new ResponseLoginViewModel();
}
return res;
}
return new ResponseLoginViewModel();
}
}
2: My ŮŽApplicationUser class inherits both from IdentityUser and from the interface I created called: IEntityWithDomainEvent.
public interface IEntityWithDomainEvent
{
IReadOnlyCollection<BaseEvent> DomainEvents { get; }
void AddDomainEvent(BaseEvent domainEvent);
void RemoveDomainEvent(BaseEvent domainEvent);
void ClearDomainEvents();
}
But I can't put .Entries() at the time of savechange, so I've passed it in the stupidest way for now.
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
try
{
await _mediator.DispatchDomainEvents(this);
return await base.SaveChangesAsync(cancellationToken);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
throw new Exception(e.Message);
}
}
DispatchDomainEvent:
public static class MediatorExtensions
{
public static async Task DispatchDomainEvents(this IMediator mediator, DbContext context)
{
var entities = context.ChangeTracker
.Entries<BaseEntity>()
.Where(e => e.Entity.DomainEvents.Any())
.Select(e => e.Entity);
var domainEvents = entities
.SelectMany(e => e.DomainEvents)
.ToList();
entities.ToList().ForEach(e => e.ClearDomainEvents());
// TODO : Problem two is solved for now by the following silly method
if (domainEvents.Count == 0)
{
var entities2 = context.ChangeTracker
.Entries<ApplicationUser>()
.Where(e => e.Entity.DomainEvents.Any())
.Select(e => e.Entity);
domainEvents = entities2
.SelectMany(e => e.DomainEvents)
.ToList();
entities2.ToList().ForEach(e => e.ClearDomainEvents());
}
foreach (var domainEvent in domainEvents)
await mediator.Publish(domainEvent);
}
}
Thank you in advance for your help
Update1:
In LoginUserCommandHandler: Please read the TODO section. The problem is in the DispatchDomainEvent section in the TODO section.
After update user with _usermanager.updateuser in LoginUserCommandHandler, because the database is saved, that's why the value of return res in LoginUserCommandHandler is not returned
Not sure what actual problems are but as for DispatchDomainEvents it seems that BaseEntity should implement IEntityWithDomainEvent and if it does - you can just use the interface simplifying the implementation:
public static async Task DispatchDomainEvents(this IMediator mediator, DbContext context)
{
var entities = context.ChangeTracker
.Entries<IEntityWithDomainEvent>()
.Where(e => e.Entity.DomainEvents.Any())
.Select(e => e.Entity);
var domainEvents = entities
.SelectMany(e => e.DomainEvents)
.ToList();
entities.ToList().ForEach(e => e.ClearDomainEvents());
foreach (var domainEvent in domainEvents)
await mediator.Publish(domainEvent);
}
Also possibly double enumeration of entities is not ideal, so:
public static async Task DispatchDomainEvents(this IMediator mediator, DbContext context)
{
var entities = context.ChangeTracker
.Entries<IEntityWithDomainEvent>()
.Where(e => e.Entity.DomainEvents.Any())
.Select(e => e.Entity);
foreach (var entity in entities)
{
foreach (var domainEvent in entity.DomainEvents)
{
await mediator.Publish(domainEvent);
}
entity.ClearDomainEvents();
}
}

MassTransit Testing Saga StateMachine - The state machine was not properly configured

I am trying to test / execute a State Machine Saga from MassTransit but I am receiving this error
Message:
MassTransit.ConfigurationException : Failed to create the state machine connector for Invoices.Features.EntryInvoices.ProcessEntryInvoiceSagaState
----> MassTransit.ConfigurationException : The state machine was not properly configured:
[Failure] ProcessInvoiceReceived was not specified
Have tried to search on the documentation but didn't find anything related. Don't know where I am missing, maybe saga configuration or something els.
here is my Saga:
using ...;
namespace Invoices.Features.EntryInvoices
{
public class ProcessEntryInvoiceSaga:MassTransitStateMachine<ProcessEntryInvoiceSagaState>
{
public State Started { get; private set; }
public State Processing { get; private set; }
public State Closed { get; private set; }
public Event<ProcessInvoice> ProcessInvoiceReceived { get; private set; }
public Event<ProductsAdded> ProductsAddedEvent { get; private set; }
public Event<DuplicatesRegistered> DuplicatesRegisteredEvent { get; private set; }
public ProcessEntryInvoiceSaga()
{
InstanceState(x => x.CurrentState);
Event(() => ProcessInvoiceReceived);
Event(() => ProductsAddedEvent);
Event(() => DuplicatesRegisteredEvent);
Initially(
When(ProcessInvoiceReceived)
.Then(x => x.Saga.InvoiceId = x.Message.InvoiceId)
.Activity(x=> x.OfType<ProcessEntryInvoiceStartActivity>())
.TransitionTo(Started)
);
During(Started,
When(ProductsAddedEvent)
.TransitionTo(Processing),
When(DuplicatesRegisteredEvent)
.TransitionTo(Processing));
During(Processing,
When(ProductsAddedEvent)
.TransitionTo(Closed),
When(DuplicatesRegisteredEvent)
.TransitionTo(Closed));
WhenEnter(Closed, binder => binder
.Activity(x=>x.OfType<ProcessEntryInvoiceFinishActivity>())
);
SetCompleted(async instance =>
{
State currentState = await this.GetState(instance);
return Closed.Equals(currentState);
});
}
}
public class ProcessEntryInvoiceSagaState : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; }
public long InvoiceId { get; set; }
}
}
And the test that I am trying to execute:
using ...;
namespace Invoices.Test.Features
{
public class ProcessEntryInvoiceSagaTest
{
Mock<IEntryInvoiceRepository> _entryInvoiceRepository;
ITestHarness _harness;
[SetUp]
public async Task Setup()
{
_entryInvoiceRepository = new Mock<IEntryInvoiceRepository>();
var provider = new ServiceCollection()
.AddSingleton(_entryInvoiceRepository.Object)
.AddSingleton<ISagaRepository<ProcessEntryInvoiceSagaState>, InMemorySagaRepository<ProcessEntryInvoiceSagaState>>()
.AddMassTransitTestHarness(cfg =>
{
cfg.AddSagaStateMachine<ProcessEntryInvoiceSaga, ProcessEntryInvoiceSagaState>().InMemoryRepository();
cfg.AddConsumer<MockSagaStepConsumers>();
})
.BuildServiceProvider(true);
_harness = provider.GetRequiredService<ITestHarness>();
await _harness.Start();
}
[Test]
public async Task TestSetNewProductItemWithSuccess()
{
var openedInvoice = new EntryInvoice() {...};
var form = new ProcessInvoice()
{
InvoiceId = 123
};
await _harness.Bus.Publish(form);
Assert.That(await _harness.Consumed.Any<ProcessInvoice>());
Assert.That(await _harness.Published.Any<AddProductsToInventory>());
Assert.That(await _harness.Published.Any<RegisterDuplicatesOfInvoice>());
Assert.That(await _harness.Consumed.Any<DuplicatesRegistered>());
Assert.That(await _harness.Consumed.Any<ProductsAdded>());
Assert.That(await _harness.Published.Any<EntryInvoiceClosed>());
_entryInvoiceRepository.Verify(i => i.Update(It.IsAny<EntryInvoice>()), Times.AtLeast(2));
_entryInvoiceRepository.Verify(i => i.SaveChanges(), Times.AtLeast(2));
openedInvoice.Status.Should().Be(InvoiceStatus.Closed);
}
}
public class MockSagaStepConsumers : IConsumer<AddProductsToInventory>, IConsumer<RegisterDuplicatesOfInvoice>
{
public async Task Consume(ConsumeContext<AddProductsToInventory> context)
{
await context.Publish(new ProductsAdded()
{
CorrelationId = context.MessageId ?? Guid.Empty
});
}
public async Task Consume(ConsumeContext<RegisterDuplicatesOfInvoice> context)
{
await context.Publish(new DuplicatesRegistered()
{
CorrelationId = context.MessageId ?? Guid.Empty,
InvoiceId = context.Message.InvoiceId
});
}
}
}
Found the issue on
Event(() => ProcessInvoiceReceived);
Event(() => ProductsAddedEvent);
Event(() => DuplicatesRegisteredEvent);
The documentation says that the events are autoconfigured but only if they have CorrelationId or CorrelatedBy<Guid> interface.
I just added the interface CorrelatedBy<Guid> to my messages and set a CorrelationID to the first message and this allowed me to continue the process.

MassTransit messages types must not be System types exception

I'm pretty new to MassTransit and don't understand what am I doing wrong to get the following exception: Messages types must not be System types.
Here are my definitions:
[BsonIgnoreExtraElements]
public class ArcProcess : SagaStateMachineInstance, ISagaVersion
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; }
public int Version { get; set; }
public Guid ActivationId { get; set; }
}
public static class MessageContracts
{
static bool _initialized;
public static void Initialize()
{
if (_initialized)
return;
GlobalTopology.Send.UseCorrelationId<StartProcessingMessage>(x => x.ActivationId);
GlobalTopology.Send.UseCorrelationId<ReconstructionFinishedMessage>(x => x.ActivationId);
GlobalTopology.Send.UseCorrelationId<ProcessingFinishedMessage>(x => x.ActivationId);
_initialized = true;
}
}
2 of my consumers are:
public class StartReconstructionConsumer : IConsumer<StartProcessingMessage>
{
readonly ILogger<StartReconstructionConsumer> _Logger;
private readonly int _DelaySeconds = 5;
public StartReconstructionConsumer(ILogger<StartReconstructionConsumer> logger)
{
_Logger = logger;
}
public async Task Consume(ConsumeContext<StartProcessingMessage> context)
{
var activationId = context.Message.ActivationId;
_Logger.LogInformation($"Received Scan: {activationId}");
await Task.Delay(_DelaySeconds * 1000);
_Logger.LogInformation($"Finish Scan: {activationId}");
await context.Publish<ReconstructionFinishedMessage>(new { ActivationId = activationId });
}
}
public class ProcessingFinishedConsumer : IConsumer<ProcessingFinishedMessage>
{
readonly ILogger<ProcessingFinishedConsumer> _Logger;
public ProcessingFinishedConsumer(ILogger<ProcessingFinishedConsumer> logger)
{
_Logger = logger;
}
public async Task Consume(ConsumeContext<ProcessingFinishedMessage> context)
{
_Logger.LogInformation($"Finish {context.Message.ActivationId}");
await Task.CompletedTask;
}
}
And here is the StateMachine definition:
public class ArcStateMachine: MassTransitStateMachine<ArcProcess>
{
static ArcStateMachine()
{
MessageContracts.Initialize();
}
public ArcStateMachine()
{
InstanceState(x => x.CurrentState);
Initially(
When(ProcessingStartedEvent)
.Then(context =>
{
Console.WriteLine(">> ProcessingStartedEvent");
context.Instance.ActivationId = context.Data.ActivationId;
})
.TransitionTo(ProcessingStartedState));
During(ProcessingStartedState,
When(ReconstructionFinishedEvent)
.Then(context =>
{
Console.WriteLine(">> ReconstructionFinishedEvent");
context.Instance.ActivationId = context.Data.ActivationId;
})
.Publish(context =>
{
return context.Init<ProcessingFinishedMessage>(new { ActivationId = context.Data.ActivationId });
})
.TransitionTo(ProcessingFinishedState)
.Finalize());
}
public State ProcessingStartedState { get; }
public State ReconstructionStartedState { get; }
public State ReconstructionFinishedState { get; }
public State ProcessingFinishedState { get; }
public Event<StartProcessingMessage> ProcessingStartedEvent { get; }
public Event<ReconstructionStartedMessage> ReconstructionStartedEvent { get; }
public Event<ReconstructionFinishedMessage> ReconstructionFinishedEvent { get; }
public Event<ProcessingFinishedMessage> ProcessingFinishedEvent { get; }
}
And the setup for MassTransit looks the following:
var rabbitHost = Configuration["RABBIT_MQ_HOST"];
if (rabbitHost.IsNotEmpty())
{
services.AddMassTransit(cnf =>
{
var connectionString = Configuration["MONGO_DB_CONNECTION_STRING"];
var machine = new ArcStateMachine();
var repository = MongoDbSagaRepository<ArcProcess>.Create(connectionString,
"mongoRepo", "WorkflowState");
cnf.AddConsumer(typeof(StartReconstructionConsumer));
cnf.AddConsumer(typeof(ProcessingFinishedConsumer));
cnf.UsingRabbitMq((context, cfg) =>
{
cfg.Host(new Uri(rabbitHost), hst =>
{
hst.Username("guest");
hst.Password("guest");
});
cfg.ConfigureEndpoints(context);
cfg.ReceiveEndpoint(BusConstants.SagaQueue,
e => e.StateMachineSaga(machine, repository));
});
});
services.AddMassTransitHostedService();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "MyApp", Version = "v1" });
});
}
I have several questions about it:
When actually the event is published as a result of publishing a message? I.e. in my example await _BusInstance.Bus.Publish<StartProcessingMessage>(new { ActivationId = id }); is called from a WebApi which is consumed by StartReconstructionConsumer but when actually the state machine starts to act with Initially(When(ProcessingStartedEvent)...?
My processing should ensure I'm already in the ProcessingStartedState state in order to During(ProcessingStartedState, When(ReconstructionFinishedEvent)... to act correctly. So how do I ensure that my consumer that fires upon receive of StartProcessingMessage can publish the ReconstructionFinishedMessage that should initiate that During? Am I building the messages exchange correctly?
Currently for the await context.Publish<ReconstructionFinishedMessage>(new { ActivationId = activationId }); I get an exception in the logs that states R-FAULT rabbitmq://localhost/saga.service d4070000-7b3b-704d-0f10-08d99942c959 Nanox.GC.Shared.AppCore.Messages.ReconstructionFinishedMessage ReconCaller.Saga.ArcProcess(00:00:04.1132604) while that guid in the message is actually the MessageId. And my message in the rabbitmq is routed to saga.service_error with an exception Messages types must not be System types: System.Threading.Tasks.Task<Nanox.GC.Shared.AppCore.Messages.ProcessingFinishedMessage> (Parameter 'T').
It seems like I'm missing here really big..
My intent is to initiate processing that will have several stages processed by a few consumers sequentially. So here I tried to build a simple StateMachine that starts whenever someone called StartProcessing, then each consumer will do its job and fire the FinishedStepX which will promote the state machine to a new step and initiate the next consumer up until all the processing is done and the state machine will report ProcessingComplete.
Thanks for any help n advance
First, your bus configuration is a bit strange, so I've cleaned that up:
services.AddMassTransit(cnf =>
{
var connectionString = Configuration["MONGO_DB_CONNECTION_STRING"];
cfg.AddSagaStateMachine<ArcStateMachine, ArcProcess>()
.Endpoint(e => e.Name = BusConstants.SagaQueue)
.MongoDbRepository(connectionString, r =>
{
r.DatabaseName = "mongoRepo";
r.CollectionName = "WorkflowState";
});
cnf.AddConsumer<StartReconstructionConsumer>();
cnf.AddConsumer<ProcessingFinishedConsumer>();
cnf.UsingRabbitMq((context, cfg) =>
{
cfg.Host(new Uri(rabbitHost), hst =>
{
hst.Username("guest");
hst.Password("guest");
});
cfg.ConfigureEndpoints(context);
});
});
And the publish issue is related to the method being used, only PublishAsync allows the use of message initializers:
During(ProcessingStartedState,
When(ReconstructionFinishedEvent)
.Then(context =>
{
Console.WriteLine(">> ReconstructionFinishedEvent");
context.Instance.ActivationId = context.Data.ActivationId;
})
.PublishAsync(context =>
{
return context.Init<ProcessingFinishedMessage>(new { ActivationId = context.Data.ActivationId });
})
.TransitionTo(ProcessingFinishedState)
.Finalize());
That should sort you out.
With the generous help of #Chris Patterson the working solution would be:
Definitions:
[BsonIgnoreExtraElements]
public class ArcProcess : SagaStateMachineInstance, ISagaVersion
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; }
public int Version { get; set; }
public Guid ActivationId { get; set; }
}
public interface StartProcessingMessage
{
Guid ActivationId { get; }
}
public interface ProcessingFinishedMessage
{
Guid ActivationId { get; }
}
public static class MessageContracts
{
static bool _initialized;
public static void Initialize()
{
if (_initialized)
return;
GlobalTopology.Send.UseCorrelationId<StartProcessingMessage>(x => x.ActivationId);
GlobalTopology.Send.UseCorrelationId<ProcessingFinishedMessage>(x => x.ActivationId);
_initialized = true;
}
}
Consumers:
public class StartProcessingConsumer : IConsumer<StartProcessingMessage>
{
readonly ILogger<StartProcessingConsumer> _Logger;
private readonly int _DelaySeconds = 5;
public StartProcessingConsumer(ILogger<StartProcessingConsumer> logger)
{
_Logger = logger;
}
public async Task Consume(ConsumeContext<StartProcessingMessage> context)
{
var activationId = context.Message.ActivationId;
_Logger.LogInformation($"Received Scan: {activationId}");
await Task.Delay(_DelaySeconds * 1000);
_Logger.LogInformation($"Finish Scan: {activationId}");
await context.Publish<ProcessingFinishedMessage>(new { ActivationId = activationId });
}
}
public class ProcessingFinishedConsumer : IConsumer<ProcessingFinishedMessage>
{
readonly ILogger<ProcessingFinishedConsumer> _Logger;
public ProcessingFinishedConsumer(ILogger<ProcessingFinishedConsumer> logger)
{
_Logger = logger;
}
public async Task Consume(ConsumeContext<ProcessingFinishedMessage> context)
{
_Logger.LogInformation($"Finish {context.Message.ActivationId}");
await Task.CompletedTask;
}
}
StateMachine definition:
public class ArcStateMachine: MassTransitStateMachine<ArcProcess>
{
static ArcStateMachine()
{
MessageContracts.Initialize();
}
public ArcStateMachine()
{
InstanceState(x => x.CurrentState);
Initially(
When(ProcessingStartedEvent)
.Then(context =>
{
context.Instance.ActivationId = context.Data.ActivationId;
})
.TransitionTo(ProcessingStartedState));
During(ProcessingStartedState,
When(ProcessingFinishedEvent)
.Then(context =>
{
context.Instance.ActivationId = context.Data.ActivationId;
})
.Finalize());
}
public State ProcessingStartedState { get; }
public State ProcessingFinishedState { get; }
public Event<StartProcessingMessage> ProcessingStartedEvent { get; }
public Event<ProcessingFinishedMessage> ProcessingFinishedEvent { get; }
}
MassTransit setup:
var rabbitHost = Configuration["RABBIT_MQ_HOST"];
if (rabbitHost.IsNotEmpty())
{
services.AddMassTransit(cnf =>
{
var connectionString = Configuration["MONGO_DB_CONNECTION_STRING"];
cnf.AddSagaStateMachine<ArcStateMachine, ArcProcess>()
.Endpoint(e => e.Name = BusConstants.SagaQueue)
.MongoDbRepository(connectionString, r =>
{
r.DatabaseName = "mongoRepo";
r.CollectionName = "WorkflowState";
});
cnf.AddConsumer(typeof(StartProcessingConsumer));
cnf.AddConsumer(typeof(ProcessingFinishedConsumer));
cnf.UsingRabbitMq((context, cfg) =>
{
cfg.Host(new Uri(rabbitHost), hst =>
{
hst.Username("guest");
hst.Password("guest");
});
cfg.ConfigureEndpoints(context);
});
});
services.AddMassTransitHostedService();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "MyApp", Version = "v1" });
});
}
This example helped me a lot in understanding how the basics of MassTrasit work.

How to receive integration event from RabbitMQ broker using MassTransit?

I am trying to receive an event from the RabbitMQ broker but something wents wrong, the Consume method of my consumer is never called, although the message is visible on the bus. Here's my IntegrationEvent class:
public abstract class IntegrationEvent
{
protected IntegrationEvent(Guid entityId,
string eventType)
{
EntityId = entityId;
EventType = eventType;
}
public Guid Id { get; } = Guid.NewGuid();
public DateTime CreatedAtUtc { get; } = DateTime.UtcNow;
public Guid EntityId { get; }
public string EventType { get; }
public DateTime? PublishedAtUtc { get; set; }
}
And the example inheritor:
public sealed class UserCreatedIntegrationEvent : IntegrationEvent
{
public UserCreatedIntegrationEvent(Guid id,
string login,
string firstName,
string lastName,
string mailAddress)
: base(id,
nameof(UserCreatedIntegrationEvent))
{
Login = login;
FirstName = firstName;
LastName = lastName;
MailAddress = mailAddress;
}
public string Login { get; }
public string FirstName { get; }
public string LastName { get; }
public string MailAddress { get; }
}
Publication logic:
public async Task PublishAsync(params IntegrationEvent[] events)
{
var globalPublicationTasks = events
.Select(#event =>
{
#event.PublishedAtUtc = DateTime.UtcNow;
return _publishEndpoint.Publish(#event);
});
await Task.WhenAll(globalPublicationTasks);
}
Receiver classes and the dependencies registry code:
public sealed class IntegrationEventListener : BackgroundService
{
public IntegrationEventListener(IBusControl busControl,
IServiceProvider serviceProvider,
IOptions<RabbitMQSettings> busConfiguration)
: base(busControl,
serviceProvider,
busConfiguration,
NullLogger.Instance)
{
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
var handler = BusControl
.ConnectReceiveEndpoint(BusConfiguration.HostName, receiveEndpointConfigurator =>
{
receiveEndpointConfigurator
.Consumer<IntegrationEventTransmitter>(ServiceProvider);
});
await handler.Ready;
}
catch (Exception e)
{
...
}
}
}
public sealed class IntegrationEventTransmitter : IntegrationEventHandler<IntegrationEvent>
{
public override async Task HandleAsync(IntegrationEvent #event)
{
throw new System.NotImplementedException();
}
}
public abstract class IntegrationEventHandler<TIntegrationEvent>
: IIntegrationEventHandler<TIntegrationEvent>,
IConsumer<TIntegrationEvent>
where TIntegrationEvent : IntegrationEvent
{
public async Task Consume(ConsumeContext<TIntegrationEvent> context) =>
await HandleAsync(context.Message);
public abstract Task HandleAsync(TIntegrationEvent #event);
}
...
.AddRabbitMQ(configuration,
ExchangeType.Fanout,
true)
.AddScoped<IntegrationEventTransmitter>()
.AddHostedService<IntegrationEventListener>();
...
internal static IServiceCollection RegisterRabbitMQDependencies(
this IServiceCollection services,
IConfiguration configuration,
string exchangeType)
{
var rabbitMQSettings = configuration
.GetSection(RabbitMQSettingsSectionKey)
.Get<RabbitMQSettings>();
services
.AddMassTransit(configurator =>
{
configurator.AddConsumers(typeof(IntegrationEventHandler<IntegrationEvent>).Assembly);
})
.AddSingleton(serviceProvider => MassTransit.Bus.Factory.CreateUsingRabbitMq(configurator =>
{
configurator
.Host(rabbitMQSettings.HostName,
rabbitMQSettings.VirtualHostName,
hostConfigurator =>
{
hostConfigurator.Username(rabbitMQSettings.UserName);
hostConfigurator.Password(rabbitMQSettings.Password);
});
configurator.ExchangeType = exchangeType;
}))
.AddSingleton<IPublishEndpoint>(provider => provider.GetRequiredService<IBusControl>())
.AddSingleton<ISendEndpointProvider>(provider => provider.GetRequiredService<IBusControl>())
.AddSingleton<IBus>(provider => provider.GetRequiredService<IBusControl>())
.Configure<RabbitMQSettings>(configuration.GetSection(RabbitMQSettingsSectionKey));
return services;
}
In the RabbitMQ management panel i can notice that message is being properly published on the bus, the consumer is also connected to the broker but for some reason it does not consume the message. What am i doing wrong?
You should not connect a receiving endpoint, as it's completely unnecessary in this case. As Chris mentioned, configuring MassTransit for ASP.NET Core is properly described in the documentation, and it makes total sense to follow the documentation to avoid unnecessary complexity.
In your particular case, you don't start the bus, although it's even mentioned in the Common Mistakes article as the first thing.
Just do the following:
Use AddMassTransit in Startup and configure the receive endpoint normally
Add the handler directly there, or use a consumer class instead. It does not need to be a background service, MassTransit will call it when it receives a message
Register the MassTransit host by calling AddMassTransitHostedService

ASP.NET5 Dependency Injection into a none controller class

I'm struggling to get dependency injection working in a none controller class in ASP.NET 5.
I'm trying to inject an instance of IHelloMessage in ResponseWriter.
I have the following code in startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IHelloMessage, HelloMessage>();
}
public void Configure(IApplicationBuilder app)
{
app.Run(async context =>
{
await Task.Run(() =>
{
new ResponseWriter().Write(context);
});
});
}
I have the following code in ResponseWriter.cs:
public class ResponseWriter
{
public IHelloMessage HelloMessage { get; set; }
public ResponseWriter()
{
}
public ResponseWriter(IHelloMessage helloMessage)
{
HelloMessage = helloMessage;
}
public void Write(HttpContext HttpContext)
{
HttpContext.Response.WriteAsync(HelloMessage.Text);
}
}
and here's the code for HelloMessage:
public interface IHelloMessage
{
string Text { get; set; }
}
public class HelloMessage : IHelloMessage
{
public string Text { get; set; }
public HelloMessage()
{
Text = "Hello world at " + DateTime.Now.ToString();
}
}
When I run the app, I get the following error:
I'm sure I'm missing something silly - any help would be appreciated!
You are calling your parameter less constructor: new ResponseWriter().Write(context); so your HelloMessage is null.
If you want to use dependency injection you must use IAppBuilder.ApplicationService.GetService or IAppBuilder.ApplicationService.GetRequiredService methods
Your statup.cs can be:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IHelloMessage, HelloMessage>();
}
public void Configure(IApplicationBuilder app)
{
app.Run(async context =>
{
await Task.Run(() =>
{
new ResponseWriter(app.ApplicationServices.GetRequiredService<IHelloMessage>()).Write(context);
});
});
}

Categories