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

Fixes #1369 - Projections inherited from a base class that hides the Id property, projects to an empty Id. #1371

Merged
merged 9 commits into from
Nov 3, 2019
46 changes: 46 additions & 0 deletions documentation/documentation/documents/json/newtonsoft.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,49 @@ Marten actually has to keep two Newtonsoft.Json serializers, with one being a "c
the customization is done with a nested closure so that the same configuration is always applied to both internal <code>JsonSerializer's</code>.
</div>

## Enum Storage

Marten allows how enum values are being stored. By default, they are stored as integers but it is possible to change that to storing them as strings.

To do that you need to change the serialization settings in the `DocumentStore` options.

<[sample:customize_json_net_enum_storage_serialization]>

## Fields Names Casing

Marten by default stores field names "as they are" (C# naming convention is PascalCase for public properties).

You can have them also automatically formatted to:
- `CamelCase`,
- `snake_case`
by changing the serialization settings in the `DocumentStore` options.

<[sample:customize_json_net_camelcase_casing_serialization]>

<[sample:customize_json_net_snakecase_casing_serialization]>

## Collection Storage

Marten by default stores the collections as strongly typed (so with $type and $value). Because of that and current `MartenQueryable` limitations, it might result in not properly resolved nested collections queries.

Changing the collection storage to `AsArray` using a custom `JsonConverter` will store it as regular JSON array for the following:
- `ICollection<>`,
- `IList<>`,
- `IReadOnlyCollection<>`,
- `IEnumerable<>`.

That improves the nested collections queries handling.

To do that you need to change the serialization settings in the `DocumentStore` options.

<[sample:customize_json_net_snakecase_collectionstorage]>

## Non Public Members Storage

By default `Newtonsoft.Json` only deserializes properties with public setters.

You can allow deserialisation of properties with non-public setters by changing the serialization settings in the `DocumentStore` options.

<[sample:customize_json_net_snakecase_nonpublicmembersstorage_nonpublicsetters]>


Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ private class CustomJsonSerializer: ISerializer

public CollectionStorage CollectionStorage => throw new NotImplementedException();

public NonPublicMembersStorage NonPublicMembersStorage => throw new NotImplementedException();

public T FromJson<T>(TextReader reader)
{
throw new NotImplementedException();
Expand Down
148 changes: 148 additions & 0 deletions src/Marten.Testing/Events/Projections/QuestMonsters.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Baseline;

namespace Marten.Testing.Events.Projections
{
public class Root
{
public Guid Id { get; set; }
}

public interface IMonstersView
{
Guid Id { get; }
string[] Monsters { get; }
}

public class QuestMonsters
{
public Guid Id { get; set; }

private readonly IList<string> _monsters = new List<string>();

public void Apply(MonsterSlayed slayed)
{
_monsters.Fill(slayed.Name);
}

public string[] Monsters
{
get { return _monsters.ToArray(); }
set
{
_monsters.Clear();
_monsters.AddRange(value);
}
}
}

public class QuestMonstersWithPrivateIdSetter: IMonstersView
{
public Guid Id { get; private set; }

private readonly IList<string> _monsters = new List<string>();

public void Apply(MonsterSlayed slayed)
{
_monsters.Fill(slayed.Name);
}

public string[] Monsters
{
get { return _monsters.ToArray(); }
set
{
_monsters.Clear();
_monsters.AddRange(value);
}
}
}

public class QuestMonstersWithProtectedIdSetter: IMonstersView
{
public Guid Id { get; protected set; }

private readonly IList<string> _monsters = new List<string>();

public void Apply(MonsterSlayed slayed)
{
_monsters.Fill(slayed.Name);
}

public string[] Monsters
{
get { return _monsters.ToArray(); }
set
{
_monsters.Clear();
_monsters.AddRange(value);
}
}
}

public class QuestMonstersWithBaseClass: Root, IMonstersView
{
private readonly IList<string> _monsters = new List<string>();

public void Apply(MonsterSlayed slayed)
{
_monsters.Fill(slayed.Name);
}

public string[] Monsters
{
get { return _monsters.ToArray(); }
set
{
_monsters.Clear();
_monsters.AddRange(value);
}
}
}

public class QuestMonstersWithBaseClassAndIdOverloaded: Root, IMonstersView
{
public Guid Id { get; set; }

private readonly IList<string> _monsters = new List<string>();

public void Apply(MonsterSlayed slayed)
{
_monsters.Fill(slayed.Name);
}

public string[] Monsters
{
get { return _monsters.ToArray(); }
set
{
_monsters.Clear();
_monsters.AddRange(value);
}
}
}

public class QuestMonstersWithBaseClassAndIdOverloadedWithNew: Root, IMonstersView
{
public new Guid Id { get; set; }

private readonly IList<string> _monsters = new List<string>();

public void Apply(MonsterSlayed slayed)
{
_monsters.Fill(slayed.Name);
}

public string[] Monsters
{
get { return _monsters.ToArray(); }
set
{
_monsters.Clear();
_monsters.AddRange(value);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Baseline;
using Marten.Services;
using Marten.Storage;
using Xunit;
Expand All @@ -11,16 +7,12 @@ namespace Marten.Testing.Events.Projections
{
public class inline_aggregation_by_stream_with_multiples: DocumentSessionFixture<NulloIdentityMap>
{
private QuestStarted started = new QuestStarted { Name = "Find the Orb" };
private MembersJoined joined = new MembersJoined { Day = 2, Location = "Faldor's Farm", Members = new string[] { "Garion", "Polgara", "Belgarath" } };
private MonsterSlayed slayed1 = new MonsterSlayed { Name = "Troll" };
private MonsterSlayed slayed2 = new MonsterSlayed { Name = "Dragon" };
private readonly QuestStarted started = new QuestStarted { Name = "Find the Orb" };
private readonly MembersJoined joined = new MembersJoined { Day = 2, Location = "Faldor's Farm", Members = new string[] { "Garion", "Polgara", "Belgarath" } };
private readonly MonsterSlayed slayed1 = new MonsterSlayed { Name = "Troll" };
private readonly MonsterSlayed slayed2 = new MonsterSlayed { Name = "Dragon" };

private MembersJoined joined2 = new MembersJoined { Day = 5, Location = "Sendaria", Members = new string[] { "Silk", "Barak" } };

public inline_aggregation_by_stream_with_multiples()
{
}
private readonly MembersJoined joined2 = new MembersJoined { Day = 5, Location = "Sendaria", Members = new string[] { "Silk", "Barak" } };

[Theory]
[InlineData(TenancyStyle.Single)]
Expand Down Expand Up @@ -77,26 +69,4 @@ public async Task run_multiple_aggregates_async()
.ShouldHaveTheSameElementsAs("Garion", "Polgara", "Belgarath", "Silk", "Barak");
}
}

public class QuestMonsters
{
public Guid Id { get; set; }

private readonly IList<string> _monsters = new List<string>();

public void Apply(MonsterSlayed slayed)
{
_monsters.Fill(slayed.Name);
}

public string[] Monsters
{
get { return _monsters.ToArray(); }
set
{
_monsters.Clear();
_monsters.AddRange(value);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.Linq;
using Marten.Services;
using Shouldly;
using Xunit;

namespace Marten.Testing.Events.Projections
{
public class inline_aggregation_with_base_view_class: DocumentSessionFixture<NulloIdentityMap>
{
private readonly MonsterSlayed slayed1 = new MonsterSlayed { Name = "Troll" };
private readonly MonsterSlayed slayed2 = new MonsterSlayed { Name = "Dragon" };
private readonly Guid streamId;

public inline_aggregation_with_base_view_class()
{
StoreOptions(_ =>
{
_.AutoCreateSchemaObjects = AutoCreate.All;
_.Events.InlineProjections.AggregateStreamsWith<QuestMonstersWithBaseClass>();
_.Events.InlineProjections.AggregateStreamsWith<QuestMonstersWithBaseClassAndIdOverloaded>();
_.Events.InlineProjections.AggregateStreamsWith<QuestMonstersWithBaseClassAndIdOverloadedWithNew>();
});

streamId = theSession.Events
.StartStream<QuestMonstersWithBaseClass>(slayed1, slayed2).Id;

theSession.SaveChanges();
}

[Fact]
public void run_inline_aggregation_with_base_view_class()
{
VerifyProjection<QuestMonstersWithBaseClass>();
}

[Fact]
public void run_inline_aggregation_with_base_class_and_id_overloaded()
{
VerifyProjection<QuestMonstersWithBaseClassAndIdOverloaded>();
}

[Fact]
public void run_inline_aggregation_with_base_class_and_id_overloaded_with_new()
{
VerifyProjection<QuestMonstersWithBaseClassAndIdOverloadedWithNew>();
}

private void VerifyProjection<T>() where T : IMonstersView
{
var loadedView = theSession.Load<T>(streamId);

loadedView.Id.ShouldBe(streamId);
loadedView.Monsters.ShouldHaveTheSameElementsAs("Troll", "Dragon");

var queriedView = theSession.Query<T>()
.Single(x => x.Id == streamId);

queriedView.Id.ShouldBe(streamId);
queriedView.Monsters.ShouldHaveTheSameElementsAs("Troll", "Dragon");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using System;
using System.Linq;
using Marten.Services;
using Shouldly;
using Xunit;

namespace Marten.Testing.Events.Projections
{
public class inline_aggregation_with_non_public_setter: DocumentSessionFixture<NulloIdentityMap>
{
private readonly MonsterSlayed slayed1 = new MonsterSlayed { Name = "Troll" };
private readonly MonsterSlayed slayed2 = new MonsterSlayed { Name = "Dragon" };
private readonly Guid streamId;

public inline_aggregation_with_non_public_setter()
{
StoreOptions(_ =>
{
_.AutoCreateSchemaObjects = AutoCreate.All;
_.UseDefaultSerialization(nonPublicMembersStorage: NonPublicMembersStorage.NonPublicSetters);
_.Events.InlineProjections.AggregateStreamsWith<QuestMonstersWithPrivateIdSetter>();
_.Events.InlineProjections.AggregateStreamsWith<QuestMonstersWithProtectedIdSetter>();
});

streamId = theSession.Events
.StartStream<QuestMonstersWithBaseClass>(slayed1, slayed2).Id;

theSession.SaveChanges();
}

[Fact]
public void run_inline_aggregation_with_private_id_setter()
{
VerifyProjection<QuestMonstersWithPrivateIdSetter>();
}

[Fact]
public void run_inline_aggregation_with_protected_id_setter()
{
VerifyProjection<QuestMonstersWithProtectedIdSetter>();
}

private void VerifyProjection<T>() where T : IMonstersView
{
var loadedView = theSession.Load<T>(streamId);

loadedView.Id.ShouldBe(streamId);
loadedView.Monsters.ShouldHaveTheSameElementsAs("Troll", "Dragon");

var queriedView = theSession.Query<T>()
.Single(x => x.Id == streamId);

queriedView.Id.ShouldBe(streamId);
queriedView.Monsters.ShouldHaveTheSameElementsAs("Troll", "Dragon");
}
}
}
Loading