Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Opt out for identity map optimizations of inline aggregates #3292

Merged
merged 2 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/scenarios/command_handler_workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,36 @@ the standard `IDocumentSession.SaveChangesAsync()` method call. At that point, i
`Order` stream between our handler calling `FetchForWriting()` and `IDocumentSession.SaveChangesAsync()`, the entire command will fail with a Marten
`ConcurrencyException`.

## Inline Aggregates <Badge type="tip" text="7.22" />

::: warning
You may need to opt out of this behavior if you are modifying the aggregate document returned by `FetchForWriting`
before new events are applied to it to not arrive at incorrectly applied projection data.
:::

`FetchForWriting()` works with all possible projection lifecycles as of Marten 7. However, there's a wrinkle with `Inline`
projections you should be aware of. It's frequently a valuable optimization to use _Lightweight_ sessions that omit
the identity map behavior. Let's say that we have a configuration like this:

snippet: sample_using_lightweight_sessions_with_inline_single_stream_projection

As an optimization, Marten is quietly turning on the identity map behavior for just a single aggregate document type
when `FetchForWriting()` is called with an `Inline` projection as shown below:

snippet: sample_usage_of_identity_map_for_inline_projections

This was done specifically to prevent unnecessary database fetches for the exact same data within common command handler
workflow operations. There can of course be a downside if you happen to be making any kind of mutations to the aggregate
document somewhere in between calling `FetchForWriting()` and `SaveChangesAsync()`, so to opt out of this behavior if
that causes you any trouble, use this:

::: info
Marten's default behavior of using sessions with the _Identity Map_ functionality option turned on was admittedly copied
from RavenDb almost a decade ago, and the Marten team has been too afraid to change the default behavior to the more performant, _Lightweight_ sessions because of the
very real risk of introducing difficult to diagnose regression errors. There you go folks, a 10 year old decision that this
author still regrets, but we're probably stuck with for the foreseeable future.
:::

## Explicit Optimistic Concurrency

This time let's explicitly opt into optimistic concurrency checks by telling Marten what the expected starting
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,103 @@
using System;
using System.Threading.Tasks;
using Marten;
using Marten.Events;
using Marten.Events.Projections;
using Marten.Exceptions;
using Marten.Storage;
using Marten.Testing.Harness;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Xunit;
using Shouldly;

namespace EventSourcingTests.Aggregation;

public class fetching_inline_aggregates_for_writing : OneOffConfigurationsContext
{
public static async Task example()
{


#region sample_using_lightweight_sessions_with_inline_single_stream_projection

var builder = Host.CreateApplicationBuilder();
builder.Services.AddMarten(opts =>
{
opts.Connection(builder.Configuration.GetConnectionString("postgres"));

// An inline single stream projection that builds SimpleAggregate
// A "snapshot" in Marten still boils down to a SingleStreamProjection<T>
// for that "T" in Marten internals
opts.Projections.Snapshot<SimpleAggregate>(SnapshotLifecycle.Inline);
})

// This is a commonly used, frequently helpful performance optimization
.UseLightweightSessions();

using var host = builder.Build();
await host.StartAsync();

#endregion

#region sample_usage_of_identity_map_for_inline_projections

// Little helper extension method to quickly get at the Marten
// DocumentStore from an IHost
var store = host.DocumentStore();

// This session is *not* using identity map by default as
// a performance optimization
using var session = store.LightweightSession();

// Some aggregate id that would probably be passed in through a CQRS command message
// or maybe an HTTP request argument
var id = Guid.NewGuid();

// In your command handler, you would use this call to fetch both the current state
// of the SimpleAggregate as well as getting Marten ready to do optimistic concurrency
// checks for you as well
var stream = await session.Events.FetchForWriting<SimpleAggregate>(id);

// Just showing you that this is the current version of the projected
// aggregate document that was fetched out of the database by Marten
var aggregate = stream.Aggregate;

// The command would append new events to the event stream...
stream.AppendMany(new AEvent(), new BEvent());

// Persist the new events to the existing event stream, and oh, yeah,
// also update the SimpleAggregate document with the new events
// As of Marten 7.21, Marten is able to start with the version of the aggregate
// document that was initially loaded as part of FetchForWriting() instead
// of having to fetch it all over again from the database
await session.SaveChangesAsync();

#endregion
}

public static async Task disable_optimization()
{
var builder = Host.CreateApplicationBuilder();
builder.Services.AddMarten(opts =>
{
opts.Connection(builder.Configuration.GetConnectionString("postgres"));

// An inline single stream projection that builds SimpleAggregate
// A "snapshot" in Marten still boils down to a SingleStreamProjection<T>
// for that "T" in Marten internals
opts.Projections.Snapshot<SimpleAggregate>(SnapshotLifecycle.Inline);

opts.Events.UseIdentityMapForInlineAggregates = false;
})

// This is a commonly used, frequently helpful performance optimization
.UseLightweightSessions();

using var host = builder.Build();
await host.StartAsync();
}

[Fact]
public async Task fetch_new_stream_for_writing_Guid_identifier()
{
Expand Down
6 changes: 6 additions & 0 deletions src/EventSourcingTests/EventGraphTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ public void switch_to_quick_and_back_to_rich()
theGraph.EventAppender.ShouldBeOfType<RichEventAppender>();
}

[Fact]
public void use_identity_map_optimization_is_true_by_default()
{
theGraph.UseIdentityMapForInlineAggregates.ShouldBeTrue();
}

public class HouseRemodeling
{
public Guid Id { get; set; }
Expand Down
8 changes: 8 additions & 0 deletions src/Marten/Events/EventGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ internal EventGraph(StoreOptions options)

internal EventMetadataCollection Metadata { get; } = new();

/// <summary>
/// Setting this to true will direct Marten to automatically use the identity map for inline projections
/// in calls to FetchForWriting as an optimization to reduce repeated queries for the same aggregate.
/// The default is true. Disable this call if applying state changes to the loaded aggregate in your own
/// command handlers
/// </summary>
public bool UseIdentityMapForInlineAggregates { get; set; } = true;

/// <summary>
/// TimeProvider used for event timestamping metadata. Replace for controlling the timestamps
/// in testing
Expand Down
9 changes: 6 additions & 3 deletions src/Marten/Events/Fetching/FetchInlinedPlan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@ public async Task<IEventStream<TDoc>> FetchForWriting(DocumentSessionBase sessio
await _identityStrategy.EnsureEventStorageExists<TDoc>(session, cancellation).ConfigureAwait(false);
await session.Database.EnsureStorageExistsAsync(typeof(TDoc), cancellation).ConfigureAwait(false);

// Opt into the identity map mechanics for this aggregate type just in case
// you're using a lightweight session
session.UseIdentityMapFor<TDoc>();
if (session.Options.Events.UseIdentityMapForInlineAggregates)
{
// Opt into the identity map mechanics for this aggregate type just in case
// you're using a lightweight session
session.UseIdentityMapFor<TDoc>();
}

if (forUpdate)
{
Expand Down
8 changes: 8 additions & 0 deletions src/Marten/Events/IEventStoreOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ public interface IEventStoreOptions

public EventAppendMode AppendMode { get; set; }

/// <summary>
/// Setting this to true will direct Marten to automatically use the identity map for inline projections
/// in calls to FetchForWriting as an optimization to reduce repeated queries for the same aggregate.
/// The default is true. Disable this call if applying state changes to the loaded aggregate in your own
/// command handlers
/// </summary>
public bool UseIdentityMapForInlineAggregates { get; set; }

/// <summary>
/// Register an event type with Marten. This isn't strictly necessary for normal usage,
/// but can help Marten with asynchronous projections where Marten hasn't yet encountered
Expand Down
Loading