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

Projections generated code may fail compilation due to order of switch case statements #2751

Merged
merged 5 commits into from
Oct 19, 2023
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using JasperFx.CodeGeneration;
using JasperFx.Core;
using JasperFx.Core.Reflection;
using Lamar.IoC.Instances;
using Marten;
using Marten.Events.Aggregation;
using Marten.Events.CodeGeneration;
using Marten.Events.Projections;
using Marten.Internal.CodeGeneration;
using Shouldly;
using Xunit;

namespace EventSourcingTests.Projections.CodeGeneration;


public class EventTypePatternMatchFrameOrderTests
{
[Theory]
[MemberData(nameof(GetEventTypeCombinations))]
public void SortByEventTypeHierarchy_WithCombinations_SortsAsExpected(TypeArrayData events) =>
RunComparerTest(events);

public static IEnumerable<object[]> GetEventTypeCombinations() =>
GetCombinationsEventsData(typeof(Base), typeof(FooBase), typeof(FooA), typeof(FooX), typeof(BarBase), typeof(BarA), typeof(BarX), typeof(FooBarA), typeof(FooBarX));


[Theory]
[MemberData(nameof(GetEventTypePermutations))]
public void SortByEventTypeHierarchy_WithPermutations_SortsAsExpected(TypeArrayData events) =>
RunComparerTest(events);

public static IEnumerable<object[]> GetEventTypePermutations() =>
GetPermutationsEventsData(typeof(FooBase), typeof(FooA), typeof(FooX), typeof(BarBase), typeof(FooBarA), typeof(FooBarX));

private static void RunComparerTest(TypeArrayData events)
{
var frames = events.Data.ToDummyEventProcessingFrames();
var sortedFrames = EventTypePatternMatchFrame.SortByEventTypeHierarchy(frames);

var eventTypes = sortedFrames.Select(p => p.EventType).ToArray();
eventTypes.ShouldHaveDerivedTypesBeforeBaseTypes();
}

private static IEnumerable<object[]> GetPermutationsEventsData(params Type[] events) =>
events.GetPermutations()
.Select(p => new object[] { new TypeArrayData(p.ToArray()) });

private static IEnumerable<object[]> GetCombinationsEventsData(params Type[] events) =>
events.GetCombinations()
.Where(p => p.Count() > 3)
.Select(p => new object[] { new TypeArrayData(p.ToArray()) });

public class Base{}
public class FooBase : Base { }
public class BarBase: Base { }

public class FooA : FooBase { }
public class FooX: FooBase { }

public class BarA: BarBase { }
public class BarX: BarBase { }

public class FooBarA: BarBase { }
public class FooBarX: BarBase { }

public class TypeArrayData
{
public Type[] Data { get; }

public TypeArrayData(Type[] data)
{
Data = data;
}

public override string ToString()
{
return $"[{string.Join(", ", Data.Select(x => x.Name))}]";
}
}
}

internal static class EventTypePatternMatchFrameOrderTestsExtensions
{
internal static void ShouldHaveDerivedTypesBeforeBaseTypes(this Type[] types)
{
for (var i = 0; i < types.Length; i++)
{
var type = types[i];
var index = types.IndexOf(type.BaseType);
if (index != -1)
index.ShouldBeGreaterThan(i, $"'{type.Name}' should come before '{type.BaseType?.Name}'.");
}
}

internal static List<EventProcessingFrame> ToDummyEventProcessingFrames(this IEnumerable<Type> types) =>
types
.Select(p => new EventProcessingFrame(false, typeof(object), p))
.ToList();

internal static IEnumerable<IEnumerable<T>> GetCombinations<T>(this IEnumerable<T> elements)
{
elements = elements.ToArray();

if (!elements.Any())
{
yield return Enumerable.Empty<T>();
}
else
{
var head = elements.Take(1);
var tail = elements.Skip(1);

foreach (var combination in GetCombinations(tail))
{
yield return combination;
yield return head.Concat(combination);
}
}
}

internal static IEnumerable<T[]> GetPermutations<T>(this IEnumerable<T> elements)
{
var list = elements.ToArray();
var n = list.Length;
var indexes = new int[n];
for (var i = 0; i < n; i++)
{
indexes[i] = 0;
}

yield return list;

var j = 0;
while (j < n)
{
if (indexes[j] < j)
{
var swapIndex = j % 2 == 0 ? 0 : indexes[j];
(list[swapIndex], list[j]) = (list[j], list[swapIndex]);

yield return list.Copy();

indexes[j]++;
j = 0;
}
else
{
indexes[j] = 0;
j++;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using JasperFx.CodeGeneration;
using JasperFx.Core;
using JasperFx.Core.Reflection;
using Lamar.IoC.Instances;
using Marten;
using Marten.Events.Aggregation;
using Marten.Events.CodeGeneration;
using Marten.Events.Projections;
using Marten.Internal.CodeGeneration;
using Shouldly;
using Xunit;

namespace EventSourcingTests.Projections.CodeGeneration;


public class ProjectionCodeGenerationApplyOrderTests
{
[Fact]
public void Snapshot_GeneratedCodeFile_Compiles()
{
var options = new StoreOptions();
options.Connection("Dummy");

// Given
options.Projections.Snapshot<Something>(SnapshotLifecycle.Inline);

// When
var store = new DocumentStore(options);

// Then
var projectionCodeFile = store.Events.As<ICodeFileCollection>().BuildFiles()
.OfType<SingleStreamProjection<Something>>().FirstOrDefault();

projectionCodeFile.ShouldNotBeNull();
projectionCodeFile.Compile(options);

var provider = options
.BuildFiles()
.OfType<DocumentProviderBuilder>()
.SingleOrDefault(e => e.ProviderName == typeof(Something).ToSuffixedTypeName("Provider"));

provider.ShouldNotBeNull();
}

public class SomethingBase{}

public class SomethingCreated : SomethingAdded
{
public SomethingCreated(Guid id) : base(id) { }
}

public class SomethingAdded : SomethingBase
{
public SomethingAdded(Guid id)
{
Id = id;
}
public Guid Id { get; set; }
}

public class SomethingUpdated : SomethingBase
{
public SomethingUpdated(Guid id, string somethingSomething)
{
Id = id;
SomethingSomething = somethingSomething;
}

public Guid Id { get; set; }
public string SomethingSomething { get; set; }
}

public class SomethingManuallySynched: SomethingUpdated
{
public SomethingManuallySynched(Guid id, string somethingSomething) : base(id, somethingSomething) { }

}

public class SomethingAutoSynched: SomethingUpdated
{
public SomethingAutoSynched(Guid id, string somethingSomething) : base(id, somethingSomething) { }
}

public record Something(Guid Id)
{
public string SomethingSomething { get; set; }

public static Something Create(SomethingCreated @event) => new(@event.Id);

public Something Apply(SomethingBase @event) => this;

public Something Apply(SomethingAdded @event) => this;

public Something Apply(SomethingAutoSynched @event) => this with { SomethingSomething = @event.SomethingSomething };

public Something Apply(SomethingManuallySynched @event) => this with { SomethingSomething = @event.SomethingSomething };

public Something Apply(SomethingUpdated @event) => this with { SomethingSomething = @event.SomethingSomething };



}
}
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ private GeneratedMethod buildApplyEventMethod()
eventHandlers[slot.EventType].Deletion = new ShouldDeleteFrame(slot);

var frames = eventHandlers.OfType<EventProcessingFrame>().ToList();
frames.Sort(new EventTypeComparer());

var patternMatching = new EventTypePatternMatchFrame(frames);
method.Frames.Add(patternMatching);

Expand Down
33 changes: 32 additions & 1 deletion src/Marten/Events/CodeGeneration/EventTypePatternMatchFrame.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Frames;
using JasperFx.CodeGeneration.Model;
using JasperFx.Core.Reflection;

namespace Marten.Events.CodeGeneration;

Expand All @@ -21,7 +23,8 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
if (_inner.Any())
{
writer.Write($"BLOCK:switch ({_event.Usage})");
foreach (var frame in _inner) frame.GenerateCode(method, writer);
foreach (var frame in SortByEventTypeHierarchy(_inner))
frame.GenerateCode(method, writer);

writer.FinishBlock();
}
Expand All @@ -37,4 +40,32 @@ public override IEnumerable<Variable> FindVariables(IMethodVariables chain)

foreach (var variable in _inner.SelectMany(x => x.FindVariables(chain))) yield return variable;
}

/// <summary>
/// Sort event processing frames by event type hierarchy
/// </summary>
/// <param name="frames"></param>
/// <returns></returns>
internal static IEnumerable<EventProcessingFrame> SortByEventTypeHierarchy(IEnumerable<EventProcessingFrame> frames)
{
return new SortedSet<EventProcessingFrame>(frames, new EventTypeComparer());
}

/// <summary>
/// Sort frames by event type hierarchy
/// <remarks>Comparer is not safe to use outside of intended purpose</remarks>
/// </summary>
private class EventTypeComparer: IComparer<EventProcessingFrame>
{
public int Compare(EventProcessingFrame x, EventProcessingFrame y)
{
if (x.EventType.CanBeCastTo(y.EventType))
return -1;

if (y.EventType.CanBeCastTo(x.EventType))
return 1;

return 0;
}
}
}
Loading