Skip to content

TypeConverter for serializing and deserializing ValueOf types #3

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, EmailAddress> { }

...

EmailAddress emailAddress = EmailAddress.From("[email protected]");

```

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<string, ValidatedClientRef>
{
protected override void Validate()
Expand All @@ -41,9 +39,20 @@ public class ValidatedClientRef : ValueOf<string, ValidatedClientRef>
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<string, ClientId>))]
public class ClientId : ValueOf<string, ClientId> { }
```

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.
78 changes: 78 additions & 0 deletions ValueOf.Tests/TypeConverter.cs
Original file line number Diff line number Diff line change
@@ -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<string, ClientId>))]
public class ClientId : ValueOf<string, ClientId> { }

public class ClientIdWithoutTypeConverter : ValueOf<string, ClientIdWithoutTypeConverter> { }


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<JsonSerializationException>(() => JsonConvert.DeserializeObject<ClientIdWithoutTypeConverterModel>(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<ClientIdModel>(json);

Assert.AreEqual(ClientId.From("asdf12345"), model.ClientId);
}
}
}
1 change: 1 addition & 0 deletions ValueOf.Tests/ValueOf.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="nunit" Version="3.6.1" />
<PackageReference Include="System.ValueTuple" Version="4.3.1" />
</ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions ValueOf/ValueOf.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Use ValueTuples for multi property values e.g `class Address : ValueOf&lt;(strin
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.ComponentModel.TypeConverter" Version="4.3.0" />
</ItemGroup>

</Project>
44 changes: 44 additions & 0 deletions ValueOf/ValueOfTypeConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.ComponentModel;
using System.Globalization;

namespace ValueOf
{
/// <summary>
/// Type converter for <see cref="ValueOf{TValue, TThis}"/> types to allow seamingless serialization and deserialization.
/// Use this type as parameter for the <see cref="TypeConverterAttribute"/>.
/// </summary>
public class ValueOfTypeConverter<TValue, TThis> : TypeConverter
where TThis : ValueOf<TValue, TThis>, 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<TValue, TThis>.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);
}
}
}