Creating abstract generic base Saga class to avoid boilerplate

Hi everyone,

To give some context first, I have an ASP.NET Core 3.1 application that works like this:

  1. The app receives requests using a regular MVC Controller (e.g. POST to /Client to create one)
  2. The controller then builds a command and sends it like so: await _endpointInstance.Send(command);
  3. Then I have a separate console application that has all the Saga handler that listen to the commands that trigger mutations to the Client entity:
public class ClientSaga : Saga<ClientSagaData>,
        IAmStartedByMessages<CreateClient>,
        IHandleSagaNotFound,
        IHandleMessages<UpdateClient>,
        IHandleTimeouts<UpdateClient>,
        IHandleMessages<DeleteClient>,
        IHandleTimeouts<DeleteClient>,
        IHandleTimeouts<RetryDeleteClient>,
        IHandleMessages<ClientReadModelCreated>,
        IHandleMessages<ClientReadModelUpdated>,
        IHandleMessages<ClientReadModelDeleted>,
        IHandleMessages<ClientReadModelCreationCancelled>,
        IHandleMessages<ClientReadModelUpdateCancelled>,
        IHandleMessages<ClientReadModelDeleteCancelled>
    {
  1. Then these handlers do some boilerplate code. First, we map the message properties to the SagaData. Then, we build and send a CreateTransaction command (handled by another class). And finally, it builds and sends a CreateClientReadModel command (handled by another class).

  2. As you can see, my ClientSaga acts as an orchestrator but it also has boilerplate code since I do the same for every entity and I’d have to repeat the same code from point #4.

  3. So what I did to avoid the boilerplate code was to create a generic abstract base class:

public abstract class BaseSagaWithGenerics<TSagaData, 
                                            TCreateCommand, 
                                            TUpdateCommand, 
                                            TDeleteCommand> : Saga<TSagaData>,
    IAmStartedByMessages<TCreateCommand>,
    IHandleSagaNotFound,
    IHandleMessages<TUpdateCommand>,
    IHandleTimeouts<TUpdateCommand>
    ......
    where TSagaData : class, IContainSagaData, IBaseSagaData, new()
    where TCreateCommand
{

The idea here is to have default generic implementations for my handlers depending on the types sent when we inherit from this class and then overriding parts (not boilerplate) of those methods:

public class DegreeSaga : BaseSagaWithGenerics<DegreeSagaData, 
    CreateDegree, 
    UpdateDegree, 
    DeleteDegree>
{
    public override DegreeSagaData MapSagaData(CreateDegree createCommand)
    {
        return new DegreeSagaData
        {
            ObjectId = createCommand.ObjectId,
            Name = createCommand.Name,
            LawDegree = createCommand.LawDegree,
            Regions = createCommand.Regions,
            AlternateNames = createCommand.AlternateNames,
            LegacyId = createCommand.LegacyId
        };
    }

But here the problem is that the implementation for the Handle method have the same signature so the c# compiler won’t let me implement this:

|Error|CS0695|'BaseSagaWithGenerics<TSagaData, TCreateCommand, TUpdateCommand, TDeleteCommand>' cannot implement both 'IHandleMessages<TCreateCommand>' and 'IHandleMessages<TUpdateCommand>' because they may unify for some type parameter substitutions|BaseSaga.cs|101|Active|

Is there a way to implement this? I also tried with base classes constraints but also no luck there.

Thanks in advance for your help!

Saul.

Hi @saulenriquejr

I don’t know if there is really a way to prevent this compiler error. Looking at your saga I’d be more curious what exactly you are using sagas for? This looks a lot like data access orchestrated via Sagas. Since sagas are primarily intended to manage long running business processes, where do sagas provide additional value to compared to managing data access more directly, e.g. in a regular handler?

So yes, the thing is that my DegreeSaga is also in charge of creating a transaction every time a CreateDegree command is received. That means that every create command will trigger a transaction creation inside our handlers. I think that’s an issue and I’ll need to move that boilerplate code to another part of the system.

You might want to consider moving this to a custom behavior?

That looks interesting, I’ll check this and see how it goes. Thanks!