@@ -56,50 +56,27 @@ public MemberService(
56
56
public virtual Task < Member [ ] > GetByIdsAsync ( string [ ] memberIds , string responseGroup = null , string [ ] memberTypes = null )
57
57
{
58
58
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 =>
60
60
{
61
- var members = new List < Member > ( ) ;
62
-
61
+ IList < Member > members ;
63
62
using ( var repository = _repositoryFactory ( ) )
64
63
{
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 ) ) ;
69
64
repository . DisableChangesTracking ( ) ;
70
65
//There is loading for all corresponding members conceptual model entities types
71
66
//query performance when TPT inheritance used it is too slow, for improve performance we are passing concrete member types in to the repository
72
67
var memberTypeInfos = AbstractTypeFactory < Member > . AllTypeInfos . Where ( t => t . MappedType != null ) ;
73
68
if ( memberTypes != null )
74
69
{
75
- memberTypeInfos = memberTypeInfos . Where ( x => memberTypes . Any ( mt => x . IsAssignableTo ( mt ) ) ) ;
70
+ var types = memberTypes ;
71
+ memberTypeInfos = memberTypeInfos . Where ( x => types . Any ( x . IsAssignableTo ) ) ;
76
72
}
77
73
78
74
memberTypes = memberTypeInfos . Select ( t => t . MappedType . AssemblyQualifiedName ) . Distinct ( ) . ToArray ( ) ;
79
75
80
76
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 ) ;
90
78
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 ) ;
103
80
}
104
81
105
82
#region Load member security accounts by separate request
@@ -110,22 +87,22 @@ public virtual Task<Member[]> GetByIdsAsync(string[] memberIds, string responseG
110
87
}
111
88
112
89
var hasSecurityAccountMembers = members . OfType < IHasSecurityAccounts > ( ) . ToArray ( ) ;
113
- if ( ! hasSecurityAccountMembers . Any ( ) )
90
+ if ( hasSecurityAccountMembers . Length == 0 )
114
91
{
115
92
return members . ToArray ( ) ;
116
93
}
117
94
118
95
var usersSearchResult = await _userSearchService . SearchUsersAsync ( new UserSearchCriteria
119
96
{
120
97
MemberIds = hasSecurityAccountMembers . Select ( x => x . Id ) . ToList ( ) ,
121
- Take = int . MaxValue
98
+ Take = int . MaxValue ,
122
99
} ) ;
123
100
124
101
foreach ( var hasAccountMember in hasSecurityAccountMembers )
125
102
{
126
103
hasAccountMember . SecurityAccounts = usersSearchResult . Results . Where ( x => x . MemberId . EqualsInvariant ( hasAccountMember . Id ) ) . ToList ( ) ;
127
104
128
- if ( hasAccountMember . SecurityAccounts . Any ( ) )
105
+ if ( hasAccountMember . SecurityAccounts . Count > 0 )
129
106
{
130
107
hasAccountMember . SecurityAccounts . ToList ( ) . ForEach ( x => cacheEntry . AddExpirationToken ( SecurityCacheRegion . CreateChangeTokenForUser ( x ) ) ) ;
131
108
}
@@ -165,82 +142,151 @@ public virtual async Task SaveChangesAsync(Member[] members)
165
142
var pkMap = new PrimaryKeyResolvingMap ( ) ;
166
143
var changedEntries = new List < GenericChangedEntry < Member > > ( ) ;
167
144
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 )
169
150
{
170
- var existingMemberEntities = await repository . GetMembersByIdsAsync ( members . Where ( m => ! m . IsTransient ( ) ) . Select ( m => m . Id ) . ToArray ( ) ) ;
151
+ var dataSourceMember = FromModel ( member , pkMap ) ;
171
152
172
- foreach ( var member in members )
153
+ var dataTargetMember = existingMemberEntities . FirstOrDefault ( m => m . Id == member . Id ) ;
154
+ if ( dataTargetMember != null )
173
155
{
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 ;
179
159
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 ) ;
181
164
182
- var dataTargetMember = existingMemberEntities . FirstOrDefault ( m => m . Id == member . Id ) ;
183
- if ( dataTargetMember != null )
165
+ if ( ! dataTargetMember . GetType ( ) . IsInstanceOfType ( dataSourceMember ) )
184
166
{
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") ;
205
168
}
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 ) ) ;
206
176
}
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 ( ) ;
208
195
await _eventPublisher . Publish ( new MemberChangingEvent ( changedEntries ) ) ;
196
+
197
+ await repository . RemoveMembersByIdsAsync ( members . Select ( m => m . Id ) . ToArray ( ) ) ;
209
198
await repository . UnitOfWork . CommitAsync ( ) ;
210
- pkMap . ResolvePrimaryKeys ( ) ;
211
199
ClearCache ( members ) ;
212
200
213
201
await _eventPublisher . Publish ( new MemberChangedEvent ( changedEntries ) ) ;
214
202
}
215
203
}
216
204
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 )
218
206
{
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
+ }
226
225
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 ) ;
230
230
231
- await _eventPublisher . Publish ( new MemberChangedEvent ( changedEntries ) ) ;
232
- }
231
+ foreach ( var member in models . Where ( x => ! x . IsTransient ( ) ) )
232
+ {
233
+ CustomerCacheRegion . ExpireMemberById ( member . Id ) ;
233
234
}
234
235
}
235
236
236
- protected virtual void ClearCache ( IEnumerable < Member > members )
237
+ protected virtual void ClearSearchCache ( IList < Member > models )
237
238
{
238
239
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
+ }
239
253
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 )
241
258
{
242
- CustomerCacheRegion . ExpireMemberById ( member . Id ) ;
259
+ return null ;
243
260
}
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 ;
244
290
}
245
291
246
292
#endregion IMemberService Members
0 commit comments