Last active
January 9, 2026 02:02
-
-
Save jeremydmiller/2c7cd35b1a164070df26e88abfeec72e to your computer and use it in GitHub Desktop.
Demonstrating Composite, Multi-Stage Projections in Marten
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public class AppointmentDetailsProjection : MultiStreamProjection<AppointmentDetails, Guid> | |
| { | |
| public AppointmentDetailsProjection() | |
| { | |
| Options.CacheLimitPerTenant = 1000; | |
| Identity<Updated<Appointment>>(x => x.Entity.Id); | |
| Identity<IEvent<ProviderAssigned>>(x => x.StreamId); | |
| Identity<IEvent<AppointmentRouted>>(x => x.StreamId); | |
| } | |
| public override async Task EnrichEventsAsync(SliceGroup<AppointmentDetails, Guid> group, IQuerySession querySession, CancellationToken cancellation) | |
| { | |
| // TODO -- need a way to track an IAggregateCache for lookups in these | |
| // projections. Thinking of Specialty that's probably going to be very | |
| // static. | |
| // Look up and apply specialty information from the document store | |
| // Specialty is just reference data stored as a document in Marten | |
| await group | |
| .EnrichWith<Specialty>() | |
| .ForEvent<AppointmentRequested>() | |
| .ForEntityId(x => x.SpecialtyCode) | |
| // TODO -- make a short hand for this | |
| .EnrichAsync((slice, _, specialty) => | |
| { | |
| slice.Reference(specialty); | |
| }); | |
| // Also reference data (for now) | |
| await group | |
| .EnrichWith<Patient>() | |
| .ForEvent<AppointmentRequested>() | |
| .ForEntityId(x => x.PatientId) | |
| .EnrichAsync((slice, _, patient) => | |
| { | |
| slice.Reference(patient); | |
| }); | |
| // Look up and apply provider information | |
| await group | |
| .EnrichWith<Provider>() | |
| .ForEvent<ProviderAssigned>() | |
| .ForEntityId(x => x.ProviderId) | |
| .EnrichAsync((slice, e, provider) => | |
| { | |
| slice.Reference(provider); | |
| }); | |
| await group | |
| .EnrichWith<Board>() | |
| .ForEvent<AppointmentRouted>() | |
| .ForEntityId(x => x.BoardId) | |
| .EnrichAsync((slice, _, board) => | |
| { | |
| slice.Reference(board); | |
| }); | |
| // look up board | |
| // look up patients from Appointment requested | |
| } | |
| public override AppointmentDetails Evolve(AppointmentDetails snapshot, Guid id, IEvent e) | |
| { | |
| switch (e.Data) | |
| { | |
| case AppointmentRequested requested: | |
| snapshot ??= new AppointmentDetails(e.StreamId); | |
| snapshot.SpecialtyCode = requested.SpecialtyCode; | |
| snapshot.PatientId = requested.PatientId; | |
| break; | |
| // This is an upstream projection. Triggering off of a synthetic | |
| // event that Marten publishes from the early stage | |
| // to this projection running in a secondary stage | |
| case Updated<Appointment> updated: | |
| snapshot ??= new AppointmentDetails(updated.Entity.Id); | |
| snapshot.Status = updated.Entity.Status; | |
| snapshot.EstimatedTime = updated.Entity.EstimatedTime; | |
| snapshot.SpecialtyCode = updated.Entity.SpecialtyCode; | |
| break; | |
| case References<Patient> patient: | |
| snapshot.PatientFirstName = patient.Entity.FirstName; | |
| snapshot.PatientLastName = patient.Entity.LastName; | |
| break; | |
| case References<Specialty> specialty: | |
| snapshot.SpecialtyCode = specialty.Entity.Code; | |
| snapshot.SpecialtyDescription = specialty.Entity.Description; | |
| break; | |
| case References<Provider> provider: | |
| snapshot.ProviderId = provider.Entity.Id; | |
| snapshot.ProviderFirstName = provider.Entity.FirstName; | |
| snapshot.ProviderLastName = provider.Entity.LastName; | |
| break; | |
| case References<Board> board: | |
| snapshot.BoardName = board.Entity.Name; | |
| snapshot.BoardId = board.Entity.Id; | |
| break; | |
| } | |
| return snapshot; | |
| } | |
| } | |
| // This is effectively a de-normalized view built from | |
| // events in the Appointment stream with information from | |
| // several reference Documents | |
| // It's triggered when the Appointment projection running in | |
| // the previous stage updates itself | |
| public class AppointmentDetails | |
| { | |
| public Guid Id { get; } | |
| public AppointmentDetails(Guid id) | |
| { | |
| Id = id; | |
| } | |
| public string PatientFirstName { get; set; } | |
| public string PatientLastName { get; set; } | |
| public Guid PatientId { get; set; } | |
| public Guid ProviderId { get; set; } | |
| public string SpecialtyCode { get; set; } | |
| public string SpecialtyDescription { get; set; } | |
| public string BoardName { get; set; } | |
| public Guid? BoardId { get; set; } | |
| public string ProviderFirstName { get; set; } | |
| public string ProviderLastName { get; set; } | |
| public ProviderRole ProviderRole { get; set; } | |
| public DateTimeOffset Requested { get; set; } | |
| public DateTimeOffset? EstimatedTime { get; set; } | |
| public DateTimeOffset? CompletedTime { get; set; } | |
| public AppointmentStatus Status { get; set; } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment