RayTree integrates with Entity Framework Core via ISaveChangesInterceptor, automatically detecting entity changes and writing them to the outbox within the same transaction.
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddChangeTracking(tracking =>
{
tracking.ForEntity<Product>()
.UsePostgreSqlOutbox(connectionString, "products");
tracking.UseJsonSerializer();
tracking.UseGzipCompressor();
});The interceptor is automatically attached to all registered DbContext types.
- SavingChanges - Interceptor detects
Added,Modified, andDeletedentities - SavedChanges - Writes changes to the outbox within the same EF Core transaction
- Publisher - Background service reads unpublished outbox entries and publishes to the queue
If the transaction rolls back, the outbox entry is also rolled back - ensuring exactly-once semantics.
tracking.ForEntity<Product>()
.UsePostgreSqlOutbox(connectionString, "products");tracking.ForEntity<Product>()
.UsePostgreSqlOutbox(conn, "products");
tracking.ForEntity<Order>()
.UsePostgreSqlOutbox(conn, "orders");tracking.ForEntity<Product>(filter: e => e.Property<string>("Name").CurrentValue != null)
.UsePostgreSqlOutbox(conn, "products");By default, the interceptor is attached to all DbContext types registered in DI.
tracking.ExcludeDbContext<ReportingDbContext>();tracking.ForDbContext<SalesDbContext>(ctx =>
{
ctx.ForEntity<Order>()
.UsePostgreSqlOutbox(conn, "orders");
ctx.ForEntity<Customer>()
.UsePostgreSqlOutbox(conn, "customers");
});
tracking.ForDbContext<InventoryDbContext>(ctx =>
{
ctx.ForEntity<Product>()
.UsePostgreSqlOutbox(conn, "products");
});The interceptor implements both sync and async methods:
// Works with both
await context.SaveChangesAsync(); // Async path
context.SaveChanges(); // Sync pathusing var tx = await context.Database.BeginTransactionAsync();
context.Products.Add(new Product { Name = "New" });
await context.SaveChangesAsync();
// Outbox entry is written here, within the transaction
// If you roll back, the outbox entry is also removed
await tx.RollbackAsync();You can track changes manually even with the EF Core interceptor configured:
var tracker = serviceProvider.GetRequiredService<IEntityChangeTracker>();
// Typed convenience methods capture the full entity state in EntityChange<T>.State
await tracker.TrackInsertAsync(new ExternalEntity { Id = 1, Name = "New" });
await tracker.TrackUpdateAsync(new ExternalEntity { Id = 1, Name = "Updated" });
await tracker.TrackDeleteAsync(new ExternalEntity { Id = 1, Name = "Updated" });
// Generic overload when the change type is determined at runtime
await tracker.TrackChangeAsync(entity, ChangeType.Update);