Skip to content

Commit

Permalink
Merge pull request #708 from SteveDunn/message-pack-attribute
Browse files Browse the repository at this point in the history
Add message pack attribute onto value objects
  • Loading branch information
SteveDunn authored Nov 19, 2024
2 parents ce60ef3 + 3b8d89e commit 3e426c9
Show file tree
Hide file tree
Showing 10 changed files with 1,137 additions and 65 deletions.
2 changes: 1 addition & 1 deletion docs/site/Writerside/topics/reference/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ If you're using the generator in a .NET Framework project and using the old styl
</Configuration>
```

## Does it support C# 11 features?
## Does it support modern features of C#?
This is primarily a source generator. The source it generates is mostly C# 6 for compatibility. But if you use features from a later language version, for instance, `records` from C# 9, then it will also generate records.

Source generation is driven by attributes, and, if you're using .NET 7 or above, the generic version of the `ValueObject` attribute is exposed:
Expand Down
129 changes: 92 additions & 37 deletions docs/site/Writerside/topics/reference/Integration.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,80 @@
# Serialize Value Objects

Vogen integrates with various serializers, including:

* JSON (`System.Text.Json` and `Newtonsoft.Json`)
* BSON
* Dapper
* LinqToDB
* EF Core
* ASP.NET Core (for MVC routes etc.), by generating a `TypeConverter`
* protobuf-net (see the section in the [FAQ](FAQ.md#can-i-use-protobuf-net) for usage)

… and many others. See the `Conversions` attribute for a full list.

This conversion code can be generated in the **same project** as the value object or in a different project. Using a different project is allows you to follow architecture patterns where you separate infrastructure from other layers.

## Conversion code in the same project

Here are two types where we want serializers for MessagePack and Mongo's BSON format:

```c#
[ValueObject<string>(
conversions: Conversions.MessagePack | Conversions.Bson)]
public readonly partial struct Name { }

[ValueObject<int>(
conversions: Conversions.MessagePack | Conversions.Bson)]
public readonly partial struct Age { }
```

If you're following an architecture pattern where you separate infrastructure from other layers, then you'll want the generated converters to live in another project. This is described below.

## Conversion code in a different project

<note>
This topic is incomplete and is currently being improved.
Currently, the following conversions can be used from another project, but as Vogen evolves other conversions will be supported (please raise a GitHub issue if you'd like to see something implemented):

* BSON
* EFCore
* MessagePack

</note>

Create a `partial class` (not a `struct`), and specify what converters you want for what type. For the two examples above, it'd be:

Vogen integrates with other systems and technologies.
```c#
[BsonSerializer<Domain.Name>]
[BsonSerializer<Domain.Age>]
public partial class BsonConversions;

The generated Value Objects can be converted to and from JSON.
[MessagePack<Domain.Name>]
[MessagePack<Domain.Age>]
public partial class MessagePackConversions;
```

They can be used in Dapper, LinqToDB, and EF Core.
... or just have them in one class:

And it generates TypeConverter code, so that Value Objects can be used in things like ASP.NET Core MVC routes.
```c#
[BsonSerializer<Domain.Name>]
[MessagePack<Domain.Name>]
[BsonSerializer<Domain.Age>]
[MessagePack<Domain.Age>]
public partial class Conversions;
```

Integration is handled by the `conversions` parameter in the `ValueObject` attribute. The current choices are:
And reference them with something like `Conversions.NameBsonSerializer` etc. Here's an example to register all MessagePack formatters:

```c#
var customResolver = MessagePack.Resolvers.CompositeResolver.Create(
Conversions.MessagePackFormatters,
[MessagePack.Resolvers.StandardResolver.Instance]
);
```

## Integrations

For generating conversions in the same project, use the `conversions` parameter in the `ValueObject` attribute:

```c#
using System;
Expand All @@ -35,43 +96,51 @@ public enum Conversions

/// <summary>
/// Use the default converters for the value object.
/// This will be the value provided in the <see cref="ValueObjectAttribute"/>, which falls back to
/// This will be the value provided in the
/// <see cref="ValueObjectAttribute"/>, which falls back to
/// <see cref="TypeConverter"/> and <see cref="NewtonsoftJson"/>
/// </summary>
Default = TypeConverter | SystemTextJson,

/// <summary>
/// Creates a <see cref="TypeConverter"/> for converting from the value object to and from a string
/// Creates a <see cref="TypeConverter"/> for converting from the
/// value object to and from a string
/// </summary>
TypeConverter = 1 << 1,

/// <summary>
/// Creates a Newtonsoft.Json.JsonConverter for serializing the value object to its primitive value
/// Creates a Newtonsoft.Json.JsonConverter for serializing the
/// value object to its primitive value
/// </summary>
NewtonsoftJson = 1 << 2,

/// <summary>
/// Creates a System.Text.Json.Serialization.JsonConverter for serializing the value object to its primitive value
/// Creates a System.Text.Json.Serialization.JsonConverter for
/// serializing the value object to its primitive value
/// </summary>
SystemTextJson = 1 << 3,

/// <summary>
/// Creates an EF Core Value Converter for extracting the primitive value
/// Creates an EF Core Value Converter for extracting the
/// primitive value
/// </summary>
EfCoreValueConverter = 1 << 4,

/// <summary>
/// Creates a Dapper TypeHandler for converting to and from the type
/// Creates a Dapper TypeHandler for converting to and
/// from the type
/// </summary>
DapperTypeHandler = 1 << 5,

/// <summary>
/// Creates a LinqToDb ValueConverter for converting to and from the type
/// Creates a LinqToDb ValueConverter for converting to and
/// from the type
/// </summary>
LinqToDbValueConverter = 1 << 6,

/// <summary>
/// Sets the SerializeFn and DeSerializeFn members in JsConfig in a static constructor.
/// Sets the SerializeFn and DeSerializeFn members in JsConfig
/// in a static constructor.
/// </summary>
ServiceStackDotText = 1 << 7,

Expand All @@ -81,8 +150,10 @@ public enum Conversions
Bson = 1 << 8,

/// <summary>
/// Creates and registers a codec and copier for Microsoft Orleans.
/// This feature requires .NET 8 and C#12 and cannot be polly-filled.
/// Creates and registers a codec and copier
/// for Microsoft Orleans.
/// This feature requires .NET 8 and C#12 and
/// cannot be polly-filled.
/// </summary>
Orleans = 1 << 9,

Expand All @@ -100,33 +171,17 @@ public enum Conversions

The default, as specified above in the `Defaults` property, is `TypeConverter` and `SystemTextJson`.

[//]: # (TODO: merge this in)
If you don't want any conversions, then specify `Conversions.None`.

Other converters/serializers are:
If you want your own conversion, then again specify none and implement them yourself, just like any other type. Be aware that even serializers will get the same compilation errors for `new` and `default` when trying to create VOs.

* Newtonsoft.Json (NSJ)
* ServiceStack.Text
* Dapper
* EFCore
* [LINQ to DB](https://github.com/linq2db/linq2db)
* protobuf-net (see the FAQ section below)

They are controlled by the `Conversions` enum. The following has serializers for NSJ and STJ:
There may be other steps that you need to do to use these integrations, for instance, for Dapper, register it—something like this:

```c#
[ValueObject<float>(conversions:
Conversions.NewtonsoftJson | Conversions.SystemTextJson)]
public readonly partial struct Celsius { }
SqlMapper.AddTypeHandler(new Customer.DapperTypeHandler());
```

If you don't want any conversions, then specify `Conversions.None`.

If you want your own conversion, then again specify none, and implement them yourself, just like any other type. But be aware that even serializers will get the same compilation errors for `new` and `default` when trying to create VOs.
See the [examples folder](https://github.com/SteveDunn/Vogen/tree/main/samples/Vogen.Examples/SerializationAndConversion) for more information.

If you want to use Dapper, remember to register it—something like this:

```c#
SqlMapper.AddTypeHandler(new Customer.DapperTypeHandler());
```

See the examples folder for more information.
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,7 @@ public class MessagePackScenario_using_conversion_attributes : IScenario
{
public Task Run()
{
var customResolver = MessagePack.Resolvers.CompositeResolver.Create(
[new PersonIdMessagePackFormatter(), new NameMessagePackFormatter(), new AgeMessagePackFormatter()],
[MessagePack.Resolvers.StandardResolver.Instance]
);

var options = MessagePackSerializerOptions.Standard.WithResolver(customResolver);
var options = MessagePackSerializerOptions.Standard;

var originalObject = new Person
{
Expand Down
22 changes: 19 additions & 3 deletions src/Vogen/GenerateCodeForMessagePack.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using Vogen.Generators.Conversions;

namespace Vogen;

internal class GenerateCodeForMessagePack
internal class GenerateCodeForMessagePack : IGenerateConversion
{
public static void GenerateForMarkerClasses(SourceProductionContext context, ImmutableArray<MarkerClassDefinition> conversionMarkerClasses)
{
Expand Down Expand Up @@ -162,7 +164,7 @@ private static string GenerateSource(string accessibility,
string underlyingTypeName = underlyingSymbol.EscapedFullName();

string nativeReadMethod = TryGetNativeReadMethod(underlyingSymbol);

if (!string.IsNullOrEmpty(nativeReadMethod))
{
return $$"""
Expand Down Expand Up @@ -250,5 +252,19 @@ public static MessagePackStandalone FromWorkItem(VoWorkItem voWorkItem)

return new(voWorkItem.FullNamespace, voWorkItem.WrapperType, voWorkItem.UnderlyingType, accessor);
}
}
}

public string GenerateAnyAttributes(TypeDeclarationSyntax tds, VoWorkItem item)
{
if (!item.HasConversion(Conversions.MessagePack))
{
return string.Empty;
}

string fqName = $"{item.WrapperType.EscapedFullName()}MessagePackFormatter";

return $"[global::MessagePack.MessagePackFormatter(typeof({fqName}))]";
}

public string GenerateAnyBody(TypeDeclarationSyntax tds, VoWorkItem item) => "";
}
3 changes: 2 additions & 1 deletion src/Vogen/Util.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public static string EscapeTypeNameForTripleSlashComment(string typeName) =>
new GenerateTypeConverterConversions(),
new GenerateDapperConversions(),
new GenerateEfCoreTypeConversions(),
new GenerateLinqToDbConversions()
new GenerateLinqToDbConversions(),
new GenerateCodeForMessagePack()
};

public static string SanitizeToALegalFilename(string input) => input.Replace('@', '_');
Expand Down
Loading

0 comments on commit 3e426c9

Please sign in to comment.