Hi,
I am seeking your assistance.
In the .Core project, I've added the following:
public class HostEntityBase<TPrimaryKey> : FullAuditedEntity<TPrimaryKey>, IMayHaveTenant
{
public virtual int? TenantId { get; set; }
[Required]
[StringLength(HostEntityConsts.MaxLabelLength)]
public virtual string Label { get; protected set; }
public virtual string Notes { get; protected set; }
public virtual void UpdateLabel(string newLabel)
{
//Validate newLabel
Check.NotNull(newLabel, nameof(newLabel));
Label = newLabel;
}
public virtual void UpdateNotes(string newNotes)
{
Notes = newNotes;
}
protected HostEntityBase()
{
}
}
public class HostEntityWithIntKeyBase : HostEntityBase<int>
{
}
[Table("DrcClothingSize")]
public class ClothingSize : HostEntityWithIntKeyBase, IMayHaveTenant
{
/// <summary>
/// EF
/// </summary>
protected ClothingSize()
{
}
public static ClothingSize Create(string label, string notes = null)
{
var clothingSize = new ClothingSize();
clothingSize.UpdateLabel(label);
clothingSize.UpdateNotes(notes);
return clothingSize;
}
}
public interface IReferenceDataManager<THostEntityBase> : IReferenceDataManager<THostEntityBase, int>
{
}
public interface IReferenceDataManager<THostEntityBase, TPrimaryKey>
{
/// <summary>
/// Gets a list of all available <typeparamref name="THostEntity"/> in the host
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<IReadOnlyList<THostEntityBase>> GetAsync(string filter);
/// <summary>
/// Adds a <typeparamref name="THostEntity"/> record
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task CreateAsync(THostEntityBase input);
/// <summary>
/// Adds a <typeparamref name="THostEntity"/> record
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task UpdateAsync(THostEntityBase input);
/// <summary>
/// Deletes an existing <typeparamref name="THostEntity"/> record
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task DeleteAsync(TPrimaryKey input);
}
public class ReferenceDataManager<THostEntityBase> :
ReferenceDataManager<THostEntityBase, int>,
IReferenceDataManager<THostEntityBase>
where THostEntityBase : HostEntityBase<int>
{
public ReferenceDataManager(
ICacheManager cacheManager,
IRepository<THostEntityBase, int> repository,
IUnitOfWorkManager unitOfWorkManager,
string cacheName = null)
: base(
cacheManager,
repository,
unitOfWorkManager,
cacheName)
{
}
}
public class ReferenceDataManager<THostEntityBase, TPrimaryKey> :
IReferenceDataManager<THostEntityBase, TPrimaryKey>,
ISingletonDependency
where THostEntityBase : HostEntityBase<TPrimaryKey>
{
public string CacheName { get; private set; }
protected ICacheManager CacheManager { get; private set; }
protected IUnitOfWorkManager UnitOfWorkManager { get; private set; }
protected IRepository<THostEntityBase, TPrimaryKey> Repository { get; private set; }
public ReferenceDataManager(ICacheManager cacheManager,
IRepository<THostEntityBase, TPrimaryKey> repository,
IUnitOfWorkManager unitOfWorkManager,
string cacheName = null)
{
UnitOfWorkManager = unitOfWorkManager;
Repository = repository;
CacheManager = cacheManager;
CacheName = cacheName ?? GenerateDefaultCacheName();
}
public virtual async Task<IReadOnlyList<THostEntityBase>> GetAsync(string filter = null)
{
return (await GetListAsync(filter));
}
[UnitOfWork]
public virtual async Task CreateAsync(THostEntityBase input)
{
if ( (await GetAsync() as IReadOnlyList<THostEntityBase>)
.Any(t => t.Label.ToLower().Contains(input.Label.ToLower())))
{
throw new AbpException($"There is already an entity with Label = {input.Label}");
}
var existingEntity = (await GetAsync(input.Label));
if (existingEntity != null)
{
}
using (UnitOfWorkManager.Current.SetTenantId(null))
{
await Repository.InsertAsync(input);
await UnitOfWorkManager.Current.SaveChangesAsync();
}
}
[UnitOfWork]
public virtual async Task UpdateAsync(THostEntityBase input)
{
//Check the entity doesnt exist
var existingEntity = ((await GetAsync() as IReadOnlyList<THostEntityBase>)
.FirstOrDefault(t => t.Label.ToLower().Contains(input.Label.ToLower())));
if (existingEntity != null)
{
if (!existingEntity.Id.Equals(input.Id))
{
throw new AbpException($"There is already an entity with Label = {input.Label}");
}
}
existingEntity.UpdateLabel(input.Label.Trim());
existingEntity.UpdateNotes(input.Notes.Trim());
using (UnitOfWorkManager.Current.SetTenantId(null))
{
await Repository.UpdateAsync(existingEntity);
await UnitOfWorkManager.Current.SaveChangesAsync();
}
}
[UnitOfWork]
public virtual async Task DeleteAsync(TPrimaryKey input)
{
//Get all records from cache
var existingEntity = (await GetAsync() as IReadOnlyList<THostEntityBase>).FirstOrDefault(t => t.Id.Equals(input));
if (existingEntity == null)
{
return;
}
using (UnitOfWorkManager.Current.SetTenantId(null))
{
await Repository.DeleteAsync(existingEntity.Id);
await UnitOfWorkManager.Current.SaveChangesAsync();
}
}
[UnitOfWork]
private async Task<List<THostEntityBase>> GetListAsync(string filter)
{
//Get THostEntityBase from host not any specific tenant
using (UnitOfWorkManager.Current.SetTenantId(null))
{
return await Repository
.GetAllListAsync(
WithFitler(filter)
);
}
}
/// <summary>
/// Prepares a dynamic filter to insert into GetAllListAsync
/// </summary>
/// <param name="filter"></param>
/// <returns></returns>
private Expression<Func<THostEntityBase, bool>> WithFitler(string filter)
{
if (filter.IsNullOrEmpty())
{
return (t => true);
}
return (t => t.Label.ToLower().Contains(filter.ToLower()) || t.Notes.ToLower().Contains(filter.ToLower()));
}
protected virtual string GenerateDefaultCacheName()
{
return GetType().FullName;
}
}
This is just a ReferenceDataManager class that can handle Reading/Updating/Deleting/Creating for all objects that inherit from HostEntityBase.
Now in the .Application project, I have the following:
public interface IClothingSizeAppService : IApplicationService
{
/// <summary>
/// Gets a list of all available ClothingSizes in the host
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<GetClothingSizesOutput> GetClothingSizes(GetClothingSizeInput input);
/// <summary>
/// Adds or updates a clothing size record
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task CreateOrUpdateClothingSize(CreateOrUpdateClothingSizeInput input);
/// <summary>
/// Deletes an existing ClothingSize record
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task DeleteClothingSize(EntityDto input);
}
[AbpAuthorize(AppPermissions.Pages_HOSTREFDATA_CLOTHINGSIZE)]
public class ClothingSizeAppService : OnlineSystemsAppServiceBase, IClothingSizeAppService
{
private readonly IReferenceDataManager<ClothingSize> _clothingSizeManager;
private readonly ICacheManager _cacheManager;
private readonly IRepository<ClothingSize> _repository;
private readonly IUnitOfWorkManager _unitOfWorkManager;
public ClothingSizeAppService(ICacheManager cacheManager,
IRepository<ClothingSize> repository,
IUnitOfWorkManager unitOfWorkManager)
{
_cacheManager = cacheManager;
_repository = repository;
_unitOfWorkManager = unitOfWorkManager;
_clothingSizeManager = new ReferenceDataManager<ClothingSize>(_cacheManager, _repository, _unitOfWorkManager, "ClothingSizes");
}
public async Task<GetClothingSizesOutput> GetClothingSizes(GetClothingSizeInput input)
{
var clothingSizes = await _clothingSizeManager.GetAsync(input.Filter);
return new GetClothingSizesOutput
{
Items = clothingSizes.MapTo<List<ClothingSizeListDto>>()
};
}
public async Task CreateOrUpdateClothingSize(CreateOrUpdateClothingSizeInput input)
{
if (input.ClothingSize.Id.HasValue)
{
await UpdateClothingSizeAsync(input);
}
else
{
await CreateClothingSize(input);
}
}
[AbpAuthorize(AppPermissions.Pages_HOSTREFDATA_CLOTHINGSIZE_DELETE)]
public async Task DeleteClothingSize(EntityDto input)
{
var existingClothingSize = await _repository.GetAsync(input.Id);
if (existingClothingSize == null)
{
return;
}
await _clothingSizeManager.DeleteAsync(input.Id);
}
#region Private methods
[AbpAuthorize(AppPermissions.Pages_HOSTREFDATA_CLOTHINGSIZE_EDIT)]
private async Task CreateClothingSize(CreateOrUpdateClothingSizeInput input)
{
var newClothingSize = ClothingSize.Create(input.ClothingSize.Label, input.ClothingSize.Notes);
//Check that this new clothing size doesn't exist
await CheckClothingSizeIfAlreadyExists(newClothingSize.Label);
await _clothingSizeManager.CreateAsync(newClothingSize);
}
[AbpAuthorize(AppPermissions.Pages_HOSTREFDATA_CLOTHINGSIZE_EDIT)]
private async Task UpdateClothingSizeAsync(CreateOrUpdateClothingSizeInput input)
{
//Check that this new clothing size doesn't exist
await CheckClothingSizeIfAlreadyExists(input.ClothingSize.Label, input.ClothingSize.Id);
var existingClothingSize = await _repository.GetAsync(input.ClothingSize.Id.Value);
existingClothingSize.UpdateLabel(input.ClothingSize.Label);
existingClothingSize.UpdateNotes(input.ClothingSize.Notes);
await _clothingSizeManager.UpdateAsync(existingClothingSize);
}
private async Task CheckClothingSizeIfAlreadyExists(string label, int? expectedId = null)
{
var allClothingSizes = await _clothingSizeManager.GetAsync(label);
var existingClothingSize = allClothingSizes.Count > 0 ? allClothingSizes[0] : null;
if (existingClothingSize == null)
{
return;
}
if (expectedId != null && existingClothingSize.Id == expectedId.Value)
{
return;
}
throw new UserFriendlyException(L("ThisClothingsizeAlreadyExists", label));
}
#endregion
}
The problem is in this line:
public ClothingSizeAppService(ICacheManager cacheManager,
IRepository<ClothingSize> repository,
IUnitOfWorkManager unitOfWorkManager)
{
_cacheManager = cacheManager;
_repository = repository;
_unitOfWorkManager = unitOfWorkManager;
_clothingSizeManager = new ReferenceDataManager<ClothingSize>(_cacheManager, _repository, _unitOfWorkManager, "ClothingSizes");
}
I would like to be able to inject IReferenceDataManager<ClothingSize> to the constructor.
I have also another 7 similar classes to ClothingSize and all have the same behavior and share the same object properties. So I thought of creating such a generic way of handling stuff.
Then on the ApplicationService level, I would create a new AppService per Entity.
I know that the framework is already doing it with IRepository<Entity> and others.
Can you please guide me on how to do the same in my custom code?
Many Thanks, Bilal
13 Answer(s)
-
0
Hi,
In order to register generic interfaces like that, you need to register them with all possible entities. You can take a look at EfAutoRepositoryTypes here <a class="postlink" href="https://github.com/aspnetboilerplate/aspnetboilerplate/blob/master/src/Abp.EntityFramework/EntityFramework/Repositories/EfAutoRepositoryTypes.cs">https://github.com/aspnetboilerplate/as ... ryTypes.cs</a> and EntityFrameworkGenericRepositoryRegistrar here <a class="postlink" href="https://github.com/aspnetboilerplate/aspnetboilerplate/blob/master/src/Abp.EntityFramework/EntityFramework/Repositories/EntityFrameworkGenericRepositoryRegistrar.cs">https://github.com/aspnetboilerplate/as ... gistrar.cs</a>.
-
0
Can you please explain a bit more on this code? I am not getting the idea.
Thank you.
public static class EfAutoRepositoryTypes { public static AutoRepositoryTypesAttribute Default { get; private set; } static EfAutoRepositoryTypes() { Default = new AutoRepositoryTypesAttribute( typeof(IRepository<>), typeof(IRepository<,>), typeof(EfRepositoryBase<,>), typeof(EfRepositoryBase<,,>) ); } } var autoRepositoryAttr = dbContextType.GetSingleAttributeOrNull<AutoRepositoryTypesAttribute>() ?? EfAutoRepositoryTypes.Default; foreach (var entityTypeInfo in DbContextHelper.GetEntityTypeInfos(dbContextType)) { var primaryKeyType = EntityHelper.GetPrimaryKeyType(entityTypeInfo.EntityType); if (primaryKeyType == typeof(int)) { var genericRepositoryType = autoRepositoryAttr.RepositoryInterface.MakeGenericType(entityTypeInfo.EntityType); if (!iocManager.IsRegistered(genericRepositoryType)) { var implType = autoRepositoryAttr.RepositoryImplementation.GetGenericArguments().Length == 1 ? autoRepositoryAttr.RepositoryImplementation.MakeGenericType(entityTypeInfo.EntityType) : autoRepositoryAttr.RepositoryImplementation.MakeGenericType(entityTypeInfo.DeclaringType, entityTypeInfo.EntityType); iocManager.Register( genericRepositoryType, implType, DependencyLifeStyle.Transient ); } }
-
0
Thanks, no need to explain it. I went through all the related classes and it makes perfect sense :)
Given the fact, the AbpDbContext could have an unknown number of IDbSet<T> ahead of time, this code is needed.
In my case, I just want to map IFoo<T> or IFoo<T,Z> to Foo<T> or Foo<T,Z>, so I use this code and it seems to be working fine.
private void RegisterForReferenceDataManager() { IocManager.RegisterIfNot( typeof(IReferenceDataManager<,>), typeof(ReferenceDataManager<,>), DependencyLifeStyle.Singleton); }
One question though, what's the recommendation to use Singleton or Transient? When will each be used?
In case I am registering the classes as above, do I still need to have Foo<T> implement ISingletonDependency? As I understand, this interface is used only when doing the below so that CW would know how to register an interface and implementation. Correct?
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
-
0
Hi,
You don't need ISingletonDependency because you register it by yourself. You can use singletons for objects like cache and app-wide global variable container classes but don't take this as a alwyas true suggestion. It depends on your needs mostly.
-
0
Ok great. Thanks.
You are right, it depends. In my case, I am caching some reference data. However, this data might change, so I will invalidate the data once it changes. But as a manager, I only need a single instance from it, no need to have an instance every time it is being asked for.
Regards Bilal
-
0
You are right, if it is used for caching it should be singleton.
-
0
Hi, I am facing a big issue now with this design! I will paste the code here and tell you where the problem is.
I've highlighted in bold the method that I dunno how to proceed from now on. I followed the pattern you use in some of the classes to create a manager and using caching also.
I am stuck here and really appreciate your assistance.
Regards Bilal
public class HostEntityBase<TPrimaryKey> : FullAuditedEntity<TPrimaryKey>, IMayHaveTenant { public virtual int? TenantId { get; set; } [Required] [StringLength(HostEntityConsts.MaxLabelLength)] public virtual string Label { get; protected set; } public virtual string Notes { get; protected set; } public virtual void UpdateLabel(string newLabel) { //Validate newLabel Check.NotNull(newLabel, nameof(newLabel)); Label = newLabel; } public virtual void UpdateNotes(string newNotes) { Notes = newNotes; } } public class HostEntityWithIntKeyBase : HostEntityBase<int> { } public class ClothingSize : HostEntityWithIntKeyBase, IMayHaveTenant { /// <summary> /// EF /// </summary> protected ClothingSize() { } public static ClothingSize Create(string label, string notes = null) { var clothingSize = new ClothingSize(); clothingSize.UpdateLabel(label); clothingSize.UpdateNotes(notes); return clothingSize; } } /// <summary> /// Used to cache HostEntities /// </summary> public class HostEntityCacheItem { public const string CacheStoreName = "DrcHostEntities"; public List<HostEntityCacheItemSingle> Items { get; set; } public HostEntityCacheItem() { Items = new List<HostEntityCacheItemSingle>(); } } public class HostEntityCacheItemSingle { public object Id { get; set; } public string Label { get; set; } public string Notes { get; set; } } public static class DrcCacheManagerExtensions { public static ITypedCache<string, HostEntityCacheItem> GetHostEntityCache(this ICacheManager cacheManager) { return cacheManager.GetCache<string, HostEntityCacheItem>(HostEntityCacheItem.CacheStoreName); } } public class ReferenceDataManager<THostEntityBase> : ReferenceDataManager<THostEntityBase, int>, IReferenceDataManager<THostEntityBase> where THostEntityBase : HostEntityBase<int> { public ReferenceDataManager( ICacheManager cacheManager, IRepository<THostEntityBase, int> repository, IUnitOfWorkManager unitOfWorkManager, string cacheName = null) : base( cacheManager, repository, unitOfWorkManager, cacheName) { } } public class ReferenceDataManager<THostEntityBase, TPrimaryKey> : IReferenceDataManager<THostEntityBase, TPrimaryKey>, ISingletonDependency where THostEntityBase : HostEntityBase<TPrimaryKey> { public string CacheName { get; private set; } protected ICacheManager CacheManager { get; private set; } protected IUnitOfWorkManager UnitOfWorkManager { get; private set; } protected IRepository<THostEntityBase, TPrimaryKey> Repository { get; private set; } public ReferenceDataManager(ICacheManager cacheManager, IRepository<THostEntityBase, TPrimaryKey> repository, IUnitOfWorkManager unitOfWorkManager, string cacheName = null) { UnitOfWorkManager = unitOfWorkManager; Repository = repository; CacheManager = cacheManager; CacheName = cacheName ?? GenerateDefaultCacheName(); } public virtual async Task<IReadOnlyList<THostEntityBase>> GetAsync() { return (await GetListAsync()); } } **[UnitOfWork] private async Task<List<THostEntityBase>> GetListAsync() { var entity = await GetListFromCacheAsync(); var instances = new List<THostEntityBase>(); foreach(HostEntityCacheItemSingle e in entity.Items) { instances.Add(new HostEntityBase<TPrimaryKey> { Id = } ); } return instances; }** [UnitOfWork] private async Task<HostEntityCacheItem> GetListFromCacheAsync() { return await CacheManager.GetHostEntityCache().GetAsync("0", async () => { //Get a list from the database var list = (await GetHostEntityFromDatabaseAsync()); var newItem = new HostEntityCacheItem(); foreach (var l in list) { newItem.Items.Add(new HostEntityCacheItemSingle { Id = l.Id, Label = l.Label, Notes = l.Notes }); } return newItem; }); } [UnitOfWork] protected async virtual Task<List<THostEntityBase>> GetHostEntityFromDatabaseAsync() { //Get THostEntityBase from host not any specific tenant using (UnitOfWorkManager.Current.SetTenantId(null)) { return await Repository .GetAllListAsync(); } }
-
0
Hi,
Can you explain why this block is causing problem for you ? Or what is the problem here ? Do you get an exception ?
-
0
My problem is, I am storing in cache a class having Id of type object. Then, I am trying to map a non-generic instance to a generic-instance.
I ended up storing the HostEntityBase<T> in cache, this way, when I retrieve data I don't have to map or convert. I am not sure this is the correct way,
Can you please check if this ok or you suggest any enhancement? My only worries is that the HostEntityBase<T> has few methods and I was planning to store pure data in cache that is why I created the CacheItem above.
Thank you :)
public class HostEntityCacheItem<THostEntityBase> { public static string CacheStoreName = $"Drc{typeof(THostEntityBase).FullName.ToString()}"; public List<THostEntityBase> Items { get; set; } public HostEntityCacheItem() :this(new List<THostEntityBase>()) { } public HostEntityCacheItem(List<THostEntityBase> list) { Items = new List<THostEntityBase>(list); } } [UnitOfWork] private async Task<HostEntityCacheItem<THostEntityBase>> GetListFromCacheAsync() { return await CacheManager.GetHostEntityCache<THostEntityBase>().GetAsync(GenerateDefaultCacheKeyName(), async () => { //Get a list from the database var list = (await GetHostEntityFromDatabaseAsync()); return new HostEntityCacheItem<THostEntityBase>(list); }); }
-
0
Hi,
It is better to store cache item you have defined in cache. You should be able to map a non-generic class to a generic class. This might give you an idea <a class="postlink" href="https://github.com/AutoMapper/AutoMapper/issues/681">https://github.com/AutoMapper/AutoMapper/issues/681</a>.
-
0
Thanks, I didn't find the thread useful. There was no solution.
The one thing I figured out is maybe I should have defined another class that inherits from the ReferenceDataManager in my case and used concrete types something like:
public class ConcreteReferenceDataManager : ReferenceDataManager<HostEntityBase, int>
Then in this class I can implement caching since all types are known.
Problem is when I had a CacheItem with Id of type int, I was storing the cache item in Cache. But then I had to map back to a type whose Id is of type TPrimaryKey. so I guess it doesnt work like that.
I saw many examples in your code, but none tries to store a concrete object and then retrieve it as a generic object.
-
0
Maybe I should have defined the Id in CacheItem as of type TPrimaryKey, then it will work.
-
0
Please let us know the result of this,
thanks.