65.9K
CodeProject 正在变化。 阅读更多。
Home

不同的存储库模式实现

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2022年12月19日

MIT

5分钟阅读

viewsIcon

18418

重新思考存储库模式

引言

经典的存储库模式是实现应用程序中数据库访问的一种简单方法。它满足了小型应用程序的许多常规设计目标。另一方面,CQS和CQRS为更大、更复杂的应用程序提供了更复杂但结构良好的设计模式。

在本文中,我将开发基本的存储库模式,应用CQS中一些基本的好实践,并实现一个完全通用的提供程序。

这不是在DotNetCore中带有几个附加功能的、千篇一律的IRepository实现。

  1. 没有为每个实体类单独实现。你不会看到这个
    public class WeatherForecastRepository : GenericRepository<WeatherForecast>, 
                                             IWeatherForcastRepository
    {
        public WeatherForecastRepository(DbContextClass dbContext) : base(dbContext) {}
    }
    
    public interface IProductRepository : IGenericRepository<WeatherForecast> { } 
  2. 没有单独的UnitOfWork类:它已内置。
  3. 所有标准数据I/O都使用单个数据代理。
  4. 设计中使用了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中的基本数据存储设置。它

  1. 设置DI容器。
  2. 从测试提供程序加载数据。
  3. 测试记录数是否正确。
  4. 测试任意一条记录是否正确。
[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);
    }
}

剖析一下

  1. 返回null时会发生什么,它意味着什么?
  2. 那个add/update/delete真的成功了吗?我怎么知道?
  3. 如何处理取消令牌?现在大多数async方法都接受一个取消令牌。
  4. 当你的DBSet包含一百万条记录时会发生什么(也许DBA昨晚做错了什么)?
  5. 我的应用程序中有一个数据存储实体对应一个这样的实现。

实现

请求和结果

请求对象封装了我们要请求的内容,结果对象封装了我们期望返回的数据和状态信息。它们是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. 它们永远不应请求所有内容。在极端情况下,一个表可能有1,000,000+行。每个请求都应受到限制。请求定义了StartIndexPageSize来限制数据并提供分页。如果你将页面大小设置为1,000,000,你的数据管道和前端能优雅地处理它吗?
  2. 它们需要处理排序和过滤。请求将这些定义为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();
}

实现执行实际工作。

  1. 注入DBContext工厂。
  2. 通过DbContext工厂实现单元工作DbContext。
  3. 使用上下文的Add方法将记录添加到EF。
  4. 调用SaveChangesAsync,传入取消令牌,并期望报告一次更改。
  5. 如果出现问题,提供状态信息。
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");
    }
}

UpdateDelete处理程序相同,但使用不同的dbContext方法:UpdateRemove

项请求处理程序

接口。

public interface IItemRequestHandler
{
    public ValueTask<ItemQueryResult<TRecord>> ExecuteAsync<TRecord>
                                               (ItemQueryRequest request)
        where TRecord : class, new();
}

以及服务器实现。请注意

  1. 注入DBContext工厂。
  2. 通过DbContext工厂实现单元工作DbContext。
  3. 关闭跟踪。此事务不涉及变异。
  4. 检查是否可以使用Id获取项 - 记录实现了IGuidIdentity
  5. 如果不行,尝试FindAsync,它使用内置的Key方法来获取记录。
  6. 如果出现问题,提供状态信息。
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日:初始版本
© . All rights reserved.