Skip to content

Instantly share code, notes, and snippets.

@jeremydmiller
Last active January 9, 2026 02:02
Show Gist options
  • Select an option

  • Save jeremydmiller/2c7cd35b1a164070df26e88abfeec72e to your computer and use it in GitHub Desktop.

Select an option

Save jeremydmiller/2c7cd35b1a164070df26e88abfeec72e to your computer and use it in GitHub Desktop.
Demonstrating Composite, Multi-Stage Projections in Marten
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