Skip to content

Commit

Permalink
Support for more deserialization controls via ITypeSerializers
Browse files Browse the repository at this point in the history
Motivation
----------
The current ITypeSerializer implementation works under the assumption that
all serialization/deserialization requests should have the same behavior.
Any client library which implements a custom ITypeSerializer overrides
this behavior for all requests to Couchbase.

However, there are instances where a specific request may require custom
options.  The particular example addressed here is change tracking in the
Linq2Couchbase library.  It needs to control the object creation process
for some deserialization requests in order to create change tracking
proxies.

Additionally, we need a method for custom ITypeSerializer implementations
to provide member name resolution information to consumers.  This will
allow Linq2Couchbase to determine the correct attribute names to use when
building N1QL queries.  Currently, it is forced to assume that the
Newtonsoft.Json behavior is in use.

Finally, there is currently no method to override the deserialization
process for N1QL queries on a per-request basis.

Modifications
-------------
Created a new interface which extends ITypeSerializer named
IExtendedTypeSerializer.

Added GetMemberName method to IExtendedTypeSerializer, which provides
member name resolution information to consumers.

Added DeserializationOptions to IExtendedTypeSerializer, which allows
consumers to set the options they'd like.  Currently, this object supports
only one option, CustomTypeCreator, which allows the consumer to override
the type creation process on a type-by-type basis.

Also provided a SupportedDeserializationOptions object.  This allows the
IExtendedTypeSerializer to define which options it does or does not
support.

Updated the DefaultSerializer to support all of the new interfaces,
methods, and options provided.

Additionally, created a new interface IQueryRequestWithMapper, inherited
form IQueryRequest, which adds a DataMapper to IQueryRequest.  Added this
interface to the default QueryRequest implentation.  This allows the data
mapper used for N1QL queries can be customized on a per-request basis.

Results
-------
For users using the DefaultSerializer based on Newtonsoft.Json, they will
immediately have access to the new features on IExtendedTypeSerializer.
This includes a method to resolve member names, and the ability to
override the type creation process.  This will allow Linq2Couchbase to
transparently implement change tracking proxies.

For projects using a custom ITypeSerializer implementation, consumers such
as Linq2Couchbase can detect support for advanced features by testing for
the IExtendedTypeSerializer interface.  If present, they can then test for
specific features via the SupportedDeserializationOptions property.

Backwards compatibility is fully maintained by these changes.
Additionally, the use of SupportedDeserializationOptions will allow the
addition of more deserialization options in the future without creating
backwards compatibility issues.

Change-Id: I60db3a6a93d787d9e5e48ed1984e7b31566d348e
Reviewed-on: http://review.couchbase.org/56960
Reviewed-by: Jeffry Morris <jeffrymorris@gmail.com>
Tested-by: Jeffry Morris <jeffrymorris@gmail.com>
  • Loading branch information
brantburnett authored and jeffrymorris committed Nov 13, 2015
1 parent 2a3fba5 commit db07eb1
Show file tree
Hide file tree
Showing 14 changed files with 770 additions and 9 deletions.
237 changes: 237 additions & 0 deletions Src/Couchbase.UnitTests/Core/Serialization/DefaultSerializerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Couchbase.Core.Serialization;
using Moq;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using NUnit.Framework;

namespace Couchbase.UnitTests.Core.Serialization
{
[TestFixture]
public class DefaultSerializerTests
{
#region DeserializationOptions

[Test]
public void DeserializationOptions_Modification_UpdatesEffectiveSettings()
{
// Arrange

var options = new DeserializationOptions();
var effectiveSettings = new JsonSerializerSettings();

var serializer = new Mock<DefaultSerializer>
{
CallBase = true
};

serializer.Setup(p => p.GetDeserializationSettings(It.IsAny<JsonSerializerSettings>(), options))
.Returns(effectiveSettings);

// Act

serializer.Object.DeserializationOptions = options;

// Assert

Assert.AreEqual(effectiveSettings, serializer.Object.EffectiveDeserializationSettings);
}

#endregion

#region Deserialize With ICustomObjectCreator

[Test]
public void Deserialize_Stream_WithICustomObjectCreator_CreatesCustomObjects()
{
// Arrange

var creator = new FakeCustomObjectCreator();

var settings = new JsonSerializerSettings()
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};

var serializer = new DefaultSerializer(settings, settings)
{
DeserializationOptions = new DeserializationOptions()
{
CustomObjectCreator = creator
}
};

var stream = new MemoryStream(Encoding.UTF8.GetBytes("{\"subNode\":{\"property\":\"value\"}}"));

// Act

var result = serializer.Deserialize<JsonDocument>(stream);

// Assert

Assert.NotNull(result);
Assert.NotNull(result.SubNode);
Assert.AreEqual(typeof(DocumentSubNodeInherited), result.SubNode.GetType());
Assert.AreEqual("value", result.SubNode.Property);
}

[Test]
public void Deserialize_ByteArray_WithICustomObjectCreator_CreatesCustomObjects()
{
// Arrange

var creator = new FakeCustomObjectCreator();

var settings = new JsonSerializerSettings()
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};

var serializer = new DefaultSerializer(settings, settings)
{
DeserializationOptions = new DeserializationOptions()
{
CustomObjectCreator = creator
}
};

var jsonBuffer = Encoding.UTF8.GetBytes("{\"subNode\":{\"property\":\"value\"}}");

// Act

var result = serializer.Deserialize<JsonDocument>(jsonBuffer, 0, jsonBuffer.Length);

// Assert

Assert.NotNull(result);
Assert.NotNull(result.SubNode);
Assert.AreEqual(typeof(DocumentSubNodeInherited), result.SubNode.GetType());
Assert.AreEqual("value", result.SubNode.Property);
}

#endregion

#region GetMemberName

[Test]
public void GetMemberName_Null_ArgumentNullException()
{
// Arrange

var serializer = new DefaultSerializer();

// Act/Assert

Assert.Throws<ArgumentNullException>(() => serializer.GetMemberName(null));
}

[Test]
public void GetMemberName_BasicProperty_ReturnsPropertyName()
{
// Arrange

var settings = new JsonSerializerSettings()
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};

var serializer = new DefaultSerializer(settings, settings);

// Act

var result = serializer.GetMemberName(typeof (JsonDocument).GetProperty("BasicProperty"));

// Assert

Assert.AreEqual("basicProperty", result);
}

[Test]
public void GetMemberName_NamedProperty_ReturnsNameFromAttribute()
{
// Arrange

var settings = new JsonSerializerSettings()
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};

var serializer = new DefaultSerializer(settings, settings);

// Act

var result = serializer.GetMemberName(typeof(JsonDocument).GetProperty("NamedProperty"));

// Assert

Assert.AreEqual("useThisName", result);
}

[Test]
public void GetMemberName_IgnoredProperty_ReturnsNull()
{
// Arrange

var settings = new JsonSerializerSettings()
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};

var serializer = new DefaultSerializer(settings, settings);

// Act

var result = serializer.GetMemberName(typeof(JsonDocument).GetProperty("IgnoredProperty"));

// Assert

Assert.IsNull(result);
}

#endregion

#region Helpers

private class JsonDocument
{
public string BasicProperty { get; set; }

[JsonProperty("useThisName")]
public string NamedProperty { get; set; }

[JsonIgnore]
public string IgnoredProperty { get; set; }

public DocumentSubNode SubNode { get; set; }
}

private class DocumentSubNode
{
public string Property { get; set; }
}

private class DocumentSubNodeInherited : DocumentSubNode
{
}

private class FakeCustomObjectCreator : ICustomObjectCreator
{
public bool CanCreateObject(Type type)
{
return type == typeof (DocumentSubNode);
}

public object CreateObject(Type type)
{
return new DocumentSubNodeInherited();
}
}

#endregion
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Couchbase.Core.Serialization;
using Moq;
using NUnit.Framework;

namespace Couchbase.UnitTests.Core.Serialization
{
[TestFixture]
public class DeserializationOptionsTests
{
#region HasSettings

[Test]
public void HasSettings_DefaultObject_ReturnsFalse()
{
// Arrange

var settings = new DeserializationOptions();

// Act

var result = settings.HasSettings;

// Assert

Assert.False(result);
}

[Test]
public void HasSettings_WithCustomObjectCreator_ReturnsTrue()
{
// Arrange

var settings = new DeserializationOptions()
{
CustomObjectCreator = new Mock<ICustomObjectCreator>().Object
};

// Act

var result = settings.HasSettings;

// Assert

Assert.True(result);
}

#endregion
}
}
3 changes: 3 additions & 0 deletions Src/Couchbase.UnitTests/Couchbase.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Core\Serialization\DefaultSerializerTests.cs" />
<Compile Include="Core\Serialization\DeserializationOptionsTests.cs" />
<Compile Include="CouchbaseBucketTests.cs" />
<Compile Include="N1Ql\QueryClientTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
Expand Down
89 changes: 89 additions & 0 deletions Src/Couchbase.UnitTests/N1Ql/QueryClientTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Couchbase.Configuration.Client;
using Couchbase.N1QL;
using Couchbase.Views;
using Moq;
using NUnit.Framework;

namespace Couchbase.UnitTests.N1Ql
{
[TestFixture]
public class QueryClientTests
{
#region GetDataMapper

[Test]
public void GetDataMapper_IQueryRequest_ReturnsClientDataMapper()
{
// Arrange

var dataMapper = new Mock<IDataMapper>();

var queryRequest = new Mock<IQueryRequest>();

var queryClient = new QueryClient(new HttpClient(), dataMapper.Object, new ClientConfiguration(),
new ConcurrentDictionary<string, QueryPlan>());

// Act

var result = queryClient.GetDataMapper(queryRequest.Object);

// Assert

Assert.AreEqual(dataMapper.Object, result);
}

[Test]
public void GetDataMapper_IQueryRequestWithDataMapper_NoDataMapper_ReturnsClientDataMapper()
{
// Arrange

var clientDataMapper = new Mock<IDataMapper>();

var queryRequest = new Mock<IQueryRequestWithDataMapper>();
queryRequest.SetupProperty(p => p.DataMapper, null);

var queryClient = new QueryClient(new HttpClient(), clientDataMapper.Object, new ClientConfiguration(),
new ConcurrentDictionary<string, QueryPlan>());

// Act

var result = queryClient.GetDataMapper(queryRequest.Object);

// Assert

Assert.AreEqual(clientDataMapper.Object, result);
}

[Test]
public void GetDataMapper_IQueryRequestWithDataMapper_HasDataMapper_ReturnsRequestDataMapper()
{
// Arrange

var clientDataMapper = new Mock<IDataMapper>();
var requestDataMapper = new Mock<IDataMapper>();

var queryRequest = new Mock<IQueryRequestWithDataMapper>();
queryRequest.SetupProperty(p => p.DataMapper, requestDataMapper.Object);

var queryClient = new QueryClient(new HttpClient(), clientDataMapper.Object, new ClientConfiguration(),
new ConcurrentDictionary<string, QueryPlan>());

// Act

var result = queryClient.GetDataMapper(queryRequest.Object);

// Assert

Assert.AreEqual(requestDataMapper.Object, result);
}

#endregion
}
}
Loading

0 comments on commit db07eb1

Please sign in to comment.