UseLazyLoadingProxies codefirst null navigation 属性 仅在数据库创建后

UseLazyLoadingProxies codefirst null navigation property only after database creation

有问题的代码(出现在下面的 Core.run() 中)

user.UserWordRequests.Add(
    new UserWordRequest
    {
        Date = DateTime.Now,
        Word = word
    }
);

问题描述

  1. 数据库不存在
  2. 执行第一次控制台程序,上面user.UserWordRequests中的代码为空。 预期值为空列表
  3. 数据库现在存在,因为步骤 2 已创建它。
  4. 再次执行控制台程序,现在user.UserWordRequests是空列表(或填充了数据,取决于数据库中的数据)。

更多数据,这里是第 2 步中的用户对象:

在第 4 步中:

有用的代码

数据库模型

public class User
{
    public User()
    {
    }

    [Key]
    public int Id { get; set; }
    [Required]
    public string Name { get; set; }
    public virtual ICollection<UserWordRequest> UserWordRequests { get; set; }
}

public class Word
{
    public Word()
    {
    }

    [Key]
    public int Id { get; set; }
    [Required]
    public string Value { get; set; }

    public virtual ICollection<UserWordRequest> UserWordRequests { get; set; }
}

public class UserWordRequest
{
    public UserWordRequest()
    {
    }
    
    [Key]
    public int Id { get; set; }

    [Required]
    public int UserId { get; set; }
    public virtual User User { get; set; }

    [Required]
    public int WordId { get; set; }
    public virtual Word Word { get; set; }

    [Required]
    public DateTime Date { get; set; }  
}

主程序

class Program
{
    private static async Task Main(string[] args)
    {
        var host = 
            Host.CreateDefaultBuilder(args)
            .ConfigureLogging(loggingBuilder =>
            {
                loggingBuilder.ClearProviders(); // Disable console messages
            })
            .ConfigureServices((hostContext, services) =>
            {
                // Auto mapping config !!!
                var config = new ConfigurationBuilder()
                                .SetBasePath(Path.Combine(AppContext.BaseDirectory))
                                .AddJsonFile("appsettings.json")
                                .Build();
                var settings = config/*.GetSection("GeneralSection")*/.Get<Config>();

                // AddHostedService
                services
                .AddHostedService<ConsoleHostedService>()
                .AddDbContext<DataBaseContext>
                (
                    options =>
                    options
                        .UseLazyLoadingProxies(true)
                        .UseSqlite(
                           settings.General.DataBaseConnection                               
                        )
                        ,
                    ServiceLifetime.Singleton,
                    ServiceLifetime.Singleton
                    
                )
                //.AddEntityFrameworkProxies()                    
                .AddSingleton((Config) => { return settings; })
                .AddSingleton<Logger>()
                .AddSingleton<Util.File>()
                .AddSingleton<Core>()
                .AddSingleton<DataBaseContext>()                  
                ;
            }).Build();
        host.Services.GetService<DataBaseContext>().Database.Migrate();

        // Then run application
        host.Run();
    }
}

控制台托管服务class

public class ConsoleHostedService : IHostedService
{
    private readonly IHostApplicationLifetime _appLifetime;
    private readonly Core _Core;
    private readonly Logger _Logger;
    private readonly DataBaseContext _db;

    public ConsoleHostedService(
        IHostApplicationLifetime appLifetime,
        Core Core,
        Logger Logger,
        DataBaseContext DataBaseContext)
    {
        _appLifetime = appLifetime;
        _Core = Core;
        _Logger = Logger;
        _db = DataBaseContext;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _appLifetime.ApplicationStarted.Register(() =>
        {
            Task.Run(async () =>
            {
                try
                {
                    // Start program
                    //_db.Database.Migrate();
                    _Core.run();
                }
                catch (Exception e)
                {
                    _Logger.Log(Logger.LogType.Error, "", e);
                }
                finally
                {
                    // Stop the application once the work is done
                    _appLifetime.StopApplication();
                }
            });
        });

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

数据库上下文

public class DataBaseContext : DbContext
    {
        public DataBaseContext(DbContextOptions<DataBaseContext> options) : base(options)
        {

        }

        public virtual DbSet<Word> Words { get; set; }
        public virtual DbSet<User> Users { get; set; }
        public virtual DbSet<Log> Logs { get; set; }
        public virtual DbSet<UserWordRequest> UserWordRequests { get; set; }
    }

    /// <summary>
    ///     ❗❗❗ We need this class only for doing add-migration, but this is not used by anyone
    /// </summary>
    public class DataBaseContextFactory : IDesignTimeDbContextFactory<DataBaseContext>
    {
        public DataBaseContext CreateDbContext(string[] args)
        {
            var optionsBuilder = new DbContextOptionsBuilder<DataBaseContext>();
            optionsBuilder.UseSqlite("Data Source=this_name_is_not_used.db");

            return new DataBaseContext(optionsBuilder.Options);
        }
    }

核心class❗❗❗问题代码

public Core(DataBaseContext DataBaseContext, Logger Logger, File File, Config Config)
{
    _db = DataBaseContext;
    _Logger = Logger;
    _File = File;
    _Config = Config;

    _Random = new Random();

    _Logger.Log(Logger.LogType.Info, "Core");
}

public void run()
{
    try
    {
        ... some code
        
        var word = _db.Words.FirsOrDefault();
        var user = _db.Users.FirsOrDefault();
        
        // ‼‼‼ PROBLEMATIC CODE 
        user.UserWordRequests.Add(
            new UserWordRequest
            {
                Date = DateTime.Now,
                Word = word
            }
        );
        
        _db.SaveChanges();
        ... some code
    }
    catch(Exception e)
    {
        _Logger.Log(Logger.LogType.Error, "", e);
    }
    
}

使用的版本

net6.0 和:

<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="6.0.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="6.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.0" />

总结,学习了

使用 or 代替 new 以避免错误。

错误解释

第一次执行控制台应用程序时,创建数据库时,代码使用 new 而不是 CreateCreateProxy:

创建用户
var user = new User();
user.prop = "value";
...
db.Users.Add(user)

稍后,在第一次控制台应用程序执行中,应用程序执行此代码并且 UserWordRequests 为 null 而不是空列表。

var user = db.Users.Where(...).FirstOrDefault();

user.UserWordRequests.Add(
    new UserWordRequest
    {
        Date = DateTime.Now,
        Word = ...
    }
);

解决方案使用 CreateCreateProxy 来创建用户。

var proxy = _db.Users.CreateProxy();
proxy.Name = user.Name;
_db.Users.Add(proxy);

那么,UserWordRequests是空列表,不是null。

但是...为什么第二次没有发生这种情况?因为用户是在数据库中创建的,代码没有执行插入,所以它通过数据库查询获得它们。