Intro to Microsoft Orleans

Things are constantly evolving in application architecture. No sooner than I had completed my blog post about Event Sourcing with Apache Pulsar than Microsoft dropped their own Event-Driven Architecture solution, Microsoft Orleans, into my lap. I’m still in the process of migrating my existing code from the framework I’d adapted from Mr. Zimarev’s book, but all of Alexey’s code is gone now, replaced completely by the Orleans framework. (I even replaced the ValueObject implementation with the recommended implementation from Microsoft).

Surprisingly, very little of the application had to be rewritten. The domain objects remained almost verbatim; they will serve as the GrainState for our JournaledGrains. The worker service disappeared entirely. The web application will now both serve the Blazor client application, and host the Orleans runtime. We will continue to use HTTP client/server semantics for the Blazor application, which allows us to simply replace our web API calls to our custom Pulsar framework with calls to the Orleans framework:

[HttpPost]
public async Task<IActionResult> Issued(Event.Vote.Ballot.Issued @event) => HandleEvent(@event)

to

[HttpPost]
public async Task<IActionResult> Issued(Event.Vote.Ballot.Issued @event)
{
var grain = _grainFactory.GetGrain(Guid.NewGuid());
await grain.Issue(ElectionId.FromGuid(@event.ElectionId));
return Ok(grain.GetPrimaryKey());
}


This could probably be abstracted into a one-liner like the previous code as well with a bit of effort, but I’m ok with 3 lines for now. Those 3 lines are quite powerful. With some configuration and attributes, I can replace all of my projection code and the PulsarService with the Orleans framework, and it will handle all the persistence, both the read models and the event log.

The actual Grain itself isn’t very complicated, and it should be very reminiscent of the original EventFramework code:

public class BallotGrain : JournaledGrain<Ballot>, IBallotGrain
{
    public async Task Vote(CompetitionId competitionId, CandidateId candidateId, int? rank)
    {
        RaiseEvent(new Vote.Ballot.Voted
        {
            Id = this.GetPrimaryKey(),
            CompetitionId = competitionId,
            CandidateId = candidateId,
            Rank = rank
        });
        await ConfirmEvents();
    }

    public async Task Issue(ElectionId electionId)
    {
        RaiseEvent(new Vote.Ballot.Issued
        {
            Id = this.GetPrimaryKey(),
            ElectionId = electionId,
            IssuedAt = DateTimeOffset.Now
        });
        await ConfirmEvents();
    }

    public async Task Cast(ElectionId electionId)
    {
        RaiseEvent(new Vote.Ballot.Cast
        {
            Id = this.GetPrimaryKey(),
            ElectionId = electionId,
            CastAt = DateTimeOffset.Now
        });
        await ConfirmEvents();
    }
}

Finally, our domain objects now have an overloaded Apply method, which allows us to respond to events:

public class Ballot
{
    public BallotId? Id { get; private set; }
    public ElectionId? ElectionId { get; private set; }
    public IssuedAt? IssuedAt { get; private set; }
    public CastAt? CastAt { get; private set; }
    public Votes? Votes { get; private set; } 
    
    public void Apply(Event.Vote.Ballot.Issued @event)
    {
        Id = BallotId.FromGuid(@event.Id);
        ElectionId = ElectionId.FromGuid(@event.ElectionId);
        IssuedAt = IssuedAt.FromDateTimeOffset(@event.IssuedAt);
        Votes = Votes.New();
    }

    public void Apply(Event.Vote.Ballot.Voted @event)
    {
        if (CastAt != null)
            throw new Exception("This ballot has already been cast");
        Votes ??= Votes.New();
        var competitionId = CompetitionId.FromGuid(@event.CompetitionId);
        var candidateId = CandidateId.FromGuid(@event.CandidateId);
        Votes.Add(competitionId, candidateId);
        Votes[competitionId][candidateId] = @event.Rank;
    }

    public void Apply(Event.Vote.Ballot.Cast @event)
    {
        if (CastAt != null)
            throw new Exception("This ballot has already been cast");
        CastAt = CastAt.FromDateTimeOffset(@event.CastAt);
    }
}

Looking at them again, it’s hard to call them domain objects any more. Still, the original code from the EventFramework is there under all of the Apply overloads. All of our work from Event Storming and DDD has been salvaged. What we are not doing is maintaining child collections, though with some effort we could do that too. We would just respond to the appropriate events with code that updates a local collection. Provided the collection is serializable, it will be persisted complete with its child objects. I’m not convinced that this does us any good, however.

This is my initial foray into Microsoft Orleans. My main concern is with how tied to Microsoft technologies this is. While persistence is automagic, it is also tied to Azure Tables and Queues. It is not costly, but it is not immediately replaced with something else, either. I’ve not even scratched the surface here, but I wanted to share my experience of migrating an existing Event Sourced system to the Orleans framework. I look forward to completing and testing this application, then exploring deployment to Kubernetes which is a supported configuration of the Orleans cluster. I’ll post those results later on.

Advertisement
,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: