I've been spinning my wheels trying to get the MassTransitStateMachine working and I seem to not understand exactly how this is supposed to work.
The error I'm receiving (full code below) is PayloadNotFoundException when I attempt to call stateMachine.RaiseEvent(instance, stateMachine.DeployApplicationRequest, request) combined with the .Send() Activity in the state machine. I've been trying to trace this through but have gotten nowhere.
Here's the relevant code and explanation of intent:
I receive a request to deploy an application via a webAPI post, and convert the request to a DeployApplicationRequest.
public record DeployApplicationRequest
{
// State machine properties
public Guid CorrelationId { get; init; }
public ChatApplication ChatApplication { get; init; }
public string ChannelId { get; init; }
public string UserId { get; init; }
// Command
public DeployApplication DeployApplication { get; init; }
}
public enum ChatApplication
{
Slack,
Teams
}
This request encapsulates two things: The state that the API needs to maintain so it knows how to route the results back to the requester (state machine properties) and the Command to be sent to the service bus.
The command, DeployApplication, looks like this:
public record DeployApplication
{
public Guid CorrelationId { get; init;}
public string InitiatedBy { get; init; }
public DateTime CreatedDate { get; init; }
public string Environment { get; init; }
public string PullRequestId { get; init; }
}
I have created a state instance to preserve the details of the request (such as whether it came in via Slack or Teams). I do not want to publish this information to the bus:
public class DeployApplicationState : SagaStateMachineInstance
{
public Guid CorrelationId { get; set; }
public string CurrentState { get; set; }
public ChatApplication ChatApplication { get; set; }
public string ChannelId { get; set; }
public string UserId { get; set; }
}
And I have created a StateMachine to handle this:
public class DeployApplicationStateMachine : MassTransitStateMachine<DeployApplicationState>
{
private readonly ILogger<DeployApplicationStateMachine> logger;
public DeployApplicationStateMachine(ILogger<DeployApplicationStateMachine> logger)
{
this.logger = logger;
InstanceState(x => x.CurrentState);
Event(() => DeployApplicationRequest, x => x.CorrelateById(context => context.Message.CorrelationId));
Initially(
When(DeployApplicationRequest)
.Then(x => {
x.Instance.CorrelationId = x.Data.CorrelationId;
x.Instance.ChannelId = x.Data.ChannelId;
x.Instance.ChatApplication = x.Data.ChatApplication;
x.Instance.UserId = x.Data.UserId;
})
.Send(context => context.Init<DeployApplication>(context.Data.DeployApplication))
.TransitionTo(Submitted));
}
public Event<DeployApplicationRequest> DeployApplicationRequest { get; private set; }
public State Submitted { get; private set; }
}
To trigger the initial event (since the request is not coming in via a Consumer but rather through a controller), I have injected the state machine into the MassTransit client, and I'm calling the RaiseEvent method:
public class MassTransitDeployClient : IDeployClient
{
private readonly DeployApplicationStateMachine stateMachine;
public MassTransitDeployClient(DeployApplicationStateMachine stateMachine)
{
this.stateMachine = stateMachine;
}
public async Task Send(DeployApplicationRequest request)
{
var instance = new DeployApplicationState
{
CorrelationId = request.CorrelationId,
ChannelId = request.ChannelId,
ChatApplication = request.ChatApplication,
UserId = request.UserId
};
await stateMachine.RaiseEvent(instance, stateMachine.DeployApplicationRequest, request);
// This works for sending to the bus, but I lose the state information
//await sendEndpointProvider.Send(request.DeployApplication);
}
}
And the container configuration is as follows:
services.AddMassTransit(x =>
{
x.AddSagaStateMachine<DeployApplicationStateMachine, DeployApplicationState>()
.InMemoryRepository();
x.UsingInMemory((context, cfg) =>
{
cfg.ConfigureEndpoints(context);
});
});
EndpointConvention.Map<DeployApplication>(new Uri($"queue:{typeof(DeployApplication).FullName}"));
services.AddMassTransitHostedService();
Raising the event works just fine, and the state machine transfers to Submitted successfully if I do not include .Send(...) in the state machine. The second I introduce that Activity I get the PayloadNotFoundException. I've tried about 15 different ways of doing this over the weekend with no success, and I'm hoping someone may be able to help me see the error of my ways.
Thanks for reading! Fantastic library, by the way. I'll be looking for ways to contribute to this going forward, this is one of the most useful libraries I've come across (and Chris your Youtube videos are excellent).
Saga state machines are not meant to be invoked directly from controllers. You should send (or publish) a message that will then be dispatched to the saga via the message broker.
IF you want to do it that way, you can use MassTransit Mediator instead, but you'll still be sending a message via Mediator to the saga, which will then be handled by MassTransit.
TL;DR - you don't use RaiseEvent with saga state machines.
Related
Full code (github)...
I'm moving from MediatR to MassTransit to publish my domain events to a queue.
I'm using an interface IDomainEvent in different domain events that implement such interface (in this case PersonCreated and PersonPositionCreated). Then I have an entity 'Person' with a list of IDomainEvent in which I register all the domain events occurred. I Also have a consumer for each specific event.
After persist my entity, I want to iterate all the events of the entity and publish them to the queue.
// Event interface.
public class IDomainEvent
{
}
// Events.
public class PersonCreated : IDomainEvent
{
public int Id { get; set; }
}
public class PersonPositionCreated : IDomainEvent
{
public string Position { get; set; }
}
// Entity.
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string Position { get; set; }
public List<IDomainEvent> Events { get; set; };
}
// Consumers.
public class PersonCreatedConsumer : IConsumer<PersonCreated>
{
public Task Consume(ConsumeContext<PersonCreated> context)
{
Debug.Print(context.Message.Id.ToString());
return Task.CompletedTask;
}
}
public class PersonPositionCreatedConsumer : IConsumer<PersonPositionCreated>
{
public Task Consume(ConsumeContext<PersonPositionCreated> context)
{
Debug.Print(context.Message.Position);
return Task.CompletedTask;
}
}
// My command.
//...
// Creates Person.
Person person = new Person(){ Id = 1, Name = "Alice", Position = "Developer" };
// Registers the events to the list.
person.Events.Add(new PersonCreated() { Id = person.Id });
person.Events.Add(new PersonPositionCreated() { Position = person.Position });
foreach (IDomainEvent personEvent in person.Events)
{
// This way, it publish an IDomainEvent and I don't want to use a IConsumer<IDoaminEvent> because I need specific consumers.
// How can I tell the bus that I sending the specific event and not the IDomainEvent?
//(I know that inside the iteration I'm dealing with IDomainEvent but it have access to the class that implement the interface).
// NOTE: That way works with MediatR specific handlers.
| |
\ /
\ /
\/
_bus.Publish(personEvent);
}
// Of course this two lines works!
//_bus.Publish<PersonCreated>(new PersonCreated() { Id = 1 });
//_bus.Publish<PersonPositionCreated>(new PersonPositionCreated() { Position = "Developer" });
//...
How can I tell the bus that I am sending the specific event and not the IDomainEvent?
(I know that inside the iteration I'm dealing with IDomainEvent, but it has access to the class that implement the interface).
You should call:
_bus.Publish((object)personEvent);
By casting it to an object, MassTransit will call GetType() and then publish to the exchange (and ultimately consumer) for the object type (aka, implemented message type).
My CQRS file layout is as in the picture. Whenever I enable the handler inside the GetAllBooks folder, I get an error.
Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: MediatR.IRequestHandler2[BookAPI.Application.Features.Queries.Book.GetAllBooks.GetAllBookQueryRequest,System.Collections.Generic.List1[BookAPI.Application.Features.Queries.Book.GetAllBooks.GetAllBookQueryResponse]] Lifetime: Transient ImplementationType: BookAPI.Application.Features.Queries.Book.GetAllBooks.GetAllBookQueryHandler': Unable to resolve service for type 'BookAPI.Application.Repositories.IBookReadRepository' while attempting to activate 'BookAPI.Application.Features.Queries.Book.GetAllBooks.GetAllBookQueryHandler'.)
GetAllBookQueryHandler
public class GetAllBookQueryHandler : IRequestHandler<GetAllBookQueryRequest, List<GetAllBookQueryResponse>>
{
private IBookReadRepository bookReadRepository;
public GetAllBookQueryHandler(IBookReadRepository bookReadRepository)
{
this.bookReadRepository = bookReadRepository;
}
public async Task<List<GetAllBookQueryResponse>> Handle(GetAllBookQueryRequest request, CancellationToken cancellationToken)
{
List<B.Book> books = bookReadRepository.GetAll().Include(x=>x.Authors).Include(x=>x.Category).Include(x=>x.BookImages).ToList();
List<GetAllBookQueryResponse> responses = new();
foreach (B.Book book in books)
{
responses.Add(
new GetAllBookQueryResponse
{
Name= book.Name,
CategoryName=book.Category.Name,
AuthorName=book.Authors.First().Name,
Img=book.BookImages.First().Path,
UnitPrice=book.UnitPrice,
}
);
}
return responses;
}
}
GetAllBookQueryRequest
public class GetAllBookQueryRequest : IRequest<List<GetAllBookQueryResponse>>
{
//This place is empty as all books are requested
}
GetAllBookQueryResponse
public class GetAllBookQueryResponse
{
public int Id { get; set; }
public string Name { get; set; }
public string CategoryName { get; set; }
public string AuthorName { get; set; }
public string Img { get; set; }
public ushort UnitPrice { get; set; }
}
ServiceRegistration for IoC
using MediatR;
using Microsoft.Extensions.DependencyInjection;
namespace BookAPI.Application
{
public static class ServiceRegistration
{
public static void AddApplicationServices(this IServiceCollection services)
{
//find all handler, request and response and add IoC
services.AddMediatR(typeof(ServiceRegistration));
services.AddHttpClient();
}
}
}
Program.cs
I add services
builder.Services.AddApplicationServices();
Book Controller
.
.
.
readonly IMediator mediator;
public BookController(IBookWriteRepository bookWriteRepository, IWebHostEnvironment webHostEnvironment, IFileService fileService, IMediator mediator)
{
bookWriteRepository = bookWriteRepository;
_fileService = fileService;
this.mediator = mediator;
}
[HttpGet]
public async Task<IActionResult> GetAllBooks([FromQuery] GetAllBookQueryRequest getAllBookQueryRequest)
{
return Ok(await mediator.Send(getAllBookQueryRequest));
}
.
.
.
I guess it doesn't see the service I introduced, but I don't understand why GetAllBookHandler is throwing an error in the operation and not the others. For example, my handlers that list and create customers are working.
I was stuck here with a problem. I am using the publish topology of MassTransit, and I did NOT start the bus after configuring it because I saw from somewhere(forgot it, sorry) saying that the bus could publish messages without starting it.
It worked fine publishing messages. So I want to ask, should I stop the bus after publishing under the condition that I did not start it?
And if yes, what would be the benefits of stopping it rather than leaving it?
Here is my Publisher class:
public class MessagePublisher<TMessage> : IPublisher<TMessage> where TMessage : class
{
private readonly IBusControl _busConsControl;
public MessagePublisher(BusConfigurationDto config)
{
_busConsControl = config.CreateBus();
}
public async Task PublishMessageAsync(TMessage message)
{
await _busConsControl.Publish(message);
}
}
And BusConfigurationDto Class:
public class BusConfigurationDto
{
public string Host { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public string ExchangeName { get; set; }
public string QueueName { get; set; }
public IBusControl CreateBus()
{
IBusControl bus = Bus.Factory.CreateUsingRabbitMq(cfg =>
{
IRabbitMqHost host = cfg.Host(new Uri(Host), hostConfigurator =>
{
hostConfigurator.Username(UserName);
hostConfigurator.Password(Password);
});
cfg.ReceiveEndpoint(host, QueueName, ep =>
{
ep.Bind(ExchangeName, s => { s.Durable = true; });
});
});
return bus;
}
}
Appreciate if someone proficient could help, many thanks!
You should Start the bus at application startup, and Stop it when your application exits. Otherwise, you won't know if the configuration is right, and if RabbitMQ is available, until sometime much later when your publisher attempts to publish a message. It's also expensive to start/stop the bus every time you publish a message, which is why keeping it up and running is recommended.
I am creating a web api project and want to add validation to my model, so I added a DataAnnotation attribute.
I then tested the project by trying to pass my object from a separate mvc project. I recieved a 500 server error.
Removing the DataAnnotation allows me to pass the object successfully. Why?
I have looked at a couple of tutorials such as this and this, they show how to handle validation errors, but this has not helped.
UPDATE
Removing the data annotation from MyProperty in Class1, solution B (but leaving it on the class in solution A) means values can be passed successfully! Is this a problem with deserilazing the object? If so how do I solve it?
B = My web service reciving the object
A = My mvc project sending the object
my code to send the resquest
public class Class1
{
[Required(ErrorMessage = "MyProperty value is required")]//remove this line to make it work
public int MyProperty { get; set; }
public Class2 MyOtherProperty { get; set; }
}
public class Class2
{
public string SomeProperty { get; set; }
}
async Task<string> Test2()
{
var form = new Class1();
form.MyProperty = 123;
form.Class2 = new Class2();
using (HttpClient client = new HttpClient())
{
client.BaseAddress = new Uri("http://localhost:58814/api/");
var post = await client.PostAsJsonAsync<Class1>("Values", form);
var putt = await client.PutAsJsonAsync<Class1>("Values", form);
}
return "";
}
my code to recieve the request (the breakpoint applied is not being hit)
// POST api/values
public void Post([FromBody]Class1 value)
{
}
// PUT api/values/5
public void Put([FromBody]Class1 value)
{
}
I have resolve the issue thanks to the advice from this link - Scott Hanselman's blog
My sending project and receiving project had identical classes (including data annotations). Removing the annotations from the class in the receiving project resolved the binding issue but I still needed validation on the properties. Also changing the annotation from required to Range also resolved the binding issue (I only mention these as they were some steps I took to try and debug).
The final solution for me was to change this
public class Network
{
[Required(ErrorMessage = "NetworkID is required")]
public int NetworkID { get; set; }
}
to this
[DataContract]
public class Network
{
[DataMember(IsRequired = true)]
public int NetworkID { get; set; }
}
I am a newbie to with unity and unit of work pattern and I am trying to write a code, which connects to my webservice and does all the work.
Everything goes well until I use the Database but I get lost when I try to use the webservice.
I have wasted my 2 precious days, searching every single possible article related to it and applying it to my code, but no luck till date.
I know, by writing connection string to web.config and calling it in dbcontext class controller will connect to the required database, but I am not connecting to any database, so what changes I need to do in web/app.config. Also, even if I write my connection logic in dbcontext constructor, it still searches and fills the dbcontext with sql server details. I presume thats happening because I am using DBSet.
Guys, you are requested to have a look at my code, I have done and show me some hope that I can do it. Let me know, if you want any other info related to the code that you want to see.
thanks
DBCONTEXT
public class CVSContext : DbContext
{
public DbSet<CVSViewModel> CVS { get; set; }
public DbSet<Contact> Contacts { get; set; }
public DbSet<Account> Accounts { get; set; }
public CVSContext()
{
//CRM Start
var clientCredentials = new System.ServiceModel.Description.ClientCredentials();
clientCredentials.UserName.UserName = "";
clientCredentials.UserName.Password = "";
var serviceProxy = new Microsoft.Xrm.Sdk.Client.OrganizationServiceProxy(new Uri("http://Organization.svc"), null, clientCredentials, null);
serviceProxy.ServiceConfiguration.CurrentServiceEndpoint.Behaviors.Add(new ProxyTypesBehavior());
HttpContext.Current.Session.Add("ServiceProxy", serviceProxy);
//CRM End
}
}
GENERIC REPOSITORY
public class GenericRepository<TEntity> where TEntity : class
{
internal CVSContext context;
internal DbSet<TEntity> dbSet;
public GenericRepository(CVSContext context)
{
this.context = context;
this.dbSet = context.Set<TEntity>();
}
}
UNIT OF WORK
public interface IUnitOfWork : IDisposable
{
int SaveChanges();
}
public interface IDALContext : IUnitOfWork
{
ICVSRepository CVS { get; set; }
IContactRepository Contacts { get; set; }
//IAccountRepository Accounts { get; set; }
}
public class DALContext : IDALContext
{
private CVSContext dbContext;
private ICVSRepository cvs;
private IContactRepository contacts;
// private IAccountRepository accounts;
public DALContext()
{
dbContext = new CVSContext();
}
public ICVSRepository CVS
{
get
{
if (cvs == null)
cvs = new CVSRepository(dbContext);
return cvs;
}
set
{
if (cvs == value)
cvs = value;
}
}
public IContactRepository Contacts
{
get
{
if (contacts == null)
contacts = new ContactRepository(dbContext);
return contacts;
}
set
{
if (contacts == value)
contacts = value;
}
}
public int SaveChanges()
{
return this.SaveChanges();
}
public void Dispose()
{
if(contacts != null)
contacts.Dispose();
//if(accounts != null)
// accounts.Dispose();
if(dbContext != null)
dbContext.Dispose();
GC.SuppressFinalize(this);
}
}
SERVICE
public interface ICVSService
{
Contact CreateContact(Guid contactName, string productName, int price);
List<CVSViewModel> GetCVS();
List<Contact> GetContacts();
List<Account> GetAccounts();
}
public class CVSService : ICVSService, IDisposable
{
private IDALContext context;
public CVSService(IDALContext dal)
{
context = dal;
}
public List<CVSViewModel> GetCVS()
{
return context.CVS.All().ToList();
}
public List<Contact> GetContacts()
{
return context.Contacts.All().ToList();
}
public List<Account> GetAccounts()
{
return context.Accounts.All().ToList();
}
public Contact CreateContact(Guid contactName, string accountName, int price)
{
var contact = new Contact() { ContactId = contactName };
var account = new Account() { ContactName = accountName, Rent = price, Contact = contact };
//context.Contacts.Create(contact);
context.SaveChanges();
return contact;
}
public void Dispose()
{
if (context != null)
context.Dispose();
}
}
CONTROLLER
public ActionResult Index()
{
ViewData.Model = service.GetContacts();
return View();
}
It's all about proper abstractions. The common abstraction that is used between some data source (could be a db or ws) is the Repository pattern, or at a higher level the Unit of Work pattern. In fact Entity Framework DbContext is an implementation of the Unit of Work pattern, but it is tailored for databases. You can't use to communicate with a web service.
In that case you will have to write your own IRepository<T> abstraction and have a database specific implementation that uses a DbContext under the covers and a web service specific implementation that wraps a web service client proxy under the covers.
However, when your application gets more complex, you often find yourself wanting to have some sort of transaction like behavior. This is what the Unit of Work pattern if for: it presents a business transaction. Using the unit of work pattern to wrap multiple WS calls however, will get painful very soon. It's a lot of work to get right and in that case you will be much better of using a message based architecture.
With a message based architecture you define a single atomic operation (a business transaction or use case) as a specific message, for instance:
public class MoveCustomerCommand
{
public int CustomerId { get; set; }
public Address NewAddress { get; set; }
}
This is just an object (DTO) with a set of properties, but without behavior. Nice about this is that you can pass these kinds of objects over the wire using WCF or any other technology or process them locally without the need for the consumer to know.
Take a look at this article that describes it in detail. This article builds on top of that model and describes how you can write highly maintainable WCF services using this model.