Skip to content

Commit caf6158

Browse files
authored
Merge pull request #2566 from JKamsker/bugs/fix-2563-ConcurrentAccess
Fixes concurrent access issues using a concurrent dictionary
2 parents d083dd0 + e159fb7 commit caf6158

File tree

9 files changed

+275
-238
lines changed

9 files changed

+275
-238
lines changed

LiteDB/Client/Database/LiteCollection.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ internal LiteCollection(string name, BsonAutoId autoId, ILiteEngine engine, Bson
4747
else
4848
{
4949
_entity = mapper.GetEntityMapper(typeof(T));
50+
_entity.WaitForInitialization();
51+
5052
_id = _entity.Id;
5153

5254
if (_id != null && _id.AutoId)

LiteDB/Client/Mapper/BsonMapper.Deserialize.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ public object Deserialize(Type type, BsonValue value)
213213
}
214214

215215
var entity = this.GetEntityMapper(type);
216+
entity.WaitForInitialization();
216217

217218
// initialize CreateInstance
218219
if (entity.CreateInstance == null)
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Reflection;
6+
using System.Threading;
7+
8+
namespace LiteDB;
9+
10+
public partial class BsonMapper
11+
{
12+
/// <summary>
13+
/// Mapping cache between Class/BsonDocument
14+
/// </summary>
15+
private readonly ConcurrentDictionary<Type, EntityMapper> _entities = new();
16+
17+
/// <summary>
18+
/// Get property mapper between typed .NET class and BsonDocument - Cache results
19+
/// </summary>
20+
internal EntityMapper GetEntityMapper(Type type)
21+
{
22+
if (_entities.TryGetValue(type, out EntityMapper mapper))
23+
{
24+
return mapper;
25+
}
26+
27+
using var cts = new CancellationTokenSource();
28+
mapper = new EntityMapper(type, cts.Token);
29+
if (_entities.TryAdd(type, mapper))
30+
{
31+
this.BuildEntityMapper(mapper);
32+
}
33+
cts.Cancel();
34+
cts.Dispose();
35+
36+
return mapper;
37+
}
38+
39+
/// <summary>
40+
/// Use this method to override how your class can be, by default, mapped from entity to Bson document.
41+
/// Returns an EntityMapper from each requested Type
42+
/// </summary>
43+
protected void BuildEntityMapper(EntityMapper mapper)
44+
{
45+
var idAttr = typeof(BsonIdAttribute);
46+
var ignoreAttr = typeof(BsonIgnoreAttribute);
47+
var fieldAttr = typeof(BsonFieldAttribute);
48+
var dbrefAttr = typeof(BsonRefAttribute);
49+
50+
var members = this.GetTypeMembers(mapper.ForType);
51+
var id = this.GetIdMember(members);
52+
53+
foreach (var memberInfo in members)
54+
{
55+
// checks [BsonIgnore]
56+
if (CustomAttributeExtensions.IsDefined(memberInfo, ignoreAttr, true)) continue;
57+
58+
// checks field name conversion
59+
var name = this.ResolveFieldName(memberInfo.Name);
60+
61+
// check if property has [BsonField]
62+
var field = (BsonFieldAttribute)CustomAttributeExtensions.GetCustomAttributes(memberInfo, fieldAttr, true)
63+
.FirstOrDefault();
64+
65+
// check if property has [BsonField] with a custom field name
66+
if (field != null && field.Name != null)
67+
{
68+
name = field.Name;
69+
}
70+
71+
// checks if memberInfo is id field
72+
if (memberInfo == id)
73+
{
74+
name = "_id";
75+
}
76+
77+
// create getter/setter function
78+
var getter = Reflection.CreateGenericGetter(mapper.ForType, memberInfo);
79+
var setter = Reflection.CreateGenericSetter(mapper.ForType, memberInfo);
80+
81+
// check if property has [BsonId] to get with was setted AutoId = true
82+
var autoId = (BsonIdAttribute)CustomAttributeExtensions.GetCustomAttributes(memberInfo, idAttr, true)
83+
.FirstOrDefault();
84+
85+
// get data type
86+
var dataType = memberInfo is PropertyInfo
87+
? (memberInfo as PropertyInfo).PropertyType
88+
: (memberInfo as FieldInfo).FieldType;
89+
90+
// check if datatype is list/array
91+
var isEnumerable = Reflection.IsEnumerable(dataType);
92+
93+
// create a property mapper
94+
var member = new MemberMapper
95+
{
96+
AutoId = autoId == null ? true : autoId.AutoId,
97+
FieldName = name,
98+
MemberName = memberInfo.Name,
99+
DataType = dataType,
100+
IsEnumerable = isEnumerable,
101+
UnderlyingType = isEnumerable ? Reflection.GetListItemType(dataType) : dataType,
102+
Getter = getter,
103+
Setter = setter
104+
};
105+
106+
// check if property has [BsonRef]
107+
var dbRef = (BsonRefAttribute)CustomAttributeExtensions.GetCustomAttributes(memberInfo, dbrefAttr, false)
108+
.FirstOrDefault();
109+
110+
if (dbRef != null && memberInfo is PropertyInfo)
111+
{
112+
BsonMapper.RegisterDbRef(this, member, _typeNameBinder,
113+
dbRef.Collection ?? this.ResolveCollectionName((memberInfo as PropertyInfo).PropertyType));
114+
}
115+
116+
// support callback to user modify member mapper
117+
this.ResolveMember?.Invoke(mapper.ForType, memberInfo, member);
118+
119+
// test if has name and there is no duplicate field
120+
// when member is not ignore
121+
if (member.FieldName != null &&
122+
mapper.Members.Any(x => x.FieldName.Equals(name, StringComparison.OrdinalIgnoreCase)) == false &&
123+
!member.IsIgnore)
124+
{
125+
mapper.Members.Add(member);
126+
}
127+
}
128+
}
129+
130+
/// <summary>
131+
/// Gets MemberInfo that refers to Id from a document object.
132+
/// </summary>
133+
protected virtual MemberInfo GetIdMember(IEnumerable<MemberInfo> members)
134+
{
135+
return Reflection.SelectMember(members,
136+
x => CustomAttributeExtensions.IsDefined(x, typeof(BsonIdAttribute), true),
137+
x => x.Name.Equals("Id", StringComparison.OrdinalIgnoreCase),
138+
x => x.Name.Equals(x.DeclaringType.Name + "Id", StringComparison.OrdinalIgnoreCase));
139+
}
140+
141+
/// <summary>
142+
/// Returns all member that will be have mapper between POCO class to document
143+
/// </summary>
144+
protected virtual IEnumerable<MemberInfo> GetTypeMembers(Type type)
145+
{
146+
var members = new List<MemberInfo>();
147+
148+
var flags = this.IncludeNonPublic
149+
? (BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
150+
: (BindingFlags.Public | BindingFlags.Instance);
151+
152+
members.AddRange(type.GetProperties(flags)
153+
.Where(x => x.CanRead && x.GetIndexParameters().Length == 0)
154+
.Select(x => x as MemberInfo));
155+
156+
if (this.IncludeFields)
157+
{
158+
members.AddRange(type.GetFields(flags).Where(x => !x.Name.EndsWith("k__BackingField") && x.IsStatic == false)
159+
.Select(x => x as MemberInfo));
160+
}
161+
162+
return members;
163+
}
164+
165+
/// <summary>
166+
/// Get best construtor to use to initialize this entity.
167+
/// - Look if contains [BsonCtor] attribute
168+
/// - Look for parameterless ctor
169+
/// - Look for first contructor with parameter and use BsonDocument to send RawValue
170+
/// </summary>
171+
protected virtual CreateObject GetTypeCtor(EntityMapper mapper)
172+
{
173+
Type type = mapper.ForType;
174+
List<CreateObject> Mappings = new List<CreateObject>();
175+
bool returnZeroParamNull = false;
176+
foreach (ConstructorInfo ctor in type.GetConstructors())
177+
{
178+
ParameterInfo[] pars = ctor.GetParameters();
179+
// For 0 parameters, we can let the Reflection.CreateInstance handle it, unless they've specified a [BsonCtor] attribute on a different constructor.
180+
if (pars.Length == 0)
181+
{
182+
returnZeroParamNull = true;
183+
continue;
184+
}
185+
186+
KeyValuePair<string, Type>[] paramMap = new KeyValuePair<string, Type>[pars.Length];
187+
int i;
188+
for (i = 0; i < pars.Length; i++)
189+
{
190+
ParameterInfo par = pars[i];
191+
MemberMapper mi = null;
192+
foreach (MemberMapper member in mapper.Members)
193+
{
194+
if (member.MemberName.ToLower() == par.Name.ToLower() && member.DataType == par.ParameterType)
195+
{
196+
mi = member;
197+
break;
198+
}
199+
}
200+
201+
if (mi == null)
202+
{
203+
break;
204+
}
205+
206+
paramMap[i] = new KeyValuePair<string, Type>(mi.FieldName, mi.DataType);
207+
}
208+
209+
if (i < pars.Length)
210+
{
211+
continue;
212+
}
213+
214+
CreateObject toAdd = (BsonDocument value) =>
215+
Activator.CreateInstance(type, paramMap.Select(x =>
216+
this.Deserialize(x.Value, value[x.Key])).ToArray());
217+
if (ctor.GetCustomAttribute<BsonCtorAttribute>() != null)
218+
{
219+
return toAdd;
220+
}
221+
else
222+
{
223+
Mappings.Add(toAdd);
224+
}
225+
}
226+
227+
if (returnZeroParamNull)
228+
{
229+
return null;
230+
}
231+
232+
return Mappings.FirstOrDefault();
233+
}
234+
}

LiteDB/Client/Mapper/BsonMapper.Serialize.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ private BsonDocument SerializeObject(Type type, object obj, int depth)
181181
var t = obj.GetType();
182182
var doc = new BsonDocument();
183183
var entity = this.GetEntityMapper(t);
184+
entity.WaitForInitialization();
184185

185186
// adding _type only where property Type is not same as object instance type
186187
if (type != t)

0 commit comments

Comments
 (0)