Skip to content

Commit f2b5187

Browse files
committed
Merge branch 'release/3.825.0'
2 parents f83081a + fe5b673 commit f2b5187

File tree

4 files changed

+132
-86
lines changed

4 files changed

+132
-86
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Project>
33
<!-- These properties will be shared for all projects -->
44
<PropertyGroup>
5-
<VersionPrefix>3.824.0</VersionPrefix>
5+
<VersionPrefix>3.825.0</VersionPrefix>
66
<VersionSuffix>
77
</VersionSuffix>
88
<VersionSuffix Condition=" '$(VersionSuffix)' != '' AND '$(BuildNumber)' != '' ">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
# Overview
1+
# Virto Commerce Customer Module
22

33
[![CI status](https://github.com/VirtoCommerce/vc-module-customer/workflows/Module%20CI/badge.svg?branch=dev)](https://github.com/VirtoCommerce/vc-module-customer/actions?query=workflow%3A"Module+CI") [![Quality gate](https://sonarcloud.io/api/project_badges/measure?project=VirtoCommerce_vc-module-customer&metric=alert_status&branch=dev)](https://sonarcloud.io/dashboard?id=VirtoCommerce_vc-module-customer) [![Reliability rating](https://sonarcloud.io/api/project_badges/measure?project=VirtoCommerce_vc-module-customer&metric=reliability_rating&branch=dev)](https://sonarcloud.io/dashboard?id=VirtoCommerce_vc-module-customer) [![Security rating](https://sonarcloud.io/api/project_badges/measure?project=VirtoCommerce_vc-module-customer&metric=security_rating&branch=dev)](https://sonarcloud.io/dashboard?id=VirtoCommerce_vc-module-customer) [![Sqale rating](https://sonarcloud.io/api/project_badges/measure?project=VirtoCommerce_vc-module-customer&metric=sqale_rating&branch=dev)](https://sonarcloud.io/dashboard?id=VirtoCommerce_vc-module-customer)
44

5-
The Virto Commerce Customer module represents contacts management system. The main purpose of this functionality is to keep the users contact information. The VC Customer Module helps to view, search and edit contact information.
5+
The Virto Commerce Customer module represents contacts management system. The main purpose of this functionality is to keep the users contact information. The Customer Module helps to view, search, and edit contact information.
66

77
## Key features
88

99
* Сontacts arrangement in hierarchical structure
1010
* Module extensibility with custom contact types
11-
* "Organization", "Employee", "Customer" and "Vendor" contact types supported out of the box
11+
* **Organization**, **Employee**, **Customer** and **Vendor** contact types supported out of the box
1212

1313
## Documentation
1414

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

src/VirtoCommerce.CustomerModule.Web/module.manifest

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<module xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
33
<id>VirtoCommerce.Customer</id>
4-
<version>3.824.0</version>
4+
<version>3.825.0</version>
55
<version-tag />
66

77
<platformVersion>3.879.0</platformVersion>

0 commit comments

Comments
 (0)