Skip to content

Commit 8a5f52f

Browse files
VCST-3154: GetByOuterId in CRUD service (#2909)
Co-authored-by: Artem Dudarev <artem@virtoworks.com>
1 parent 8c4e473 commit 8a5f52f

File tree

11 files changed

+252
-68
lines changed

11 files changed

+252
-68
lines changed

src/VirtoCommerce.Platform.Core/Extensions/CrudServiceExtensions.cs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,43 @@ public static Task<TModel> GetNoCloneAsync<TModel>(this ICrudService<TModel> cru
2828
public static async Task<TModel> GetByIdAsync<TModel>(this ICrudService<TModel> crudService, string id, string responseGroup = null, bool clone = true)
2929
where TModel : Entity
3030
{
31-
if (id == null)
31+
if (string.IsNullOrEmpty(id))
3232
{
3333
return null;
3434
}
3535

36-
var entities = await crudService.GetAsync(new[] { id }, responseGroup, clone);
36+
var entities = await crudService.GetAsync([id], responseGroup, clone);
37+
38+
return entities?.FirstOrDefault();
39+
}
40+
41+
/// <summary>
42+
/// Returns data from the cache without cloning. This consumes less memory, but the returned data must not be modified.
43+
/// </summary>
44+
public static Task<IList<TModel>> GetByOuterIdsNoCloneAsync<TModel>(this IOuterEntityService<TModel> outerEntityService, IList<string> outerIds, string responseGroup = null)
45+
where TModel : Entity, IHasOuterId
46+
{
47+
return outerEntityService.GetByOuterIdsAsync(outerIds, responseGroup, clone: false);
48+
}
49+
50+
/// <summary>
51+
/// Returns data from the cache without cloning. This consumes less memory, but the returned data must not be modified.
52+
/// </summary>
53+
public static Task<TModel> GetByOuterIdNoCloneAsync<TModel>(this IOuterEntityService<TModel> outerEntityService, string outerId, string responseGroup = null)
54+
where TModel : Entity, IHasOuterId
55+
{
56+
return outerEntityService.GetByOuterIdAsync(outerId, responseGroup, clone: false);
57+
}
58+
59+
public static async Task<TModel> GetByOuterIdAsync<TModel>(this IOuterEntityService<TModel> outerEntityService, string outerId, string responseGroup = null, bool clone = true)
60+
where TModel : Entity, IHasOuterId
61+
{
62+
if (string.IsNullOrEmpty(outerId))
63+
{
64+
return null;
65+
}
66+
67+
var entities = await outerEntityService.GetByOuterIdsAsync([outerId], responseGroup, clone);
3768

3869
return entities?.FirstOrDefault();
3970
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Collections.Generic;
2+
using System.Threading.Tasks;
3+
using VirtoCommerce.Platform.Core.Common;
4+
5+
namespace VirtoCommerce.Platform.Core.GenericCrud;
6+
7+
/// <summary>
8+
/// Generic interface for outer entities to use with CRUD services.
9+
/// </summary>
10+
/// <typeparam name="T"></typeparam>
11+
public interface IOuterEntityService<T> : ICrudService<T>
12+
where T : Entity, IHasOuterId
13+
{
14+
/// <summary>
15+
/// Returns a list of model instances for specified outer IDs (integration key)
16+
/// </summary>
17+
/// <param name="outerIds">List of outer IDs</param>
18+
/// <param name="responseGroup"></param>
19+
/// <param name="clone">If false, returns data from the cache without cloning. This consumes less memory, but the returned data must not be modified.</param>
20+
/// <returns></returns>
21+
Task<IList<T>> GetByOuterIdsAsync(IList<string> outerIds, string responseGroup = null, bool clone = true);
22+
}

src/VirtoCommerce.Platform.Data/GenericCrud/CrudService.cs

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ namespace VirtoCommerce.Platform.Data.GenericCrud
2121
/// </summary>
2222
/// <typeparam name="TModel">The type of service layer model</typeparam>
2323
/// <typeparam name="TEntity">The type of data access layer entity (EF) </typeparam>
24-
/// <typeparam name="TChangeEvent">The type of *change event</typeparam>
24+
/// <typeparam name="TChangingEvent">The type of *changing event</typeparam>
2525
/// <typeparam name="TChangedEvent">The type of *changed event</typeparam>
26-
public abstract class CrudService<TModel, TEntity, TChangeEvent, TChangedEvent> : ICrudService<TModel>
26+
public abstract class CrudService<TModel, TEntity, TChangingEvent, TChangedEvent> : ICrudService<TModel>
2727
where TModel : Entity, ICloneable
2828
where TEntity : Entity, IDataEntity<TEntity, TModel>
29-
where TChangeEvent : GenericChangedEntryEvent<TModel>
29+
where TChangingEvent : GenericChangedEntryEvent<TModel>
3030
where TChangedEvent : GenericChangedEntryEvent<TModel>
3131
{
3232
private readonly IEventPublisher _eventPublisher;
@@ -38,7 +38,7 @@ public abstract class CrudService<TModel, TEntity, TChangeEvent, TChangedEvent>
3838
/// </summary>
3939
/// <param name="repositoryFactory">Repository factory to get access to the data source</param>
4040
/// <param name="platformMemoryCache">The cache used to temporary store returned values</param>
41-
/// <param name="eventPublisher">The publisher to propagate platform-wide events (TChangeEvent, TChangedEvent)</param>
41+
/// <param name="eventPublisher">The publisher to propagate platform-wide events (TChangingEvent, TChangedEvent)</param>
4242
protected CrudService(Func<IRepository> repositoryFactory, IPlatformMemoryCache platformMemoryCache, IEventPublisher eventPublisher)
4343
{
4444
_repositoryFactory = repositoryFactory;
@@ -215,7 +215,7 @@ public virtual async Task SaveChangesAsync(IList<TModel> models)
215215
}
216216

217217
// Raise domain events
218-
await _eventPublisher.Publish(EventFactory<TChangeEvent>(changedEntries));
218+
await _eventPublisher.Publish(EventFactory<TChangingEvent>(changedEntries));
219219
await CommitAsync(repository);
220220
}
221221

@@ -238,7 +238,7 @@ protected virtual Task<IList<TEntity>> LoadExistingEntities(IRepository reposito
238238
{
239239
var ids = models.Where(x => !x.IsTransient()).Select(x => x.Id).ToList();
240240

241-
return ids.Any()
241+
return ids.Count > 0
242242
? LoadEntities(repository, ids)
243243
: Task.FromResult<IList<TEntity>>(Array.Empty<TEntity>());
244244
}
@@ -263,33 +263,33 @@ public virtual async Task DeleteAsync(IList<string> ids, bool softDelete = false
263263
{
264264
var models = await GetAsync(ids);
265265

266-
using (var repository = _repositoryFactory())
267-
{
268-
// Raise domain events before deletion
269-
var changedEntries = models.Select(x => new GenericChangedEntry<TModel>(x, EntryState.Deleted)).ToList();
270-
await _eventPublisher.Publish(EventFactory<TChangeEvent>(changedEntries));
266+
using var repository = _repositoryFactory();
271267

272-
if (softDelete)
273-
{
274-
await SoftDelete(repository, ids);
275-
await repository.UnitOfWork.CommitAsync();
276-
}
277-
else
268+
// Raise domain events before deletion
269+
var changedEntries = models.Select(x => new GenericChangedEntry<TModel>(x, EntryState.Deleted)).ToList();
270+
await _eventPublisher.Publish(EventFactory<TChangingEvent>(changedEntries));
271+
272+
if (softDelete)
273+
{
274+
await SoftDelete(repository, ids);
275+
await repository.UnitOfWork.CommitAsync();
276+
}
277+
else
278+
{
279+
var keyMap = new PrimaryKeyResolvingMap();
280+
foreach (var model in models)
278281
{
279-
var keyMap = new PrimaryKeyResolvingMap();
280-
foreach (var model in models)
281-
{
282-
var entity = FromModel(model, keyMap);
283-
repository.Remove(entity);
284-
}
285-
await repository.UnitOfWork.CommitAsync();
286-
await AfterDeleteAsync(models, changedEntries);
282+
var entity = FromModel(model, keyMap);
283+
repository.Remove(entity);
287284
}
288-
ClearCache(models);
289-
290-
// Raise domain events after deletion
291-
await _eventPublisher.Publish(EventFactory<TChangedEvent>(changedEntries));
285+
await repository.UnitOfWork.CommitAsync();
286+
await AfterDeleteAsync(models, changedEntries);
292287
}
288+
289+
ClearCache(models);
290+
291+
// Raise domain events after deletion
292+
await _eventPublisher.Publish(EventFactory<TChangedEvent>(changedEntries));
293293
}
294294

295295
/// <summary>
@@ -342,7 +342,7 @@ protected virtual TEntity FromModel(TModel model, PrimaryKeyResolvingMap keyMap)
342342

343343
protected virtual GenericChangedEntryEvent<TModel> EventFactory<TEvent>(IList<GenericChangedEntry<TModel>> changedEntries)
344344
{
345-
return (GenericChangedEntryEvent<TModel>)typeof(TEvent).GetConstructor(new[] { typeof(IEnumerable<GenericChangedEntry<TModel>>) }).Invoke(new object[] { changedEntries });
345+
return (GenericChangedEntryEvent<TModel>)typeof(TEvent).GetConstructor([typeof(IEnumerable<GenericChangedEntry<TModel>>)]).Invoke([changedEntries]);
346346
}
347347
}
348348
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.EntityFrameworkCore;
6+
using VirtoCommerce.Platform.Core.Caching;
7+
using VirtoCommerce.Platform.Core.Common;
8+
using VirtoCommerce.Platform.Core.Domain;
9+
using VirtoCommerce.Platform.Core.Events;
10+
using VirtoCommerce.Platform.Core.GenericCrud;
11+
12+
namespace VirtoCommerce.Platform.Data.GenericCrud;
13+
14+
/// <summary>
15+
/// Generic service to simplify CRUD implementation with outer entities.
16+
/// To implement the service for applied purpose, inherit your search service from this.
17+
/// </summary>
18+
/// <typeparam name="TModel">The type of service layer model</typeparam>
19+
/// <typeparam name="TEntity">The type of data access layer entity (EF) </typeparam>
20+
/// <typeparam name="TChangingEvent">The type of *changing event</typeparam>
21+
/// <typeparam name="TChangedEvent">The type of *changed event</typeparam>
22+
public abstract class OuterEntityService<TModel, TEntity, TChangingEvent, TChangedEvent>
23+
: CrudService<TModel, TEntity, TChangingEvent, TChangedEvent>, IOuterEntityService<TModel>
24+
where TModel : Entity, IHasOuterId, ICloneable
25+
where TEntity : Entity, IHasOuterId, IDataEntity<TEntity, TModel>
26+
where TChangingEvent : GenericChangedEntryEvent<TModel>
27+
where TChangedEvent : GenericChangedEntryEvent<TModel>
28+
{
29+
private readonly Func<IRepository> _repositoryFactory;
30+
31+
/// <summary>
32+
/// Construct new OuterEntityService
33+
/// </summary>
34+
/// <param name="repositoryFactory">Repository factory to get access to the data source</param>
35+
/// <param name="platformMemoryCache">The cache used to temporarily store returned values</param>
36+
/// <param name="eventPublisher">The publisher to propagate platform-wide events (TChangingEvent, TChangedEvent)</param>
37+
protected OuterEntityService(Func<IRepository> repositoryFactory, IPlatformMemoryCache platformMemoryCache, IEventPublisher eventPublisher)
38+
: base(repositoryFactory, platformMemoryCache, eventPublisher)
39+
{
40+
_repositoryFactory = repositoryFactory;
41+
}
42+
43+
public virtual async Task<IList<TModel>> GetByOuterIdsAsync(IList<string> outerIds, string responseGroup = null, bool clone = true)
44+
{
45+
using var repository = _repositoryFactory();
46+
47+
var query = GetEntitiesQuery(repository);
48+
49+
query = outerIds.Count == 1
50+
? query.Where(x => x.OuterId.Equals(outerIds[0]))
51+
: query.Where(x => outerIds.Contains(x.OuterId));
52+
53+
var ids = await query
54+
.Select(x => x.Id)
55+
.ToArrayAsync();
56+
57+
if (ids.Length == 0)
58+
{
59+
return [];
60+
}
61+
62+
return await GetAsync(ids, responseGroup, clone);
63+
}
64+
65+
protected abstract IQueryable<TEntity> GetEntitiesQuery(IRepository repository);
66+
}

src/VirtoCommerce.Platform.Data/Infrastructure/DbContextRepositoryBase.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,5 @@ protected virtual void Dispose(bool disposing)
100100
}
101101
}
102102
#endregion
103-
104103
}
105104
}

0 commit comments

Comments
 (0)