不同的存储库模式实现





5.00/5 (5投票s)
重新思考存储库模式
引言
经典的存储库模式是实现应用程序中数据库访问的一种简单方法。它满足了小型应用程序的许多常规设计目标。另一方面,CQS和CQRS为更大、更复杂的应用程序提供了更复杂但结构良好的设计模式。
在本文中,我将开发基本的存储库模式,应用CQS中一些基本的好实践,并实现一个完全通用的提供程序。
这不是在DotNetCore中带有几个附加功能的、千篇一律的IRepository实现。
- 没有为每个实体类单独实现。你不会看到这个public class WeatherForecastRepository : GenericRepository<WeatherForecast>, IWeatherForcastRepository { public WeatherForecastRepository(DbContextClass dbContext) : base(dbContext) {} } public interface IProductRepository : IGenericRepository<WeatherForecast> { }
- 没有单独的UnitOfWork类:它已内置。
- 所有标准数据I/O都使用单个数据代理。
- 设计中使用了CQS请求、结果和处理程序模式。
命名、术语和实践
- DI:依赖注入
- CQS:命令/查询分离
代码是:
- Net7.0
- C# 10
- 启用了可空引用类型
Repo
Repo和本文最新版本在此处:Blazr.IRepository。
数据存储
该解决方案需要一个真实的数据存储进行测试:它实现了一个Entity Framework内存数据库。
我是一名Blazor开发者,所以我测试数据类是WeatherForecast。数据提供程序的代码在附录中。
这是DBContext工厂使用的DbContext。
public sealed class InMemoryWeatherDbContext : DbContext
{
    public DbSet<WeatherForecast> WeatherForecast { get; set; } = default!;
    public InMemoryWeatherDbContext
       (DbContextOptions<InMemoryWeatherDbContext> options) : base(options) { }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
        => modelBuilder.Entity<WeatherForecast>().ToTable("WeatherForecast");
}
测试工厂和上下文
以下XUnit测试演示了DI中的基本数据存储设置。它
- 设置DI容器。
- 从测试提供程序加载数据。
- 测试记录数是否正确。
- 测试任意一条记录是否正确。
[Fact]
public async Task DBContextTest()
{
    // Gets the control test data
    var testProvider = WeatherTestDataProvider.Instance();
    // Build our services container
    var services = new ServiceCollection();
    // Define the DbSet and Server Type for the DbContext Factory
    services.AddDbContextFactory<InMemoryWeatherDbContext>(options
        => options.UseInMemoryDatabase($"WeatherDatabase-{Guid.NewGuid().ToString()}"));
    var rootProvider = services.BuildServiceProvider();
    //define a scoped container
    var providerScope = rootProvider.CreateScope();
    var provider = providerScope.ServiceProvider;
    // get the DbContext factory and add the test data
    var factory = provider.GetService<IDbContextFactory<InMemoryWeatherDbContext>>();
    if (factory is not null)
        WeatherTestDataProvider.Instance().LoadDbContext
                                <InMemoryWeatherDbContext>(factory);
    // Check the data has been loaded
    var dbContext = factory!.CreateDbContext();
    Assert.NotNull(dbContext);
    var count = dbContext.Set<WeatherForecast>().Count();
    Assert.Equal(testProvider.WeatherForecasts.Count(), count);
    // Test an arbitrary record
    var testRecord = testProvider.GetRandomRecord()!;
    var record = await dbContext.Set<WeatherForecast>().SingleOrDefaultAsync
                 (item => item.Uid.Equals(testRecord.Uid));
    Assert.Equal(testRecord, record);
    // Dispose of the resources correctly
    providerScope.Dispose();
    rootProvider.Dispose();
}
经典存储库模式实现
这是我在网上找到的一个简洁的实现。
    public abstract class Repository<T> : IRepository<T> where T : class
    {
        protected readonly DbContextClass _dbContext;
        protected GenericRepository(DbContextClass context)
            => _dbContext = context;
        public async Task<T> GetById(int id)
            => await _dbContext.Set<T>().FindAsync(id);
        public async Task<IEnumerable<T>> GetAll()
            => await _dbContext.Set<T>().ToListAsync();
        public async Task Add(T entity)
             => await _dbContext.Set<T>().AddAsync(entity);
        public void Delete(T entity)
            => _dbContext.Set<T>().Remove(entity);
        public void Update(T entity)
           =>  _dbContext.Set<T>().Update(entity);
    }
}
剖析一下
- 返回null时会发生什么,它意味着什么?
- 那个add/update/delete真的成功了吗?我怎么知道?
- 如何处理取消令牌?现在大多数async方法都接受一个取消令牌。
- 当你的DBSet包含一百万条记录时会发生什么(也许DBA昨晚做错了什么)?
- 我的应用程序中有一个数据存储实体对应一个这样的实现。
实现
请求和结果
请求对象封装了我们要请求的内容,结果对象封装了我们期望返回的数据和状态信息。它们是records:定义一次然后消费。
Commands
命令是对数据存储进行更改的请求:Create/Update/Delete操作。我们可以这样定义一个
public record CommandRequest<TRecord>
{
    public required TRecord Item { get; init; }
    public CancellationToken Cancellation { get; set; } = new ();
}
命令只返回状态信息:不返回数据。我们可以这样定义一个结果
public record CommandResult
{
    public bool Successful { get; init; }
    public string Message { get; init; } = string.Empty;
    private CommandResult() { }
    public static CommandResult Success(string? message = null)
        => new CommandResult { Successful = true, Message= message ?? string.Empty };
    public static CommandResult Failure(string message)
        => new CommandResult { Message = message};
}
此时,值得注意的是返回规则的一个小例外:插入记录的Id。如果你不使用Guid为记录提供唯一标识符,那么数据库生成的Id就是状态信息。
项请求
查询是从数据存储获取数据的请求:不进行变异。我们可以这样定义一个项查询
public sealed record ItemQueryRequest
{
    public required Guid Uid { get; init; }
    public CancellationToken Cancellation { get; set; } = new();
}
以及返回的结果:请求的数据和状态。
public sealed record ItemQueryResult<TRecord>
{
    public TRecord? Item { get; init;} 
    public bool Successful { get; init; }
    public string Message { get; init; } = string.Empty;
    private ItemQueryResult() { }
    public static ItemQueryResult<TRecord> 
           Success(TRecord Item, string? message = null)
        => new ItemQueryResult<TRecord> 
           { Successful=true, Item= Item, Message= message ?? string.Empty };
    public static ItemQueryResult<TRecord> Failure(string message)
        => new ItemQueryResult<TRecord> { Message = message};
}
列表查询
列表查询带来了一些额外的挑战
- 它们永远不应请求所有内容。在极端情况下,一个表可能有1,000,000+行。每个请求都应受到限制。请求定义了StartIndex和PageSize来限制数据并提供分页。如果你将页面大小设置为1,000,000,你的数据管道和前端能优雅地处理它吗?
- 它们需要处理排序和过滤。请求将这些定义为Linq表达式。
public sealed record ListQueryRequest<TRecord>
{
    public int StartIndex { get; init; } = 0;
    public int PageSize { get; init; } = 1000;
    public CancellationToken Cancellation { get; set; } = new ();
    public bool SortDescending { get; } = false;
    public Expression<Func<TRecord, bool>>? FilterExpression { get; init; }
    public Expression<Func<TRecord, object>>? SortExpression { get; init; }
}
结果返回项、总项数(用于分页)和状态信息。Items始终作为IEnumerable返回。
public sealed record ListQueryResult<TRecord>
{
    public IEnumerable<TRecord> Items { get; init;} = Enumerable.Empty<TRecord>();  
    public bool Successful { get; init; }
    public string Message { get; init; } = string.Empty;
    public long TotalCount { get; init; }
    private ListQueryResult() { }
    public static ListQueryResult<TRecord> Success
      (IEnumerable<TRecord> Items, long totalCount, string? message = null)
        => new ListQueryResult<TRecord> {Successful=true,  Items= Items, 
           TotalCount = totalCount, Message= message ?? string.Empty };
    public static ListQueryResult<TRecord> Failure(string message)
        => new ListQueryResult<TRecord> { Message = message};
}
处理程序
处理程序是小型、单一目的的类,用于处理请求并返回结果。它们将底层细节从更高级别的数据代理中抽象出来。
命令处理器
该接口提供了抽象。
public interface ICreateRequestHandler
{
    public ValueTask<CommandResult> ExecuteAsync<TRecord>
                                    (CommandRequest<TRecord> request)
        where TRecord : class, new();
}
实现执行实际工作。
- 注入DBContext工厂。
- 通过DbContext工厂实现单元工作DbContext。
- 使用上下文的Add方法将记录添加到EF。
- 调用SaveChangesAsync,传入取消令牌,并期望报告一次更改。
- 如果出现问题,提供状态信息。
public sealed class CreateRequestHandler<TDbContext>
    : ICreateRequestHandler
    where TDbContext : DbContext
{
    private readonly IDbContextFactory<TDbContext> _factory;
    public CreateRequestHandler(IDbContextFactory<TDbContext> factory)
        => _factory = factory;
    public async ValueTask<CommandResult> ExecuteAsync<TRecord>
                                          (CommandRequest<TRecord> request)
        where TRecord : class, new()
    {
        if (request == null)
            throw new DataPipelineException
            ($"No CommandRequest defined in {this.GetType().FullName}");
        using var dbContext = _factory.CreateDbContext();
        dbContext.Add<TRecord>(request.Item);
        return await dbContext.SaveChangesAsync(request.Cancellation) == 1
            ? CommandResult.Success("Record Updated")
            : CommandResult.Failure("Error updating Record");
    }
}
Update和Delete处理程序相同,但使用不同的dbContext方法:Update和Remove。
项请求处理程序
接口。
public interface IItemRequestHandler
{
    public ValueTask<ItemQueryResult<TRecord>> ExecuteAsync<TRecord>
                                               (ItemQueryRequest request)
        where TRecord : class, new();
}
以及服务器实现。请注意
- 注入DBContext工厂。
- 通过DbContext工厂实现单元工作DbContext。
- 关闭跟踪。此事务不涉及变异。
- 检查是否可以使用Id获取项 - 记录实现了IGuidIdentity。
- 如果不行,尝试FindAsync,它使用内置的Key方法来获取记录。
- 如果出现问题,提供状态信息。
public sealed class ItemRequestHandler<TDbContext>
    : IItemRequestHandler
    where TDbContext : DbContext
{
    private readonly IDbContextFactory<TDbContext> _factory;
    public ItemRequestHandler(IDbContextFactory<TDbContext> factory)
        => _factory = factory;
    public async ValueTask<ItemQueryResult<TRecord>> 
                 ExecuteAsync<TRecord>(ItemQueryRequest request)
        where TRecord : class, new()
    {
        if (request == null)
            throw new DataPipelineException
            ($"No ListQueryRequest defined in {this.GetType().FullName}");
        using var dbContext = _factory.CreateDbContext();
        dbContext.ChangeTracker.QueryTrackingBehavior = 
                                QueryTrackingBehavior.NoTracking;
        TRecord? record = null;
        // first check if the record implements IGuidIdentity. 
        // If so, we can do a cast and then do the query via the Uid property directly.
        if ((new TRecord()) is IGuidIdentity)
            record = await dbContext.Set<TRecord>().SingleOrDefaultAsync
            (item => ((IGuidIdentity)item).Uid == request.Uid, request.Cancellation);
        // Try and use the EF FindAsync implementation
        if (record is null)
            record = await dbContext.FindAsync<TRecord>(request.Uid);
        if (record is null)
            return ItemQueryResult<TRecord>.Failure
                       ("No record retrieved");
        return ItemQueryResult<TRecord>.Success(record);
    }
}
列表请求处理程序
接口。
public interface IListRequestHandler
{
    public ValueTask<ListQueryResult<TRecord>> ExecuteAsync<TRecord>
                              (ListQueryRequest<TRecord> request)
        where TRecord : class, new();
}
以及实现。
请注意有两个内部方法
- _getItemsAsync获取项。这会构建一个- IQueryable对象并返回一个已物化的- IEnumerable。在工厂处理- DbContext之前,您必须执行查询。
- _getCountAsync根据过滤器获取所有记录的计数。
private async ValueTask<IEnumerable<TRecord>> _getItemsAsync<TRecord>
                                   (ListQueryRequest<TRecord> request)
    where TRecord : class, new()
{
    if (request == null)
        throw new DataPipelineException
              ($"No ListQueryRequest defined in {this.GetType().FullName}");
    using var dbContext = _factory.CreateDbContext();
    dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
    IQueryable<TRecord> query = dbContext.Set<TRecord>();
    if (request.FilterExpression is not null)
        query = query
            .Where(request.FilterExpression)
            .AsQueryable();
    if (request.SortExpression is not null)
        query = request.SortDescending
            ? query.OrderByDescending(request.SortExpression)
            : query.OrderBy(request.SortExpression);
    if (request.PageSize > 0)
        query = query
            .Skip(request.StartIndex)
            .Take(request.PageSize);
    return query is IAsyncEnumerable<TRecord>
        ? await query.ToListAsync()
        : query.ToList();
}
存储库类替换
首先是接口。
非常重要的一点是每个方法上的泛型TRecord定义,而不是接口上的。这消除了对特定实体实现的需要。
public interface IDataBroker
{
    public ValueTask<ListQueryResult<TRecord>> GetItemsAsync<TRecord>
           (ListQueryRequest<TRecord> request) where TRecord : class, new();
    public ValueTask<ItemQueryResult<TRecord>> GetItemAsync<TRecord>
           (ItemQueryRequest request) where TRecord : class, new();
    public ValueTask<CommandResult> UpdateItemAsync<TRecord>
           (CommandRequest<TRecord> request) where TRecord : class, new();
    public ValueTask<CommandResult> CreateItemAsync<TRecord>
           (CommandRequest<TRecord> request) where TRecord : class, new();
    public ValueTask<CommandResult> DeleteItemAsync<TRecord>
           (CommandRequest<TRecord> request) where TRecord : class, new();
}
以及实现。每个处理程序都在DI中注册,并注入到代理中。
public sealed class RepositoryDataBroker : IDataBroker
{
    private readonly IListRequestHandler _listRequestHandler;
    private readonly IItemRequestHandler _itemRequestHandler;
    private readonly IUpdateRequestHandler _updateRequestHandler;
    private readonly ICreateRequestHandler _createRequestHandler;
    private readonly IDeleteRequestHandler _deleteRequestHandler;
    public RepositoryDataBroker(
        IListRequestHandler listRequestHandler,
        IItemRequestHandler itemRequestHandler,
        ICreateRequestHandler createRequestHandler,
        IUpdateRequestHandler updateRequestHandler,
        IDeleteRequestHandler deleteRequestHandler)
    {
        _listRequestHandler = listRequestHandler;
        _itemRequestHandler = itemRequestHandler;
        _createRequestHandler = createRequestHandler;
        _updateRequestHandler = updateRequestHandler;
        _deleteRequestHandler = deleteRequestHandler;
    }
    public ValueTask<ItemQueryResult<TRecord>> GetItemAsync<TRecord>
           (ItemQueryRequest request) where TRecord : class, new()
        => _itemRequestHandler.ExecuteAsync<TRecord>(request);
    public ValueTask<ListQueryResult<TRecord>> GetItemsAsync<TRecord>
           (ListQueryRequest<TRecord> request) where TRecord : class, new()
        => _listRequestHandler.ExecuteAsync<TRecord>(request);
    public ValueTask<CommandResult> CreateItemAsync<TRecord>
           (CommandRequest<TRecord> request) where TRecord : class, new()
        => _createRequestHandler.ExecuteAsync<TRecord>(request);
    public ValueTask<CommandResult> UpdateItemAsync<TRecord>
           (CommandRequest<TRecord> request) where TRecord : class, new()
        => _updateRequestHandler.ExecuteAsync<TRecord>(request);
    public ValueTask<CommandResult> DeleteItemAsync<TRecord>
           (CommandRequest<TRecord> request) where TRecord : class, new()
        => _deleteRequestHandler.ExecuteAsync<TRecord>(request);
}
测试数据代理
我们现在可以为数据代理定义一组测试。这里包含了两个。其余的在Repo中。
前两个方法用于创建根DI容器并填充数据库。
private ServiceProvider BuildRootContainer()
{
    var services = new ServiceCollection();
    // Define the DbSet and Server Type for the DbContext Factory
    services.AddDbContextFactory<InMemoryWeatherDbContext>(options
      => options.UseInMemoryDatabase($"WeatherDatabase-{Guid.NewGuid().ToString()}"));
    // Define the Broker and Handlers
    services.AddScoped<IDataBroker, RepositoryDataBroker>();
    services.AddScoped<IListRequestHandler, 
             ListRequestHandler<InMemoryWeatherDbContext>>();
    services.AddScoped<IItemRequestHandler, 
             ItemRequestHandler<InMemoryWeatherDbContext>>();
    services.AddScoped<IUpdateRequestHandler, 
             UpdateRequestHandler<InMemoryWeatherDbContext>>();
    services.AddScoped<ICreateRequestHandler, 
             CreateRequestHandler<InMemoryWeatherDbContext>>();
    services.AddScoped<IDeleteRequestHandler, 
             DeleteRequestHandler<InMemoryWeatherDbContext>>();
    // Create the container
    return services.BuildServiceProvider();
}
private IDbContextFactory<InMemoryWeatherDbContext> 
        GetPopulatedFactory(IServiceProvider provider)
{
    // get the DbContext factory and add the test data
    var factory = provider.GetService<IDbContextFactory<InMemoryWeatherDbContext>>();
    if (factory is not null)
        WeatherTestDataProvider.Instance().LoadDbContext
                                <InMemoryWeatherDbContext>(factory);
    return factory!;
}
GetItems测试
[Fact]
public async Task GetItemsTest()
{
    // Get our test provider to use as our control
    var testProvider = WeatherTestDataProvider.Instance();
    // Build the root DI Container
    var rootProvider = this.BuildRootContainer();
    //define a scoped container
    var providerScope = rootProvider.CreateScope();
    var provider = providerScope.ServiceProvider;
    // get the DbContext factory and add the test data
    var factory = this.GetPopulatedFactory(provider);
    // Check we can retrieve the first 1000 records
    var dbContext = factory!.CreateDbContext();
    Assert.NotNull(dbContext);
    var databroker = provider.GetRequiredService<IDataBroker>();
    var request = new ListQueryRequest<WeatherForecast>();
    var result = await databroker.GetItemsAsync<WeatherForecast>(request);
    Assert.NotNull(result);
    Assert.Equal(testProvider.WeatherForecasts.Count(), result.TotalCount);
    providerScope.Dispose();
    rootProvider.Dispose();
}
AddItem测试
[Fact]
public async Task AddItemTest()
{
    // Get our test provider to use as our control
    var testProvider = WeatherTestDataProvider.Instance();
    // Build the root DI Container
    var rootProvider = this.BuildRootContainer();
    //define a scoped container
    var providerScope = rootProvider.CreateScope();
    var provider = providerScope.ServiceProvider;
    // get the DbContext factory and add the test data
    var factory = this.GetPopulatedFactory(provider);
    // Check we can retrieve the first 1000 records
    var dbContext = factory!.CreateDbContext();
    Assert.NotNull(dbContext);
    var databroker = provider.GetRequiredService<IDataBroker>();
    // Create a Test record
    var newRecord = new WeatherForecast { Uid = Guid.NewGuid(), 
                    Date = DateOnly.FromDateTime(DateTime.Now), 
                    TemperatureC = 50, Summary = "Add Testing" };
    // Add the Record
    {
        var request = new CommandRequest<WeatherForecast>() { Item = newRecord };
        var result = await databroker.CreateItemAsync<WeatherForecast>(request);
        Assert.NotNull(result);
        Assert.True(result.Successful);
    }
    // Get the new record
    {
        var request = new ItemQueryRequest() { Uid = newRecord.Uid };
        var result = await databroker.GetItemAsync<WeatherForecast>(request);
        Assert.Equal(newRecord, result.Item);
    }
    // Check the record count has incremented
    {
        var request = new ListQueryRequest<WeatherForecast>();
        var result = await databroker.GetItemsAsync<WeatherForecast>(request);
        Assert.NotNull(result);
        Assert.Equal(testProvider.WeatherForecasts.Count() + 1, result.TotalCount);
    }
    providerScope.Dispose();
    rootProvider.Dispose();
}
总结
我在这里呈现的是一个混合的存储库模式。它保持了存储库模式的简洁性,并增加了一些CQS模式的最佳特性。
将底层的EF和Linq代码抽象到单独的处理程序中,可以使类保持小巧、简洁和单一目的。
单一的数据代理简化了核心和表示域的数据管道配置。
对于那些认为在EF之上实现任何数据库管道是一种反模式的人,我的回答是:我将EF用作另一个对象关系映射器[ORB]。你可以将此管道插入Dapper、LinqToDb等。我从不在我的数据/基础设施域中构建核心业务逻辑代码(数据关系):[个人观点]这是疯狂的想法。
附录
数据存储
测试系统实现了一个Entity Framework内存数据库。
我是一名Blazor开发者,因此我演示数据类自然是WeatherForecast。这是我的数据类。请注意,它是一个记录类型,用于不变性,并且我为测试目的设置了一些任意的默认值。
public sealed record WeatherForecast : IGuidIdentity
{
    [Key] public Guid Uid { get; init; } = Guid.Empty;
    public DateOnly Date { get; init; } = DateOnly.FromDateTime(DateTime.Now);
    public int TemperatureC { get; init; } = 60;
    public string? Summary { get; init; } = <span class="pl-pds">"Testing";
}
首先是一个生成数据集的类。这是一个单例模式类(不是DI单例)。诸如GetRandomRecord之类的方法用于测试。
public sealed class WeatherTestDataProvider
{
    private int RecordsToGenerate;
    public IEnumerable<WeatherForecast> WeatherForecasts { get; private set; } = 
                                        Enumerable.Empty<WeatherForecast>();
    private WeatherTestDataProvider()
        => this.Load();
    public void LoadDbContext<TDbContext>(IDbContextFactory<TDbContext> factory) 
                where TDbContext : DbContext
    {
        using var dbContext = factory.CreateDbContext();
        var weatherForcasts = dbContext.Set<WeatherForecast>();
        // Check if we already have a full data set
        // If not clear down any existing data and start again
        if (weatherForcasts.Count() == 0)
        {
            dbContext.AddRange(this.WeatherForecasts);
            dbContext.SaveChanges();
        }
    }
    public void Load(int records = 100)
    {
        RecordsToGenerate = records;
        if (WeatherForecasts.Count() == 0)
            this.LoadForecasts();
    }
    private void LoadForecasts()
    {
        var forecasts = new List<WeatherForecast>();
        for (var index = 0; index < RecordsToGenerate; index++)
        {
            var rec = new WeatherForecast
            {
                Uid = Guid.NewGuid(),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)],
                Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                TemperatureC = Random.Shared.Next(-20, 55),
            };
            forecasts.Add(rec);
        }
        this.WeatherForecasts = forecasts;
    }
    public WeatherForecast GetForecast()
    {
        return new WeatherForecast
        {
            Uid = Guid.NewGuid(),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)],
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(-1)),
            TemperatureC = Random.Shared.Next(-20, 55),
        };
    }
    public WeatherForecast? GetRandomRecord()
    {
        var record = new WeatherForecast();
        if (this.WeatherForecasts.Count() > 0)
        {
            var ran = new Random().Next(0, WeatherForecasts.Count());
            return this.WeatherForecasts.Skip(ran).FirstOrDefault();
        }
        return null;
    }
    private static WeatherTestDataProvider? _weatherTestData;
    public static WeatherTestDataProvider Instance()
    {
        if (_weatherTestData is null)
            _weatherTestData = new WeatherTestDataProvider();
        return _weatherTestData;
    }
    public static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", 
         "Hot", "Sweltering", "Scorching"
    };
}
DbContext
public sealed class InMemoryWeatherDbContext
    : DbContext
{
    public DbSet<WeatherForecast> WeatherForecast { get; set; } = default!;
    public InMemoryWeatherDbContext
    (DbContextOptions<InMemoryWeatherDbContext> options) : base(options) { }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
        => modelBuilder.Entity<WeatherForecast>().ToTable("WeatherForecast");
}
历史
- 2022年12月19日:初始版本

