Snapshot with multiple subclasses inline aggregates to unexpected type #2876
Replies: 4 comments
-
I think you're just being a little more clever than the original author of this Marten feature. We'll take a look soon. This isn't anything that's tested or was ever anticipated. But bug reports that include failing tests as reproductions are the easiest to fix :-) |
Beta Was this translation helpful? Give feedback.
-
@StevenVerwerft I did some investigation, and this isn't going to be a quick win at all. I think if you want this behavior, either stick with live aggregation or switch to a SingleStreamAggregation that does all the actual work and "knows" whether to create one subclass or the other depending on the first event in Create() methods. I don't think this can be made to work w/o potentially destabilizing other projections. I'd be willing to revisit this in v7, but not right now. |
Beta Was this translation helpful? Give feedback.
-
Hi Jeremy, thanks for looking into this! For now we have moved on to using unique and separate events for each aggregate. I tried creating view projections, specifying the create events, but this seems to result in the same behavior. I also tried defining the schema such that the subclasses would be created in separate tables, that's where I've noticed something weird happening. Although only starting 2 streams, each of the projection tables are getting the projections for both streams. This results in 4 records in total. I updated the test file to illustrate what I mean. It works with the single stream aggregations as you suggested. Appreciate the work! using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Marten.Events.Aggregation;
using Marten.Events.Projections;
using Marten.Testing.Harness;
using Shouldly;
using Xunit;
namespace EventSourcingTests.Projections;
public class inline_aggregation_with_multiple_subclasses : OneOffConfigurationsContext
{
public inline_aggregation_with_multiple_subclasses()
{
StoreOptions(x =>
{
x.Schema.For<ConcreteViewA>();
x.Schema.For<ConcreteViewB>();
x.Projections.Add<ConcreteProjectionA>(ProjectionLifecycle.Inline);
x.Projections.Add<ConcreteProjectionB>(ProjectionLifecycle.Inline);
});
}
[Fact]
public async Task can_create_correct_views_in_separate_tables()
{
var streamIdA = Guid.NewGuid();
var eventsA = new object[] { new ConcreteACreated(streamIdA), new SomePropertySet("property set on A") };
var streamIdB = Guid.NewGuid();
var eventsB = new object[] { new ConcreteBCreated(streamIdB), new SomePropertySet("property set on B") };
theSession.Events.StartStream(streamIdA, eventsA);
theSession.Events.StartStream(streamIdB, eventsB);
await theSession.SaveChangesAsync();
var projectionsA = await theSession.Query<ConcreteViewA>()
.ToListAsync<ConcreteViewA>(CancellationToken.None);
var projectionsB = await theSession.Query<ConcreteViewB>()
.ToListAsync<ConcreteViewB>(CancellationToken.None);
Assert.Multiple(
() => projectionsA.Count.ShouldBe(1),
() => projectionsA.Select(x => x.Id).ShouldNotContain(streamIdB),
() => projectionsB.Count.ShouldBe(1),
() => projectionsB.Select(x => x.Id).ShouldNotContain(streamIdA));
}
}
public record ConcreteACreated(Guid Id);
public record ConcreteBCreated(Guid Id);
public record SomePropertySet(string Value);
public abstract class SomeBaseView
{
public Guid Id { get; set; }
public string SomeBaseProperty { get; set; }
}
public class ConcreteViewA : SomeBaseView
{
}
public class ConcreteViewB : SomeBaseView
{
}
public class ConcreteProjectionA: SingleStreamProjection<ConcreteViewA>
{
public ConcreteProjectionA()
{
CreateEvent<ConcreteACreated>(@event => new ConcreteViewA {Id = @event.Id});
}
public void Apply(ConcreteViewA view, SomePropertySet @event) => view.SomeBaseProperty = @event.Value;
}
public class ConcreteProjectionB: SingleStreamProjection<ConcreteViewB>
{
public ConcreteProjectionB()
{
CreateEvent<ConcreteBCreated>(@event => new ConcreteViewB {Id = @event.Id});
}
public void Apply(ConcreteViewB view, SomePropertySet @event) => view.SomeBaseProperty = @event.Value;
} |
Beta Was this translation helpful? Give feedback.
-
I'm converting this to a discussion. This isn't anything I'm wild about supporting. |
Beta Was this translation helpful? Give feedback.
-
Hi all,
I'm trying to create snapshots for multiple subclasses inheriting from a common abstract base class. The idea is to define apply methods for common events on a base class, the subclasses define apply methods for events that are unique for the subclass.
I have created a test scenario with an abstract class and two subclasses to illustrate what I intend to do. Live aggregation seems to work fine. However, the inline aggregation seems to convert one of the concrete subclass types to the other subclass type. When I take a look at the table in the db, two snapshots are indeed created, but both are created as
ConcreteTypeB
.Not sure if this is a bug, me missing some configuration, or just something Marten was not intended for. But right now it looks like I would need to define unique events for each subclass, which would lead me to duplicate a lot of event handler logic.
Thanks,
Steven
Beta Was this translation helpful? Give feedback.
All reactions