ABP .NET创建项目(三)

发布时间 2023-08-15 10:58:41作者: 动量不守恒的浩

ABP.NET 创建项目(三)(进阶部分)

ABP.NET 创建项目(二)(进阶部分)的基础上增加代码

增加缓存方法。
好处:比如在多次重复的数据库查询操作中,结果相同,但利用缓存可以使得第一次同普通查询一样查询,而后续的重复操作查询可以直接用(return)缓存中存储的查询结果而非再次进行数据库查询操作。旨在缩短加载时间(在大项目数据条目极大的数据库或者同样的查询请求很多的情形下十分常见)

11.需增加(Red)&需改写(Green)


11.1:需要更改的原始文件(Green)

相关文档

一 :Practice20223CoreModule.cs:

using Abp.AutoMapper;
using Abp.Dependency;
using Abp.Localization;
using Abp.Modules;
using Abp.Reflection.Extensions;
using Abp.Runtime.Security;
using Abp.Timing;
using Abp.Zero;
using Abp.Zero.Configuration;
using Practice2023.Authorization.Roles;
using Practice2023.Authorization.Users;
using Practice2023.Configuration;
using Practice2023.Localization;
using Practice2023.MultiTenancy;
using Practice2023.Students;
using Practice2023.Students.Caches;
using Practice2023.Timing;
using System;

namespace Practice2023
{
    //AbpAutoMapperModule
    //[DependsOn(typeof(AbpAutoMapperModule))] 表示该模块依赖于 AbpAutoMapperModule。
    //AbpAutoMapperModule: 提供了与 AutoMapper 的集成,用于对象映射
    //用于下面Configuration.Modules.AbpAutoMapper()
    [DependsOn(
        typeof(AbpZeroCoreModule),
        typeof(AbpAutoMapperModule))]
    public class Practice2023CoreModule : AbpModule
    {
        public override void PreInitialize()
        {
            Configuration.Auditing.IsEnabledForAnonymousUsers = true;
            // Declare entity types
            Configuration.Modules.Zero().EntityTypes.Tenant = typeof(Tenant);
            Configuration.Modules.Zero().EntityTypes.Role = typeof(Role);
            Configuration.Modules.Zero().EntityTypes.User = typeof(User);
            Practice2023LocalizationConfigurer.Configure(Configuration.Localization);
            // Enable this line to create a multi-tenant application.
            Configuration.MultiTenancy.IsEnabled = Practice2023Consts.MultiTenancyEnabled;
            // Configure roles
            AppRoleConfig.Configure(Configuration.Modules.Zero().RoleManagement);
            Configuration.Settings.Providers.Add<AppSettingProvider>();
            Configuration.Localization.Languages.Add(new LanguageInfo("fa", "فارسی", "famfamfam-flags ir"));
            Configuration.Settings.SettingEncryptionConfiguration.DefaultPassPhrase = Practice2023Consts.DefaultPassPhrase;
            SimpleStringCipher.DefaultPassPhrase = Practice2023Consts.DefaultPassPhrase;

            //新增加项
            // Configure auto mapper
            //可以根据需要在应用程序中的不同位置使用 GetAssembly 方法来获取模块的程序集,以便进行一些反射操作或其他需要使用程序集的场景。
            //扫描程序集,寻找继承自AutoMapper.Profile的类。
            var thisAssembly = typeof(Practice2023CoreModule).GetAssembly();
            Configuration.Modules.AbpAutoMapper().Configurators.Add(cfg => cfg.AddMaps(thisAssembly));
        }
        public override void Initialize()
        {
            IocManager.RegisterAssemblyByConvention(typeof(Practice2023CoreModule).GetAssembly());
        }
        public override void PostInitialize()
        {
            IocManager.Resolve<AppTimes>().StartupTime = Clock.Now;

            //新添加部分
            // Init Caches
            CacheInitialize();
        }
        //添加的部分
        //在这段代码中,CacheInitialize() 方法用于初始化缓存配置。
        //具体来说,它配置了一个名为StudentCache.CacheKey的缓存项,并设置了默认的滑动过期时间为1天。
        //通过调用 Configuration.Caching.Configure() 方法,并传入缓存项的键(StudentCache.CacheKey)和一个回调函数,可以对指定的缓存项进行配置
        //在这里,回调函数中设置了默认的滑动过期时间为1天,表示存储在这个缓存项中的数据将在最后一次访问后的1天内过期,过期后将不再有效。
        public void CacheInitialize()
        {
            Configuration.Caching.Configure(StudentCache.CacheKey, cache =>
            {
                cache.DefaultSlidingExpireTime = TimeSpan.FromDays(1);
            });
        }
    }
}

这里增加的代码为

//AbpAutoMapperModule
//[DependsOn(typeof(AbpAutoMapperModule))] 表示该模块依赖于 AbpAutoMapperModule。
//AbpAutoMapperModule: 提供了与 AutoMapper 的集成,用于对象映射
//用于下面Configuration.Modules.AbpAutoMapper()
[DependsOn(
    typeof(AbpZeroCoreModule),
    typeof(AbpAutoMapperModule))]
//新增加项
// Configure auto mapper
//可以根据需要在应用程序中的不同位置使用 GetAssembly 方法来获取模块的程序集,以便进行一些反射操作或其他需要使用程序集的场景。
var thisAssembly = typeof(Practice2023CoreModule).GetAssembly();
Configuration.Modules.AbpAutoMapper().Configurators.Add(cfg => cfg.AddMaps(thisAssembly));

依赖模块AbpAutoMapperModule.扫描程序集,寻找继承自AutoMapper.Profile的类。找到所有的CreatMap.这里主要找的是我们新创建的StudentCoreMapProfile.cs.

//新添加部分
// Init Caches
CacheInitialize();
public void CacheInitialize()
{
    //这里的StudentCache.CacheKey是在StudentCache.cs中定义的"Students"
    Configuration.Caching.Configure(StudentCache.CacheKey, cache =>
    {
        //设置缓存滑动过期时长
        cache.DefaultSlidingExpireTime = TimeSpan.FromDays(1);
    });
}

这里应该是新建一个缓存空间(有点像是新建名为CacheKey也就是名为"Students"的文件夹,里面的缓存的文件被命名为"Students_{id}")
在后面利用自定义的PrintCacheContent()函数可以看到确实新增了一个缓存名为"Students"的空间。(剩余四个是模板原生类的缓存空间)

实际上发现似乎在`CacheInitialize()`后并没有新建该Students(ConsolewriteLine不出来)而是在真正往缓存里放入东西时才会看到该名为Students的缓存名.

11.2:需要新建的文件(Red)

一:IStudentCache.cs,StudentCache.cs,StudentCacheItem.cs,StudentCoreMapProfile.cs,Practice2023CacheExtensions:

第二者内定义了与缓存相关的函数方法.
第三者定义了缓存类的属性(正常情况与Student类没区别,但若想自定义缓存的字段的话就会有区别).
第四者定义了缓存类(第三者)与Student类的映射.
第五者

using System.Threading.Tasks;
namespace Practice2023.Students.Caches
{
    public interface IStudentCache
    {
        Task<StudentCacheItem> GetAsync(long id);
        void Remove(long id);
    }
}
using Abp.Dependency;
using Abp.Domain.Repositories;
using Abp.Events.Bus.Entities;
using Abp.Events.Bus.Handlers;
using Abp.ObjectMapping;
using Abp.Runtime.Caching;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace Practice2023.Students.Caches
{
    public class StudentCache :
        IEventHandler<EntityCreatedEventData<Student>>,
        IEventHandler<EntityUpdatedEventData<Student>>,
        IEventHandler<EntityDeletedEventData<Student>>, 
        ITransientDependency, IStudentCache
    
    {
        private readonly IRepository<Student, long> _repository;
        private readonly ICacheManager _cacheManager;
        private readonly IObjectMapper _objectMapper;

        //=>换成=是一样的
        public static string CacheKey => "Students";

        public StudentCache(
            IRepository<Student, long> repository, 
            ICacheManager cacheManager, 
            IObjectMapper objectMapper)
        {
            _repository = repository;
            _cacheManager = cacheManager;
            _objectMapper = objectMapper;
        }

        //用于打印当前缓存的缓存名
        public void PrintCacheContent()
        {
            foreach (var cacheName in _cacheManager.GetAllCaches().Select(x => x.Name))
            {
                Console.WriteLine($"cacheName: {cacheName}");
            }
        }
        //调用了原生cacheManager.GetStudentCacheAsync().GetAsync()
        public async Task<StudentCacheItem> GetAsync(long id)
        {
            PrintCacheContent();
            //第一个参数$"Student_{id}"应该是该缓存子文件的命名,后面是该缓存存储的内容
            return await _cacheManager.GetStudentCacheAsync().GetAsync($"Student_{id}", async () =>
            {
                var student = await _repository.GetAll()
                    .Include(x => x.Extra)
                    .Include(x => x.ClassGroups).ThenInclude(x => x.ClassGroup)
                    .FirstOrDefaultAsync(x => x.Id == id);

                if (student == null) return null;

                return _objectMapper.Map<StudentCacheItem>(student);
                //StudentCacheItem<--Student
                //这里面的return是返回StudentCacheItem类型给async()
                //看起来查询的语句和Student的GetAsync没什么区别
            });
        }
        //通过`Practice2023CacheExtensions.cs`间接调用了原生cacheManager.cacheManager.GetCache()
        public void Remove(long id)
        {
            _cacheManager.GetStudentCacheAsync().Remove($"Student_{id}");
        }
        //在这里,IEventHandler<EntityxxxxEventData>的作用是当表记录有所更新时,将触发事件,
        //程序里面主要是移除数据缓存
        //这三个HandleEvent是重载了三个输入类型吗,为什么不用泛型呢
        public void HandleEvent(EntityCreatedEventData<Student> eventData)
        {
            Remove(eventData.Entity.Id);
        }

        public void HandleEvent(EntityUpdatedEventData<Student> eventData)
        {
            Remove(eventData.Entity.Id);
        }

        public void HandleEvent(EntityDeletedEventData<Student> eventData)
        {
            Remove(eventData.Entity.Id);
        }
    }
}

其中CacheKey => "Students"是用于前面CacheInitialize()调用的名.
HandleEvent是重载的时间触发器,对应数据库实体发生增,改,删时触发对应的事件.这里对应的事件均为缓存的清除操作(利用原生CacheManager中的缓存方法)
这里实现缓存的是CacheManager.GetCache.GetAsync($"Student_{id}", async () =>...).在第二次执行该函数时会检测是否存在"Student_{id}",存在则直接调取该缓存.

using Practice2023.ClassGroups;
using System.Collections.Generic;

namespace Practice2023.Students.Caches
{
    public class StudentCacheItem//和Student基本无差,所以可做map映射
    {
        /// <summary>
        /// ID
        /// </summary>
        public long Id { get; set; }
        /// <summary>
        /// 姓名
        /// </summary>
        public string Name { get; set; }
        /// <summary>
        /// 性别
        /// </summary>
        public string Sex { get; set; }
        /// <summary>
        /// 身份证号码
        /// </summary>
        public string IdCardNumber { get; set; }
        /// <summary>
        /// 学号
        /// </summary>
        public string Number { get; set; }
        /// <summary>
        /// 额外信息
        /// </summary>
        public StudentExtraCacheItem Extra { get; set; } = new StudentExtraCacheItem();
        /// <summary>
        /// 所在班组
        /// </summary>
        public ICollection<StudentClassGroupCacheItem> ClassGroups { get; set; } = new List<StudentClassGroupCacheItem>();
    }
    public class StudentExtraCacheItem//和StudentExtra基本无差,所以可做map映射
    {
        public string Extra1 { get; set; }
        public string Extra2 { get; set; }
        public string Extra3 { get; set; }
    }
    public class StudentClassGroupCacheItem//和StudentClassGroup基本无差,所以可做map映射
    {
        /// <summary>
        /// 班组ID
        /// </summary>
        public long ClassGroupId { get; set; }
        /// <summary>
        /// 班组名称
        /// </summary>
        public string ClassGroupName { get; set; }
        /// <summary>
        /// 班组类型
        /// </summary>
        public ClassGroupType ClassGroupType { get; set; }
        /// <summary>
        /// 班组类型
        /// </summary>
        public string ClassGroupTypeDescription { get; set; }
    }
}

这里和Student.cs的定义差不多,因为缓存也可以只缓存部分信息,所以可以自定义

using Abp.Runtime.Caching;
using Practice2023.Students.Caches;

namespace Practice2023
{
    public static class Practice2023CacheExtensions
    {
        public static ITypedCache<string, StudentCacheItem> GetStudentCacheAsync(this ICacheManager cacheManager)
        {
            return cacheManager.GetCache<string, StudentCacheItem>(StudentCache.CacheKey);
        }
    }
}

这部分是关键的缓存空间位置获取.
在上面可以看到ITypedCache类型存在GetAsync(string,StudentCacheItem),Remove(string)方法.


11.3:这里还需要对StudentAppService.cs进行修改(以及IStudentAppService.cs),增加GetCacheAsync()方法进去Swagger接口

一 :StudentAppService.cs:

using Abp.Application.Services.Dto;
using Abp.Domain.Repositories;
using Abp.Extensions;
using Abp.Linq.Extensions;
using Abp.Runtime.Caching;
using Abp.UI;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Practice2023.Students.Caches;
using Practice2023.Students.Dto;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Practice2023.Students
{
    public class StudentAppService : Practice2023AppServiceBase, IStudentAppService
    {
        private readonly IRepository<Student, long> _studentRepository;
        private readonly IRepository<StudentExtra, long> _studentExtraRepository;
        private readonly IRepository<StudentClassGroup, long> _studentClassGroupRepository;
        //新加项
        private readonly IStudentCache _studentCache;

        public StudentAppService(
            IRepository<Student, long> studentRepository,
            IRepository<StudentExtra, long> studentExtraRepository,
            IRepository<StudentClassGroup, long> studentClassGroupRepository,
            IStudentCache studentCache)
        {
            _studentRepository = studentRepository;
            _studentExtraRepository = studentExtraRepository;
            _studentClassGroupRepository = studentClassGroupRepository;
            _studentCache = studentCache;
        }
        //新增部分
        /// <summary>
        /// 获取学生信息缓存
        /// </summary>
        public async Task<StudentCacheItem> GetCacheAsync(long id)//缓存获取操作
        {
            return await _studentCache.GetAsync(id);
        }

        /// <summary>
        /// 获取学生信息
        /// </summary>
        public async Task<StudentDto> GetAsync(long id)
        {
            var entity = await _studentRepository.GetAll()
                .Include(x => x.Extra)
                .Include(x => x.ClassGroups).ThenInclude(x => x.ClassGroup)
                .FirstOrDefaultAsync(x => x.Id == id);

            if (entity == null) return null;

            return ObjectMapper.Map<StudentDto>(entity);
        }

        /// <summary>
        /// 获取学生信息分页列表
        /// </summary>
        [HttpPost]
        public async Task<PagedResultDto<StudentDto>> GetPagedListAsync(PagedStudentResultRequestDto input)
        {
            var query = _studentRepository.GetAll()
                .Include(x => x.Extra)
                .Include(x => x.ClassGroups).ThenInclude(x => x.ClassGroup)
                .WhereIf(!input.Keyword.IsNullOrWhiteSpace(), x =>
                    x.Name.Contains(input.Keyword) ||
                    x.Number.Contains(input.Keyword) ||
                    x.IdCardNumber.Contains(input.Keyword));

            var test = query.OrderByDescending(x => x.Id).PageBy(input).ToQueryString();

            var totalCount = await query.CountAsync();
            var queryResult = await query.OrderByDescending(x => x.Id).PageBy(input).ToListAsync();

            return new PagedResultDto<StudentDto>(totalCount, ObjectMapper.Map<List<StudentDto>>(queryResult));
        }

        private async Task CheckIdCardNumberAsync(string idCardNumber, long? id = null)
        {
            if (idCardNumber.IsNullOrWhiteSpace()) return;
            var isExists = await _studentRepository.GetAll().WhereIf(id.HasValue, x => x.Id != id).AnyAsync(x => x.IdCardNumber == idCardNumber);
            if (isExists) throw new UserFriendlyException($"身份证号码“{idCardNumber}”已存在!");
        }

        private async Task CheckNumberAsync(string number, long? id = null)
        {
            if (number.IsNullOrWhiteSpace()) return;
            var isExists = await _studentRepository.GetAll().WhereIf(id.HasValue, x => x.Id != id).AnyAsync(x => x.Number == number);
            if (isExists) throw new UserFriendlyException($"学号“{number}”已存在!");
        }

        /// <summary>
        /// 创建学生
        /// </summary>
        public async Task CreateAsync(CreateStudentDto input)
        {
            await CheckIdCardNumberAsync(input.IdCardNumber);
            await CheckNumberAsync(input.Number);

            var entity = new Student(input.Name, input.Sex, input.IdCardNumber, input.Number); // 也可以使用AutoMapper

            entity.AddExtra(input.Extra.Extra1, input.Extra.Extra2, input.Extra.Extra3);

            foreach (var classGroupId in input.ClassGroupIds)
            {
                entity.AddToClassGroup(classGroupId);
            }

            await _studentRepository.InsertAsync(entity);
        }

        /// <summary>
        /// 更新学生
        /// </summary>
        public async Task UpdateAsync(UpdateStudentDto input)
        {
            await CheckIdCardNumberAsync(input.IdCardNumber, input.Id);
            await CheckNumberAsync(input.Number, input.Id);

            var entity = await _studentRepository.GetAll()
                .Include(x => x.Extra)
                .Include(x => x.ClassGroups).ThenInclude(x => x.ClassGroup)
                .FirstOrDefaultAsync(x => x.Id == input.Id);

            if (entity == null) return;

            entity.Name = input.Name;
            entity.Sex = input.Sex;
            entity.IdCardNumber = input.IdCardNumber;
            entity.Number = input.Number;

            ObjectMapper.Map(input.Extra, entity.Extra);

            var oriClassGroupIds = entity.ClassGroups.Select(x => x.ClassGroupId).ToList();
            var newClassGroupIds = input.ClassGroupIds.Except(oriClassGroupIds);
            var delClassGroupIds = oriClassGroupIds.Except(input.ClassGroupIds);

            foreach (var newClassGroupId in newClassGroupIds)
            {
                entity.AddToClassGroup(newClassGroupId);
            }

            foreach(var delClassGroupId in delClassGroupIds)
            {
                entity.RemoveToClassGroup(delClassGroupId);
            }

            await _studentRepository.UpdateAsync(entity);
        }

        /// <summary>
        /// 删除学生
        /// </summary>
        public async Task DeleteAsync(long id)
        {
            var entity = await _studentRepository.FirstOrDefaultAsync(x => x.Id == id);
            if (entity == null) return;

            await _studentClassGroupRepository.DeleteAsync(x => x.StudentId == entity.Id);
            await _studentExtraRepository.DeleteAsync(x => x.StudentId == entity.Id);
            await _studentRepository.DeleteAsync(x => x.Id == entity.Id);
        }
    }
}

新增部分

private readonly IStudentCache _studentCache;

public StudentAppService(
            IRepository<Student, long> studentRepository,
            IRepository<StudentExtra, long> studentExtraRepository,
            IRepository<StudentClassGroup, long> studentClassGroupRepository,
            IStudentCache studentCache)
        {
            _studentRepository = studentRepository;
            _studentExtraRepository = studentExtraRepository;
            _studentClassGroupRepository = studentClassGroupRepository;
            _studentCache = studentCache;
        }
public async Task<StudentCacheItem> GetCacheAsync(long id)//缓存获取操作
        {
            return await _studentCache.GetAsync(id);
        }

12.测试效果

以下依次是第一次使用使用查询时间,第二次使用查询时间

在数据量很大的情况下,第一次与后面几次的运行时间会相差很大.这里由于数据并不大因此...

13.DeBug与问题解答

Q1&A1:在Dbcontext中并没有设置Dbset,因此自然没有Cache表