Skip to content

Commit 0086a5c

Browse files
rklfssJKamsker
andauthored
Added BsonRefId<T> to allow explicit DbRefs in LINQ expressions (#2741)
* Add Contributor Covenant Code of Conduct This document outlines the Code of Conduct for community members, detailing pledges, standards of behavior, enforcement responsibilities, and consequences for violations. * Update README with new build status and badges Updated build status badge and added pre-release badge. * Added BsonRefId<T> to allow explicit DbRefs in LINQ expressions * Applied code review suggestions * Applied suggestion from code review * Make IsDbRef backward compatible + add guards * Undo unintended markdown change --------- Co-authored-by: Jonas Kamsker <11245306+JKamsker@users.noreply.github.com>
1 parent 84644ce commit 0086a5c

File tree

6 files changed

+304
-42
lines changed

6 files changed

+304
-42
lines changed

LiteDB.Tests/Mapper/LinqBsonExpression_Tests.cs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,22 @@ public class Order
8383

8484
[BsonRef("users")]
8585
public List<User> Users { get; set; }
86+
87+
[BsonRef("products")]
88+
public Product[] Products { get; set; }
89+
}
90+
91+
public class OrderCustomer : Customer
92+
{
93+
// Just used to test derived types serialization in BsonRefId tests
94+
}
95+
96+
public class OrderMissingDbRefCollectionName
97+
{
98+
[BsonId]
99+
public int Id { get; set; }
100+
101+
public Product Product { get; set; }
86102
}
87103

88104
public class Account
@@ -524,6 +540,79 @@ public void Linq_InvocationExpression()
524540
//the right expr of exprMerged uses @ (instead of $) because the rootParameter is different for exprLeft and exprRight
525541
}
526542

543+
[Fact]
544+
public void Linq_MemberInit_DbRef()
545+
{
546+
var p1Id = ObjectId.NewObjectId();
547+
var p2Id = ObjectId.NewObjectId();
548+
549+
TestExpr<Order>(
550+
x => new Order
551+
{
552+
OrderNumber = 1,
553+
Customer = new BsonRefId<OrderCustomer>(10),
554+
Users = new List<User>
555+
{
556+
new BsonRefId<User>(101),
557+
new BsonRefId<User>(x.Users[0].Id + 100),
558+
},
559+
Products = new Product[]
560+
{
561+
new BsonRefId<Product>(p1Id),
562+
new BsonRefId<Product>(p2Id),
563+
},
564+
},
565+
@"{
566+
_id: @p0,
567+
Customer: { $id: @p1, $ref: @p2, $type: @p3 },
568+
Users:[
569+
{ $id: @p4, $ref: @p5 },
570+
{ $id: ($.Users[0].$id + @p6), $ref: @p7 }
571+
],
572+
Products: [
573+
{ $id: @p8, $ref: @p9 },
574+
{ $id: @p10, $ref: @p11 }
575+
]
576+
}",
577+
[
578+
1,
579+
10,
580+
"customers",
581+
DefaultTypeNameBinder.Instance.GetName(typeof(OrderCustomer)),
582+
101,
583+
"users",
584+
100,
585+
"users",
586+
p1Id,
587+
"products",
588+
p2Id,
589+
"products",
590+
]);
591+
}
592+
593+
[Fact]
594+
public void Linq_MemberInit_DbRef_Throws_When_CollectionName_Is_Missing()
595+
{
596+
var mapper = new BsonMapper();
597+
598+
mapper.ResolveMember = (type, _, member) =>
599+
{
600+
if (type == typeof(OrderMissingDbRefCollectionName) && member.MemberName == nameof(OrderMissingDbRefCollectionName.Product))
601+
{
602+
member.IsDbRef = true;
603+
}
604+
};
605+
606+
mapper.Invoking(x => x.GetExpression<OrderMissingDbRefCollectionName, object>(o => new OrderMissingDbRefCollectionName
607+
{
608+
Id = 1,
609+
Product = new BsonRefId<Product>(ObjectId.NewObjectId())
610+
}))
611+
.Should()
612+
.Throw<NotSupportedException>()
613+
.WithMessage("*DbRef collection name*");
614+
}
615+
527616
#region Test helper
528617

529618
/// <summary>
@@ -582,4 +671,4 @@ private void TestException<T, TException>(Func<Expression<Func<T, object>>> fn)
582671

583672
#endregion
584673
}
585-
}
674+
}

LiteDB/Client/Mapper/BsonMapper.Serialize.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ private BsonDocument SerializeObject(Type type, object obj, int depth)
188188
// adding _type only where property Type is not same as object instance type
189189
if (type != t)
190190
{
191-
doc["_type"] = new BsonValue(_typeNameBinder.GetName(t));
191+
doc["_type"] = SerializeTypeName(t);
192192
}
193193

194194
foreach (var member in entity.Members.Where(x => x.Getter != null))
@@ -211,5 +211,13 @@ private BsonDocument SerializeObject(Type type, object obj, int depth)
211211

212212
return doc;
213213
}
214+
215+
/// <summary>
216+
/// Returns the name of the given type for serialization (type discriminator).
217+
/// </summary>
218+
internal BsonValue SerializeTypeName(Type type)
219+
{
220+
return new BsonValue(_typeNameBinder.GetName(type));
221+
}
214222
}
215-
}
223+
}

LiteDB/Client/Mapper/BsonMapper.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -227,22 +227,22 @@ public BsonMapper UseLowerCaseDelimiter(char delimiter = '_')
227227
/// </summary>
228228
internal static void RegisterDbRef(BsonMapper mapper, MemberMapper member, ITypeNameBinder typeNameBinder, string collection)
229229
{
230-
member.IsDbRef = true;
230+
member.DbRefCollectionName = collection;
231231

232232
if (member.IsEnumerable)
233233
{
234-
RegisterDbRefList(mapper, member, typeNameBinder, collection);
234+
RegisterDbRefList(mapper, member, typeNameBinder);
235235
}
236236
else
237237
{
238-
RegisterDbRefItem(mapper, member, typeNameBinder, collection);
238+
RegisterDbRefItem(mapper, member, typeNameBinder);
239239
}
240240
}
241241

242242
/// <summary>
243243
/// Register a property as a DbRef - implement a custom Serialize/Deserialize actions to convert entity to $id, $ref only
244244
/// </summary>
245-
private static void RegisterDbRefItem(BsonMapper mapper, MemberMapper member, ITypeNameBinder typeNameBinder, string collection)
245+
private static void RegisterDbRefItem(BsonMapper mapper, MemberMapper member, ITypeNameBinder typeNameBinder)
246246
{
247247
// get entity
248248
var entity = mapper.GetEntityMapper(member.DataType);
@@ -263,12 +263,12 @@ private static void RegisterDbRefItem(BsonMapper mapper, MemberMapper member, IT
263263
var bsonDocument = new BsonDocument
264264
{
265265
["$id"] = m.Serialize(id.GetType(), id, 0),
266-
["$ref"] = collection
266+
["$ref"] = member.DbRefCollectionName
267267
};
268268

269269
if (member.DataType != obj.GetType())
270270
{
271-
bsonDocument["$type"] = typeNameBinder.GetName(obj.GetType());
271+
bsonDocument["$type"] = mapper.SerializeTypeName(obj.GetType());
272272
}
273273

274274
return bsonDocument;
@@ -311,7 +311,7 @@ private static void RegisterDbRefItem(BsonMapper mapper, MemberMapper member, IT
311311
/// <summary>
312312
/// Register a property as a DbRefList - implement a custom Serialize/Deserialize actions to convert entity to $id, $ref only
313313
/// </summary>
314-
private static void RegisterDbRefList(BsonMapper mapper, MemberMapper member, ITypeNameBinder typeNameBinder, string collection)
314+
private static void RegisterDbRefList(BsonMapper mapper, MemberMapper member, ITypeNameBinder typeNameBinder)
315315
{
316316
// get entity from list item type
317317
var entity = mapper.GetEntityMapper(member.UnderlyingType);
@@ -334,12 +334,12 @@ private static void RegisterDbRefList(BsonMapper mapper, MemberMapper member, IT
334334
var bsonDocument = new BsonDocument
335335
{
336336
["$id"] = m.Serialize(id.GetType(), id, 0),
337-
["$ref"] = collection
337+
["$ref"] = member.DbRefCollectionName
338338
};
339339

340340
if (member.UnderlyingType != item.GetType())
341341
{
342-
bsonDocument["$type"] = typeNameBinder.GetName(item.GetType());
342+
bsonDocument["$type"] = mapper.SerializeTypeName(item.GetType());
343343
}
344344

345345
result.Add(bsonDocument);
@@ -402,4 +402,4 @@ private static void RegisterDbRefList(BsonMapper mapper, MemberMapper member, IT
402402

403403
#endregion
404404
}
405-
}
405+
}

LiteDB/Client/Mapper/BsonRefId.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System;
2+
3+
namespace LiteDB;
4+
5+
/// <summary>
6+
/// Helper to assign the ID of a DbRef property (see <see cref="BsonRefAttribute"/>) in expression trees.
7+
/// </summary>
8+
/// <typeparam name="T">The type of the referenced entity.</typeparam>
9+
/// <example><code>
10+
/// db.GetCollection&lt;A&gt;()
11+
/// .UpdateMany(
12+
/// x => new A
13+
/// {
14+
/// Id = x.Id,
15+
/// Bref = new BsonRefId&lt;B&gt;(100),
16+
/// },
17+
/// x => x.Id == 11);
18+
/// </code></example>
19+
public sealed class BsonRefId<T>
20+
{
21+
/// <summary>
22+
/// Assigns the ID of the referenced entity of type <typeparamref name="T"/>.
23+
/// </summary>
24+
/// <param name="id">The ID to assign.</param>
25+
public BsonRefId(BsonValue id)
26+
{
27+
}
28+
29+
public static implicit operator T(BsonRefId<T> _)
30+
{
31+
throw new NotSupportedException("The type BsonRefId<T> can only be used in LiteDB LINQ expressions.");
32+
}
33+
}

0 commit comments

Comments
 (0)