diff --git a/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs
new file mode 100644
index 00000000000..b49eacb8147
--- /dev/null
+++ b/src/MongoDB.Driver.Encryption/CsfleSchemaBuilder.cs
@@ -0,0 +1,368 @@
+/* Copyright 2010-present MongoDB Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization;
+
+namespace MongoDB.Driver.Encryption
+{
+ ///
+ /// A builder class for creating Client-Side Field Level Encryption (CSFLE) schemas.
+ ///
+ public class CsfleSchemaBuilder
+ {
+ private readonly Dictionary _schemas = new();
+
+ private CsfleSchemaBuilder()
+ {
+ }
+
+ ///
+ /// Creates a new instance of the and configures it using the provided action.
+ ///
+ /// An action to configure the schema builder.
+ public static CsfleSchemaBuilder Create(Action configure)
+ {
+ var builder = new CsfleSchemaBuilder();
+ configure(builder);
+ return builder;
+ }
+
+ ///
+ /// Adds an encrypted collection schema for a specific collection namespace.
+ ///
+ /// The type of the document in the collection.
+ /// The namespace of the collection.
+ /// An action to configure the encrypted collection builder.
+ /// The current instance.
+ public CsfleSchemaBuilder Encrypt(CollectionNamespace collectionNamespace, Action> configure)
+ {
+ var builder = new EncryptedCollectionBuilder();
+ configure(builder);
+ _schemas.Add(collectionNamespace.FullName, builder.Build());
+ return this;
+ }
+
+ ///
+ /// Builds and returns the resulting CSFLE schema.
+ ///
+ public IDictionary Build()
+ {
+ if (!_schemas.Any())
+ {
+ throw new InvalidOperationException("No schemas were added. Use Encrypt to add a schema.");
+ }
+
+ return _schemas;
+ }
+ }
+
+ ///
+ /// A builder class for creating encrypted collection schemas.
+ ///
+ /// The type of the document in the collection.
+ public class EncryptedCollectionBuilder
+ {
+ private readonly BsonDocument _schema = new("bsonType", "object");
+ private readonly RenderArgs _args = new(BsonSerializer.LookupSerializer(), BsonSerializer.SerializerRegistry);
+
+ internal EncryptedCollectionBuilder()
+ {
+ }
+
+ ///
+ /// Configures encryption metadata for the collection.
+ ///
+ /// The key ID to use for encryption.
+ /// The encryption algorithm to use.
+ /// The current instance.
+ public EncryptedCollectionBuilder EncryptMetadata(Guid? keyId = null, EncryptionAlgorithm? algorithm = null)
+ {
+ if (keyId is null && algorithm is null)
+ {
+ throw new ArgumentException("At least one of keyId or algorithm must be specified.");
+ }
+
+ _schema["encryptMetadata"] = new BsonDocument
+ {
+ { "keyId", () => new BsonArray { new BsonBinaryData(keyId!.Value, GuidRepresentation.Standard) }, keyId is not null },
+ { "algorithm", () => MapCsfleEncryptionAlgorithmToString(algorithm!.Value), algorithm is not null }
+ };
+ return this;
+ }
+
+ ///
+ /// Adds a pattern property to the schema with encryption settings.
+ ///
+ /// The regex pattern for the property.
+ /// The BSON type of the property.
+ /// The encryption algorithm to use.
+ /// The key ID to use for encryption.
+ /// The current instance.
+ public EncryptedCollectionBuilder PatternProperty(
+ string pattern,
+ BsonType bsonType,
+ EncryptionAlgorithm? algorithm = null,
+ Guid? keyId = null)
+ => PatternProperty(pattern, [bsonType], algorithm, keyId);
+
+ ///
+ /// Adds a pattern property to the schema with encryption settings.
+ ///
+ /// The regex pattern for the property.
+ /// The BSON types of the property.
+ /// The encryption algorithm to use.
+ /// The key ID to use for encryption.
+ /// The current instance.
+ public EncryptedCollectionBuilder PatternProperty(
+ string pattern,
+ IEnumerable bsonTypes = null,
+ EncryptionAlgorithm? algorithm = null,
+ Guid? keyId = null)
+ {
+ AddToPatternProperties(pattern, CreateEncryptDocument(bsonTypes, algorithm, keyId));
+ return this;
+ }
+
+ ///
+ /// Adds a nested pattern property to the schema.
+ ///
+ /// The type of the nested field.
+ /// The field.
+ /// An action to configure the nested builder.
+ /// The current instance.
+ public EncryptedCollectionBuilder PatternProperty(
+ Expression> path,
+ Action> configure)
+ => PatternProperty(new ExpressionFieldDefinition(path), configure);
+
+ ///
+ /// Adds a nested pattern property to the schema.
+ ///
+ /// The type of the nested field.
+ /// The field.
+ /// An action to configure the nested builder.
+ /// The current instance.
+ public EncryptedCollectionBuilder PatternProperty(
+ FieldDefinition path,
+ Action> configure)
+ {
+ var nestedBuilder = new EncryptedCollectionBuilder();
+ configure(nestedBuilder);
+
+ var fieldName = path.Render(_args).FieldName;
+
+ AddToPatternProperties(fieldName, nestedBuilder.Build());
+ return this;
+ }
+
+ ///
+ /// Adds a property to the schema with encryption settings.
+ ///
+ /// The type of the field.
+ /// The field.
+ /// The BSON type of the property.
+ /// The encryption algorithm to use.
+ /// The key ID to use for encryption.
+ /// The current instance.
+ public EncryptedCollectionBuilder Property(
+ Expression> path,
+ BsonType bsonType,
+ EncryptionAlgorithm? algorithm = null,
+ Guid? keyId = null)
+ => Property(path, [bsonType], algorithm, keyId);
+
+ ///
+ /// Adds a property to the schema with encryption settings.
+ ///
+ /// The type of the field.
+ /// The field.
+ /// The BSON types of the property.
+ /// The encryption algorithm to use.
+ /// The key ID to use for encryption.
+ /// The current instance.
+ public EncryptedCollectionBuilder Property(
+ Expression> path,
+ IEnumerable bsonTypes = null,
+ EncryptionAlgorithm? algorithm = null,
+ Guid? keyId = null)
+ => Property(new ExpressionFieldDefinition(path), bsonTypes, algorithm, keyId);
+
+ ///
+ /// Adds a property to the schema with encryption settings.
+ ///
+ /// The field.
+ /// The BSON type of the property.
+ /// The encryption algorithm to use.
+ /// The key ID to use for encryption.
+ /// The current instance.
+ public EncryptedCollectionBuilder Property(
+ FieldDefinition path,
+ BsonType bsonType,
+ EncryptionAlgorithm? algorithm = null,
+ Guid? keyId = null)
+ => Property(path, [bsonType], algorithm, keyId);
+
+ ///
+ /// Adds a property to the schema with encryption settings.
+ ///
+ /// The field.
+ /// The BSON types of the property.
+ /// The encryption algorithm to use.
+ /// The key ID to use for encryption.
+ /// The current instance.
+ public EncryptedCollectionBuilder Property(
+ FieldDefinition path,
+ IEnumerable bsonTypes = null,
+ EncryptionAlgorithm? algorithm = null,
+ Guid? keyId = null)
+ {
+ var fieldName = path.Render(_args).FieldName;
+ AddToProperties(fieldName, CreateEncryptDocument(bsonTypes, algorithm, keyId));
+ return this;
+ }
+
+ ///
+ /// Adds a nested property to the schema.
+ ///
+ /// The type of the nested field.
+ /// The field.
+ /// An action to configure the nested builder.
+ /// The current instance.
+ public EncryptedCollectionBuilder Property(
+ Expression> path,
+ Action> configure)
+ => Property(new ExpressionFieldDefinition(path), configure);
+
+
+ ///
+ /// Adds a nested property to the schema.
+ ///
+ /// The type of the nested field.
+ /// The field.
+ /// An action to configure the nested builder.
+ /// The current instance.
+ public EncryptedCollectionBuilder Property(
+ FieldDefinition path,
+ Action> configure)
+ {
+ var nestedBuilder = new EncryptedCollectionBuilder();
+ configure(nestedBuilder);
+
+ var fieldName = path.Render(_args).FieldName;
+ AddToProperties(fieldName, nestedBuilder.Build());
+ return this;
+ }
+
+ internal BsonDocument Build() => _schema;
+
+ private static BsonDocument CreateEncryptDocument(
+ IEnumerable bsonTypes = null,
+ EncryptionAlgorithm? algorithm = null,
+ Guid? keyId = null)
+ {
+ BsonValue bsonTypeVal = null;
+
+ if (bsonTypes != null)
+ {
+ var convertedBsonTypes = bsonTypes.Select(MapBsonTypeToString).ToList();
+
+ if (convertedBsonTypes.Count == 0)
+ {
+ throw new ArgumentException("At least one BSON type must be specified.", nameof(bsonTypes));
+ }
+
+ bsonTypeVal = convertedBsonTypes.Count == 1
+ ? convertedBsonTypes[0]
+ : new BsonArray(convertedBsonTypes);
+ }
+
+ return new BsonDocument
+ {
+ { "encrypt", new BsonDocument
+ {
+ { "bsonType", () => bsonTypeVal, bsonTypeVal is not null },
+ { "algorithm", () => MapCsfleEncryptionAlgorithmToString(algorithm!.Value), algorithm is not null },
+ {
+ "keyId",
+ () => new BsonArray(new[] { new BsonBinaryData(keyId!.Value, GuidRepresentation.Standard) }),
+ keyId is not null
+ },
+ }
+ }
+ };
+ }
+
+ private void AddToPatternProperties(string field, BsonDocument document)
+ {
+ if (!_schema.TryGetValue("patternProperties", out var value))
+ {
+ value = new BsonDocument();
+ _schema["patternProperties"] = value;
+ }
+ var patternProperties = value.AsBsonDocument;
+ patternProperties[field] = document;
+ }
+
+ private void AddToProperties(string field, BsonDocument document)
+ {
+ if (!_schema.TryGetValue("properties", out var value))
+ {
+ value = new BsonDocument();
+ _schema["properties"] = value;
+ }
+ var properties = value.AsBsonDocument;
+ properties[field] = document;
+ }
+
+ private static string MapBsonTypeToString(BsonType type)
+ {
+ return type switch
+ {
+ BsonType.Array => "array",
+ BsonType.Binary => "binData",
+ BsonType.Boolean => "bool",
+ BsonType.DateTime => "date",
+ BsonType.Decimal128 => "decimal",
+ BsonType.Document => "object",
+ BsonType.Double => "double",
+ BsonType.Int32 => "int",
+ BsonType.Int64 => "long",
+ BsonType.JavaScript => "javascript",
+ BsonType.JavaScriptWithScope => "javascriptWithScope",
+ BsonType.ObjectId => "objectId",
+ BsonType.RegularExpression => "regex",
+ BsonType.String => "string",
+ BsonType.Symbol => "symbol",
+ BsonType.Timestamp => "timestamp",
+ _ => throw new ArgumentException($"Unsupported BSON type: {type}.", nameof(type))
+ };
+ }
+
+ private static string MapCsfleEncryptionAlgorithmToString(EncryptionAlgorithm algorithm)
+ {
+ return algorithm switch
+ {
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random => "AEAD_AES_256_CBC_HMAC_SHA_512-Random",
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic => "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic",
+ _ => throw new ArgumentException($"Unexpected algorithm type: {algorithm}.", nameof(algorithm))
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs
new file mode 100644
index 00000000000..90d8364b712
--- /dev/null
+++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs
@@ -0,0 +1,710 @@
+/* Copyright 2010-present MongoDB Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+using System;
+using System.Collections.Generic;
+using FluentAssertions;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+using MongoDB.Driver.Encryption;
+using Xunit;
+
+namespace MongoDB.Driver.Tests.Encryption
+{
+ public class CsfleSchemaBuilderTests
+ {
+ private readonly CollectionNamespace _collectionNamespace = CollectionNamespace.FromFullName("medicalRecords.patients");
+ private const string _keyIdString = "6f4af470-00d1-401f-ac39-f45902a0c0c8";
+ private static Guid _keyId = Guid.Parse(_keyIdString);
+
+ [Fact]
+ public void CsfleSchemaBuilder_works_as_expected()
+ {
+ var builder = CsfleSchemaBuilder.Create(schemaBuilder =>
+ {
+ schemaBuilder.Encrypt(_collectionNamespace, builder =>
+ {
+ builder
+ .EncryptMetadata(keyId: _keyId)
+ .Property(p => p.MedicalRecords, BsonType.Array,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random)
+ .Property("bloodType", BsonType.String,
+ algorithm: EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random)
+ .Property(p => p.Ssn, BsonType.Int32,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)
+ .Property(p => p.Insurance, innerBuilder =>
+ {
+ innerBuilder
+ .Property(i => i.PolicyNumber, BsonType.Int32,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic);
+ })
+ .PatternProperty("_PIIString$", BsonType.String, EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)
+ .PatternProperty("_PIIArray$", BsonType.Array, EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random)
+ .PatternProperty(p => p.Insurance, innerBuilder =>
+ {
+ innerBuilder
+ .PatternProperty("_PIIString$", BsonType.String,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)
+ .PatternProperty("_PIINumber$", BsonType.Int32,
+ algorithm: EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic);
+ });
+
+ } );
+ });
+
+ var expected = new Dictionary
+ {
+ [_collectionNamespace.FullName] = """
+ {
+ "bsonType": "object",
+ "encryptMetadata": {
+ "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }]
+ },
+ "properties": {
+ "insurance": {
+ "bsonType": "object",
+ "properties": {
+ "policyNumber": {
+ "encrypt": {
+ "bsonType": "int",
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
+ }
+ }
+ }
+ },
+ "medicalRecords": {
+ "encrypt": {
+ "bsonType": "array",
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
+ }
+ },
+ "bloodType": {
+ "encrypt": {
+ "bsonType": "string",
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
+ }
+ },
+ "ssn": {
+ "encrypt": {
+ "bsonType": "int",
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
+ }
+ }
+ },
+ "patternProperties": {
+ "_PIIString$": {
+ "encrypt": {
+ "bsonType": "string",
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic",
+ },
+ },
+ "_PIIArray$": {
+ "encrypt": {
+ "bsonType": "array",
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random",
+ },
+ },
+ "insurance": {
+ "bsonType": "object",
+ "patternProperties": {
+ "_PIINumber$": {
+ "encrypt": {
+ "bsonType": "int",
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic",
+ },
+ },
+ "_PIIString$": {
+ "encrypt": {
+ "bsonType": "string",
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic",
+ },
+ },
+ },
+ },
+ },
+ }
+ """
+ };
+
+ AssertOutcomeCsfleSchemaBuilder(builder, expected);
+ }
+
+ [Fact]
+ public void CsfleSchemaBuilder_with_multiple_types_works_as_expected()
+ {
+ var testCollectionNamespace = CollectionNamespace.FromFullName("test.class");
+
+ var builder = CsfleSchemaBuilder.Create(schemaBuilder =>
+ {
+ schemaBuilder.Encrypt(_collectionNamespace, builder =>
+ {
+ builder
+ .EncryptMetadata(keyId: _keyId)
+ .Property(p => p.MedicalRecords, BsonType.Array,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random);
+ });
+
+ schemaBuilder.Encrypt(_collectionNamespace, builder =>
+ {
+ builder.Property(t => t.TestString, BsonType.String);
+ });
+ });
+
+ var expected = new Dictionary
+ {
+ [_collectionNamespace.FullName] = """
+ {
+ "bsonType": "object",
+ "encryptMetadata": {
+ "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }]
+ },
+ "properties": {
+ "medicalRecords": {
+ "encrypt": {
+ "bsonType": "array",
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
+ }
+ },
+ },
+ }
+ """,
+ [testCollectionNamespace.FullName] = """
+ {
+ "bsonType": "object",
+ "properties": {
+ "TestString": {
+ "encrypt": {
+ "bsonType": "string",
+ }
+ },
+ }
+ }
+ """
+ };
+
+ AssertOutcomeCsfleSchemaBuilder(builder, expected);
+ }
+
+ [Fact]
+ public void CsfleSchemaBuilder_with_no_schemas_throws()
+ {
+ var builder = CsfleSchemaBuilder.Create(_ =>
+ {
+ // No schemas added
+ });
+
+ var exception = Record.Exception(() => builder.Build());
+
+ exception.Should().NotBeNull();
+ exception.Should().BeOfType();
+ }
+
+ [Theory]
+ [InlineData(
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
+ null,
+ """ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" """)]
+ [InlineData(
+ null,
+ _keyIdString,
+ """ "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
+ public void EncryptedCollection_Metadata_works_as_expected(EncryptionAlgorithm? algorithm, string keyString, string expectedContent)
+ {
+ Guid? keyId = keyString is null ? null : Guid.Parse(keyString);
+ var builder = new EncryptedCollectionBuilder();
+
+ builder.EncryptMetadata(keyId, algorithm);
+
+ var expected = $$"""
+ {
+ "bsonType": "object",
+ "encryptMetadata": {
+ {{expectedContent}}
+ }
+ }
+ """;
+
+ AssertOutcomeCollectionBuilder(builder, expected);
+ }
+
+ [Theory]
+ [InlineData(BsonType.Array,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
+ null,
+ """ "bsonType": "array", "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" """)]
+ [InlineData(BsonType.Array,
+ null,
+ _keyIdString,
+ """ "bsonType": "array", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
+ [InlineData(BsonType.Array,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
+ _keyIdString,
+ """ "bsonType": "array", "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
+ public void EncryptedCollection_PatternProperty_works_as_expected(BsonType bsonType, EncryptionAlgorithm? algorithm, string keyString, string expectedContent)
+ {
+ Guid? keyId = keyString is null ? null : Guid.Parse(keyString);
+ var builder = new EncryptedCollectionBuilder();
+
+ builder.PatternProperty("randomRegex*", bsonType, algorithm, keyId);
+
+ var expected = $$"""
+ {
+ "bsonType": "object",
+ "patternProperties": {
+ "randomRegex*": {
+ "encrypt": {
+ {{expectedContent}}
+ }
+ }
+ }
+ }
+ """;
+
+ AssertOutcomeCollectionBuilder(builder, expected);
+ }
+
+ [Theory]
+ [InlineData(null,
+ null,
+ null,
+ "")]
+ [InlineData(new[] {BsonType.Array, BsonType.String},
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
+ null,
+ """ "bsonType": ["array", "string"], "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" """)]
+ [InlineData(new[] {BsonType.Array, BsonType.String},
+ null,
+ _keyIdString,
+ """ "bsonType": ["array", "string"], "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
+ [InlineData(new[] {BsonType.Array, BsonType.String},
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
+ _keyIdString,
+ """ "bsonType": ["array", "string"], "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
+ public void EncryptedCollection_PatternProperty_with_multiple_bson_types_works_as_expected(IEnumerable bsonTypes, EncryptionAlgorithm? algorithm, string keyString, string expectedContent)
+ {
+ Guid? keyId = keyString is null ? null : Guid.Parse(keyString);
+ var builder = new EncryptedCollectionBuilder();
+
+ builder.PatternProperty("randomRegex*", bsonTypes, algorithm, keyId);
+
+ var expected = $$"""
+ {
+ "bsonType": "object",
+ "patternProperties": {
+ "randomRegex*": {
+ "encrypt": {
+ {{expectedContent}}
+ }
+ }
+ }
+ }
+ """;
+
+ AssertOutcomeCollectionBuilder(builder, expected);
+ }
+
+ [Fact]
+ public void EncryptedCollection_PatternProperty_nested_works_as_expected()
+ {
+ Guid? keyId = Guid.Parse(_keyIdString);
+ var builder = new EncryptedCollectionBuilder();
+
+ builder.PatternProperty(p => p.Insurance, innerBuilder =>
+ {
+ innerBuilder
+ .EncryptMetadata(keyId)
+ .Property("policyNumber", BsonType.Int32,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)
+ .PatternProperty("randomRegex*", BsonType.String,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random);
+ });
+
+ var expected = """
+ {
+ "bsonType": "object",
+ "patternProperties": {
+ "insurance": {
+ "bsonType": "object",
+ "encryptMetadata": {
+ "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }]
+ },
+ "properties": {
+ "policyNumber": {
+ "encrypt": {
+ "bsonType": "int",
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
+ }
+ }
+ },
+ "patternProperties": {
+ "randomRegex*": {
+ "encrypt": {
+ "bsonType": "string",
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
+ }
+ }
+ }
+ }
+ }
+ }
+ """;
+
+ AssertOutcomeCollectionBuilder(builder, expected);
+ }
+
+ [Fact]
+ public void EncryptedCollection_PatternProperty_nested_with_string_works_as_expected()
+ {
+ Guid? keyId = Guid.Parse(_keyIdString);
+ var builder = new EncryptedCollectionBuilder();
+
+ builder.PatternProperty("insurance", innerBuilder =>
+ {
+ innerBuilder
+ .EncryptMetadata(keyId)
+ .Property("policyNumber", BsonType.Int32,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)
+ .PatternProperty("randomRegex*", BsonType.String,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random);
+ });
+
+ var expected = """
+ {
+ "bsonType": "object",
+ "patternProperties": {
+ "insurance": {
+ "bsonType": "object",
+ "encryptMetadata": {
+ "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }]
+ },
+ "properties": {
+ "policyNumber": {
+ "encrypt": {
+ "bsonType": "int",
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
+ }
+ }
+ },
+ "patternProperties": {
+ "randomRegex*": {
+ "encrypt": {
+ "bsonType": "string",
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
+ }
+ }
+ }
+ }
+ }
+ }
+ """;
+
+ AssertOutcomeCollectionBuilder(builder, expected);
+ }
+
+ [Theory]
+ [InlineData(BsonType.Array,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
+ null,
+ """ "bsonType": "array", "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" """)]
+ [InlineData(BsonType.Array,
+ null,
+ _keyIdString,
+ """ "bsonType": "array", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
+ [InlineData(BsonType.Array,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
+ _keyIdString,
+ """ "bsonType": "array", "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
+ public void EncryptedCollection_Property_with_expression_works_as_expected(BsonType bsonType, EncryptionAlgorithm? algorithm, string keyString, string expectedContent)
+ {
+ Guid? keyId = keyString is null ? null : Guid.Parse(keyString);
+ var builder = new EncryptedCollectionBuilder();
+
+ builder.Property(p => p.MedicalRecords, bsonType, algorithm, keyId);
+
+ var expected = $$"""
+ {
+ "bsonType": "object",
+ "properties": {
+ "medicalRecords": {
+ "encrypt": {
+ {{expectedContent}}
+ }
+ }
+ }
+ }
+ """;
+
+ AssertOutcomeCollectionBuilder(builder, expected);
+ }
+
+ [Theory]
+ [InlineData(null,
+ null,
+ null,
+ "")]
+ [InlineData(new[] {BsonType.Array, BsonType.String},
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
+ null,
+ """ "bsonType": ["array", "string"], "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" """)]
+ [InlineData(new[] {BsonType.Array, BsonType.String},
+ null,
+ _keyIdString,
+ """ "bsonType": ["array", "string"], "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
+ [InlineData(new[] {BsonType.Array, BsonType.String},
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
+ _keyIdString,
+ """ "bsonType": ["array", "string"], "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
+ public void EncryptedCollection_Property_with_multiple_bson_types_works_as_expected(IEnumerable bsonTypes, EncryptionAlgorithm? algorithm, string keyString, string expectedContent)
+ {
+ Guid? keyId = keyString is null ? null : Guid.Parse(keyString);
+ var builder = new EncryptedCollectionBuilder();
+
+ builder.Property(p => p.MedicalRecords, bsonTypes, algorithm, keyId);
+
+ var expected = $$"""
+ {
+ "bsonType": "object",
+ "properties": {
+ "medicalRecords": {
+ "encrypt": {
+ {{expectedContent}}
+ }
+ }
+ }
+ }
+ """;
+
+ AssertOutcomeCollectionBuilder(builder, expected);
+ }
+
+ [Theory]
+ [InlineData(BsonType.Array,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
+ null,
+ """ "bsonType": "array", "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random" """)]
+ [InlineData(BsonType.Array,
+ null,
+ _keyIdString,
+ """ "bsonType": "array", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
+ [InlineData(BsonType.Array,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random,
+ _keyIdString,
+ """ "bsonType": "array", "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random", "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }] """)]
+ public void EncryptedCollection_Property_with_string_works_as_expected(BsonType bsonType, EncryptionAlgorithm? algorithm, string keyString, string expectedContent)
+ {
+ Guid? keyId = keyString is null ? null : Guid.Parse(keyString);
+ var builder = new EncryptedCollectionBuilder();
+
+ builder.Property("medicalRecords", bsonType, algorithm, keyId);
+
+ var expected = $$"""
+ {
+ "bsonType": "object",
+ "properties": {
+ "medicalRecords": {
+ "encrypt": {
+ {{expectedContent}}
+ }
+ }
+ }
+ }
+ """;
+
+ AssertOutcomeCollectionBuilder(builder, expected);
+ }
+
+ [Fact]
+ public void EncryptedCollection_Property_nested_works_as_expected()
+ {
+ Guid? keyId = Guid.Parse(_keyIdString);
+ var builder = new EncryptedCollectionBuilder();
+
+ builder.Property(p => p.Insurance, innerBuilder =>
+ {
+ innerBuilder
+ .EncryptMetadata(keyId)
+ .Property("policyNumber", BsonType.Int32,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)
+ .PatternProperty("randomRegex*", BsonType.String,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random);
+ });
+
+ var expected = """
+ {
+ "bsonType": "object",
+ "properties": {
+ "insurance": {
+ "bsonType": "object",
+ "encryptMetadata": {
+ "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }]
+ },
+ "properties": {
+ "policyNumber": {
+ "encrypt": {
+ "bsonType": "int",
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
+ }
+ }
+ },
+ "patternProperties": {
+ "randomRegex*": {
+ "encrypt": {
+ "bsonType": "string",
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
+ }
+ }
+ }
+ }
+ }
+ }
+ """;
+
+ AssertOutcomeCollectionBuilder(builder, expected);
+ }
+
+ [Fact]
+ public void EncryptedCollection_Property_nested_with_string_works_as_expected()
+ {
+ Guid? keyId = Guid.Parse(_keyIdString);
+ var builder = new EncryptedCollectionBuilder();
+
+ builder.Property("insurance", innerBuilder =>
+ {
+ innerBuilder
+ .EncryptMetadata(keyId)
+ .Property("policyNumber", BsonType.Int32,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)
+ .PatternProperty("randomRegex*", BsonType.String,
+ EncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random);
+ });
+
+ var expected = """
+ {
+ "bsonType": "object",
+ "properties": {
+ "insurance": {
+ "bsonType": "object",
+ "encryptMetadata": {
+ "keyId": [{ "$binary" : { "base64" : "b0r0cADRQB+sOfRZAqDAyA==", "subType" : "04" } }]
+ },
+ "properties": {
+ "policyNumber": {
+ "encrypt": {
+ "bsonType": "int",
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
+ }
+ }
+ },
+ "patternProperties": {
+ "randomRegex*": {
+ "encrypt": {
+ "bsonType": "string",
+ "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
+ }
+ }
+ }
+ }
+ }
+ }
+ """;
+
+ AssertOutcomeCollectionBuilder(builder, expected);
+ }
+
+ [Fact]
+ public void EncryptedCollection_Property_with_empty_bson_types_throws()
+ {
+ var builder = new EncryptedCollectionBuilder();
+
+ var recordedException = Record.Exception(() => builder.Property("test", []));
+ recordedException.Should().NotBeNull();
+ recordedException.Should().BeOfType();
+ }
+
+ [Fact]
+ public void EncryptedCollection_Metadata_with_empty_algorithm_and_key_throws()
+ {
+ var builder = new EncryptedCollectionBuilder();
+
+ var recordedException = Record.Exception(() => builder.EncryptMetadata(null, null));
+ recordedException.Should().NotBeNull();
+ recordedException.Should().BeOfType();
+ }
+
+ private void AssertOutcomeCsfleSchemaBuilder(CsfleSchemaBuilder builder, Dictionary expectedSchema)
+ {
+ var builtSchema = builder.Build();
+ expectedSchema.Should().HaveCount(builtSchema.Count);
+ foreach (var collectionNamespace in expectedSchema.Keys)
+ {
+ var parsed = BsonDocument.Parse(expectedSchema[collectionNamespace]);
+ builtSchema[collectionNamespace].Should().BeEquivalentTo(parsed);
+ }
+ }
+
+ private void AssertOutcomeCollectionBuilder(EncryptedCollectionBuilder builder, string expected)
+ {
+ var builtSchema = builder.Build();
+ var expectedSchema = BsonDocument.Parse(expected);
+ builtSchema.Should().BeEquivalentTo(expectedSchema);
+ }
+
+ internal class TestClass
+ {
+ public ObjectId Id { get; set; }
+
+ public string TestString { get; set; }
+ }
+
+ internal class Patient
+ {
+ [BsonId]
+ public ObjectId Id { get; set; }
+
+ [BsonElement("name")]
+ public string Name { get; set; }
+
+ [BsonElement("ssn")]
+ public int Ssn { get; set; }
+
+ [BsonElement("bloodType")]
+ public string BloodType { get; set; }
+
+ [BsonElement("medicalRecords")]
+ public List MedicalRecords { get; set; }
+
+ [BsonElement("insurance")]
+ public Insurance Insurance { get; set; }
+ }
+
+ internal class MedicalRecord
+ {
+ [BsonElement("weight")]
+ public int Weight { get; set; }
+
+ [BsonElement("bloodPressure")]
+ public string BloodPressure { get; set; }
+ }
+
+ internal class Insurance
+ {
+ [BsonElement("provider")]
+ public string Provider { get; set; }
+
+ [BsonElement("policyNumber")]
+ public int PolicyNumber { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/MongoDB.Driver.Tests/Encryption/MongoEncryptionCreateCollectionExceptionTests.cs b/tests/MongoDB.Driver.Tests/Encryption/MongoEncryptionCreateCollectionExceptionTests.cs
index 5f282702bb0..9e53c873b86 100644
--- a/tests/MongoDB.Driver.Tests/Encryption/MongoEncryptionCreateCollectionExceptionTests.cs
+++ b/tests/MongoDB.Driver.Tests/Encryption/MongoEncryptionCreateCollectionExceptionTests.cs
@@ -1 +1 @@
-
\ No newline at end of file
+//TODO Do we need to keep this empty file...?
\ No newline at end of file