Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ private class CustomJsonSerializer : ISerializer

public Casing Casing => throw new NotImplementedException();

public CollectionStorage CollectionStorage => throw new NotImplementedException();

public T FromJson<T>(TextReader reader)
{
throw new NotImplementedException();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Marten.Services.Json;
using Newtonsoft.Json;
using Shouldly;
using Xunit;

namespace Marten.Testing.Linq
{
public class TypeWithInnerCollectionsWithJsonConverterAttribute
{
public Guid Id { get; set; }
public string Flatten { get; set; }
public string[] Array { get; set; }
public List<string> List { get; set; }
public IList<string> IList { get; set; }

[JsonConverter(typeof(JsonNetCollectionToArrayJsonConverter))]
public IEnumerable<string> Enumerable { get; set; }

[JsonConverter(typeof(JsonNetCollectionToArrayJsonConverter))]
public IEnumerable<string> IEnumerableFromArray { get; set; }

[JsonConverter(typeof(JsonNetCollectionToArrayJsonConverter))]
public IEnumerable<string> IEnumerbaleFromList { get; set; }

[JsonConverter(typeof(JsonNetCollectionToArrayJsonConverter))]
public ICollection<string> ICollection { get; set; }

[JsonConverter(typeof(JsonNetCollectionToArrayJsonConverter))]
public IReadOnlyCollection<string> IReadonlyCollection { get; set; }

[JsonConverter(typeof(JsonNetCollectionToArrayJsonConverter))]
public IReadOnlyCollection<TypeWithInnerCollectionsWithJsonConverterAttribute> IReadonlyCollectionOfInnerClasses { get; set; }

public static TypeWithInnerCollectionsWithJsonConverterAttribute Create(params string[] array)
{
return new TypeWithInnerCollectionsWithJsonConverterAttribute()
{
Id = Guid.NewGuid(),
Flatten = array.Aggregate((i, j) => i + j),
Array = array,
List = array.ToList(),
IList = array.ToList(),
Enumerable = array.AsEnumerable(),
IEnumerableFromArray = array,
IEnumerbaleFromList = array.ToList(),
ICollection = array.ToList(),
IReadonlyCollection = array.ToList(),
IReadonlyCollectionOfInnerClasses = new List<TypeWithInnerCollectionsWithJsonConverterAttribute>
{
new TypeWithInnerCollectionsWithJsonConverterAttribute()
{
Id = Guid.NewGuid(),
Flatten = array.Aggregate((i, j) => i + j),
Array = array,
List = array.ToList(),
IList = array.ToList(),
Enumerable = array.AsEnumerable(),
IEnumerableFromArray = array,
ICollection = array.ToList(),
IEnumerbaleFromList = array.ToList(),
IReadonlyCollection = array.ToList(),
}
}
};
}
};

[ControlledQueryStoryteller]
public class query_with_inner_query_with_CollectionToArrayJsonConverter_onProperty : IntegratedFixture
{
private static readonly TypeWithInnerCollectionsWithJsonConverterAttribute[] TestData = new TypeWithInnerCollectionsWithJsonConverterAttribute[]
{
TypeWithInnerCollectionsWithJsonConverterAttribute.Create("one", "two"),
TypeWithInnerCollectionsWithJsonConverterAttribute.Create("two", "three"),
TypeWithInnerCollectionsWithJsonConverterAttribute.Create("four", "five"),
};

private const string SearchPhrase = "two";

public static readonly TheoryData<Expression<Func<TypeWithInnerCollectionsWithJsonConverterAttribute, bool>>> Predicates = new TheoryData<Expression<Func<TypeWithInnerCollectionsWithJsonConverterAttribute, bool>>>
{
x => x.Array.Contains(SearchPhrase),
x => x.Enumerable.Contains(SearchPhrase),
x => x.IEnumerableFromArray.Contains(SearchPhrase),
x => x.IEnumerbaleFromList.Contains(SearchPhrase),
x => x.List.Contains(SearchPhrase),
x => x.IList.Contains(SearchPhrase),
x => x.ICollection.Contains(SearchPhrase),
x => x.IReadonlyCollection.Contains(SearchPhrase),
x => x.IReadonlyCollection.Where(e => e == SearchPhrase).Any(),
x => x.IReadonlyCollectionOfInnerClasses.Where(e => e.Flatten == "onetwo").Any() || x.IReadonlyCollectionOfInnerClasses.Where(e => e.Flatten == "twothree").Any()
};

[Theory]
[MemberData(nameof(Predicates))]
public async Task having_type_with_CollectionToArrayJsonConverter_can_query_against_array_of_string(Expression<Func<TypeWithInnerCollectionsWithJsonConverterAttribute, bool>> predicate)
{
SetupTestData();

using (var query = theStore.QuerySession())
{
var results = await query.Query<TypeWithInnerCollectionsWithJsonConverterAttribute>()
.Where(predicate)
.ToListAsync();

results.Count.ShouldBe(2);
results.All(e => e.Enumerable.Contains(SearchPhrase)).ShouldBeTrue();
}
}

private void SetupTestData()
{
using (var session = theStore.OpenSession())
{
session.Store(TestData);
session.SaveChanges();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Shouldly;
using Xunit;

namespace Marten.Testing.Linq
{
public class TypeWithInnerCollections
{
public Guid Id { get; set; }
public string Flatten { get; set; }
public string[] Array { get; set; }
public List<string> List { get; set; }
public IList<string> IList { get; set; }

public IEnumerable<string> Enumerable { get; set; }

public IEnumerable<string> IEnumerableFromArray { get; set; }

public IEnumerable<string> IEnumerbaleFromList { get; set; }

public ICollection<string> ICollection { get; set; }

public IReadOnlyCollection<string> IReadonlyCollection { get; set; }

public IReadOnlyCollection<TypeWithInnerCollections> IReadonlyCollectionOfInnerClasses { get; set; }

public static TypeWithInnerCollections Create(params string[] array)
{
return new TypeWithInnerCollections()
{
Id = Guid.NewGuid(),
Flatten = array.Aggregate((i, j) => i + j),
Array = array,
List = array.ToList(),
IList = array.ToList(),
Enumerable = array.AsEnumerable(),
IEnumerableFromArray = array,
IEnumerbaleFromList = array.ToList(),
ICollection = array.ToList(),
IReadonlyCollection = array.ToList(),
IReadonlyCollectionOfInnerClasses = new List<TypeWithInnerCollections>
{
new TypeWithInnerCollections()
{
Id = Guid.NewGuid(),
Flatten = array.Aggregate((i, j) => i + j),
Array = array,
List = array.ToList(),
IList = array.ToList(),
Enumerable = array.AsEnumerable(),
IEnumerableFromArray = array,
ICollection = array.ToList(),
IEnumerbaleFromList = array.ToList(),
IReadonlyCollection = array.ToList(),
}
}
};
}
}

[ControlledQueryStoryteller]
public class query_with_inner_query_with_global_CollectionStorage_WithArray : IntegratedFixture
{
private static readonly TypeWithInnerCollections[] TestData = new TypeWithInnerCollections[]
{
TypeWithInnerCollections.Create("one", "two"),
TypeWithInnerCollections.Create("two", "three"),
TypeWithInnerCollections.Create("four", "five"),
};

private const string SearchPhrase = "two";

public static readonly TheoryData<Expression<Func<TypeWithInnerCollections, bool>>> Predicates = new TheoryData<Expression<Func<TypeWithInnerCollections, bool>>>
{
x => x.Array.Contains(SearchPhrase),
x => x.Enumerable.Contains(SearchPhrase),
x => x.IEnumerableFromArray.Contains(SearchPhrase),
x => x.IEnumerbaleFromList.Contains(SearchPhrase),
x => x.List.Contains(SearchPhrase),
x => x.IList.Contains(SearchPhrase),
x => x.ICollection.Contains(SearchPhrase),
x => x.IReadonlyCollection.Contains(SearchPhrase),
x => x.IReadonlyCollection.Where(e => e == SearchPhrase).Any(),
x => x.IReadonlyCollectionOfInnerClasses.Where(e => e.Flatten == "onetwo").Any() || x.IReadonlyCollectionOfInnerClasses.Where(e => e.Flatten == "twothree").Any()
};

[Theory]
[MemberData(nameof(Predicates))]
public async Task having_store_options_with_CollectionStorage_AsArray_can_query_against_array_of_string(Expression<Func<TypeWithInnerCollections, bool>> predicate)
{
StoreOptions(options =>
{
options.UseDefaultSerialization(collectionStorage: CollectionStorage.AsArray);
});
SetupTestData();

using (var query = theStore.QuerySession())
{
var results = await query.Query<TypeWithInnerCollections>()
.Where(predicate)
.ToListAsync();

results.Count.ShouldBe(2);
results.All(e => e.Enumerable.Contains(SearchPhrase)).ShouldBeTrue();
}
}

private void SetupTestData()
{
using (var session = theStore.OpenSession())
{
session.Store(TestData);
session.SaveChanges();
}
}
}
}
10 changes: 9 additions & 1 deletion src/Marten/ISerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ public interface ISerializer
/// <returns></returns>
string ToJson(object document);


/// <summary>
/// Deserialize a JSON string into an object of type T
/// </summary>
Expand Down Expand Up @@ -52,7 +51,10 @@ public interface ISerializer
EnumStorage EnumStorage { get; }

Casing Casing { get; }

CollectionStorage CollectionStorage { get; }
}

// ENDSAMPLE

public enum EnumStorage
Expand All @@ -67,4 +69,10 @@ public enum Casing
CamelCase,
SnakeCase
}

public enum CollectionStorage
{
Default,
AsArray
}
}
43 changes: 43 additions & 0 deletions src/Marten/Services/Json/JsonNetCollectionToArrayJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;

namespace Marten.Services.Json
{
/// <summary>
/// Serialize collection type property to JSON array using a custom Newtonsoft.Json JsonConverter
/// Note that without using custom `JsonConverter`, `Newtonsoft.Json` stores it as $type and $value.
/// Or you may need to resort to `Newtonsoft.Json.TypeNameHandling.None` which has its own side-effects
/// </summary>
public class JsonNetCollectionToArrayJsonConverter : JsonConverter
{
public static JsonNetCollectionToArrayJsonConverter Instance = new JsonNetCollectionToArrayJsonConverter();

private readonly static List<Type> _types = new List<Type>
{
typeof(ICollection<>),
typeof(IList<>),
typeof(IReadOnlyCollection<>),
typeof(IEnumerable<>)
};

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var o = value as IEnumerable<object>;
serializer.Serialize(writer, o?.ToArray());
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return serializer.Deserialize(reader, objectType);
}

public override bool CanConvert(Type objectType)
{
return _types.Contains(objectType)
|| objectType.IsArray
|| (objectType.IsGenericType && _types.Contains(objectType.GetGenericTypeDefinition()));
}
}
}
49 changes: 49 additions & 0 deletions src/Marten/Services/Json/JsonNetContractResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Reflection;
using Marten.Util;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace Marten.Services.Json
{
public class JsonNetContractResolver : DefaultContractResolver
{
public Casing Casing { get; }

public CollectionStorage CollectionStorage { get; }

public JsonNetContractResolver()
{
}

public JsonNetContractResolver(Casing casing, CollectionStorage collectionStorage)
{
Casing = casing;
CollectionStorage = collectionStorage;

SetNamingStrategy(casing);
}

protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);

if (CollectionStorage == CollectionStorage.AsArray && JsonNetCollectionToArrayJsonConverter.Instance.CanConvert(property.PropertyType))
{
property.Converter = JsonNetCollectionToArrayJsonConverter.Instance;
}
return property;
}

private void SetNamingStrategy(Casing casing)
{
if (casing == Casing.CamelCase)
{
NamingStrategy = new CamelCaseNamingStrategy { ProcessDictionaryKeys = true, OverrideSpecifiedNames = true };
}
else if (casing == Casing.SnakeCase)
{
NamingStrategy = new SnakeCaseNamingStrategy { ProcessDictionaryKeys = true, OverrideSpecifiedNames = true };
};
}
}
}
Loading