dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.65k stars 3.15k forks source link

No runtime error thrown when abstract base class's navigator not marked as virtual when enable lazyloading #31099

Open phiree opened 1 year ago

phiree commented 1 year ago

code

  public class Product{
  public string Name {get;set;}
}
//  base class, abstract, has a product
  public abstract class BaseOrder
  {
     public Product Product{get;set;}
 }
// saleorder inherits from BaseOrder
 public class SaleOrder:BaseOrder
{
  public virtual Seller Seller{get;set;}
}

// enable lazyloading :
 builder.Services.AddDbContext<TradePlatformContext>(options =>
            options
             .UseLazyLoadingProxies()
                                .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))
                                .ConfigureWarnings(w => w.Log(RelationalEventId.MultipleCollectionIncludeWarning))
                                )

                ;  

// get a saleOrder

var saleOrder= dbcontext.Set<SaleOrder>.Find(saleorderId)

Problem

no exception thown , but set saleOrder.Product to null instead. if i remove "virtual" of Seller( direct property of saleorder), a runtime error will be thorwn.
why efcore didn't check "virtual " modifier of base class's property?

Expect

throw Exception like this:

System.InvalidOperationException:“Property 'SaleOrder.Product' is not virtual. 'UseChangeTrackingProxies' requires all entity types to be public, unsealed, have virtual properties, and have a public or protected constructor. 'UseLazyLoadingProxies' requires only the navigation properties be virtual.”

Include provider and version information

EF Core version: Database provider:Pomelo.EntityFrameworkCore.MySql Target framework: (e.g. .NET 7.0) Operating system: IDE: Visual Studio 2022 17.6.2

ajcvickers commented 1 year ago

@phiree Your example code uses fields instead of properties. EF Core doesn't map fields by default.

phiree commented 1 year ago

i have updated example code.

ajcvickers commented 1 year ago

@phiree I am not able to reproduce this; see my code below. Please attach a small, runnable project or post a small, runnable code listing that reproduces what you are seeing so that we can investigate.

using (var context = new SomeDbContext())
{
    await context.Database.EnsureDeletedAsync();
    await context.Database.EnsureCreatedAsync();
}

public class SomeDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))
            .UseLazyLoadingProxies()
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>();
        modelBuilder.Entity<BaseOrder>();
        modelBuilder.Entity<SaleOrder>();
    }
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
}

// base order has a product
public class BaseOrder
{
    public int Id { get; set; }
    public virtual Product Product { get; set; }
}

// saleorder inherits from BaseOrder
public class SaleOrder : BaseOrder
{
    public Seller Seller { get; set; }
}

public class Seller
{
    public int Id { get; set; }
}
phiree commented 1 year ago

@ajcvickers thanks for your attension . after checking your code , i found i missed a important point in my post : base class is abstract . i will edit it later. you can change BaseOrder to abstract and try again, or use the runable code below( .net 7 console app, dependencies: Microsoft.EntityFrameworkCore Microsoft.EntityFrameworkCore.InMemory Microsoft.EntityFrameworkCore.Proxies )


using Microsoft.EntityFrameworkCore;

namespace efcore_virtual_base_not_throw
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(@"
Situation:
   1) base class(Order) is abstract, it has a **no-virtual** property:Product
   2) SaleOrder inherits from Order
   3) enbale lazy loading
Problem:
    when i retrive a SaleOrder from database
    1)no exception throw
    2) saleOrder.Product is null
Expect:
    thrown InvalidOperationException( System.InvalidOperationException:“Property 'SaleOrder.Product' is not virtual. 'UseChangeTrackingProxies' requires all entity............. )
    ,to remind me add virtual to Product .
");

            Save();
            Get();

        }

        static void Save()
        {
            using (var context = new SomeDbContext())
            {
                context.Database.EnsureDeletedAsync();
                context.Database.EnsureCreatedAsync();

                var product1 = new Product { Name = "P1", Id = 1 };
                var product2 = new Product { Name = "P2", Id = 2 };

                context.Add<SaleOrder>(new SaleOrder { Id = 2, ProductWithoutVirtual = product1, ProductWithVirtual=product2 });
                context.SaveChanges();

            }
        }
        static void Get()
        {
            using (var context = new SomeDbContext())
            {

                var order = context.Set<SaleOrder>().Find(2);
                Console.WriteLine("-------------code result ------------------" );

                Console.WriteLine("Product with virtual is lazy loaded, its name is :" + order.ProductWithVirtual.Name);
                Console.WriteLine("Product without virtual: is null,  and no exception thrown.  " + (order.ProductWithoutVirtual==null?"it is null!": order.ProductWithoutVirtual.Name));

                Console.Read();
            }
        }
    }

    public class SomeDbContext : DbContext
    {

        public DbSet<Product> Products { get; set; }
        public DbSet<SaleOrder> SaleOrders { get; set; }
        public DbSet<BaseOrder> BaseOrders { get; set; }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder
                .UseInMemoryDatabase("db")//(connectionString, ServerVersion.AutoDetect(connectionString))
                .UseLazyLoadingProxies()
                // .LogTo(Console.WriteLine, LogLevel.Information)
                .EnableSensitiveDataLogging();

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {

            modelBuilder.Entity<Product>();
            //  modelBuilder.Entity<BaseOrder>();
            modelBuilder.Entity<SaleOrder>();
        }
    }
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    //  abstract  base class, has a product
    public abstract class BaseOrder
    {
        public int Id { get; set; }
        public virtual Product ProductWithVirtual { get; set; }

        public Product ProductWithoutVirtual { get; set; }
    }

    // saleorder inherits from BaseOrder
    public class SaleOrder : BaseOrder
    {

    }

}
ajcvickers commented 1 year ago

Note for triage: I am now able to reproduce this. Seems to be dependent on entity type discovery order.