zl程序教程

您现在的位置是:首页 >  .Net

当前栏目

《ASP.NET Core技术内幕与项目实战》精简集-EFCore2.9:泛型仓储实现IRepository

2023-03-20 15:33:41 时间

本节内容,部分为补充内容,部分涉及到5.2(P131-133)。主要NuGet包:如前章节所述

 

仓储模式,将数据访问层抽象出来,隐藏了底层对数据源的CRUD操作,这样在应用层或控制器中,我们直接访问仓储封装的方法即可,不需和数据源直接接触。泛型仓储以面向接口和泛型方式实现,一方面,可以非常方便的更换数据源,而业务代码不需要做任何修改,实现解耦;另一方面只要创建一个基础泛型仓储,就可以实现所有实体的CRUD操作,使用非常方便。PS:仓储实现,是接口和泛型应用的经典案例,虽然ABP、Furion、MASA等AspNetCore框架,都定义了自己的泛型仓储,可以直接使用,但理解和掌握其原理,仍然非常重要。

 

一、非泛型仓储的实现

以下案例,我们创建一个员工类的仓储接口,并实现一个内存数据源仓储和一个SQL数据源仓储,内存数据源和SQL数据源可以随意切换,而控制器方法不需要做任何改动。之前章节,已经学习过DbContext、数据库迁移、DbContext服务注册等,以下案例略过这些知识点。另外,为了简化代码,案例的逻辑并不严谨,首要还是学习仓储的实现原理,不需要过度关注细节。

//1、创建实体类Employee,做测试使用========================================================================
public class Employee
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public int Age { get; set; }
}


//2、创建仓储接口=========================================================================================
public interface IEmployeeRepository
{
    Employee AddEmployee(Employee employee);//新增员工
    Employee DeleteEmployeeById(int id);//删除指定员工
    Employee UpdateEmployee(int id, Employee employee);//更新员工
    Employee GetEmployeeById(int id);//获取指定Id员工
    IEnumerable<Employee> GetAllEmployees();//获取所有员工
}


//3-1、创建内存数据源的仓储实现===============================================================================
public class MockEmployeeRepository : IEmployeeRepository
{
    //定义一个内存数据源
    private readonly List<Employee> employees;
    public MockEmployeeRepository()
    {
        this.employees = new List<Employee>()
        {
            new Employee { Id = 1, Name = "zs1", Age = 25},
            new Employee { Id = 2, Name = "ls1", Age = 32},
            new Employee { Id = 3, Name = "ww1", Age = 23},
            new Employee { Id = 4, Name = "zl1", Age = 42},
            new Employee { Id = 5, Name = "qq1", Age = 48}
        };
    }
    //添加员工
    public Employee AddEmployee(Employee employee)
    {
        employees.Add(employee);
        return employee;
    }
    //删除员工
    public Employee DeleteEmployeeById(int id)
    {
        var employee = employees.FirstOrDefault(e=>e.Id == id);
        if (employee != null)
        {
            employees.Remove(employee);
        }
        return employee;
    }
    //更新员工
    public Employee UpdateEmployee(int id,Employee employee)
    {
        var e1 = employees.FirstOrDefault(e=>e.Id == id);
        if (e1 != null)
        {
            e1.Name = employee.Name;
            e1.Age = employee.Age;
        }
        return e1;
    }
    //获取所有员工
    public IEnumerable<Employee> GetAllEmployees()
    {
        return employees;
    }
    //获取指定员工
    public Employee GetEmployeeById(int id)
    {
        return employees.FirstOrDefault(e=>e.Id == id);
    }
}


//3-2、创建SQL数据源的仓储实现===============================================================================
public class SQLEmployeeRepository : IEmployeeRepository
{
    //注入DbContext
    private readonly MyDbContext ctx;
    public SQLEmployeeRepository(MyDbContext ctx)
    {
        this.ctx = ctx;
    }
    //添加员工
    public Employee AddEmployee(Employee employee)
    {
        ctx.Employees.Add(employee);
        ctx.SaveChanges();
        return employee;
    }
    //删除员工
    public Employee DeleteEmployeeById(int id)
    {
        var e1 = ctx.Employees.FirstOrDefault(e=>e.Id == id);
        if (e1 != null)
        {
            ctx.Employees.Remove(e1);
            ctx.SaveChanges();
        }
        return e1;
    }
    //更新员工
    public Employee UpdateEmployee(int id, Employee employee)
    {
        var e1 = ctx.Employees.FirstOrDefault(e => e.Id == id);
        e1.Name = employee.Name;
        e1.Age = employee.Age;
        ctx.SaveChanges();
        return e1;
    }
    //获取所有员工
    public IEnumerable<Employee> GetAllEmployees()
    {
        return ctx.Employees;
    }
    //获取指定员工
    public Employee GetEmployeeById(int id)
    {
        return ctx.Employees.Find(id);
    }
}


//4、在容器中注册仓储,使用SQL数据源(AspNetCore WebAPI应用)===================================================
builder.Services.AddScoped<IEmployeeRepository,SQLEmployeeRepository>();
//如果要更换为内存数据源,可更换为以下代码
//builder.Services.AddScoped<IEmployeeRepository,MockEmployeeRepository>();


//5、创建一个控制器,直接使用仓储==============================================================================
//即使切换数据源,或者修改仓储实现,以下业务代码也不需要做任何修改
//但是,如果修改了仓储接口,以下业务代码可能需要修改
[Route("api/[controller]/[action]")]
[ApiController]
public class TestController : ControllerBase
{
    //注入仓储
    private readonly IEmployeeRepository employeeRepository;
    public TestController(IEmployeeRepository employeeRepository)
    {
        this.employeeRepository = employeeRepository;
    }
    //添加员工
    [HttpPost]
    public ActionResult<Employee> AddEmployee(Employee employee)
    {
        var e1 = employeeRepository.AddEmployee(employee);
        if (e1 != null)
        {
            return e1;
        }
        return BadRequest("添加失败");
    }
    //删除员工
    [HttpDelete]
    public ActionResult<Employee> DeleteEmployeeById(int id)
    {
        var e1 = employeeRepository.DeleteEmployeeById(id);
        if (e1 != null)
        {
            return e1;
        }
        return BadRequest("删除失败");
    }
    //更新员工
    [HttpDelete]
    public ActionResult<Employee> UpdateEmployee(int id, Employee employee)
    {
        var e1 = employeeRepository.UpdateEmployee(id,employee);
        if (e1 != null)
        {
            return e1;
        }
        return BadRequest("更新失败");
    }
    //获取指定员工
    [HttpGet]
    public ActionResult<Employee> GetEmployeeById(int id)
    {
        var e1 = employeeRepository.GetEmployeeById(id);
        if (e1 != null)
        {
            return e1;
        }
        return NotFound("找不到指定员工");
    }
    //获取所有员工
    [HttpGet]
    public ActionResult<IEnumerable<Employee>> GetAllEmployees()
    {
        var employees = employeeRepository.GetAllEmployees();
        if (employees.Any() == true)
        {
            return employees.ToList();
        }
        return NotFound("找不到员工");
    }
}

 

 

二、泛型仓储的实现

仓储模式,隐藏了底层数据的CRUD操作,通过仓储接口实现了系统的解耦。但是,如果应用中有很多实体,按照上例中的套路,每个实体都要建立相应的仓储接口和实现,且仓储的操作方法基本一样,这将带来大量的重复性工作。所以,我们引入泛型仓储,只需要定义一个泛型仓储和实现,就可以实现所有实体的仓储操作。下面案例实现了一个简单的泛型仓储,定义的所有方法都使用异步方法。

1、创建泛型仓储接口

//泛型参数:TEntity为仓储的实体类型,TPrimaryKey为仓储的主键类型
public interface IRepository<TEntity,TPrimaryKey> where TEntity : class
{
    //新增实体
    Task<TEntity> AddAsync(TEntity entity);

    //更新实体
    Task<TEntity> UpdateAsync(TEntity entity);

    //删除实体
    Task DeleteAsync(TEntity entity);//删除一个实体
    Task DeleteAsync(Expression<Func<TEntity,bool>> predicate);//删除符合条件的多个实体

    //查询实体
    Task<List<TEntity>> GetAllListAsync();//查询所有
    Task<List<TEntity>> GetAllListAsync(Expression<Func<TEntity, bool>> predicate);//查询指定条件的多条
    Task<TEntity> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate);//查询指定条件的一条,如果没有,返回null
    Task<int> CountAsync();//查询全部总数
    Task<int> CountAsync(Expression<Func<TEntity,bool>> predicate);//查询指定条件总数
}

 

2、实现泛型仓储接口,使用SQL数据源

public class RepositoryBase<TEntity, TPrimaryKey> : IRepository<TEntity, TPrimaryKey> where TEntity : class
{
    //准备①:注入DbContext
    private readonly MyDbContext ctx;
    public RepositoryBase(MyDbContext ctx)
    {
        this.ctx = ctx;
    }
    //准备②:获取泛型实体的DbSet,注意使用虚方法,传入具体的实体类型时,重写这个DbSet
    public virtual DbSet<TEntity> dbSet => ctx.Set<TEntity>();
    //准备③:检查实体是否处理跟踪状态,如果是,则返回实体;如果不是,则添加跟踪状态
    protected virtual void AttachIfNot(TEntity entity)
    {
        var entry = ctx.ChangeTracker.Entries().FirstOrDefault(e=>e.Entity == entity);
        if (entry != null)
        {
            return;
        }
        dbSet.Attach(entity);
    }


    //实现新增实体
    public async Task<TEntity> AddAsync(TEntity entity)
    {
        var entityResult = await dbSet.AddAsync(entity);
        await ctx.SaveChangesAsync();
        return entityResult.Entity;
    }

    //实现更新实体。ctx.Entry(entity).State获取实体跟踪状态
    public async Task<TEntity> UpdateAsync(TEntity entity)
    {
        AttachIfNot(entity);
        ctx.Entry(entity).State = EntityState.Modified;
        await ctx.SaveChangesAsync();
        return entity;
    }

    //实体删除实体
    //删除一个实体
    public async Task DeleteAsync(TEntity entity)
    {
        AttachIfNot(entity);
        var entityResult = dbSet.Remove(entity);
        await ctx.SaveChangesAsync();
    }
    //删除指定条件的多个实体。dbSet.AsQueryable()方法获取实体的Queryable集合
    public async Task DeleteAsync(Expression<Func<TEntity, bool>> predicate)
    {
        foreach (var entity in dbSet.AsQueryable().Where(predicate).ToList())
        {
            await DeleteAsync(entity);
        };
    }

    //实现查询
    //查询所有
    public async Task<List<TEntity>> GetAllListAsync()
    {
        return await dbSet.AsQueryable().ToListAsync();
    }
    //查询指定条件的多条
    public async Task<List<TEntity>> GetAllListAsync(Expression<Func<TEntity, bool>> predicate)
    {
        return await dbSet.AsQueryable().Where(predicate).ToListAsync();
    }
    //查询指定条件的首条
    public async Task<TEntity> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate)
    {
        return await dbSet.AsQueryable().FirstOrDefaultAsync(predicate);
    }
    //实现查询总数
    public async Task<int> CountAsync()
    {
        return await dbSet.AsQueryable().CountAsync();
    }
    //查询指定条件的总数
    public async Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate)
    {
        return await dbSet.AsQueryable().Where(predicate).CountAsync();
    }
}

 

3、在容器中注册泛型仓储服务

builder.Services.AddTransient(typeof(IRepository<,>),typeof(RepositoryBase<,>));

 

4、在控制器中使用泛型仓储

public class TestController : ControllerBase
{
    //注入泛型仓储,指定泛型仓储的实体类型和主键类型
    private readonly IRepository<Employee, int> employeeRepository;
    public TestController(IRepository<Employee,int> employeeRepository)
    {
        this.employeeRepository = employeeRepository;
    }

    //查询指定条件的员工
    [HttpGet]
    public async Task<ActionResult<List<Employee>>> GetEmployeesMaxAge(int maxAge)
    {
        return await employeeRepository.GetAllListAsync(e=>e.Age <= maxAge);
    }
}

 

 

 

特别说明:
1、本系列内容主要基于杨中科老师的书籍《ASP.NET Core技术内幕与项目实战》及配套的B站视频视频教程,同时会增加极少部分的小知识点
2、本系列教程主要目的是提炼知识点,追求快准狠,以求快速复习,如果说书籍学习的效率是视频的2倍,那么“简读系列”应该做到再快3-5倍