From 815b13bc10bfa8cee23a480453b30293ca7f5f55 Mon Sep 17 00:00:00 2001 From: Michael Einsiedler Date: Sat, 4 May 2019 10:43:27 +0200 Subject: [PATCH 1/2] added TypeConverter for serializing and deserializing ValueOf types --- README.md | 21 ++++++--- ValueOf.Tests/TypeConverter.cs | 78 +++++++++++++++++++++++++++++++++ ValueOf/ValueOf.csproj | 1 + ValueOf/ValueOfTypeConverter.cs | 44 +++++++++++++++++++ 4 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 ValueOf.Tests/TypeConverter.cs create mode 100644 ValueOf/ValueOfTypeConverter.cs diff --git a/README.md b/README.md index 6187c7a..b8ea595 100644 --- a/README.md +++ b/README.md @@ -10,29 +10,27 @@ ValueOf lets you define ValueObject Types in a single line of code. Use them everywhere to strengthen your codebase. -``` +```csharp public class EmailAddress : ValueOf { } ... EmailAddress emailAddress = EmailAddress.From("foo@bar.com"); - ``` The ValueOf class implements `.Equals` and `.GetHashCode()` for you. You can use C# 7 Tuples for more complex Types with multiple values: -``` - public class Address : ValueOf<(string firstLine, string secondLine, Postcode postcode), Address> { } - +```csharp +public class Address : ValueOf<(string firstLine, string secondLine, Postcode postcode), Address> { } ``` ### Validation You can add validation to your Types by overriding the `protected void Validate() { } ` method: -``` +```csharp public class ValidatedClientRef : ValueOf { protected override void Validate() @@ -41,9 +39,20 @@ public class ValidatedClientRef : ValueOf throw new ArgumentException("Value cannot be null or empty"); } } +``` + +### Serialization and Deserialization +When serializing and deserilizing your types, e.g. with `Newtonsoft.Json` you need to give the serializer a hint that he knows how to correctly serialize and deserialize the types. +`ValueOfTpeConverter` does that for you in a generic way by simply adding an attribute to your type: + +```csharp +[TypeConverter(typeof(ValueOfTypeConverter))] +public class ClientId : ValueOf { } ``` +With this `TypeConverter` attribute in place, the `ClientId` gets serialized to a `string` and a `string` gets deserialized into a `ClientId` type. + ## See Also If you liked this, you'll probably like another project of mine [OneOf](https://github.com/mcintyre321/OneOf) which provides Discriminated Unions for C#, allowing stronger compile time guarantees when writing branching logic. \ No newline at end of file diff --git a/ValueOf.Tests/TypeConverter.cs b/ValueOf.Tests/TypeConverter.cs new file mode 100644 index 0000000..832aed7 --- /dev/null +++ b/ValueOf.Tests/TypeConverter.cs @@ -0,0 +1,78 @@ +using Newtonsoft.Json; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ValueOf.Tests +{ + [TypeConverter(typeof(ValueOfTypeConverter))] + public class ClientId : ValueOf { } + + public class ClientIdWithoutTypeConverter : ValueOf { } + + + public class ClientIdModel + { + public ClientId ClientId { get; set; } + } + + public class ClientIdWithoutTypeConverterModel + { + public ClientIdWithoutTypeConverter ClientId { get; set; } + } + + + public class TypeConverter + { + [Test] + public void ConvertToJsonWithoutTypeConverter() + { + var model = new ClientIdWithoutTypeConverterModel + { + ClientId = ClientIdWithoutTypeConverter.From("asdf12345") + }; + + var json = JsonConvert.SerializeObject(model); + + // This is usually not what we want when serializing ValueOf types. + // We don't want the value to get wrapped inside the "Value" property when serilzing. + // With the TypeConverter, this can be avoided. + Assert.AreEqual("{\"ClientId\":{\"Value\":\"asdf12345\"}}", json); + } + + [Test] + public void ConvertToJsonWithTypeConverter() + { + var model = new ClientIdModel + { + ClientId = ClientId.From("asdf12345") + }; + + var json = JsonConvert.SerializeObject(model); + Assert.AreEqual("{\"ClientId\":\"asdf12345\"}", json); + } + + [Test] + public void ConvertFromJsonWithoutTypeConverter() + { + var json = "{\"ClientId\":\"asdf12345\"}"; + + // Without using a TypeConverter, the JSON serializer does not know, how to create the ValueOf type and throws an exception. + var exception = Assert.Throws(() => JsonConvert.DeserializeObject(json)); + Assert.AreEqual("Error converting value \"asdf12345\" to type 'ValueOf.Tests.ClientIdWithoutTypeConverter'. Path 'ClientId', line 1, position 23.", exception.Message); + } + + [Test] + public void ConvertFromJsonWithTypeConverter() + { + var json = "{\"ClientId\":\"asdf12345\"}"; + var model = JsonConvert.DeserializeObject(json); + + Assert.AreEqual(ClientId.From("asdf12345"), model.ClientId); + } + } +} diff --git a/ValueOf/ValueOf.csproj b/ValueOf/ValueOf.csproj index 157e4c7..8c7103c 100644 --- a/ValueOf/ValueOf.csproj +++ b/ValueOf/ValueOf.csproj @@ -20,6 +20,7 @@ Use ValueTuples for multi property values e.g `class Address : ValueOf<(strin + \ No newline at end of file diff --git a/ValueOf/ValueOfTypeConverter.cs b/ValueOf/ValueOfTypeConverter.cs new file mode 100644 index 0000000..922981d --- /dev/null +++ b/ValueOf/ValueOfTypeConverter.cs @@ -0,0 +1,44 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +namespace ValueOf +{ + /// + /// Type converter for types to allow seamingless serialization and deserialization. + /// Use this type as parameter for the . + /// + public class ValueOfTypeConverter : TypeConverter + where TThis : ValueOf, new() + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + if (sourceType == typeof(TValue)) + { + return true; + } + + return base.CanConvertFrom(context, sourceType); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value is TValue tValue) + { + return ValueOf.From(tValue); + } + + return base.ConvertFrom(context, culture, value); + } + + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + if (destinationType == typeof(TValue)) + { + return ((TThis)value).Value; + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } +} From 1f1ce9f803453bc41a79d7917305d1ef68d63004 Mon Sep 17 00:00:00 2001 From: Michael Einsiedler Date: Sat, 4 May 2019 11:16:08 +0200 Subject: [PATCH 2/2] fixed unit tests - add Newtonsoft.Json PackageReference --- ValueOf.Tests/ValueOf.Tests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/ValueOf.Tests/ValueOf.Tests.csproj b/ValueOf.Tests/ValueOf.Tests.csproj index 13e912e..d1471a5 100644 --- a/ValueOf.Tests/ValueOf.Tests.csproj +++ b/ValueOf.Tests/ValueOf.Tests.csproj @@ -5,6 +5,7 @@ +