Base solution for your next web application
Open Closed

How to auto-map IManagerClass<T> to ManagerClass<T> #2600


User avatar
0
bilalhaidar created

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)
  • User Avatar
    0
    ismcagdas created
    Support Team

    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>.

  • User Avatar
    0
    bilalhaidar created

    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
                                );
                        }
                    }
    
  • User Avatar
    0
    bilalhaidar created

    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());
    
  • User Avatar
    0
    ismcagdas created
    Support Team

    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.

  • User Avatar
    0
    bilalhaidar created

    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

  • User Avatar
    0
    ismcagdas created
    Support Team

    You are right, if it is used for caching it should be singleton.

  • User Avatar
    0
    bilalhaidar created

    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();
                }
            }
    
  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi,

    Can you explain why this block is causing problem for you ? Or what is the problem here ? Do you get an exception ?

  • User Avatar
    0
    bilalhaidar created

    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);
                });
            }
    
  • User Avatar
    0
    ismcagdas created
    Support Team

    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>.

  • User Avatar
    0
    bilalhaidar created

    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.

  • User Avatar
    0
    bilalhaidar created

    Maybe I should have defined the Id in CacheItem as of type TPrimaryKey, then it will work.

  • User Avatar
    0
    ismcagdas created
    Support Team

    Please let us know the result of this,

    thanks.