# Entity Framework Core

MassTransit.EntityFrameworkCore (opens new window)

An example saga instance is shown below, which is orchestrated using an Automatonymous state machine. The CorrelationId will be the primary key, and CurrentState will be used to store the current state of the saga instance.

public class OrderState :
    SagaStateMachineInstance
{
    public Guid CorrelationId { get; set; }
    public string CurrentState { get; set; }

    public DateTime? OrderDate { get; set; }

    // If using Optimistic concurrency, this property is required
    public byte[] RowVersion { get; set; }
}

The instance properties are configured using a SagaClassMap.

Important

The SagaClassMap has a default mapping for the CorrelationId as the primary key. If you create your own mapping, you must follow the same convention, or at least make it a Clustered Index + Unique, otherwise you will likely experience deadlock exceptions and/or performance issues in high throughput scenarios.

public class OrderStateMap : 
    SagaClassMap<OrderState>
{
    protected override void Configure(EntityTypeBuilder<OrderState> entity, ModelBuilder model)
    {
        entity.Property(x => x.CurrentState).HasMaxLength(64);
        entity.Property(x => x.OrderDate);

        // If using Optimistic concurrency, otherwise remove this property
        entity.Property(x => x.RowVersion).IsRowVersion();
    }
}

Include the instance map in a DbContext class that will be used by the saga repository.

public class OrderStateDbContext : 
    SagaDbContext
{
    public OrderStateDbContext(DbContextOptions options)
        : base(options)
    {
    }

    protected override IEnumerable<ISagaClassMap> Configurations
    {
        get { yield return new OrderStateMap(); }
    }
}

# Container Integration

Once the class map and associated DbContext class have been created, the saga repository can be configured with the saga registration, which is done using the configuration method passed to AddMassTransit. The following example shows how the repository is configured for using Microsoft Dependency Injection Extensions, which are used by default with Entity Framework Core.

services.AddMassTransit(cfg =>
{
    cfg.AddSagaStateMachine<OrderStateMachine, OrderState>()
        .EntityFrameworkRepository(r =>
        {
            r.ConcurrencyMode = ConcurrencyMode.Pessimistic; // or use Optimistic, which requires RowVersion

            r.AddDbContext<DbContext, OrderStateDbContext>((provider,builder) =>
            {
                builder.UseSqlServer(connectionString, m =>
                {
                    m.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name);
                    m.MigrationsHistoryTable($"__{nameof(OrderStateDbContext)}");
                });
            });
        });
});

# Single DbContext

New in 7.0.5

A single DbContext can be registered in the container which can then be used to configure sagas that are mapped by the DbContext. For example, Job Consumers need three saga repositories, and the Entity Framework Core package includes the JobServiceSagaDbContext which can be configured using the AddSagaRepository method as shown below.

services.AddDbContext<JobServiceSagaDbContext>(builder =>
    builder.UseNpgsql(Configuration.GetConnectionString("JobService"), m =>
    {
        m.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name);
        m.MigrationsHistoryTable($"__{nameof(JobServiceSagaDbContext)}");
    }));

services.AddMassTransit(x =>
{
    x.AddSagaRepository<JobSaga>()
        .EntityFrameworkRepository(r =>
        {
            r.ExistingDbContext<JobServiceSagaDbContext>();
            r.LockStatementProvider = new PostgresLockStatementProvider();
        });
    x.AddSagaRepository<JobTypeSaga>()
        .EntityFrameworkRepository(r =>
        {
            r.ExistingDbContext<JobServiceSagaDbContext>();
            r.LockStatementProvider = new PostgresLockStatementProvider();
        });
    x.AddSagaRepository<JobAttemptSaga>()
        .EntityFrameworkRepository(r =>
        {
            r.ExistingDbContext<JobServiceSagaDbContext>();
            r.LockStatementProvider = new PostgresLockStatementProvider();
        });

    // other configuration, such as consumers, etc.
});

The above code using the standard Entity Framework configuration extensions to add the DbContext to the container, using PostgreSQL. Because the job service state machine receive endpoints are configured by ConfigureJobServiceEndpoints, the saga repositories must be configured separately. The AddSagaRepository method is used to register a repository for a saga that has already been added, and uses the same extension methods as the AddSaga and AddSagaStateMachine methods.

Once configured, the job service sagas can be configured as shown below.

cfg.ServiceInstance(options, instance =>
{
    instance.ConfigureJobServiceEndpoints(js =>
    {
        js.ConfigureSagaRepositories(context);
    });
});

The Job Consumers (opens new window) sample is a working version of this configuration style.

# Multiple DbContext

Multiple DbContext can be registered in the container which can then be used to configure sagas that are mapped by the DbContext and injected into other components. Calling the AddDbContext extension method will register a scoped DbContext by default. For simple scenarios where there is a single DbContext this will work. However, in scenarios where there is at least one other DbContext the dotnet command that generates Entity Framework migrations will not work. To resolve this issue, you'll need to perform the following steps:

  1. Make sure that all DbContext has a constructor that takes DbContextOptions<TOptions> instead of DbContextOptions.

  2. Run the Entity Framework Core command to create your migrations as shown below.

dotnet ef migrations add InitialCreate -c JobServiceSagaDbContext
  1. Run the Entity Framework Core command to sync with the database as shown below.
dotnet ef database update -c JobServiceSagaDbContext

# Custom schemas

# Single schema

In case there is a custom schema set up in your database and you are relying on the user credentials from the ConnectionString to access correct schema you must include the schema name when specifying the lock statement provider.

services.AddMassTransit(cfg =>
{
    cfg.AddSagaStateMachine<OrderStateMachine, OrderState>()
        .EntityFrameworkRepository(r =>
        {
            r.ConcurrencyMode = ConcurrencyMode.Pessimistic; // or use Optimistic, which requires RowVersion
            
            r.UseSqlServer("custom_schema");

            r.AddDbContext<DbContext, OrderStateDbContext>((provider,builder) =>
            {
                builder.UseSqlServer(connectionString, m =>
                {
                    m.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name);
                    m.MigrationsHistoryTable($"__{nameof(OrderStateDbContext)}");
                });
            });
        });
});

# Multiple schemas

In case there are multiple schemas defined for your models you need to define them in code, and there are a few different ways to do that. https://learn.microsoft.com/en-us/ef/core/modeling/entity-types?tabs=data-annotations#table-schema