diff --git a/src/RecurringThings.MongoDB/Configuration/GuidStringRepresentationConvention.cs b/src/RecurringThings.MongoDB/Configuration/GuidStringRepresentationConvention.cs
new file mode 100644
index 0000000..7b7b11a
--- /dev/null
+++ b/src/RecurringThings.MongoDB/Configuration/GuidStringRepresentationConvention.cs
@@ -0,0 +1,47 @@
+namespace RecurringThings.MongoDB.Configuration;
+
+using System;
+using global::MongoDB.Bson.Serialization;
+using global::MongoDB.Bson.Serialization.Conventions;
+using global::MongoDB.Bson.Serialization.Serializers;
+
+///
+/// Convention that serializes all Guid properties as strings instead of binary.
+///
+///
+///
+/// This convention applies to both and properties,
+/// ensuring consistent string representation without requiring attributes on each property.
+///
+///
+/// Uses instead of the built-in GuidSerializer to avoid
+/// the "GuidRepresentation is Unspecified" error in newer MongoDB driver versions.
+///
+///
+internal sealed class GuidStringRepresentationConvention : IMemberMapConvention
+{
+ private static readonly StringGuidSerializer GuidSerializer = new();
+
+ ///
+ /// Gets the name of this convention.
+ ///
+ public string Name => "GuidStringRepresentation";
+
+ ///
+ /// Applies the convention to a member map.
+ ///
+ /// The member map to apply the convention to.
+ public void Apply(BsonMemberMap memberMap)
+ {
+ ArgumentNullException.ThrowIfNull(memberMap);
+
+ if (memberMap.MemberType == typeof(Guid))
+ {
+ memberMap.SetSerializer(GuidSerializer);
+ }
+ else if (memberMap.MemberType == typeof(Guid?))
+ {
+ memberMap.SetSerializer(new NullableSerializer(GuidSerializer));
+ }
+ }
+}
diff --git a/src/RecurringThings.MongoDB/Configuration/MongoDbInitializer.cs b/src/RecurringThings.MongoDB/Configuration/MongoDbInitializer.cs
new file mode 100644
index 0000000..1227173
--- /dev/null
+++ b/src/RecurringThings.MongoDB/Configuration/MongoDbInitializer.cs
@@ -0,0 +1,62 @@
+namespace RecurringThings.MongoDB.Configuration;
+
+using System;
+using global::MongoDB.Bson.Serialization.Conventions;
+
+///
+/// Initializes MongoDB conventions when the assembly is loaded.
+///
+///
+/// This ensures that conventions are registered before any serialization occurs,
+/// regardless of whether is called.
+///
+internal static class MongoDbInitializer
+{
+ private static bool _initialized;
+ private static readonly object _lock = new();
+
+ ///
+ /// Registers MongoDB conventions for RecurringThings documents.
+ ///
+ ///
+ /// This method is idempotent and can be called multiple times safely.
+ /// It registers the following conventions:
+ ///
+ /// - NamedIdMemberConvention: Maps "Id" property to MongoDB "_id" field
+ /// - CamelCaseElementNameConvention: Converts property names to camelCase
+ /// - IgnoreIfNullConvention: Excludes null properties from serialization
+ /// - GuidStringRepresentationConvention: Serializes Guid properties as strings
+ ///
+ ///
+ internal static void EnsureInitialized()
+ {
+ if (_initialized)
+ {
+ return;
+ }
+
+ lock (_lock)
+ {
+ if (_initialized)
+ {
+ return;
+ }
+
+ var conventionPack = new ConventionPack
+ {
+ new NamedIdMemberConvention("Id"),
+ new CamelCaseElementNameConvention(),
+ new IgnoreIfNullConvention(true),
+ new GuidStringRepresentationConvention()
+ };
+
+ ConventionRegistry.Register(
+ "RecurringThingsConventions",
+ conventionPack,
+ t => t.FullName?.StartsWith("RecurringThings", StringComparison.Ordinal) == true);
+
+ _initialized = true;
+ }
+ }
+
+}
diff --git a/src/RecurringThings.MongoDB/Configuration/RecurringThingsBuilderExtensions.cs b/src/RecurringThings.MongoDB/Configuration/RecurringThingsBuilderExtensions.cs
index 11738cf..754a93f 100644
--- a/src/RecurringThings.MongoDB/Configuration/RecurringThingsBuilderExtensions.cs
+++ b/src/RecurringThings.MongoDB/Configuration/RecurringThingsBuilderExtensions.cs
@@ -1,7 +1,6 @@
namespace RecurringThings.MongoDB.Configuration;
using System;
-using global::MongoDB.Bson.Serialization.Conventions;
using global::MongoDB.Driver;
using Microsoft.Extensions.DependencyInjection;
using RecurringThings.Configuration;
@@ -44,12 +43,8 @@ public static RecurringThingsBuilder UseMongoDb(
configure(options);
options.Validate();
- // Register camelCase naming convention for RecurringThings documents
- var conventionPack = new ConventionPack { new CamelCaseElementNameConvention() };
- ConventionRegistry.Register(
- "RecurringThingsCamelCase",
- conventionPack,
- t => t.FullName?.StartsWith("RecurringThings", StringComparison.Ordinal) == true);
+ // Ensure MongoDB conventions are registered (idempotent)
+ MongoDbInitializer.EnsureInitialized();
// Mark MongoDB as configured
builder.MongoDbConfigured = true;
diff --git a/src/RecurringThings.MongoDB/Configuration/StringGuidSerializer.cs b/src/RecurringThings.MongoDB/Configuration/StringGuidSerializer.cs
new file mode 100644
index 0000000..2473a57
--- /dev/null
+++ b/src/RecurringThings.MongoDB/Configuration/StringGuidSerializer.cs
@@ -0,0 +1,37 @@
+namespace RecurringThings.MongoDB.Configuration;
+
+using System;
+using global::MongoDB.Bson;
+using global::MongoDB.Bson.Serialization;
+using global::MongoDB.Bson.Serialization.Serializers;
+
+///
+/// A custom GUID serializer that stores GUIDs as strings in MongoDB.
+///
+///
+/// This serializer is used instead of with
+/// because the standard serializer throws "GuidRepresentation is Unspecified" in newer MongoDB drivers.
+/// This implementation directly serializes/deserializes GUIDs as strings without the GuidRepresentation validation.
+///
+internal sealed class StringGuidSerializer : StructSerializerBase
+{
+ ///
+ public override Guid Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
+ {
+ var bsonType = context.Reader.GetCurrentBsonType();
+
+ return bsonType switch
+ {
+ BsonType.String => Guid.Parse(context.Reader.ReadString()),
+ BsonType.Binary => context.Reader.ReadBinaryData().ToGuid(),
+ BsonType.Null => throw new FormatException("Cannot deserialize null to Guid"),
+ _ => throw new FormatException($"Cannot deserialize BsonType {bsonType} to Guid")
+ };
+ }
+
+ ///
+ public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Guid value)
+ {
+ context.Writer.WriteString(value.ToString());
+ }
+}
diff --git a/src/RecurringThings.MongoDB/Documents/RecurringThingDocument.cs b/src/RecurringThings.MongoDB/Documents/RecurringThingDocument.cs
index 22f8b44..e45f3d7 100644
--- a/src/RecurringThings.MongoDB/Documents/RecurringThingDocument.cs
+++ b/src/RecurringThings.MongoDB/Documents/RecurringThingDocument.cs
@@ -2,8 +2,6 @@ namespace RecurringThings.MongoDB.Documents;
using System;
using System.Collections.Generic;
-using global::MongoDB.Bson;
-using global::MongoDB.Bson.Serialization.Attributes;
using RecurringThings.Domain;
using RecurringThings.Options;
@@ -19,16 +17,18 @@ namespace RecurringThings.MongoDB.Documents;
/// - "override": A modified occurrence from a recurrence
///
///
-/// Fields are mapped to camelCase for MongoDB conventions.
+/// Serialization is configured via conventions (no attributes required):
+/// - The Id property is mapped as the document _id using NamedIdMemberConvention
+/// - All Guid properties are serialized as strings using GuidStringRepresentationConvention
+/// - Fields are mapped to camelCase using CamelCaseElementNameConvention
+/// - Null properties are automatically ignored using IgnoreIfNullConvention
///
///
public sealed class RecurringThingDocument
{
///
- /// Gets or sets the unique identifier.
+ /// Gets or sets the unique identifier (maps to MongoDB _id field).
///
- [BsonId]
- [BsonRepresentation(BsonType.String)]
public Guid Id { get; set; }
///
@@ -55,7 +55,6 @@ public sealed class RecurringThingDocument
///
/// Present on recurrences and occurrences. Null for exceptions and overrides.
///
- [BsonIgnoreIfNull]
public string? Type { get; set; }
///
@@ -64,7 +63,6 @@ public sealed class RecurringThingDocument
///
/// Present on recurrences, occurrences, and overrides. Null for exceptions.
///
- [BsonIgnoreIfNull]
public DateTime? StartTime { get; set; }
///
@@ -74,7 +72,6 @@ public sealed class RecurringThingDocument
/// Present on occurrences and overrides. Null for recurrences and exceptions.
/// Computed as StartTime + Duration.
///
- [BsonIgnoreIfNull]
public DateTime? EndTime { get; set; }
///
@@ -84,7 +81,6 @@ public sealed class RecurringThingDocument
/// Stored as milliseconds for efficient MongoDB operations.
/// Present on recurrences, occurrences, and overrides. Null for exceptions.
///
- [BsonIgnoreIfNull]
public long? DurationMs { get; set; }
///
@@ -93,7 +89,6 @@ public sealed class RecurringThingDocument
///
/// Present on recurrences and occurrences. Null for exceptions and overrides.
///
- [BsonIgnoreIfNull]
public string? TimeZone { get; set; }
///
@@ -102,7 +97,6 @@ public sealed class RecurringThingDocument
///
/// Present only on recurrences.
///
- [BsonIgnoreIfNull]
public DateTime? RecurrenceEndTime { get; set; }
///
@@ -111,7 +105,6 @@ public sealed class RecurringThingDocument
///
/// Present only on recurrences.
///
- [BsonIgnoreIfNull]
public string? RRule { get; set; }
///
@@ -126,7 +119,6 @@ public sealed class RecurringThingDocument
/// monthly patterns with day <= 28, or patterns where no months are affected).
///
///
- [BsonIgnoreIfNull]
public string? MonthDayBehavior { get; set; }
///
@@ -135,8 +127,6 @@ public sealed class RecurringThingDocument
///
/// Present on exceptions and overrides.
///
- [BsonRepresentation(BsonType.String)]
- [BsonIgnoreIfNull]
public Guid? RecurrenceId { get; set; }
///
@@ -145,7 +135,6 @@ public sealed class RecurringThingDocument
///
/// Present on exceptions and overrides.
///
- [BsonIgnoreIfNull]
public DateTime? OriginalTimeUtc { get; set; }
///
@@ -155,7 +144,6 @@ public sealed class RecurringThingDocument
/// Denormalized from the parent recurrence at creation time.
/// Present only on overrides.
///
- [BsonIgnoreIfNull]
public long? OriginalDurationMs { get; set; }
///
@@ -165,13 +153,11 @@ public sealed class RecurringThingDocument
/// Denormalized from the parent recurrence at creation time.
/// Present only on overrides.
///
- [BsonIgnoreIfNull]
public Dictionary? OriginalExtensions { get; set; }
///
/// Gets or sets the user-defined key-value metadata.
///
- [BsonIgnoreIfNull]
public Dictionary? Extensions { get; set; }
}
diff --git a/src/RecurringThings.MongoDB/Indexing/IndexManager.cs b/src/RecurringThings.MongoDB/Indexing/IndexManager.cs
index 1d7239b..6c89328 100644
--- a/src/RecurringThings.MongoDB/Indexing/IndexManager.cs
+++ b/src/RecurringThings.MongoDB/Indexing/IndexManager.cs
@@ -5,6 +5,7 @@ namespace RecurringThings.MongoDB.Indexing;
using System.Threading;
using System.Threading.Tasks;
using global::MongoDB.Driver;
+using RecurringThings.MongoDB.Configuration;
using RecurringThings.MongoDB.Documents;
///
@@ -40,6 +41,7 @@ public sealed class IndexManager
public IndexManager(IMongoDatabase database, string collectionName = "recurring_things")
{
ArgumentNullException.ThrowIfNull(database);
+ MongoDbInitializer.EnsureInitialized();
_collection = database.GetCollection(collectionName);
}
diff --git a/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceExceptionRepository.cs b/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceExceptionRepository.cs
index a4bc3c1..6f9b6ac 100644
--- a/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceExceptionRepository.cs
+++ b/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceExceptionRepository.cs
@@ -8,6 +8,7 @@ namespace RecurringThings.MongoDB.Repositories;
using System.Threading.Tasks;
using global::MongoDB.Driver;
using RecurringThings.Domain;
+using RecurringThings.MongoDB.Configuration;
using RecurringThings.MongoDB.Documents;
using RecurringThings.Repository;
using Transactional.Abstractions;
@@ -28,6 +29,7 @@ internal sealed class MongoOccurrenceExceptionRepository : IOccurrenceExceptionR
public MongoOccurrenceExceptionRepository(IMongoDatabase database, string collectionName = "recurring_things")
{
ArgumentNullException.ThrowIfNull(database);
+ MongoDbInitializer.EnsureInitialized();
_collection = database.GetCollection(collectionName);
}
diff --git a/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceOverrideRepository.cs b/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceOverrideRepository.cs
index 6b479be..a8266d6 100644
--- a/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceOverrideRepository.cs
+++ b/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceOverrideRepository.cs
@@ -8,6 +8,7 @@ namespace RecurringThings.MongoDB.Repositories;
using System.Threading.Tasks;
using global::MongoDB.Driver;
using RecurringThings.Domain;
+using RecurringThings.MongoDB.Configuration;
using RecurringThings.MongoDB.Documents;
using RecurringThings.Repository;
using Transactional.Abstractions;
@@ -28,6 +29,7 @@ internal sealed class MongoOccurrenceOverrideRepository : IOccurrenceOverrideRep
public MongoOccurrenceOverrideRepository(IMongoDatabase database, string collectionName = "recurring_things")
{
ArgumentNullException.ThrowIfNull(database);
+ MongoDbInitializer.EnsureInitialized();
_collection = database.GetCollection(collectionName);
}
diff --git a/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceRepository.cs b/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceRepository.cs
index 061937d..3f9c5c6 100644
--- a/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceRepository.cs
+++ b/src/RecurringThings.MongoDB/Repositories/MongoOccurrenceRepository.cs
@@ -7,6 +7,7 @@ namespace RecurringThings.MongoDB.Repositories;
using System.Threading.Tasks;
using global::MongoDB.Driver;
using RecurringThings.Domain;
+using RecurringThings.MongoDB.Configuration;
using RecurringThings.MongoDB.Documents;
using RecurringThings.Repository;
using Transactional.Abstractions;
@@ -27,6 +28,7 @@ internal sealed class MongoOccurrenceRepository : IOccurrenceRepository
public MongoOccurrenceRepository(IMongoDatabase database, string collectionName = "recurring_things")
{
ArgumentNullException.ThrowIfNull(database);
+ MongoDbInitializer.EnsureInitialized();
_collection = database.GetCollection(collectionName);
}
diff --git a/src/RecurringThings.MongoDB/Repositories/MongoRecurrenceRepository.cs b/src/RecurringThings.MongoDB/Repositories/MongoRecurrenceRepository.cs
index 578daea..7549ee8 100644
--- a/src/RecurringThings.MongoDB/Repositories/MongoRecurrenceRepository.cs
+++ b/src/RecurringThings.MongoDB/Repositories/MongoRecurrenceRepository.cs
@@ -7,6 +7,7 @@ namespace RecurringThings.MongoDB.Repositories;
using System.Threading.Tasks;
using global::MongoDB.Driver;
using RecurringThings.Domain;
+using RecurringThings.MongoDB.Configuration;
using RecurringThings.MongoDB.Documents;
using RecurringThings.Repository;
using Transactional.Abstractions;
@@ -27,6 +28,7 @@ internal sealed class MongoRecurrenceRepository : IRecurrenceRepository
public MongoRecurrenceRepository(IMongoDatabase database, string collectionName = "recurring_things")
{
ArgumentNullException.ThrowIfNull(database);
+ MongoDbInitializer.EnsureInitialized();
_collection = database.GetCollection(collectionName);
}