Masstransit: Add Saga Persistence Configuration to AddMassTransit registration

Created on 3 Dec 2019  路  23Comments  路  Source: MassTransit/MassTransit

Create extension methods for the registration configurator (invoked via the .AddMassTransit container extension method) for the supported saga repositories so that they can easily be configured for the configured sagas (including state machine sagas).

This would configure the persistence components in the container so that they are properly resolved as the endpoints are configured. Should support an easy way to say "for all my sagas, use this" and let the registration sort it out (generics, vs explicit registrations, etc.).

Storage Engines

  • [x] In Memory
  • [x] NHiberate
  • [x] Entity Framework
  • [x] MongoDB
  • [x] Redis
  • [x] Marten
  • [x] DocumentDb
  • [x] Dapper
  • [x] Azure Session State

Documentation

  • [x] Consistent example stage machine and instance, following Automatonymous example
  • [x] Top level introduction to the registration syntax

Bonus Features

  • [ ] Use a bus observer to connect/disconnect database connections for saga repositories when the bus is started/stopped, so that a failing connection _might_ stop the bus from starting.

Repository Changes

Migrate the existing implementations to a new set of saga repository interfaces.

ISagaRepositoryContextFactory<TContext>

Used to created a _SagaRepositoryContext_ for each received message.

ISagaConsumeContextFactory<TContext, TSaga, TMessage>

Used to create a _SagaConsumeContext_ for a saga instance that has been loaded from storage or created by an initiating message.

ISagaRepository<TSaga>

Should remain mostly unchanged, but would have a standard implementation that uses the above interfaces to manage the repository interaction and focus purely on the saga behavior details (and dry up the saga repositories).

ILoadSagaRepository<T> and IQuerySagaRepository<T>

These are now consistent and supported by storage engines that have query and/or load support, and used by the unit testing extensions where available.

future

Most helpful comment

I think it will need to match whatever the built-in support is for the framework. If it's EF, I think something like:

cfg.AddSaga<MySaga>()
    .EntityFrameworkRepository(ef =>
    {
        ef.DbContext<MySagaDbContext>(x => x.UseSqlServer(...))
    });

Basically give the same configuration extensions support that EF uses just start it with a different preample.

All 23 comments

I'd like user to explicitly provide repo for a saga

cfg.AddSaga<Instance>()
    .UseEntityFrameworkRepository<YourDbContext>(repo => 
    {
         repo.ConcurrencyHandlingType = Pessimistic / Optimistic
    });

//or just provide connectionString
cfg.AddSaga<Instance>()
    .UseEntityFrameworkRepository(connectionString, repo => 
    {
         repo.ConcurrencyHandlingType = Pessimistic / Optimistic
    });

cfg.AddSaga<Instance>()
    .UseMongoDbRepository(connectionString, "database", repo => 
    {
         repo.CollectionName = "sagas";
    });
cfg.AddSaga<Instance>()
    .UseMongoDbRepository(IMongoDatabase, repo => 
    {
         repo.CollectionName = "sagas";
    });

if you want to do something generic for all sagas, you can still use the old way :)

I do think we need to support the case where configuration is coming from the container, so the container context is needed in the interfaces/extension methods.

Such as IOptions<DatabaseSettings> being used for connection setup, etc.

Dang it, now I feel like I need a simple dictionary-based configuration "container" so I can use all this nice stuff without an actual container...

just do not create your own DI :D

Oh, gosh no, I would never, ever do that, cough, erm: https://github.com/phatboyg/ServiceBook

And, what about references like ConnectionMultiplexer, or IDocumentClient? These are both disposable, and should be registered in the container so that they're disposed when the container is disposed. And subsequently used when the saga repository needs to create a typed database, etc. I'm wondering how best to handle these things, tbh.

And, seriously, DbContext of all things. Talk about a pain to manage.

Thoughts on this one?

  • Maybe use the SagaConsumeContext to store it as a payload, and use it throughout.
  • If registered, use it as a Scoped dependency so it's available to dependencies resolved from the container?

for EF, would you still use the extension method from microsoft:

services.AddDbContext<DbContext, SampleBatchDbContext>(x => x.UseSqlServer(hostContext.Configuration.GetConnectionString("sample-batch")));
?

outside of the .AddMassTransit? Or is the idea to make this configure option encompass that under the covers?

As long as we make sure that the user isn't forced to .UseSqlServer. Maybe they like UsePgsql or .UseMySql.

I think it will need to match whatever the built-in support is for the framework. If it's EF, I think something like:

cfg.AddSaga<MySaga>()
    .EntityFrameworkRepository(ef =>
    {
        ef.DbContext<MySagaDbContext>(x => x.UseSqlServer(...))
    });

Basically give the same configuration extensions support that EF uses just start it with a different preample.

I really like the way of making DbContext as generic parameter:

cfg.AddSaga<Instance>()
    .UseEntityFrameworkRepository<YourDbContext>(repo => 
    {
         repo.ConcurrencyHandlingType = Pessimistic / Optimistic
    });

so you don't care how context was registered in your DI

Generic parameter is fine, but it can't be like that one. It would have to be like I showed above.

ef.DbContext<MyDbContext>

Otherwise, all the generic types of the extension method must be specified.

make sense, agreed

.UseEntityFramework(x => x.DbContext<MyContext>(ef =>
{
    // this double lambda syntax seems to freak people out a bit as well, so
    // i'd rather avoid it.
});

I think if you provide ContextType as generic parameter you should register it by yourself.
so I see it like this

.UseEntityFramework(repo =>
{
    repo.UseDbContext<MyContext>();
    repo.ConcurrencyHandlingType = //;
    // if you have connection string
    repo.AddDbContext(cfg => cfg.UseSql());
});

EDIT:

I'm not sure about this line right now:
repo.AddDbContext(cfg => cfg.UseSql()); you have to do it for all sagas? for me it seems ridiculous.
probably what we could do

cfg.AddSagaDbContext<YourDbContext>(x => x.UseSql())
cfg.AddSaga<Instance>()
    .UseEntityFramework(repo => 
    {
         repo.UseContext<YourDbContext>();
         repo.ConcurrencyHandlingType = Pessimistic / Optimistic;
    });

Or user can register his own context using:

services.AddDbContext<YourDbContext>(x => x.UseSql())

we shouldn't mix up with registering dbcontext for every saga type, it also allows us to make it more or less the same for different Repository types:

cfg.AddMongoDatabase("connectionString")
cfg.AddSaga<Instance>()
    .UseMongoDB(repo => 
    {
         //configuration
    });

EDIT 2:

right now I'm not sure that we have to mix registering of your database with Saga Repository

probably we should start of doing something simple, for eg. MongoDB while you start writing a code you'll realize the best fit

LOL, okay, my original example had the database configured as one statement and then used for multiple repositories, and you said you thought it was better to have it per saga. So i changed it, and, now we're back...

var repo = cfg.AddSagaDbContext<MyContext>();

cfg.AddSaga<MySaga>()
    .Repository(repo);

cfg.AddSaga<MyOtherSaga>()
    .Repository(repo, x => x.ConcurrencyMode == ConcurrencyMode.Pessimistic);

In this case, .Repository is an extension method that uses the repo type, so that it's the proper entity framework configurator. Could just as easily have it and EntityFramework with an argument of _repo_.

馃ぃ
I'm pretty sure how the things work in our life. At the begging your are thinking that your approach is the best, after that you realize that is not gonna work the way you expected :)

var repo = cfg.AddSagaDbContext<MyContext>(); does this line register MyContext into services? if so how about situation when you have your own DbContext and you have it already registered?
(I won't be ever thinking that you have to share Saga persistence with your other Aggregates)

Need to not use Add in that case, maybe something like

cfg.AddSaga<MySaga>()
    .EntityFramework(ef =>
    {
        ef.ExistingDbContext<MySuperDbContext>();
    });

Since it's already registered in the container, it would just find it.

so why just not to do it that way:

cfg.AddSagaDbContext<YourDbContext>(x => x.UseSql())
cfg.AddSaga<Instance>()
    .UseEntityFramework(repo => 
    {
         repo.UseContext<YourDbContext>();
         repo.ConcurrencyHandlingType = Pessimistic / Optimistic;
    });

repo.UseContext<YourDbContext>(); could be use in both cases
and I'm not sure that we need this one evencfg.AddSagaDbContext<YourDbContext>(x => x.UseSql())

It could creates a lot of misunderstanding how you should register your dbcontext.
for the beginners, they always starts with Microsoft documentation "how to register EF context", after that they go to MT docs. So I think we probably have to provide default way to register DbContext using your DI and then to configure Saga Repo to use this context

@maldworth @nizmow, your thoughts?

I won't "use" .UseContext because that's the prefix for middleware, and it would lead to confusion.

I was talking not about the naming, more about approach we go

Yep.

I would like to have the cfg.AddSagaDbContext<> so that I can register it in the container correctly.

agreed with that 馃憤

Was this page helpful?
0 / 5 - 0 ratings