Search code examples
sqlitelazy-loadingcode-firstef-core-6.0

UseLazyLoadingProxies codefirst null navigation property only after database creation


Problematic code (Present in Core.run() below)

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

Decription of problem

  1. Database doesn't exists
  2. Exec first time console program, code above in user.UserWordRequests is null. Expected value is empty list.
  3. Database now exists because step 2 has created it.
  4. Exec console program again, now user.UserWordRequests is empty list (or filled with data, depends of data in db).

More data, here is user object in step 2:

enter image description here

And here in step 4:

enter image description here

Usefull code

Database models

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; }  
}

Main program

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();
    }
}

Console hosted service 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;
    }
}

DatabaseContext

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);
        }
    }

Core class ❗❗❗ PROBLEMATIC CODE

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);
    }
    
}

Versions used

net6.0 and:

<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" />

Solution

  • Summary, leason learned

    Use create or createProxy instead new for avoid errors.

    Error explained

    First time console app is executed, when database is created, code created users with new instead Create or CreateProxy:

    var user = new User();
    user.prop = "value";
    ...
    db.Users.Add(user)
    

    Later, inside first time console app execution, application executes this code and UserWordRequests is null instead empty list.

    var user = db.Users.Where(...).FirstOrDefault();
    
    user.UserWordRequests.Add(
        new UserWordRequest
        {
            Date = DateTime.Now,
            Word = ...
        }
    );
    

    The solution comes using Create or CreateProxy for user creations.

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

    Then, UserWordRequests is empty list, not null.

    But... why second time this does not happen? Because users are created in database and the code did not execute the inserts, it obtained them with a database query.