2

I have two types of events :

  • PersonChanged
  • PersonAddressChanged.

Both of them are being published also when a new Person (and a new address) is created (kind of create or update).

When a new person is created two events are published: PersonChanged and PersonAddressChanged (in that order). However, because NServiceBus is asynchronous they can be handled in any order. And when an address is changed (for an existing person) there is no PersonChanged event, only PersonAddressChanged event.

I want to write a handler for PersonAddressChanged event which will :

  1. Check if the person is in database
  2. If yes then just make an update
  3. If no then start saga and wait for PersonChanged event (assuming it is a new person)

And in PersonChanged event I need to insert the person to database, find the saga and run the handler for PersonAddressChanged once again.

Can I achieve this with NServiceBus Sagas? I cannot assume that the message processing should be in order PersonChanged → PersonAddressChanged because sometimes there will be no PersonChanged event for any particular address change.

Amith
  • 6,818
  • 6
  • 34
  • 45
bpiec
  • 1,561
  • 3
  • 23
  • 38

3 Answers3

2

You could do this with sagas, yes, although you probably shouldn't. First of, having the same event for both creation and updating the person loses semantic information in the event (which is what you are trying to recreate by checking for the user in the database). You also expose yourself to a lot of potential race conditions, and need to think about how to handle them all. My suggestion would be to rework your message flow, that will probably make your current problem go away.

But, if you want to try it anyway, you'd do something like this: (not tested or compiled, just conceptual code)

public class MySaga : Saga<MySagaDataType>, IAmStartedByMessage<PersonChanged>, IAmStartedByMessage<PersonAddressChanged> {
    public override void ConfigureHowToFindSaga() {
        // How to correlate PersonChanged and PersonAddressChanged messages
    }
    public void Handle<PersonChanged>(PersonChanged msg) {
        Insert(msg);
        if(Data.PendingAddressChange != null) 
            UpdateDatabase(msg);
        MarkAsComplete();
    }
    public void Handle<PersonAddressChanged>(PersonAddressChanged msg) {
        if(IsInDatabase(msg.Person)) {
            UpdateDatabase(msg);
            MarkAsComplete();
        }
        else {
            Data.PendingAddressChange = msg;
        }
    }
}
public class MySagaDataType : IContainSagaData {
    public PersonAddressChanged PendingAddressChange { get; set; }
}
bpiec
  • 1,561
  • 3
  • 23
  • 38
carlpett
  • 12,203
  • 5
  • 48
  • 82
  • I like this solution but I get following error: `NHibernate.Event.Default.AbstractFlushingEventListener - Could not synchronize database state with session NHibernate.PropertyValueException: Error dehydrating property value for Namespace.PersonSaga.PendingAddressChanged ---> System.IndexOutOfRangeException: Invalid index 29 for this SqlParameterCollection with Count=29`. Any ideas? – bpiec Mar 05 '14 at 08:45
  • @bpiec: Are your events implemented as interfaces or concrete classes? NServiceBus generates proxy classes around interfaces, which NHibernate might not like. Try replacing the `PersonAddressChanged` property with the contents of that class, instead (ie if `class PersonAddressChanged { public string Address { get; set; } public Guid PersonId { get; set; } }`, then move those two parameters into `MySagaDataType` and set them separately instead) – carlpett Mar 05 '14 at 09:12
  • I added several properties to my Saga class, each of them contains properties of the same name and NSB generates wrong tables in SQL Server and then cannot persist sagas :( Nevermind, I will try to find a workaround, but the answer is what I was looking for! – bpiec Mar 05 '14 at 09:35
2

I'd agree with @carlpett's statement about rethinking some of the semantics around your event.

I'd also suggest you consider a different solution than a saga here.

What about if your PersonAddressChanged message handler, when it sees that there is no current person in the database, just goes and creates one with the address its got?

Udi Dahan
  • 11,932
  • 1
  • 27
  • 35
1

If PersonAddressChanged events are only sent for an existing person, I would not bother with sagas at all.

Instead, I'd rely on retry logic to process messages that arrive out of order.

Any time the PersonAddressChanged is processed, it could assume that a person record exists. If it doesn't (ie messages are being processed out of order) the handler throws an exception and is retried later.

By the time it is reprocessed again, the PersonChanged event has been processed and all is good.

Chris Bednarski
  • 3,364
  • 25
  • 33