Skip to content
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

Add Bson serialization #645

Merged
merged 7 commits into from
Jul 24, 2024
Merged
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
1 change: 1 addition & 0 deletions Consumers.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/SourceGeneratedFilesSweaEnabled/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CheckNamespace/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypesAndNamespaces/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=a0b4bc4d_002Dd13b_002D4a37_002Db37e_002Dc9c6864e4302/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"&gt;&lt;ElementKinds&gt;&lt;Kind Name="NAMESPACE" /&gt;&lt;Kind Name="CLASS" /&gt;&lt;Kind Name="STRUCT" /&gt;&lt;Kind Name="ENUM" /&gt;&lt;Kind Name="DELEGATE" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb_AaBb" /&gt;&lt;/Policy&gt;</s:String>
Expand Down
1 change: 1 addition & 0 deletions docs/site/Writerside/hi.tree
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<toc-element topic="Casting.md"/>
<toc-element topic="Overriding-methods.md"/>
<toc-element topic="EfCoreIntegrationHowTo.md"/>
<toc-element topic="MongoIntegrationHowTo.md"/>
<toc-element topic="Use-in-Swagger.md"/>
<toc-element topic="efcore-tips.md"/>
</toc-element>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public partial class Name
```

Another way, if you're using .NET 8 or greater, is to use `EfCoreConverter` attributes on
a marker class:
a marker class, normally in a separate project from the value objects themselves:

```c#
[EfCoreConverter<Domain.CustomerId>]
Expand Down
10 changes: 8 additions & 2 deletions docs/site/Writerside/topics/reference/Integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,12 @@ public enum Conversions
/// <summary>
/// Sets the SerializeFn and DeSerializeFn members in JsConfig in a static constructor.
/// </summary>
ServiceStackDotText = 1 << 7
ServiceStackDotText = 1 << 7,

/// <summary>
/// Creates a BSON serializer for each value object.
/// </summary>
Bson = 1 << 8,
}
```

Expand All @@ -93,7 +98,8 @@ Other converters/serializers are:
They are controlled by the `Conversions` enum. The following has serializers for NSJ and STJ:

```c#
[ValueObject(conversions: Conversions.NewtonsoftJson | Conversions.SystemTextJson, underlyingType: typeof(float))]
[ValueObject<float>(conversions:
Conversions.NewtonsoftJson | Conversions.SystemTextJson)]
public readonly partial struct Celsius { }
```

Expand Down
57 changes: 57 additions & 0 deletions docs/site/Writerside/topics/reference/MongoIntegrationHowTo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Integration with MongoDB

It is possible to use value objects (VOs) in MongoDB.

To generate a converter (serializer), add the `Bson` conversion in the attribute, e.g.

```c#
[ValueObject<string>(conversions: Conversions.Bson)]
public partial class Name
{
public static readonly Name NotSet = new("[NOT_SET]");
}
```

Now that the serializers are generated, you now need to register them.
Vogen generates a static class named `RegisterBsonSerializersFor[NameOfProject]`.
This static class has a static method named `TryRegister`, which registers the serializers if they're not already registered, e.g.:

```C#
BsonSerializer.TryRegisterSerializer(new CustomerIdBsonSerializer());
BsonSerializer.TryRegisterSerializer(new EnergyUsedBsonSerializer());
```
A [MongoDB example is included in the source](https://github.com/SteveDunn/Vogen/tree/main/samples/Vogen.Examples/SerializationAndConversion/MongoScenario).

Below is a walkthrough of that sample.

The sample uses MongoDB to read and write entities (a `Person`) to a MongoDB database in a testcontainer.
Note that attributes on the value objects do not specify the BSON serializer; that is specified in global config in `ModuleInitializer.cs`:

```c#
[ValueObject]
public readonly partial struct Age;

[ValueObject<string>]
public readonly partial struct Name;

public class Person
{
public Name Name { get; set; }
public Age Age { get; set; }
}
```

This simple example registers the serializers manually:
```C#
BsonSerializer.RegisterSerializer(new NameBsonSerializer());
BsonSerializer.RegisterSerializer(new AgeBsonSerializer());
```

… but it could just as easily registered them with the generated register:
```C#
BsonSerializationRegisterForVogen_Examples.TryRegister();
```

(_replace `Vogen_Examples` with the name of *your* project_)

Next, it adds a bunch of `Person` objects to the database, each containing value objects representing age and name, and then reads them back.
5 changes: 5 additions & 0 deletions samples/AotTrimmedSample/AotTrimmedSample.v3.ncrunchproject
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>
5 changes: 5 additions & 0 deletions samples/Onion/Domain/Domain.v3.ncrunchproject
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>
5 changes: 5 additions & 0 deletions samples/Onion/Infra/Infra.v3.ncrunchproject
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>
7 changes: 5 additions & 2 deletions samples/Vogen.Examples/ModuleInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
using Vogen;
using Vogen.Examples.Types;

[assembly: VogenDefaults(staticAbstractsGeneration: StaticAbstractsGeneration.MostCommon)]
[assembly: VogenDefaults(
staticAbstractsGeneration: StaticAbstractsGeneration.MostCommon | StaticAbstractsGeneration.InstanceMethodsAndProperties,
conversions: Conversions.Default | Conversions.Bson)]

namespace Vogen.Examples;

Expand All @@ -14,7 +16,8 @@ public static class ModuleInitializer
[ModuleInitializer]
public static void Init()
{
MappingSchema.Default.SetConverter<DateTime, TimeOnly>(dt => TimeOnly.FromDateTime(dt));
MappingSchema.Default.SetConverter<DateTime, TimeOnly>(TimeOnly.FromDateTime);

SqlMapper.AddTypeHandler(new DapperDateTimeOffsetVo.DapperTypeHandler());
SqlMapper.AddTypeHandler(new DapperIntVo.DapperTypeHandler());
SqlMapper.AddTypeHandler(new DapperStringVo.DapperTypeHandler());
Expand Down
8 changes: 3 additions & 5 deletions samples/Vogen.Examples/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,20 @@ namespace Vogen.Examples
class Program
{
// ReSharper disable once UnusedParameter.Local
static Task Main(string[] args)
static async Task Main(string[] args)
{
var scenarioTypes = typeof(Program).Assembly.GetTypes()
.Where(t => typeof(IScenario).IsAssignableFrom(t) && t != typeof(IScenario)).ToList();

foreach (var eachScenarioType in scenarioTypes)
{
var instance = (IScenario)Activator.CreateInstance(eachScenarioType);
var instance = (IScenario)Activator.CreateInstance(eachScenarioType)!;
WriteBanner(instance);

instance!.Run();
await instance.Run();
}

Console.WriteLine("Finished");

return Task.CompletedTask;
}

private static void WriteBanner(IScenario scenario)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Bogus;
using JetBrains.Annotations;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver;
using Testcontainers.MongoDb;

namespace Vogen.Examples.SerializationAndConversion.Mongo;


[ValueObject]
public readonly partial struct Age;

[ValueObject<string>]
public readonly partial struct Name;

[UsedImplicitly]
public class Person
{
[BsonId]
public ObjectId Id { get; set; }
public Name Name { get; set; }
public Age Age { get; set; }
}

[UsedImplicitly]
public class MongoScenario : IScenario
{
public async Task Run()
{
string runnerOs = Environment.GetEnvironmentVariable("RUNNER_OS");

bool isLocalOrLinuxOnGitHub = string.IsNullOrEmpty(runnerOs) || runnerOs == "Linux";

if (!isLocalOrLinuxOnGitHub)
{
Console.WriteLine("Skipping because not running locally or on Linux on a GitHub action.");
return;
}

MongoDbContainer container = new MongoDbBuilder().WithImage("mongo:latest").Build();

await container.StartAsync();

var client = new MongoClient(container.GetConnectionString());

var database = client.GetDatabase("testDatabase");
var collection = database.GetCollection<Person>("peopleCollection");

BsonSerializer.RegisterSerializer(new NameBsonSerializer());
BsonSerializer.RegisterSerializer(new AgeBsonSerializer());

// or, use the generated one for all value objects...
// BsonSerializationRegisterForVogen_Examples.TryRegister();

var personFaker = new Faker<Person>()
.RuleFor(p => p.Name, f => Name.From(f.Name.FirstName()))
.RuleFor(p => p.Age, f => Age.From(DateTime.Now.Year - f.Person.DateOfBirth.Year));

foreach (Person eachPerson in personFaker.Generate(10))
{
await collection.InsertOneAsync(eachPerson);
}

Console.WriteLine("Inserted people... Now finding them...");

IAsyncCursor<Person> people = await collection.FindAsync("{}");
await people.ForEachAsync((person) => Console.WriteLine($"{person.Name} is {person.Age}"));

await container.DisposeAsync();
}
}



// Note, if you don't want any generated BSON serializers, you can specify your own generic one like the one below.
// Be aware that you'll need specify static abstracts generation in global config for this to work:
// [assembly: VogenDefaults(
// staticAbstractsGeneration: StaticAbstractsGeneration.MostCommon | StaticAbstractsGeneration.InstanceMethodsAndProperties,
// conversions: Conversions.Default | Conversions.Bson)]

// ReSharper disable once UnusedType.Global
public class BsonVogenSerializer<TValue, TUnderlyingTypeValue>
: SerializerBase<TValue> where TValue : IVogen<TValue, TUnderlyingTypeValue>
{
private readonly IBsonSerializer<TUnderlyingTypeValue> _serializer = BsonSerializer.LookupSerializer<TUnderlyingTypeValue>();

public override TValue Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) =>
TValue.From(_serializer.Deserialize(context, args));

public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, TValue value) =>
_serializer.Serialize(context, args, value.Value);
}
4 changes: 4 additions & 0 deletions samples/Vogen.Examples/Vogen.Examples.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Bogus" Version="35.6.0" />
<PackageReference Include="GitHubActionsTestLogger" Version="2.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand All @@ -17,11 +18,14 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-rc.1.22426.7" />
<PackageReference Include="MongoDB.Driver" Version="2.27.0" />
<PackageReference Include="System.Text.Json" Version="8.0.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="linq2db" Version="3.7.0" />
<PackageReference Include="ServiceStack.Text" Version="8.2.2" />
<PackageReference Include="Testcontainers" Version="3.9.0" />
<PackageReference Include="Testcontainers.MongoDb" Version="3.9.0" />
</ItemGroup>

<ItemGroup Condition=" '$(UseLocallyBuiltPackage)' != ''">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>
7 changes: 6 additions & 1 deletion src/Vogen.SharedTypes/Conversions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,10 @@ public enum Conversions
/// <summary>
/// Sets the SerializeFn and DeSerializeFn members in JsConfig in a static constructor.
/// </summary>
ServiceStackDotText = 1 << 7
ServiceStackDotText = 1 << 7,

/// <summary>
/// Creates a BSON serializer for each value object.
/// </summary>
Bson = 1 << 8
}
2 changes: 2 additions & 0 deletions src/Vogen/ValueObjectGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ private static void Execute(
WriteOpenApiSchemaCustomizationCode.WriteIfNeeded(globalConfig, spc, compilation, workItems);

WriteEfCoreSpecs.WriteIfNeeded(spc, compilation, efCoreConverterSpecs);

WriteBsonSerializers.WriteIfNeeded(spc, compilation, workItems);

if (workItems.Count > 0)
{
Expand Down
Loading
Loading