Skip to content

Commit fe5b673

Browse files
feat: Add virtual methods to MemberService (#266)
1 parent 15c7ee8 commit fe5b673

File tree

1 file changed

+127
-81
lines changed

1 file changed

+127
-81
lines changed

src/VirtoCommerce.CustomerModule.Data/Services/MemberService.cs

Lines changed: 127 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -56,50 +56,27 @@ public MemberService(
5656
public virtual Task<Member[]> GetByIdsAsync(string[] memberIds, string responseGroup = null, string[] memberTypes = null)
5757
{
5858
var cacheKey = CacheKey.With(GetType(), nameof(GetByIdsAsync), string.Join("-", memberIds), responseGroup, memberTypes == null ? null : string.Join("-", memberTypes));
59-
return _platformMemoryCache.GetOrCreateExclusiveAsync(cacheKey, async (cacheEntry) =>
59+
return _platformMemoryCache.GetOrCreateExclusiveAsync(cacheKey, async cacheEntry =>
6060
{
61-
var members = new List<Member>();
62-
61+
IList<Member> members;
6362
using (var repository = _repositoryFactory())
6463
{
65-
//It is so important to generate change tokens for all ids even for not existing members to prevent an issue
66-
//with caching of empty results for non - existing objects that have the infinitive lifetime in the cache
67-
//and future unavailability to create objects with these ids.
68-
cacheEntry.AddExpirationToken(CustomerCacheRegion.CreateChangeToken(memberIds));
6964
repository.DisableChangesTracking();
7065
//There is loading for all corresponding members conceptual model entities types
7166
//query performance when TPT inheritance used it is too slow, for improve performance we are passing concrete member types in to the repository
7267
var memberTypeInfos = AbstractTypeFactory<Member>.AllTypeInfos.Where(t => t.MappedType != null);
7368
if (memberTypes != null)
7469
{
75-
memberTypeInfos = memberTypeInfos.Where(x => memberTypes.Any(mt => x.IsAssignableTo(mt)));
70+
var types = memberTypes;
71+
memberTypeInfos = memberTypeInfos.Where(x => types.Any(x.IsAssignableTo));
7672
}
7773

7874
memberTypes = memberTypeInfos.Select(t => t.MappedType.AssemblyQualifiedName).Distinct().ToArray();
7975

8076
var dataMembers = await repository.GetMembersByIdsAsync(memberIds, responseGroup, memberTypes);
81-
foreach (var dataMember in dataMembers)
82-
{
83-
var member = AbstractTypeFactory<Member>.TryCreateInstance(dataMember.MemberType);
84-
if (member is null) continue;
85-
86-
dataMember.ToModel(member);
87-
member.ReduceDetails(responseGroup);
88-
members.Add(member);
89-
}
77+
members = ProcessModels(dataMembers, responseGroup);
9078

91-
var ancestorIds = dataMembers.SelectMany(r => r.MemberRelations)
92-
.Where(x => !string.IsNullOrEmpty(x.AncestorId))
93-
.Select(x => x.AncestorId)
94-
.ToArray();
95-
96-
var descendantIds = dataMembers.SelectMany(x => x.MemberRelations)
97-
.Where(x => !string.IsNullOrEmpty(x.DescendantId))
98-
.Select(x => x.DescendantId)
99-
.ToArray();
100-
101-
cacheEntry.AddExpirationToken(CustomerCacheRegion.CreateChangeToken(ancestorIds));
102-
cacheEntry.AddExpirationToken(CustomerCacheRegion.CreateChangeToken(descendantIds));
79+
ConfigureCache(cacheEntry, memberIds, dataMembers, members);
10380
}
10481

10582
#region Load member security accounts by separate request
@@ -110,22 +87,22 @@ public virtual Task<Member[]> GetByIdsAsync(string[] memberIds, string responseG
11087
}
11188

11289
var hasSecurityAccountMembers = members.OfType<IHasSecurityAccounts>().ToArray();
113-
if (!hasSecurityAccountMembers.Any())
90+
if (hasSecurityAccountMembers.Length == 0)
11491
{
11592
return members.ToArray();
11693
}
11794

11895
var usersSearchResult = await _userSearchService.SearchUsersAsync(new UserSearchCriteria
11996
{
12097
MemberIds = hasSecurityAccountMembers.Select(x => x.Id).ToList(),
121-
Take = int.MaxValue
98+
Take = int.MaxValue,
12299
});
123100

124101
foreach (var hasAccountMember in hasSecurityAccountMembers)
125102
{
126103
hasAccountMember.SecurityAccounts = usersSearchResult.Results.Where(x => x.MemberId.EqualsInvariant(hasAccountMember.Id)).ToList();
127104

128-
if (hasAccountMember.SecurityAccounts.Any())
105+
if (hasAccountMember.SecurityAccounts.Count > 0)
129106
{
130107
hasAccountMember.SecurityAccounts.ToList().ForEach(x => cacheEntry.AddExpirationToken(SecurityCacheRegion.CreateChangeTokenForUser(x)));
131108
}
@@ -165,82 +142,151 @@ public virtual async Task SaveChangesAsync(Member[] members)
165142
var pkMap = new PrimaryKeyResolvingMap();
166143
var changedEntries = new List<GenericChangedEntry<Member>>();
167144

168-
using (var repository = _repositoryFactory())
145+
using var repository = _repositoryFactory();
146+
147+
var existingMemberEntities = await repository.GetMembersByIdsAsync(members.Where(m => !m.IsTransient()).Select(m => m.Id).ToArray());
148+
149+
foreach (var member in members)
169150
{
170-
var existingMemberEntities = await repository.GetMembersByIdsAsync(members.Where(m => !m.IsTransient()).Select(m => m.Id).ToArray());
151+
var dataSourceMember = FromModel(member, pkMap);
171152

172-
foreach (var member in members)
153+
var dataTargetMember = existingMemberEntities.FirstOrDefault(m => m.Id == member.Id);
154+
if (dataTargetMember != null)
173155
{
174-
var memberEntityType = AbstractTypeFactory<Member>.AllTypeInfos.Where(t => t.MappedType != null && t.IsAssignableTo(member.MemberType)).Select(t => t.MappedType).FirstOrDefault();
175-
ArgumentNullException.ThrowIfNull(memberEntityType);
176-
177-
var dataSourceMember = AbstractTypeFactory<MemberEntity>.TryCreateInstance(memberEntityType.Name);
178-
ArgumentNullException.ThrowIfNull(dataSourceMember);
156+
// Workaround to trigger update of auditable fields when only updating navigation properties.
157+
// Otherwise on update trigger is fired only when non navigation properties are updated.
158+
dataTargetMember.ModifiedDate = DateTime.UtcNow;
179159

180-
dataSourceMember.FromModel(member, pkMap);
160+
// This extension is allow to get around breaking changes is introduced in EF Core 3.0 that leads to throw
161+
// Database operation expected to affect 1 row(s) but actually affected 0 row(s) exception when trying to add the new children entities with manually set keys
162+
// https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes#detectchanges-honors-store-generated-key-values
163+
repository.TrackModifiedAsAddedForNewChildEntities(dataTargetMember);
181164

182-
var dataTargetMember = existingMemberEntities.FirstOrDefault(m => m.Id == member.Id);
183-
if (dataTargetMember != null)
165+
if (!dataTargetMember.GetType().IsInstanceOfType(dataSourceMember))
184166
{
185-
/// Workaround to trigger update of auditable fields when only updating navigation properties.
186-
/// Otherwise on update trigger is fired only when non navigation properties are updated.
187-
dataTargetMember.ModifiedDate = DateTime.UtcNow;
188-
189-
/// This extension is allow to get around breaking changes is introduced in EF Core 3.0 that leads to throw
190-
/// Database operation expected to affect 1 row(s) but actually affected 0 row(s) exception when trying to add the new children entities with manually set keys
191-
/// https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/breaking-changes#detectchanges-honors-store-generated-key-values
192-
repository.TrackModifiedAsAddedForNewChildEntities(dataTargetMember);
193-
194-
if (!dataTargetMember.GetType().IsInstanceOfType(dataSourceMember))
195-
{
196-
throw new OperationCanceledException($"Unable to update an member with type {dataTargetMember.MemberType} by an member with type {dataSourceMember.MemberType} because they aren't in the inheritance hierarchy");
197-
}
198-
changedEntries.Add(new GenericChangedEntry<Member>(member, dataTargetMember.ToModel(AbstractTypeFactory<Member>.TryCreateInstance(member.MemberType)), EntryState.Modified));
199-
dataSourceMember.Patch(dataTargetMember);
200-
}
201-
else
202-
{
203-
repository.Add(dataSourceMember);
204-
changedEntries.Add(new GenericChangedEntry<Member>(member, EntryState.Added));
167+
throw new OperationCanceledException($"Unable to update an member with type {dataTargetMember.MemberType} by an member with type {dataSourceMember.MemberType} because they aren't in the inheritance hierarchy");
205168
}
169+
changedEntries.Add(new GenericChangedEntry<Member>(member, dataTargetMember.ToModel(AbstractTypeFactory<Member>.TryCreateInstance(member.MemberType)), EntryState.Modified));
170+
dataSourceMember.Patch(dataTargetMember);
171+
}
172+
else
173+
{
174+
repository.Add(dataSourceMember);
175+
changedEntries.Add(new GenericChangedEntry<Member>(member, EntryState.Added));
206176
}
207-
//Raise domain events
177+
}
178+
//Raise domain events
179+
await _eventPublisher.Publish(new MemberChangingEvent(changedEntries));
180+
await repository.UnitOfWork.CommitAsync();
181+
pkMap.ResolvePrimaryKeys();
182+
ClearCache(members);
183+
184+
await _eventPublisher.Publish(new MemberChangedEvent(changedEntries));
185+
}
186+
187+
public virtual async Task DeleteAsync(string[] ids, string[] memberTypes = null)
188+
{
189+
using var repository = _repositoryFactory();
190+
191+
var members = await GetByIdsAsync(ids, null, memberTypes);
192+
if (members?.Length > 0)
193+
{
194+
var changedEntries = members.Select(x => new GenericChangedEntry<Member>(x, EntryState.Deleted)).ToArray();
208195
await _eventPublisher.Publish(new MemberChangingEvent(changedEntries));
196+
197+
await repository.RemoveMembersByIdsAsync(members.Select(m => m.Id).ToArray());
209198
await repository.UnitOfWork.CommitAsync();
210-
pkMap.ResolvePrimaryKeys();
211199
ClearCache(members);
212200

213201
await _eventPublisher.Publish(new MemberChangedEvent(changedEntries));
214202
}
215203
}
216204

217-
public virtual async Task DeleteAsync(string[] ids, string[] memberTypes = null)
205+
protected virtual void ConfigureCache(MemoryCacheEntryOptions cacheOptions, string[] memberIds, IList<MemberEntity> entities, IList<Member> models)
218206
{
219-
using (var repository = _repositoryFactory())
220-
{
221-
var members = await GetByIdsAsync(ids, null, memberTypes);
222-
if (!members.IsNullOrEmpty())
223-
{
224-
var changedEntries = members.Select(x => new GenericChangedEntry<Member>(x, EntryState.Deleted)).ToArray();
225-
await _eventPublisher.Publish(new MemberChangingEvent(changedEntries));
207+
//It is so important to generate change tokens for all ids even for not existing members to prevent an issue
208+
//with caching of empty results for non - existing objects that have the infinitive lifetime in the cache
209+
//and future unavailability to create objects with these ids.
210+
cacheOptions.AddExpirationToken(CustomerCacheRegion.CreateChangeToken(memberIds));
211+
212+
var ancestorIds = entities.SelectMany(r => r.MemberRelations)
213+
.Where(x => !string.IsNullOrEmpty(x.AncestorId))
214+
.Select(x => x.AncestorId)
215+
.ToArray();
216+
217+
var descendantIds = entities.SelectMany(x => x.MemberRelations)
218+
.Where(x => !string.IsNullOrEmpty(x.DescendantId))
219+
.Select(x => x.DescendantId)
220+
.ToArray();
221+
222+
cacheOptions.AddExpirationToken(CustomerCacheRegion.CreateChangeToken(ancestorIds));
223+
cacheOptions.AddExpirationToken(CustomerCacheRegion.CreateChangeToken(descendantIds));
224+
}
226225

227-
await repository.RemoveMembersByIdsAsync(members.Select(m => m.Id).ToArray());
228-
await repository.UnitOfWork.CommitAsync();
229-
ClearCache(members);
226+
protected virtual void ClearCache(IEnumerable<Member> members)
227+
{
228+
var models = members as Member[] ?? members.ToArray();
229+
ClearSearchCache(models);
230230

231-
await _eventPublisher.Publish(new MemberChangedEvent(changedEntries));
232-
}
231+
foreach (var member in models.Where(x => !x.IsTransient()))
232+
{
233+
CustomerCacheRegion.ExpireMemberById(member.Id);
233234
}
234235
}
235236

236-
protected virtual void ClearCache(IEnumerable<Member> members)
237+
protected virtual void ClearSearchCache(IList<Member> models)
237238
{
238239
CustomerSearchCacheRegion.ExpireRegion();
240+
}
241+
242+
protected virtual IList<Member> ProcessModels(IList<MemberEntity> entities, string responseGroup)
243+
{
244+
return entities?
245+
.Select(x =>
246+
{
247+
var model = ToModel(x);
248+
return model is null ? null : ProcessModel(responseGroup, x, model);
249+
})
250+
.Where(x => x is not null)
251+
.ToArray();
252+
}
239253

240-
foreach (var member in members.Where(x => !x.IsTransient()))
254+
protected virtual Member ToModel(MemberEntity entity)
255+
{
256+
var model = AbstractTypeFactory<Member>.TryCreateInstance(entity.MemberType);
257+
if (model is null)
241258
{
242-
CustomerCacheRegion.ExpireMemberById(member.Id);
259+
return null;
243260
}
261+
262+
entity.ToModel(model);
263+
return model;
264+
}
265+
266+
/// <summary>
267+
/// Post-read processing of the model instance.
268+
/// A good place to make some additional actions, tune model data.
269+
/// Override to add some model data changes, calculations, etc...
270+
/// </summary>
271+
protected virtual Member ProcessModel(string responseGroup, MemberEntity entity, Member model)
272+
{
273+
model.ReduceDetails(responseGroup);
274+
return model;
275+
}
276+
277+
protected virtual MemberEntity FromModel(Member model, PrimaryKeyResolvingMap keyMap)
278+
{
279+
var memberEntityType = AbstractTypeFactory<Member>.AllTypeInfos
280+
.Where(t => t.MappedType != null && t.IsAssignableTo(model.MemberType))
281+
.Select(t => t.MappedType)
282+
.FirstOrDefault()
283+
?? throw new InvalidOperationException($"Cannot find entity type for member type: {model.MemberType}");
284+
285+
var dataSourceMember = AbstractTypeFactory<MemberEntity>.TryCreateInstance(memberEntityType.Name)
286+
?? throw new InvalidOperationException($"Cannot create instance of entity type: {memberEntityType.Name}");
287+
288+
dataSourceMember.FromModel(model, keyMap);
289+
return dataSourceMember;
244290
}
245291

246292
#endregion IMemberService Members

0 commit comments

Comments
 (0)