Skip to content
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
22 changes: 21 additions & 1 deletion docs/site/Writerside/topics/how-to/conversion-mechanisms.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,14 +339,34 @@ public class Customer
{
[BsonId]
public CustomerId Id { get; set; }

public string Name { get; set; }
}

// MongoDB driver handles conversion automatically
var customer = collection.Find(c => c.Id == customerId).FirstOrDefault();
```

**Registration Options:**

By default, Vogen generates a static class with a static constructor that automatically registers BSON serializers.
If you need to configure BSON before registration (e.g., custom serializers, conventions), use:

```c#
[assembly: VogenDefaults(
conversions: Conversions.Bson,
customizations: Customizations.ManuallyRegisterBsonSerializers)]

// Now you control when registration happens:
// 1. Configure BSON as needed
BsonSerializer.RegisterSerializer(new CustomSerializer());

// 2. Then register Vogen serializers
BsonSerializationRegisterFor[YourProjectName].TryRegister();
```

See [MongoDB Integration](MongoIntegrationHowTo.md) for details.

### MessagePack

```c#
Expand Down
6 changes: 5 additions & 1 deletion docs/site/Writerside/topics/reference/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ Most values are present in both `ValueObject` and `VogenDefaults`. The parameter
* `underlyingType`—the type of the primitive that is being wrapped—defaults to `int`
* `conversions` - specified what conversion code is generated - defaults to `Conversions.Default` which generates type converters and a converter to handle serialization using System.Text.Json
* `throws` = specifies the type of exception thrown when validation fails—defaults to `ValueObjectValidationException`
* `customizations`—simple customization switches—defaults to `Customizations.None`,
* `customizations`—simple customization switches—defaults to `Customizations.None`. Available flags:
* `Customizations.TreatNumberAsStringInSystemTextJson` - serialize numbers as strings in System.Text.Json (obsolete on .NET 5+)
* `Customizations.AddFactoryMethodForGuids` - adds a `FromNewGuid()` factory method for GUID value objects
* `Customizations.GenerateLinqPadToDump` - generates a `ToDump()` method for LinqPad
* `Customizations.ManuallyRegisterBsonSerializers` - generates BSON registration as a `TryRegister()` method without a static constructor, allowing control over registration timing,
* `deserializationStrictness` - specifies how strict deserialization is, e.g. should your `Validate` method be called, or should pre-defined instances that otherwise invalid be allowed - defaults to `DeserializationStrictness.AllowValidAndKnownInstances`
* `debuggerAttributes` - specifies the level that debug attributes are written as some IDEs don't support all of them, e.g., Rider - defaults to `DebuggerAttributeGeneration.Full` which generates `DebuggerDisplay` and a debugger proxy type for IDEs that support them
* `comparison`—species which comparison code is generated—defaults to `ComparisonGeneration.UseUnderlying` which hoists any `IComparable` implementations from the primitive
Expand Down
25 changes: 24 additions & 1 deletion docs/site/Writerside/topics/reference/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,30 @@ var newCustomerId = CustomerId.FromNewGuid();
```

> To customize the generation of Guids, please see [this tutorial](Working-with-IDs.md)
>
>

## How do I control when BSON serializers are registered?

By default, Vogen generates a static class with a static constructor that automatically registers BSON serializers when first accessed.
If you need to run BSON configuration before registration (e.g., set up custom conventions, configure serializers), use:

```c#
[assembly: VogenDefaults(
customizations: Customizations.ManuallyRegisterBsonSerializers)]
```

This generates a `TryRegister()` method without a static constructor, giving you full control:

```c#
// 1. Configure BSON as needed
BsonSerializer.RegisterSerializer(new MyCustomSerializer());
ConventionRegistry.Register("MyConventions", new ConventionPack { ... }, _ => true);

// 2. Then register Vogen-generated serializers
BsonSerializationRegisterFor[YourProjectName].TryRegister();
```

See [MongoDB Integration](MongoIntegrationHowTo.md) for more details.

## Can I use value objects instead of primitives as parameters in Blazor pages and components?

Expand Down
21 changes: 20 additions & 1 deletion docs/site/Writerside/topics/reference/MongoIntegrationHowTo.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,22 @@ public partial class Name

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.:

By default, this class has a static constructor that automatically registers all BSON serializers when first accessed.
However, if you need to run BSON configuration before the serializers are registered, you can use the `ManuallyRegisterBsonSerializers` customization:

```c#
[assembly: VogenDefaults(customizations: Customizations.ManuallyRegisterBsonSerializers)]
```

With this flag set, Vogen generates a `TryRegister()` method without a static constructor, allowing you to control when registration occurs:

```C#
// Call this manually after any BSON configuration
BsonSerializationRegisterFor[YourProjectName].TryRegister();
```

Without the flag, the static constructor automatically calls code like:

```C#
BsonSerializer.TryRegisterSerializer(new CustomerIdBsonSerializer());
Expand Down Expand Up @@ -54,4 +69,8 @@ BsonSerializationRegisterForVogen_Examples.TryRegister();

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

> **Note:** By default, the static class has a static constructor that automatically registers serializers when first accessed.
> If you need control over when registration happens (e.g., to configure BSON first), use the `Customizations.ManuallyRegisterBsonSerializers` flag
> to generate only the `TryRegister()` method without the static constructor.

Next, it adds a bunch of `Person` objects to the database, each containing value objects representing age and name, and then reads them back.
10 changes: 9 additions & 1 deletion src/Vogen.SharedTypes/Customizations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,13 @@ public enum Customizations
/// <summary>
/// Generate ToDump for LinqPad
/// </summary>
GenerateLinqPadToDump = 1 << 2
GenerateLinqPadToDump = 1 << 2,

/// <summary>
/// Manually register BSON registrations. By default, Vogen creates a static class with a static constructor that registers
/// all BSON serializers. However, you might want to run BSON configuration before this.
/// This flag tells Vogen to create a `TryRegister` method on a class named `BsonSerializationRegisterFor[YourAssemblyName]`
/// as opposed to a static constructor
/// </summary>
ManuallyRegisterBsonSerializers = 1 << 3
}
61 changes: 43 additions & 18 deletions src/Vogen/GenerateCodeForBsonSerializers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@ namespace Vogen;
internal class GenerateCodeForBsonSerializers
{
private const string _nameSuffix = "BsonSerializer";

/// <summary>
/// For each value object that has a BSON conversion attribute, write a separate file with a serializer,
/// with a class name of '[WrapperName]BsonSerializer', and a filename of '[NameSpace].[WrapperName]_bson.g.cs'
/// </summary>
/// <param name="context"></param>
/// <param name="compilation"></param>
/// <param name="workItems"></param>
public static void GenerateForApplicableValueObjects(SourceProductionContext context, Compilation compilation, List<VoWorkItem> workItems)
/// <param name="customizations"></param>
public static void GenerateForApplicableValueObjects(SourceProductionContext context,
Compilation compilation,
List<VoWorkItem> workItems,
Customizations? customizations)
{
if (!compilation.IsAtLeastCSharp12())
{
Expand All @@ -35,7 +39,7 @@ public static void GenerateForApplicableValueObjects(SourceProductionContext con
Util.AddSourceToContext(filename, context, Util.FormatSource(eachGenerated));
}

WriteRegistration(applicableWrappers, compilation, context);
WriteRegistration(applicableWrappers, compilation, context, customizations);
}

/// <summary>
Expand Down Expand Up @@ -131,7 +135,10 @@ string GenerateSerializers()
}
}

private static void WriteRegistration(List<VoWorkItem> items, Compilation compilation, SourceProductionContext context)
private static void WriteRegistration(List<VoWorkItem> items,
Compilation compilation,
SourceProductionContext context,
Customizations? customizations)
{
if (items.Count == 0)
{
Expand All @@ -140,20 +147,7 @@ private static void WriteRegistration(List<VoWorkItem> items, Compilation compil

var classNameForRegistering = ClassNameForRegistering();

string source =
$$"""
{{GeneratedCodeSegments.Preamble}}

public static class {{classNameForRegistering}}
{
static {{classNameForRegistering}}()
{
{{TextForEachRegisterCall(items)}}
}

public static void TryRegister() { }
}
""";
var source = GenerateRegistrationSource();

Util.AddSourceToContext(classNameForRegistering, context, Util.FormatSource(source));
return;
Expand All @@ -170,6 +164,37 @@ string ClassNameForRegistering()

return s;
}

string GenerateRegistrationSource()
{
bool manual = customizations is not null && customizations.Value.HasFlag(Customizations.ManuallyRegisterBsonSerializers);

return manual
? $$"""
{{GeneratedCodeSegments.Preamble}}

public static class {{classNameForRegistering}}
{
public static void TryRegister()
{
{{TextForEachRegisterCall(items)}}
}
}
"""
: $$"""
{{GeneratedCodeSegments.Preamble}}

public static class {{classNameForRegistering}}
{
static {{classNameForRegistering}}()
{
{{TextForEachRegisterCall(items)}}
}

public static void TryRegister() { }
}
""";
}
}

private static string TextForEachRegisterCall(List<VoWorkItem> wrappers)
Expand Down
2 changes: 1 addition & 1 deletion src/Vogen/ValueObjectGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ private static void Execute(
GenerateCodeForMessagePack.GenerateForApplicableValueObjects(spc, compilation, workItems);
GenerateCodeForMessagePack.GenerateForMarkerClasses(spc, markerClasses);

GenerateCodeForBsonSerializers.GenerateForApplicableValueObjects(spc, compilation, workItems);
GenerateCodeForBsonSerializers.GenerateForApplicableValueObjects(spc, compilation, workItems, globalConfig?.Customizations);
GenerateCodeForBsonSerializers.GenerateForMarkerClasses(spc, markerClasses);

GenerateCodeForOrleansSerializers.WriteIfNeeded(spc, workItems);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,29 @@ public partial struct Name;
.RunOn(TargetFramework.Net8_0);
}

[Fact]
public async Task Writes_bson_serializers_but_manually_register_the_serializers()
{
var source = """
using System;
using Vogen;

[assembly: VogenDefaults(conversions: Conversions.Bson, customizations: Customizations.ManuallyRegisterBsonSerializers)]

namespace Whatever;
[ValueObject<int>]
public partial struct Age;

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

await new SnapshotRunner<ValueObjectGenerator>()
.WithSource(source)
.IgnoreInitialCompilationErrors()
.RunOn(TargetFramework.Net8_0);
}

[Fact]
public async Task Escapes_namespaces()
{
Expand Down
Loading
Loading