diff --git a/NHibernate.Caches.Common.Tests/BinaryCacheSerializerFixture.cs b/NHibernate.Caches.Common.Tests/BinaryCacheSerializerFixture.cs new file mode 100644 index 00000000..f27d7df1 --- /dev/null +++ b/NHibernate.Caches.Common.Tests/BinaryCacheSerializerFixture.cs @@ -0,0 +1,11 @@ +using System; +using NUnit.Framework; + +namespace NHibernate.Caches.Common.Tests +{ + [TestFixture] + public class BinaryCacheSerializerFixture : CacheSerializerFixture + { + protected override Func SerializerProvider => () => new BinaryCacheSerializer(); + } +} diff --git a/NHibernate.Caches.Common.Tests/CacheSerializerFixture.cs b/NHibernate.Caches.Common.Tests/CacheSerializerFixture.cs new file mode 100644 index 00000000..15853266 --- /dev/null +++ b/NHibernate.Caches.Common.Tests/CacheSerializerFixture.cs @@ -0,0 +1,625 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.Design.Serialization; +using System.Globalization; +using System.Linq; +using System.Xml; +using System.Xml.Linq; +using NHibernate.Cache; +using NHibernate.Cache.Entry; +using NHibernate.Engine; +using NHibernate.Intercept; +using NHibernate.Properties; +using NHibernate.Type; +using NSubstitute; +using NUnit.Framework; + +namespace NHibernate.Caches.Common.Tests +{ + [TestFixture] + public abstract class CacheSerializerFixture + { + protected abstract Func SerializerProvider { get; } + + protected CacheSerializerBase DefaultSerializer { get; private set; } + + [OneTimeSetUp] + public void FixtureSetup() + { + DefaultSerializer = SerializerProvider(); + } + + [Test] + public void TestInteger() + { + var original = 15; + var data = DefaultSerializer.Serialize(original); + var copy = DefaultSerializer.Deserialize(data); + + Assert.That(copy, Is.TypeOf()); + Assert.That(copy, Is.EqualTo(original)); + } + + [Test] + public void TestObjectArray() + { + var original = GetAllNHibernateTypeValues(); + var data = DefaultSerializer.Serialize(original); + var copy = (object[]) DefaultSerializer.Deserialize(data); + AssertEqual(original, copy); + } + + [Test] + public void TestListOfObjects() + { + var original = CreateListOfObjects(); + var data = DefaultSerializer.Serialize(original); + var copy = (List) DefaultSerializer.Deserialize(data); + AssertEqual(original, copy); + } + + [Test] + public void TestHashtableIntegerKey() + { + var original = CreateHashtable(i => i); + var data = DefaultSerializer.Serialize(original); + var copy = (Hashtable) DefaultSerializer.Deserialize(data); + AssertEqual(original, copy); + } + + [Test] + public void TestHashtableGuidKey() + { + var original = CreateHashtable(i => Guid.NewGuid()); + var data = DefaultSerializer.Serialize(original); + var copy = (Hashtable) DefaultSerializer.Deserialize(data); + AssertEqual(original, copy); + } + + [Test] + public void TestHashtableStringKey() + { + var original = CreateHashtable(i => i.ToString()); + var data = DefaultSerializer.Serialize(original); + var copy = (Hashtable) DefaultSerializer.Deserialize(data); + AssertEqual(original, copy); + } + + [Test] + public void TestHashtableCharKey() + { + var original = CreateHashtable(i => (char) (64 + i)); + var data = DefaultSerializer.Serialize(original); + var copy = (Hashtable) DefaultSerializer.Deserialize(data); + AssertEqual(original, copy); + } + + [Test] + public void TestHashtableDateTime() + { + var original = CreateHashtable(i => DateTime.Now.AddDays(i)); + var data = DefaultSerializer.Serialize(original); + var copy = (Hashtable) DefaultSerializer.Deserialize(data); + AssertEqual(original, copy); + } + + [Test] + public void TestCustomObject() + { + var original = new CustomEntity {Id = 10}; + var data = DefaultSerializer.Serialize(original); + var copy = (CustomEntity) DefaultSerializer.Deserialize(data); + + Assert.That(copy.Id, Is.EqualTo(original.Id)); + } + + [Test] + public void TestNullableInt32Type() + { + var serializer = DefaultSerializer; + var original = new object[] {NullableInt32.Default, new NullableInt32(32)}; + var data = serializer.Serialize(original); + var copy = (object[]) serializer.Deserialize(data); + AssertEqual(original, copy); + } + + [Test] + public void TestCacheEntry() + { + var original = CreateCacheEntry(); + var data = DefaultSerializer.Serialize(original); + var copy = (CacheEntry) DefaultSerializer.Deserialize(data); + AssertEqual(original, copy); + } + + [Test] + public void TestCollectionCacheEntry() + { + var original = CreateCollectionCacheEntry(); + var data = DefaultSerializer.Serialize(original); + var copy = (CollectionCacheEntry) DefaultSerializer.Deserialize(data); + AssertEqual(original, copy); + } + + [Test] + public void TestCacheLock() + { + var original = CreateCacheLock(); + var data = DefaultSerializer.Serialize(original); + var copy = (CacheLock) DefaultSerializer.Deserialize(data); + AssertEqual(original, copy); + } + + [Test] + public void TestCachedItem() + { + // CacheEntry + var original = CreateCachedItem(CreateCacheEntry()); + var data = DefaultSerializer.Serialize(original); + var copy = (CachedItem) DefaultSerializer.Deserialize(data); + AssertEqual(original, copy); + + // CollectionCacheEntry + original = CreateCachedItem(CreateCollectionCacheEntry()); + data = DefaultSerializer.Serialize(original); + copy = (CachedItem) DefaultSerializer.Deserialize(data); + AssertEqual(original, copy); + } + + [Test] + public void TestAnyTypeObjectTypeCacheEntry() + { + var original = CreateObjectTypeCacheEntry(); + var data = DefaultSerializer.Serialize(original); + var copy = (AnyType.ObjectTypeCacheEntry) DefaultSerializer.Deserialize(data); + AssertEqual(original, copy); + } + + protected void AssertEqual(CacheEntry original, CacheEntry copy) + { + Assert.That(copy.Version, Is.EqualTo(original.Version)); + Assert.That(copy.Version, Is.TypeOf(original.Version.GetType())); + Assert.That(copy.Subclass, Is.EqualTo(original.Subclass)); + Assert.That(copy.AreLazyPropertiesUnfetched, Is.EqualTo(original.AreLazyPropertiesUnfetched)); + for (var i = 0; i < copy.DisassembledState.Length; i++) + { + if (original.DisassembledState[i] == null) + { + Assert.That(copy.DisassembledState[i], Is.Null); + continue; + } + + Assert.That(copy.DisassembledState[i], Is.TypeOf(original.DisassembledState[i].GetType())); + if (original.DisassembledState[i] is AnyType.ObjectTypeCacheEntry originalAnyType) + { + var copyAnyType = (AnyType.ObjectTypeCacheEntry) copy.DisassembledState[i]; + AssertEqual(originalAnyType, copyAnyType); + } + else + { + Assert.That(copy.DisassembledState[i], Is.EqualTo(original.DisassembledState[i])); + } + } + } + + protected void AssertEqual(CachedItem original, CachedItem copy) + { + Assert.That(copy.Version, Is.EqualTo(original.Version)); + Assert.That(copy.Version, Is.TypeOf(original.Version.GetType())); + Assert.That(copy.Value, Is.TypeOf(original.Value.GetType())); + switch (original.Value) + { + case CacheEntry cacheEntry: + AssertEqual(cacheEntry, (CacheEntry) copy.Value); + break; + case CollectionCacheEntry colleectionCacheEntry: + AssertEqual(colleectionCacheEntry, (CollectionCacheEntry) copy.Value); + break; + default: + Assert.That(copy.Value, Is.EqualTo(original.Value)); + break; + } + Assert.That(copy.FreshTimestamp, Is.EqualTo(original.FreshTimestamp)); + } + + protected void AssertEqual(CollectionCacheEntry original, CollectionCacheEntry copy) + { + Assert.That(copy.State, Is.TypeOf(original.State.GetType())); + + var originalArray = original.State; + var copyArray = copy.State; + + for (var i = 0; i < copyArray.Length; i++) + { + if (originalArray[i] == null) + { + Assert.That(copyArray[i], Is.Null); + continue; + } + + Assert.That(copyArray[i], Is.TypeOf(originalArray[i].GetType())); + if (originalArray[i] is AnyType.ObjectTypeCacheEntry originalAnyType) + { + var copyAnyType = (AnyType.ObjectTypeCacheEntry) copyArray[i]; + AssertEqual(originalAnyType, copyAnyType); + } + else + { + Assert.That(copyArray[i], Is.EqualTo(originalArray[i])); + } + } + } + + protected void AssertEqual(CacheLock original, CacheLock copy) + { + Assert.That(copy.Version, Is.EqualTo(original.Version)); + Assert.That(copy.Version, Is.TypeOf(original.Version.GetType())); + Assert.That(copy.Id, Is.EqualTo(original.Id)); + Assert.That(copy.Multiplicity, Is.EqualTo(original.Multiplicity)); + Assert.That(copy.Timeout, Is.EqualTo(original.Timeout)); + Assert.That(copy.UnlockTimestamp, Is.EqualTo(original.UnlockTimestamp)); + Assert.That(copy.WasLockedConcurrently, Is.EqualTo(original.WasLockedConcurrently)); + } + + protected void AssertEqual(AnyType.ObjectTypeCacheEntry original, AnyType.ObjectTypeCacheEntry copy) + { + Assert.That(copy.Id, Is.EqualTo(original.Id)); + Assert.That(copy.EntityName, Is.EqualTo(original.EntityName)); + } + + protected void AssertEqual(Hashtable original, Hashtable copy) + { + Assert.That(copy, Has.Count.EqualTo(original.Count)); + foreach (DictionaryEntry entry in original) + { + Assert.That(copy.ContainsKey(entry.Key), Is.True, $"Key {entry.Key} for value {entry.Value} was not found."); + AssertEqual(entry.Value, copy[entry.Key]); + } + } + + protected void AssertEqual(object[] original, object[] copy) + { + Assert.That(copy, Has.Length.EqualTo(original.Length)); + for (var i = 0; i < original.Length; i++) + { + AssertEqual(original[i], copy[i]); + } + } + + protected void AssertEqual(List original, List copy) + { + Assert.That(copy, Has.Count.EqualTo(original.Count)); + for (var i = 0; i < original.Count; i++) + { + AssertEqual(original[i], copy[i]); + } + } + + protected void AssertEqual(object original, object copy) + { + if (original == null) + { + Assert.That(copy, Is.Null); + return; + } + + if (original is AnyType.ObjectTypeCacheEntry anyCacheEntry) + { + Assert.That(copy, Is.TypeOf()); + AssertEqual(anyCacheEntry, (AnyType.ObjectTypeCacheEntry) copy); + return; + } + + Assert.That(copy, Is.TypeOf(original.GetType())); + Assert.That(copy, Is.EqualTo(original)); + } + + protected Hashtable CreateHashtable(Func keyProvider) + { + var hashtable = new Hashtable(); + var values = GetAllNHibernateTypeValues(); + for (var i = 0; i < values.Length; i++) + { + hashtable.Add(keyProvider(i), values[i]); + } + + return hashtable; + } + + protected List CreateListOfObjects() + { + return GetAllNHibernateTypeValues().ToList(); + } + + protected CacheEntry CreateCacheEntry() + { + return new CacheEntry + { + DisassembledState = GetAllNHibernateTypeValues(), + Version = 1, + Subclass = "TestClass", + AreLazyPropertiesUnfetched = true + }; + } + + protected CollectionCacheEntry CreateCollectionCacheEntry() + { + return new CollectionCacheEntry + { + State = GetAllNHibernateTypeValues() + }; + } + + protected CacheLock CreateCacheLock() + { + return new CacheLock + { + Timeout = 1234, Id = 1, Version = 5 + }; + } + + protected CachedItem CreateCachedItem(object data) + { + return new CachedItem + { + Value = data, FreshTimestamp = 111, Version = 5 + }; + } + + protected AnyType.ObjectTypeCacheEntry CreateObjectTypeCacheEntry() + { + return new AnyType.ObjectTypeCacheEntry + { + EntityName = "Test", + Id = 1 + }; + } + + [Serializable] + public class CustomEntity + { + public int Id { get; set; } + } + + protected Dictionary GetNHibernateTypes() + { + var entityName = nameof(CustomEntity); + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml("XmlDoc"); + return new Dictionary + { + {NHibernateUtil.AnsiString, "test"}, + {NHibernateUtil.Binary, new byte[] {1, 2, 3, 4}}, + {NHibernateUtil.BinaryBlob, new byte[] {1, 2, 3, 4}}, + {NHibernateUtil.Boolean, true}, + {NHibernateUtil.Byte, (byte) 1}, + {NHibernateUtil.Character, 'a'}, + {NHibernateUtil.CultureInfo, CultureInfo.CurrentCulture}, + {NHibernateUtil.DateTime, DateTime.Now}, + {NHibernateUtil.DateTimeNoMs, DateTime.Now}, + {NHibernateUtil.LocalDateTime, DateTime.Now}, + {NHibernateUtil.UtcDateTime, DateTime.UtcNow}, + {NHibernateUtil.LocalDateTimeNoMs, DateTime.Now}, + {NHibernateUtil.UtcDateTimeNoMs, DateTime.UtcNow}, + {NHibernateUtil.DateTimeOffset, DateTimeOffset.Now}, + {NHibernateUtil.Date, DateTime.Today}, + {NHibernateUtil.Decimal, 2.5m}, + {NHibernateUtil.Double, 2.5d}, + {NHibernateUtil.Currency, 2.5m}, + {NHibernateUtil.Guid, Guid.NewGuid()}, + {NHibernateUtil.Int16, (short) 1}, + {NHibernateUtil.Int32, 3}, + {NHibernateUtil.Int64, 3L}, + {NHibernateUtil.SByte, (sbyte) 1}, + {NHibernateUtil.UInt16, (ushort) 1}, + {NHibernateUtil.UInt32, (uint) 1}, + {NHibernateUtil.UInt64, (ulong) 1}, + {NHibernateUtil.Single, 1.1f}, + {NHibernateUtil.String, "test"}, + {NHibernateUtil.StringClob, "test"}, + {NHibernateUtil.Time, DateTime.Now}, + {NHibernateUtil.Ticks, DateTime.Now}, + {NHibernateUtil.TimeAsTimeSpan, TimeSpan.FromMilliseconds(15)}, + {NHibernateUtil.TimeSpan, TimeSpan.FromMilliseconds(1234)}, + {NHibernateUtil.DbTimestamp, DateTime.Now}, + {NHibernateUtil.TrueFalse, false}, + {NHibernateUtil.YesNo, true}, + {NHibernateUtil.Class, typeof(IType)}, + {NHibernateUtil.MetaType, entityName}, + {NHibernateUtil.Serializable, new CustomEntity {Id = 1}}, + {NHibernateUtil.Object, new CustomEntity {Id = 10}}, + {NHibernateUtil.AnsiChar, 'a'}, + {NHibernateUtil.XmlDoc, xmlDoc}, + {NHibernateUtil.XDoc, XDocument.Parse("XDoc")}, + {NHibernateUtil.Uri, new Uri("http://test.com")} + }; + } + + protected object[] GetAllNHibernateTypeValues() + { + var types = GetNHibernateTypes(); + var sessionImpl = Substitute.For(); + sessionImpl.BestGuessEntityName(Arg.Any()).Returns(o => o[0].GetType().Name); + sessionImpl.GetContextEntityIdentifier(Arg.Is(o => o is CustomEntity)).Returns(o => ((CustomEntity) o[0]).Id); + return TypeHelper.Disassemble( + types.Values.ToArray(), + types.Keys.Cast().ToArray(), + null, + sessionImpl, + null) + .Concat( + new[] + { + LazyPropertyInitializer.UnfetchedProperty, + BackrefPropertyAccessor.Unknown, + null + }) + .ToArray(); + } + + + [Serializable, TypeConverter(typeof(NullableInt32Converter))] + public struct NullableInt32 : IComparable + { + public static readonly NullableInt32 Default = new NullableInt32(); + + private readonly int _value; + + public NullableInt32(int value) + { + _value = value; + HasValue = true; + } + + public int CompareTo(object obj) + { + if (!(obj is NullableInt32 value)) + { + throw new ArgumentException("NullableInt32 can only compare to another NullableInt32 or a System.Int32"); + } + + if (value.HasValue == HasValue) + { + return HasValue ? Value.CompareTo(value.Value) : 0; + } + return HasValue ? 1 : -1; + } + + public bool HasValue { get; } + + public int Value + { + get + { + if (HasValue) + return _value; + throw new InvalidOperationException("Nullable type must have a value."); + } + } + + public static explicit operator int(NullableInt32 nullable) + { + if (!nullable.HasValue) + throw new NullReferenceException(); + + return nullable.Value; + } + + public static implicit operator NullableInt32(int value) + { + return new NullableInt32(value); + } + + public override string ToString() + { + return HasValue ? Value.ToString() : string.Empty; + } + + public override int GetHashCode() + { + return HasValue ? Value.GetHashCode() : 0; + } + + public override bool Equals(object obj) + { + if (obj is NullableInt32 int32) + { + return Equals(int32); + } + return false; + } + + public bool Equals(NullableInt32 x) + { + return Equals(this, x); + } + + public static bool Equals(NullableInt32 x, NullableInt32 y) + { + if (x.HasValue != y.HasValue) + return false; + if (x.HasValue) + return x.Value == y.Value; + return true; + } + } + + public class NullableInt32Converter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, System.Type sourceType) + { + return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + public override bool CanConvertTo(ITypeDescriptorContext context, System.Type destinationType) + { + return destinationType == typeof(InstanceDescriptor) || base.CanConvertTo(context, destinationType); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + switch (value) + { + case null: + return NullableInt32.Default; + case string _: + var stringValue = ((string) value).Trim(); + + if (stringValue == string.Empty) + return NullableInt32.Default; + + //get underlying types converter + var converter = TypeDescriptor.GetConverter(typeof(int)); + var newValue = (int) converter.ConvertFromString(context, culture, stringValue); + return new NullableInt32(newValue); + default: + return base.ConvertFrom(context, culture, value); + } + } + + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, System.Type destinationType) + { + if (destinationType != typeof(InstanceDescriptor) || !(value is NullableInt32)) + { + return base.ConvertTo(context, culture, value, destinationType); + } + + var nullable = (NullableInt32) value; + + var constructorArgTypes = new[] {typeof(int)}; + var constructor = typeof(NullableInt32).GetConstructor(constructorArgTypes); + + if (constructor == null) + { + return base.ConvertTo(context, culture, value, destinationType); + } + + var constructorArgValues = new object[] {nullable.Value}; + return new InstanceDescriptor(constructor, constructorArgValues); + } + + public override object CreateInstance(ITypeDescriptorContext context, IDictionary propertyValues) + { + return new NullableInt32((int) propertyValues["Value"]); + } + + public override bool GetCreateInstanceSupported(ITypeDescriptorContext context) + { + return true; + } + + public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, + Attribute[] attributes) + { + return TypeDescriptor.GetProperties(typeof(NullableInt32), attributes); + } + + public override bool GetPropertiesSupported(ITypeDescriptorContext context) + { + return true; + } + } + } +} diff --git a/NHibernate.Caches.Common.Tests/NHibernate.Caches.Common.Tests.csproj b/NHibernate.Caches.Common.Tests/NHibernate.Caches.Common.Tests.csproj index b8537f32..cb462c13 100644 --- a/NHibernate.Caches.Common.Tests/NHibernate.Caches.Common.Tests.csproj +++ b/NHibernate.Caches.Common.Tests/NHibernate.Caches.Common.Tests.csproj @@ -9,10 +9,21 @@ NETFX;$(DefineConstants) + + Exe + false + + + + + + + + diff --git a/NHibernate.Caches.Common.Tests/Program.cs b/NHibernate.Caches.Common.Tests/Program.cs new file mode 100644 index 00000000..6b55c96f --- /dev/null +++ b/NHibernate.Caches.Common.Tests/Program.cs @@ -0,0 +1,12 @@ +#if !NETFX +namespace NHibernate.Caches.Common.Tests +{ + public class Program + { + public static int Main(string[] args) + { + return new NUnitLite.AutoRun(typeof(Program).Assembly).Execute(args); + } + } +} +#endif diff --git a/NHibernate.Caches.Common/BinaryCacheSerializer.cs b/NHibernate.Caches.Common/BinaryCacheSerializer.cs new file mode 100644 index 00000000..f50589a9 --- /dev/null +++ b/NHibernate.Caches.Common/BinaryCacheSerializer.cs @@ -0,0 +1,30 @@ +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; + +namespace NHibernate.Caches.Common +{ + /// + public class BinaryCacheSerializer : CacheSerializerBase + { + /// + public override byte[] Serialize(object value) + { + var serializer = new BinaryFormatter(); + using (var stream = new MemoryStream()) + { + serializer.Serialize(stream, value); + return stream.ToArray(); + } + } + + /// + public override object Deserialize(byte[] value) + { + var serializer = new BinaryFormatter(); + using (var stream = new MemoryStream(value)) + { + return serializer.Deserialize(stream); + } + } + } +} diff --git a/NHibernate.Caches.Common/CacheSerializerBase.cs b/NHibernate.Caches.Common/CacheSerializerBase.cs new file mode 100644 index 00000000..d056d449 --- /dev/null +++ b/NHibernate.Caches.Common/CacheSerializerBase.cs @@ -0,0 +1,22 @@ +namespace NHibernate.Caches.Common +{ + /// + /// Base class for serializing objects that will be stored in a distributed cache. + /// + public abstract class CacheSerializerBase + { + /// + /// Serialize the object. + /// + /// The object to serialize. + /// The serialized object. + public abstract byte[] Serialize(object value); + + /// + /// Deserialize the object. + /// + /// The data of the object to deserialize. + /// The deserialized object. + public abstract object Deserialize(byte[] data); + } +} diff --git a/NHibernate.Caches.Common/NHibernate.Caches.Common.csproj b/NHibernate.Caches.Common/NHibernate.Caches.Common.csproj new file mode 100644 index 00000000..5785748f --- /dev/null +++ b/NHibernate.Caches.Common/NHibernate.Caches.Common.csproj @@ -0,0 +1,30 @@ + + + + NHibernate.Caches.Common + NHibernate.Caches.Common + Common types of NHibernate.Caches providers. + net461;netstandard2.0 + True + ..\NHibernate.Caches.snk + true + + + NETFX;$(DefineConstants) + + + + + + + + + + + ./NHibernate.Caches.readme.md + + + ./NHibernate.Caches.license.txt + + + diff --git a/NHibernate.Caches.Common/default.build b/NHibernate.Caches.Common/default.build new file mode 100644 index 00000000..5d0165a2 --- /dev/null +++ b/NHibernate.Caches.Common/default.build @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NHibernate.Caches.Everything.sln b/NHibernate.Caches.Everything.sln index e49cda03..7448ffcb 100644 --- a/NHibernate.Caches.Everything.sln +++ b/NHibernate.Caches.Everything.sln @@ -68,6 +68,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache.Memcached", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.Memcached\NHibernate.Caches.CoreDistributedCache.Memcached.csproj", "{CB7BEB99-1964-4D83-86AD-100439E04186}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Common", "NHibernate.Caches.Common\NHibernate.Caches.Common.csproj", "{15C72C05-4976-4401-91CB-31EF769AADD7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Util.JsonSerializer", "Util\NHibernate.Caches.Util.JsonSerializer\NHibernate.Caches.Util.JsonSerializer.csproj", "{2327C330-0CE6-4F58-96A3-013BEFDFCCEB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.Util.JsonSerializer.Tests", "Util\NHibernate.Caches.Util.JsonSerializer.Tests\NHibernate.Caches.Util.JsonSerializer.Tests.csproj", "{26EAA903-63F7-41B1-9197-5944CEBF00C2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -174,6 +180,18 @@ Global {CB7BEB99-1964-4D83-86AD-100439E04186}.Debug|Any CPU.Build.0 = Debug|Any CPU {CB7BEB99-1964-4D83-86AD-100439E04186}.Release|Any CPU.ActiveCfg = Release|Any CPU {CB7BEB99-1964-4D83-86AD-100439E04186}.Release|Any CPU.Build.0 = Release|Any CPU + {15C72C05-4976-4401-91CB-31EF769AADD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15C72C05-4976-4401-91CB-31EF769AADD7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15C72C05-4976-4401-91CB-31EF769AADD7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15C72C05-4976-4401-91CB-31EF769AADD7}.Release|Any CPU.Build.0 = Release|Any CPU + {2327C330-0CE6-4F58-96A3-013BEFDFCCEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2327C330-0CE6-4F58-96A3-013BEFDFCCEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2327C330-0CE6-4F58-96A3-013BEFDFCCEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2327C330-0CE6-4F58-96A3-013BEFDFCCEB}.Release|Any CPU.Build.0 = Release|Any CPU + {26EAA903-63F7-41B1-9197-5944CEBF00C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26EAA903-63F7-41B1-9197-5944CEBF00C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26EAA903-63F7-41B1-9197-5944CEBF00C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26EAA903-63F7-41B1-9197-5944CEBF00C2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -204,5 +222,8 @@ Global {63AD02AD-1F35-4341-A4C7-7EAEA238F08C} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} {03A80D4B-6C72-4F31-8680-DD6119E34CDF} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} {CB7BEB99-1964-4D83-86AD-100439E04186} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {15C72C05-4976-4401-91CB-31EF769AADD7} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {2327C330-0CE6-4F58-96A3-013BEFDFCCEB} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {26EAA903-63F7-41B1-9197-5944CEBF00C2} = {55271617-8CB8-4225-B338-069033160497} EndGlobalSection EndGlobal diff --git a/Util/NHibernate.Caches.Util.JsonSerializer.Tests/JsonCacheSerializerFixture.cs b/Util/NHibernate.Caches.Util.JsonSerializer.Tests/JsonCacheSerializerFixture.cs new file mode 100644 index 00000000..ba6a049a --- /dev/null +++ b/Util/NHibernate.Caches.Util.JsonSerializer.Tests/JsonCacheSerializerFixture.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using System.Reflection; +using Newtonsoft.Json.Serialization; +using NHibernate.Caches.Common; +using NHibernate.Caches.Common.Tests; +using NUnit.Framework; + +namespace NHibernate.Caches.Util.JsonSerializer.Tests +{ + [TestFixture] + public class JsonCacheSerializerFixture : CacheSerializerFixture + { + protected override Func SerializerProvider => CreateDefaultSerializer; + + [Test] + public void TestStrictSerialization() + { + var serializer = new JsonCacheSerializer(); + Assert.Throws(() => serializer.Serialize(new CustomEntity {Id = 10}), + "Non standard types should be registered explicitly"); + } + + private CacheSerializerBase CreateDefaultSerializer() + { + var serializer = new JsonCacheSerializer(); + serializer.RegisterType(typeof(CustomEntity), "cue"); + // Because of the type converter attribute, the default resolved json contract for NullableInt32 is JsonStringContract. + serializer.RegisterType(typeof(NullableInt32), "nint", jsonContract => + { + // Use fields instead of properties + var contract = (JsonObjectContract) jsonContract; + contract.Properties.Clear(); + var properties = typeof(NullableInt32).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) + .Select(f => new JsonProperty + { + PropertyName = f.Name, + PropertyType = f.FieldType, + DeclaringType = f.DeclaringType, + ValueProvider = new ReflectionValueProvider(f), + AttributeProvider = new ReflectionAttributeProvider(f), + Readable = true, + Writable = true + + }); + foreach (var property in properties) + { + contract.Properties.AddProperty(property); + } + }, typeof(JsonObjectContract)); + return serializer; + } + } +} diff --git a/Util/NHibernate.Caches.Util.JsonSerializer.Tests/NHibernate.Caches.Util.JsonSerializer.Tests.csproj b/Util/NHibernate.Caches.Util.JsonSerializer.Tests/NHibernate.Caches.Util.JsonSerializer.Tests.csproj new file mode 100644 index 00000000..d2952d80 --- /dev/null +++ b/Util/NHibernate.Caches.Util.JsonSerializer.Tests/NHibernate.Caches.Util.JsonSerializer.Tests.csproj @@ -0,0 +1,28 @@ + + + + NHibernate.Caches.Util.JsonSerializer.Tests + Unit tests for json serializer. + net461;netcoreapp2.0 + true + + + NETFX;$(DefineConstants) + + + Exe + false + + + + + + + + + + + + + + diff --git a/Util/NHibernate.Caches.Util.JsonSerializer.Tests/Program.cs b/Util/NHibernate.Caches.Util.JsonSerializer.Tests/Program.cs new file mode 100644 index 00000000..b20736be --- /dev/null +++ b/Util/NHibernate.Caches.Util.JsonSerializer.Tests/Program.cs @@ -0,0 +1,12 @@ +#if !NETFX +namespace NHibernate.Caches.Util.JsonSerializer.Tests +{ + public class Program + { + public static int Main(string[] args) + { + return new NUnitLite.AutoRun(typeof(Program).Assembly).Execute(args); + } + } +} +#endif diff --git a/Util/NHibernate.Caches.Util.JsonSerializer/JsonCacheSerializer.cs b/Util/NHibernate.Caches.Util.JsonSerializer/JsonCacheSerializer.cs new file mode 100644 index 00000000..69c7599f --- /dev/null +++ b/Util/NHibernate.Caches.Util.JsonSerializer/JsonCacheSerializer.cs @@ -0,0 +1,453 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using NHibernate.Cache; +using NHibernate.Cache.Entry; +using NHibernate.Caches.Common; +using NHibernate.Intercept; +using NHibernate.Properties; +using NHibernate.Type; +using NHibernate.UserTypes; +using Serializer = Newtonsoft.Json.JsonSerializer; + +namespace NHibernate.Caches.Util.JsonSerializer +{ + /// + /// A serializer that uses Json.Net to serialize the data that will be stored in a distributed cache. + /// If the cached have a method which does + /// not yield only basic value types or array of them, their return type has to be registered explicitly + /// by method. + /// + public class JsonCacheSerializer : CacheSerializerBase + { + private const string TypeMetadataName = "$type"; + private const string ShortTypeMetadataName = "$t"; + private const string ValueMetadataName = "$value"; + private const string ShortValueMetadataName = "$v"; + private const string ValuesMetadataName = "$values"; + private const string ShortValuesMetadataName = "$vs"; + + /// + /// Types that are serialized as number or string and need the type metadata property to be correctly deserialized. + /// + /// + /// When deserializing an array of objects all numbers will be deserialized as double or long by default. + /// In order to prevent that we have to add the type metadata so that Json.Net will correctly deserialize + /// the number to the original type. + /// + private static readonly Dictionary ExplicitTypes = new Dictionary + { + // Serialized as number + {typeof(short), "s"}, + {typeof(ushort), "us"}, + {typeof(int), "i"}, + {typeof(uint), "ui"}, + {typeof(ulong), "ul"}, + {typeof(sbyte), "sb"}, + {typeof(byte), "b"}, + {typeof(decimal), "d"}, + {typeof(float), "f"}, + // Serialized as string + {typeof(Guid), "g"}, + {typeof(char), "c"}, + {typeof(TimeSpan), "ts"}, + {typeof(DateTimeOffset), "do"}, + {typeof(byte[]), "ba"} + }; + + // The types that are allowed to be serialized and deserialized in order to prevent + // exposing any security vulnerability as we are not using TypeNameHandling.None + private static readonly Dictionary TypeAliases = + new Dictionary(ExplicitTypes) + { + // Added for completeness, they will never be requested by Json.NET + {typeof(long), "l"}, + {typeof(double), "db"}, + {typeof(DateTime), "dt"}, + + // Used by NHibernate + {typeof(object[]), "oa"}, + {typeof(List), "lo"}, + {typeof(Hashtable), "ht"}, + {typeof(CacheEntry), "ce"}, + {typeof(CacheLock), "cl"}, + {typeof(CachedItem), "ci"}, + {typeof(CollectionCacheEntry), "cc"}, + {typeof(AnyType.ObjectTypeCacheEntry), "at"}, + {typeof(UnfetchedLazyProperty), "up"}, + {typeof(UnknownBackrefProperty), "ub"} + }; + + private readonly Serializer _serializer; + private readonly ExplicitSerializationBinder _serializationBinder = new ExplicitSerializationBinder(); + private readonly CustomDefaultContractResolver _contractResolver = new CustomDefaultContractResolver(); + + /// + public JsonCacheSerializer() + { + var settings = new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.Auto, + DateTimeZoneHandling = DateTimeZoneHandling.RoundtripKind, + Formatting = Formatting.None, + SerializationBinder = _serializationBinder, + ContractResolver = _contractResolver + }; + settings.Converters.Add(new ExplicitTypesConverter()); + // Setup the Hashtable serialization + var contract = settings.ContractResolver.ResolveContract(typeof(Hashtable)); + contract.Converter = new HashtableConverter(); + contract.OnDeserializedCallbacks.Add((o, context) => HashtableConverter.OnDeserialized(o, _serializer)); + + _serializer = Serializer.Create(settings); + } + + /// + /// Register a type that is allowed to be serialized with an alias. + /// + /// The type allowed to be serialized. + /// The shorten name of the type. + public void RegisterType(System.Type type, string alias) + { + RegisterType(type, alias, null, null); + } + + /// + /// Register a type that is allowed to be serialized with an alias. + /// + /// The type allowed to be serialized. + /// The shorten name of the type. + /// The action to setup the of the type. + public void RegisterType(System.Type type, string alias, Action setupContractAction) + { + RegisterType(type, alias, setupContractAction, null); + } + + /// + /// Register a type that is allowed to be serialized with an alias. + /// + /// The type allowed to be serialized. + /// The shorten name of the type. + /// The action to setup the of the type. + /// The concrete type of a to use for the type. + public void RegisterType(System.Type type, string alias, Action setupContractAction, System.Type contractType) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + _serializationBinder.RegisterType(type, alias); + if (contractType != null) + { + _contractResolver.SetTypeContract(type, contractType); + } + setupContractAction?.Invoke(_serializer.ContractResolver.ResolveContract(type)); + } + + /// + public override object Deserialize(byte[] value) + { + using (var reader = new CustomJsonTextReader(new StringReader(Encoding.UTF8.GetString(value)))) + { + return _serializer.Deserialize(reader); + } + } + + /// + public override byte[] Serialize(object value) + { + using (var stringWriter = new StringWriter(new StringBuilder(256), CultureInfo.InvariantCulture)) + using (var writer = new CustomJsonTextWriter(stringWriter)) + { + writer.Formatting = _serializer.Formatting; + _serializer.Serialize(writer, value, typeof(object)); + return Encoding.UTF8.GetBytes(stringWriter.ToString()); + } + } + + /// + /// Allows to override the default that will be created for a given type. + /// + private class CustomDefaultContractResolver : DefaultContractResolver + { + private static readonly Dictionary> + ContractCreators = new Dictionary> + { + {typeof(JsonObjectContract), (r, t) => r.CreateObjectContract(t)}, + {typeof(JsonArrayContract), (r, t) => r.CreateArrayContract(t)}, + {typeof(JsonDictionaryContract), (r, t) => r.CreateDictionaryContract(t)}, + {typeof(JsonDynamicContract), (r, t) => r.CreateDynamicContract(t)}, + {typeof(JsonISerializableContract), (r, t) => r.CreateISerializableContract(t)}, + {typeof(JsonLinqContract), (r, t) => r.CreateLinqContract(t)}, + {typeof(JsonPrimitiveContract), (r, t) => r.CreatePrimitiveContract(t)}, + {typeof(JsonStringContract), (r, t) => r.CreateStringContract(t)} + }; + + private readonly Dictionary _typeContracts = new Dictionary(); + + public void SetTypeContract(System.Type type, System.Type contractType) + { + if (!ContractCreators.ContainsKey(contractType)) + { + throw new InvalidOperationException( + $"Invalid json contract type {contractType}. List of valid json contract types: {string.Join(", ", ContractCreators.Keys)}"); + } + + if (_typeContracts.ContainsKey(type)) + { + throw new InvalidOperationException($"A json contract type was already set for type {type}"); + } + _typeContracts.Add(type, contractType); + } + + protected override JsonContract CreateContract(System.Type objectType) + { + return !_typeContracts.TryGetValue(objectType, out var contractType) + ? base.CreateContract(objectType) + : ContractCreators[contractType](this, objectType); + } + } + + /// + /// A serialization binder that allows only registered types to be serialized. + /// + private class ExplicitSerializationBinder : ISerializationBinder + { + private readonly Dictionary _typeAliases; + private readonly Dictionary _aliasTypes; + + public ExplicitSerializationBinder() + { + _typeAliases = new Dictionary(TypeAliases); + _aliasTypes = _typeAliases.ToDictionary(o => o.Value, o => o.Key); + } + + public void RegisterType(System.Type type, string alias) + { + if (string.IsNullOrEmpty(alias)) + { + alias = type.AssemblyQualifiedName; + } + + if (alias == null) + { + throw new ArgumentNullException(nameof(alias)); + } + + if (_typeAliases.ContainsKey(type)) + { + throw new InvalidOperationException($"Type {type} is already registered."); + } + + if (_aliasTypes.ContainsKey(alias)) + { + throw new InvalidOperationException($"Alias {alias} is already registered."); + } + + _typeAliases.Add(type, alias); + _aliasTypes.Add(alias, type); + } + + public void BindToName(System.Type serializedType, out string assemblyName, out string typeName) + { + if (!_typeAliases.TryGetValue(serializedType, out typeName)) + { + throw new InvalidOperationException( + $"Unknown type '{serializedType.AssemblyQualifiedName}', use JsonCacheSerializer.RegisterType method to register it."); + } + + assemblyName = null; + } + + public System.Type BindToType(string assemblyName, string typeName) + { + if (!_aliasTypes.TryGetValue(typeName, out var type)) + { + throw new InvalidOperationException( + $"Unknown type '{typeName}, {assemblyName}', use JsonCacheSerializer.RegisterType method to register it."); + } + + return type; + } + } + + /// + /// A converter that preserves the key type. is not + /// implemented because it is not called by the serializer when the is located on + /// a property of type , which can happen when wrapping the value in . + /// Instead, the method should be appended to the + /// of the . + /// + private class HashtableConverter : JsonConverter + { + /// + public override void WriteJson(JsonWriter writer, object value, Serializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName(ShortTypeMetadataName); + writer.WriteValue(TypeAliases[typeof(Hashtable)]); + + var hashtable = (Hashtable) value; + foreach (DictionaryEntry entry in hashtable) + { + var type = entry.Key.GetType(); + if (type == typeof(string)) + { + writer.WritePropertyName(entry.Key.ToString()); + serializer.Serialize(writer, entry.Value, typeof(object)); + } + else + { + serializer.SerializationBinder.BindToName(type, out var assemblyName, out var typeName); + writer.WritePropertyName(string.IsNullOrEmpty(assemblyName) + ? $"{typeName}:{JsonConvert.ToString(entry.Key)}" + : $"{typeName};{assemblyName}:{JsonConvert.ToString(entry.Key)}"); + serializer.Serialize(writer, entry.Value, typeof(object)); + } + } + writer.WriteEndObject(); + } + + /// + public override bool CanRead => false; + + /// + public override object ReadJson(JsonReader reader, System.Type objectType, object existingValue, Serializer serializer) + { + throw new NotSupportedException(); + } + + public override bool CanConvert(System.Type objectType) + { + return typeof(Hashtable) == objectType; + } + + public static void OnDeserialized(object o, Serializer serializer) + { + var hashtable = (Hashtable) o; + var keys = hashtable.Keys.Cast().ToList(); + foreach (var key in keys) + { + var index = key.IndexOf(':'); + if (index < 0) + { + // Key is a string + continue; + } + + var typeAssembly = key.Substring(0, index).Split(';'); + var type = serializer.SerializationBinder.BindToType( + typeAssembly.Length > 1 ? typeAssembly[1] : null, + typeAssembly[typeAssembly.Length - 1]); + var keyString = key.Substring(index + 1); + var keyValue = serializer.Deserialize(new StringReader(keyString), type); + + hashtable.Add(keyValue, hashtable[key]); + hashtable.Remove(key); + } + } + } + + /// + /// A json converter that adds the type metadata for . + /// + private class ExplicitTypesConverter : JsonConverter + { + public override bool CanConvert(System.Type objectType) + { + return ExplicitTypes.ContainsKey(objectType); + } + + public override bool CanRead => false; + + public override object ReadJson(JsonReader reader, System.Type objectType, object existingValue, Serializer serializer) + { + throw new NotSupportedException(); + } + + public override void WriteJson(JsonWriter writer, object value, Serializer serializer) + { + writer.WriteStartObject(); + writer.WritePropertyName(ShortTypeMetadataName); + var typeName = ExplicitTypes[value.GetType()]; + writer.WriteValue(typeName); + writer.WritePropertyName(ShortValueMetadataName); + writer.WriteValue(value); + writer.WriteEndObject(); + } + } + + #region Reader & Writer + + /// + /// Restores the metadata property names modified by . + /// + private class CustomJsonTextReader : JsonTextReader + { + public CustomJsonTextReader(TextReader reader) : base(reader) + { + } + + public override bool Read() + { + var hasToken = base.Read(); + if (!hasToken || TokenType != JsonToken.PropertyName || !(Value is string str)) + { + return hasToken; + } + + switch (str) + { + case ShortTypeMetadataName: + SetToken(JsonToken.PropertyName, TypeMetadataName); + break; + case ShortValueMetadataName: + SetToken(JsonToken.PropertyName, ValueMetadataName); + break; + case ShortValuesMetadataName: + SetToken(JsonToken.PropertyName, ValuesMetadataName); + break; + } + + return true; + } + } + + /// + /// Reduces the json size by shortening the metadata properties names. + /// + private class CustomJsonTextWriter : JsonTextWriter + { + public CustomJsonTextWriter(TextWriter textWriter) : base(textWriter) + { + } + + public override void WritePropertyName(string name, bool escape) + { + switch (name) + { + case TypeMetadataName: + name = ShortTypeMetadataName; + break; + case ValueMetadataName: + name = ShortValueMetadataName; + break; + case ValuesMetadataName: + name = ShortValuesMetadataName; + break; + } + + base.WritePropertyName(name, escape); + } + } + + #endregion + + } +} diff --git a/Util/NHibernate.Caches.Util.JsonSerializer/NHibernate.Caches.Util.JsonSerializer.csproj b/Util/NHibernate.Caches.Util.JsonSerializer/NHibernate.Caches.Util.JsonSerializer.csproj new file mode 100644 index 00000000..e6f9b4a9 --- /dev/null +++ b/Util/NHibernate.Caches.Util.JsonSerializer/NHibernate.Caches.Util.JsonSerializer.csproj @@ -0,0 +1,33 @@ + + + + NHibernate.Caches.Util.JsonSerializer + NHibernate.Caches.Util.JsonSerializer + Json.NET serializer for distributed NHibernate.Caches providers. + net461;netstandard2.0 + True + ..\..\NHibernate.Caches.snk + true + + + NETFX;$(DefineConstants) + + + + + + + + + + + + + + ./NHibernate.Caches.readme.md + + + ./NHibernate.Caches.license.txt + + + diff --git a/Util/default.build b/Util/default.build new file mode 100644 index 00000000..8eb7455b --- /dev/null +++ b/Util/default.build @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/appveyor.yml b/appveyor.yml index 4182dcd3..527878e4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -100,10 +100,18 @@ test_script: - ps: >- Invoke-Command -ScriptBlock { $TestsFailed = $FALSE + $target = If ($env:TESTS -eq 'net') {$env:NETTARGETFX} Else {$env:CORETARGETFX} + $projects = @{} + $projects.Add('Common', "NHibernate.Caches.Common.Tests\bin\$env:CONFIGURATION\$target\NHibernate.Caches.Common.Tests.dll") + $projects.Add('Util.JsonSerializer', "Util\NHibernate.Caches.Util.JsonSerializer.Tests\bin\$env:CONFIGURATION\$target\NHibernate.Caches.Util.JsonSerializer.Tests.dll") + #netFx tests If ($env:TESTS -eq 'net') { @('EnyimMemcached', 'Prevalence', 'RtMemoryCache', 'SysCache', 'SysCache2', 'CoreMemoryCache', 'CoreDistributedCache') | ForEach-Object { - nunit3-console (Join-Path $env:APPVEYOR_BUILD_FOLDER "$_\NHibernate.Caches.$_.Tests\bin\$env:CONFIGURATION\$env:NETTARGETFX\NHibernate.Caches.$_.Tests.dll") "--result=$_-NetTestResult.xml;format=AppVeyor" + $projects.Add($_, "$_\NHibernate.Caches.$_.Tests\bin\$env:CONFIGURATION\$target\NHibernate.Caches.$_.Tests.dll") + } + ForEach ($project in $projects.GetEnumerator()) { + nunit3-console (Join-Path $env:APPVEYOR_BUILD_FOLDER $project.Value) "--result=$($project.Name)-NetTestResult.xml;format=AppVeyor" If ($LASTEXITCODE -ne 0) { $TestsFailed = $TRUE } @@ -113,7 +121,10 @@ test_script: #core tests If ($env:TESTS -eq 'core') { @('CoreMemoryCache', 'CoreDistributedCache', 'RtMemoryCache') | ForEach-Object { - dotnet (Join-Path $env:APPVEYOR_BUILD_FOLDER "$_\NHibernate.Caches.$_.Tests\bin\$env:CONFIGURATION\$env:CORETARGETFX\NHibernate.Caches.$_.Tests.dll") --labels=before --nocolor "--result=$_-CoreTestResult.xml" + $projects.Add($_, "$_\NHibernate.Caches.$_.Tests\bin\$env:CONFIGURATION\$target\NHibernate.Caches.$_.Tests.dll") + } + ForEach ($project in $projects.GetEnumerator()) { + dotnet (Join-Path $env:APPVEYOR_BUILD_FOLDER $project.Value) --labels=before --nocolor "--result=$($project.Name)-CoreTestResult.xml" If ($LASTEXITCODE -ne 0) { $TestsFailed = $TRUE } diff --git a/default.build b/default.build index c1724a00..637138d7 100644 --- a/default.build +++ b/default.build @@ -7,6 +7,8 @@ + +