diff --git a/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs
new file mode 100644
index 00000000000..306d3be2201
--- /dev/null
+++ b/src/MongoDB.Driver/Encryption/CsfleSchemaBuilder.cs
@@ -0,0 +1,512 @@
+/* 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
+{
+ ///
+ ///
+ ///
+ public class CsfleSchemaBuilder
+ {
+ private readonly Dictionary _typedBuilders = [];
+ private CsfleSchemaBuilder()
+ {
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static CsfleSchemaBuilder Create(Action configure)
+ {
+ var builder = new CsfleSchemaBuilder();
+ configure(builder);
+ return builder;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void Encrypt(CollectionNamespace collectionNamespace, Action> configure)
+ {
+ var typedBuilder = new TypedBuilder();
+ configure(typedBuilder);
+ _typedBuilders.Add(collectionNamespace.FullName, typedBuilder);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public IReadOnlyDictionary Build() => _typedBuilders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Build());
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public class ElementBuilder where TSelf : ElementBuilder
+ {
+ private protected CsfleEncryptionAlgorithm? _algorithm;
+ private protected Guid? _keyId;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public TSelf WithKeyId(Guid keyId)
+ {
+ _keyId = keyId;
+ return (TSelf)this;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public TSelf WithAlgorithm(CsfleEncryptionAlgorithm algorithm)
+ {
+ _algorithm = algorithm;
+ return (TSelf)this;
+ }
+
+ internal static BsonDocument GetEncryptBsonDocument(Guid? keyId, CsfleEncryptionAlgorithm? algorithm, List bsonTypes)
+ {
+ var bsonType = bsonTypes?.First(); //TODO need to support multiple types
+
+ return new BsonDocument
+ {
+ { "bsonType", () => MapBsonTypeToString(bsonType!.Value), bsonType is not null },
+ { "algorithm", () => MapCsfleEncyptionAlgorithmToString(algorithm!.Value), algorithm is not null },
+ {
+ "keyId",
+ () => new BsonArray(new[] { new BsonBinaryData(keyId!.Value, GuidRepresentation.Standard) }),
+ keyId is not null
+ },
+ };
+ }
+
+ private static string MapBsonTypeToString(BsonType type) //TODO Taken from AstTypeFilterOperation, do we have a common place where this could go?
+ {
+ 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.MaxKey => "maxKey",
+ BsonType.MinKey => "minKey",
+ BsonType.Null => "null",
+ BsonType.ObjectId => "objectId",
+ BsonType.RegularExpression => "regex",
+ BsonType.String => "string",
+ BsonType.Symbol => "symbol",
+ BsonType.Timestamp => "timestamp",
+ BsonType.Undefined => "undefined",
+ _ => throw new ArgumentException($"Unexpected BSON type: {type}.", nameof(type))
+ };
+ }
+
+ private static string MapCsfleEncyptionAlgorithmToString(CsfleEncryptionAlgorithm algorithm)
+ {
+ return algorithm switch
+ {
+ CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random => "AEAD_AES_256_CBC_HMAC_SHA_512-Random",
+ CsfleEncryptionAlgorithm.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))
+ };
+ }
+ }
+
+ ///
+ ///
+ ///
+ public class EncryptMetadataBuilder : ElementBuilder
+ {
+ internal BsonDocument Build() => new("encryptMetadata", GetEncryptBsonDocument(_keyId, _algorithm, null));
+ }
+
+
+ ///
+ ///
+ ///
+ ///
+ public abstract class SinglePropertyBuilder : ElementBuilder>
+ {
+ internal abstract BsonDocument Build(RenderArgs args);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public abstract class SinglePropertyBuilder
+ : ElementBuilder
+ where TBuilder : SinglePropertyBuilder
+ {
+ private protected List _bsonTypes;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public TBuilder WithBsonType(BsonType bsonType)
+ {
+ _bsonTypes = [bsonType];
+ return (TBuilder)this;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public TBuilder WithBsonTypes(IEnumerable bsonTypes)
+ {
+ _bsonTypes = [..bsonTypes];
+ return (TBuilder)this;
+ }
+
+ internal abstract BsonDocument Build(RenderArgs args);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public class PropertyBuilder : SinglePropertyBuilder, TDocument>
+ {
+ private readonly FieldDefinition _path;
+
+ ///
+ ///
+ ///
+ ///
+ public PropertyBuilder(FieldDefinition path)
+ {
+ _path = path;
+ }
+
+ internal override BsonDocument Build(RenderArgs args)
+ {
+ return new BsonDocument(_path.Render(args).FieldName, new BsonDocument("encrypt", GetEncryptBsonDocument(_keyId, _algorithm, _bsonTypes)));
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public class PatternPropertyBuilder : SinglePropertyBuilder, TDocument>
+ {
+ private readonly string _pattern;
+
+ ///
+ ///
+ ///
+ ///
+ public PatternPropertyBuilder(string pattern)
+ {
+ _pattern = pattern;
+ }
+
+ internal override BsonDocument Build(RenderArgs args)
+ {
+ return new BsonDocument(_pattern, new BsonDocument("encrypt", GetEncryptBsonDocument(_keyId, _algorithm, _bsonTypes)));
+ }
+ }
+
+ ///
+ ///
+ ///
+ public abstract class NestedPropertyBuilderBase
+ {
+ internal abstract BsonDocument Build(RenderArgs args);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public class NestedPropertyBuilder : NestedPropertyBuilderBase
+ {
+ private readonly FieldDefinition _path;
+ private readonly Action> _configure;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public NestedPropertyBuilder(FieldDefinition path, Action> configure)
+ {
+ _path = path;
+ _configure = configure;
+ }
+
+ internal override BsonDocument Build(RenderArgs args)
+ {
+ var fieldBuilder = new TypedBuilder();
+ _configure(fieldBuilder);
+ return new BsonDocument(_path.Render(args).FieldName, fieldBuilder.Build());
+ }
+ }
+
+ ///
+ ///
+ ///
+ public abstract class NestedPatternPropertyBuilderBase
+ {
+ internal abstract BsonDocument Build(RenderArgs args);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public class NestedPatternPropertyBuilder : NestedPatternPropertyBuilderBase
+ {
+ private readonly FieldDefinition _path;
+ private readonly Action> _configure;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public NestedPatternPropertyBuilder(FieldDefinition path, Action> configure)
+ {
+ _path = path;
+ _configure = configure;
+ }
+
+ internal override BsonDocument Build(RenderArgs args)
+ {
+ var fieldBuilder = new TypedBuilder();
+ _configure(fieldBuilder);
+ return new BsonDocument(_path.Render(args).FieldName, fieldBuilder.Build());
+ }
+ }
+
+ ///
+ ///
+ ///
+ public abstract class TypedBuilder
+ {
+ internal abstract BsonDocument Build();
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public class TypedBuilder : TypedBuilder
+ {
+ private readonly List> _nestedProperties = [];
+ private readonly List> _nestedPatternProperties = [];
+ private readonly List> _properties = [];
+ private readonly List> _patternProperties = [];
+ private EncryptMetadataBuilder _metadata;
+
+ ///
+ ///
+ ///
+ ///
+ public EncryptMetadataBuilder EncryptMetadata()
+ {
+ _metadata = new EncryptMetadataBuilder();
+ return _metadata;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public PropertyBuilder Property(FieldDefinition path)
+ {
+ var property = new PropertyBuilder(path);
+ _properties.Add(property);
+ return property;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public PropertyBuilder Property(Expression> path)
+ {
+ return Property(new ExpressionFieldDefinition(path));
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public PatternPropertyBuilder PatternProperty(string pattern)
+ {
+ var property = new PatternPropertyBuilder(pattern);
+ _patternProperties.Add(property);
+ return property;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public NestedPropertyBuilder NestedProperty(FieldDefinition path, Action> configure)
+ {
+ var nestedProperty = new NestedPropertyBuilder(path, configure);
+ _nestedProperties.Add(nestedProperty);
+ return nestedProperty;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public NestedPropertyBuilder NestedProperty(Expression> path, Action> configure)
+ {
+ return NestedProperty(new ExpressionFieldDefinition(path), configure);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public NestedPatternPropertyBuilder NestedPatternProperty(string pattern, Action> configure)
+ {
+ var nestedProperty = new NestedPatternPropertyBuilder(pattern, configure);
+ _nestedPatternProperties.Add(nestedProperty);
+ return nestedProperty;
+ }
+
+ internal override BsonDocument Build()
+ {
+ var schema = new BsonDocument("bsonType", "object");
+
+ if (_metadata is not null)
+ {
+ schema.Merge(_metadata.Build());
+ }
+
+ var args = new RenderArgs(BsonSerializer.LookupSerializer(), BsonSerializer.SerializerRegistry);
+
+
+ BsonDocument properties = null;
+ BsonDocument patternProperties = null;
+
+ if (_properties.Any())
+ {
+ properties ??= new BsonDocument();
+
+ foreach (var property in _properties)
+ {
+ properties.Merge(property.Build(args));
+ }
+ }
+
+ if (_nestedProperties.Any())
+ {
+ properties ??= new BsonDocument();
+
+ foreach (var nestedProperty in _nestedProperties)
+ {
+ properties.Merge(nestedProperty.Build(args));
+ }
+ }
+
+ if (_patternProperties.Any())
+ {
+ patternProperties ??= new BsonDocument();
+
+ foreach (var patternProperty in _patternProperties)
+ {
+ patternProperties.Merge(patternProperty.Build(args));
+ }
+ }
+
+ if (_nestedPatternProperties.Any())
+ {
+ patternProperties ??= new BsonDocument();
+
+ foreach (var nestedPatternProperty in _nestedPatternProperties)
+ {
+ patternProperties.Merge(nestedPatternProperty.Build(args));
+ }
+ }
+
+ if (properties != null)
+ {
+ schema.Add("properties", properties);
+ }
+
+ if (patternProperties != null)
+ {
+ schema.Add("patternProperties", patternProperties);
+ }
+
+ return schema;
+ }
+ }
+
+ ///
+ /// The type of possible encryption algorithms.
+ ///
+ public enum CsfleEncryptionAlgorithm
+ {
+ ///
+ /// Randomized encryption algorithm.
+ ///
+ AEAD_AES_256_CBC_HMAC_SHA_512_Random,
+
+ ///
+ /// Deterministic encryption algorithm.
+ ///
+ AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic
+ }
+}
\ No newline at end of file
diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Filters/AstTypeFilterOperation.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Filters/AstTypeFilterOperation.cs
index f44ae90b500..f05cc3f7ff9 100644
--- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Filters/AstTypeFilterOperation.cs
+++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Filters/AstTypeFilterOperation.cs
@@ -59,7 +59,7 @@ public override BsonValue Render()
}
}
- private string MapBsonTypeToString(BsonType type)
+ private string MapBsonTypeToString(BsonType type) //TODO Is this the only place where we do this conversion?
{
switch (type)
{
diff --git a/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs
new file mode 100644
index 00000000000..54b7a4ea865
--- /dev/null
+++ b/tests/MongoDB.Driver.Tests/Encryption/CsfleSchemaBuilderTests.cs
@@ -0,0 +1,213 @@
+/* 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 MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+using MongoDB.Driver.Encryption;
+using Xunit;
+
+namespace MongoDB.Driver.Tests.Encryption
+{
+ public class CsfleSchemaBuilderTests
+ {
+ private readonly Guid _keyIdExample = Guid.Parse("6f4af470-00d1-401f-ac39-f45902a0c0c8");
+
+ [Fact]
+ public void BasicCompleteTest()
+ {
+ const string collectionName = "medicalRecords.patients";
+
+ var builder = CsfleSchemaBuilder.Create(schemaBuilder =>
+ {
+ schemaBuilder.Encrypt(CollectionNamespace.FromFullName(collectionName), builder1 =>
+ {
+ builder1.EncryptMetadata().WithKeyId(_keyIdExample);
+
+ builder1.NestedProperty(p => p.Insurance, typedBuilder1 =>
+ {
+ typedBuilder1.Property(i => i.PolicyNumber).WithBsonType(BsonType.Int32)
+ .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic);
+ });
+
+ builder1.Property(p => p.MedicalRecords).WithBsonType(BsonType.Array)
+ .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random);
+ builder1.Property("bloodType").WithBsonType(BsonType.String)
+ .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random);
+ builder1.Property(p => p.Ssn).WithBsonType(BsonType.Int32)
+ .WithAlgorithm(CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic);
+ } );
+ });
+
+ var expected = new Dictionary
+ {
+ [collectionName] = """
+ {
+ "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"
+ }
+ }
+ },
+ }
+ """
+ };
+
+ AssertOutcomeBuilder2(builder, expected);
+ }
+
+ // [Fact]
+ // public void BasicPatternTest()
+ // {
+ // const string collectionName = "medicalRecords.patients";
+ //
+ // var typedBuilder = CsfleSchemaBuilder.GetTypeBuilder()
+ // .PatternProperty("_PIIString$", bsonType: BsonType.String,
+ // algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)
+ // .PatternProperty("_PIIArray$", bsonType: BsonType.Array,
+ // algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random)
+ // .PatternProperty(p => p.Insurance, builder => builder
+ // .PatternProperty("_PIINumber$", bsonType: BsonType.Int32, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)
+ // .PatternProperty("_PIIString$", bsonType: BsonType.String, algorithm: CsfleEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)
+ // );
+ //
+ // var encryptionSchemaBuilder = new CsfleSchemaBuilder()
+ // .WithType(CollectionNamespace.FromFullName(collectionName), typedBuilder);
+ //
+ // var expected = new Dictionary
+ // {
+ // [collectionName] = """
+ // {
+ // "bsonType": "object",
+ // "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",
+ // },
+ // },
+ // },
+ // },
+ // },
+ // }
+ // """
+ // };
+ //
+ // AssertOutcomeBuilder(encryptionSchemaBuilder, expected);
+ // }
+
+ private void AssertOutcomeBuilder2(CsfleSchemaBuilder builder, Dictionary expected)
+ {
+ var builtSchema = builder.Build();
+ Assert.Equal(expected.Count, builtSchema.Count);
+ foreach (var collectionNamespace in expected.Keys)
+ {
+ var parsed = BsonDocument.Parse(expected[collectionNamespace]);
+ Assert.Equal(parsed, builtSchema[collectionNamespace]);
+ }
+ }
+
+
+ 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