Search code examples
c#entity-frameworkentitypocoproxy-classes

What is the cause of this EF6 behaviour Find() does not return a proxy but Single() does return a proxy


I have a POCO class BankAccount which is public and all of the members are public properties and navigation properties are set to virtual.

Entity Framework 6.1.2 correctly loads it from the database as a POCO using the Find() method. However, as far as I am aware it should be returning a Proxy class instance and not a POCO instance! In fact, when I use Single(), SingleOrDefault(), First() or FirstOrDefault() a Proxy class instance is correctly returned.

What is going on, is this expected behaviour, and if not what can cause this to happen?

Here is the POCO class:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Runtime.Serialization;

namespace AbcBankModels
{
    //[DataContract]
    [Table("BankAccount")]
    public class BankAccount
    {
        [Key]
        [Column(Order = 1)]
        //[DataMember]
        //[Range(0, 9999999)]
        //[StringLength(7, MinimumLength = 7)]
        //[Display(Name = "Account Number")]
        public virtual string BankAccountId { get; set; }

        [Key]
        [Column(Order = 2)]
        //[DataMember]
        //[Range(0, 999999)]
        //[StringLength(6, MinimumLength = 6)]
        //[Display(Name = "Sort Code")]
        public virtual string BankBranchId { get; set; }

        //[DataMember]
        public virtual BankBranch BankBranch { get; set; }

        //[DataMember]
        //[ForeignKey("ApplicationUser")]
        public virtual string ApplicationUserId { get; set; }

        //[DataMember]
        public virtual User ApplicationUser { get; set; }

        //[DataMember]
        public virtual ICollection<BankCard> BankCardList { get; set; }

        //[DataMember]
        public virtual ICollection<BankTransaction> BankTransactionList { get; set; }

        //[DataMember]
        //[Display(Name = "Account Status")]
        //[EnumDataType(typeof(EnumAccountStatus))]
        public virtual EnumAccountStatus AccountStatus { get; set; }

        //[DataMember]
        //[Display(Name = "Account Type")]
        //[EnumDataType(typeof(EnumBankAccountType))]
        public virtual EnumBankAccountType AccountType { get; set; }

        //[DataMember]
        //[DataType(DataType.DateTime)]
        //[Display(Name = "Date Account Opened")]
        public virtual DateTime? AccountCreationDateTime { get; set; }

        //[DataMember]
        //[DataType(DataType.DateTime)]
        //[Display(Name = "Date Account Closed")]
        public virtual DateTime? AccountClosureDateTime { get; set; }

        //[DataMember]
        //[DataType(DataType.Currency)]
        //[Display(Name = "Account Overdraft Limit")]
        public virtual decimal AccountOverdraft { get; set; }

        //[DataMember]
        //[Display(Name = "Account Overdraft Interest Rate")]
        public virtual decimal AccountOverdraftInterestRate { get; set; }

        //[DataMember]
        //[Display(Name = "Account Overdraft Usage Monthly Fee")]
        public virtual decimal AccountOverdraftFacilityMonthlyCost { get; set; }

        //[DataMember]
        //[Display(Name = "Account Monthly Fee")]
        public virtual decimal AccountMonthlyCost { get; set; }

        //[DataMember]
        //[Display(Name = "Account Interest Rate")]
        public virtual decimal AccountInterestRate { get; set; }

    }
}

Here is the method which does return a proxy:

    public static BankAccount FindBankAccount(ApplicationDbContext applicationDbContext, string bankAccountId, string bankBranchId, string userId)
    {
        if (String.IsNullOrWhiteSpace(bankAccountId) || String.IsNullOrWhiteSpace(bankBranchId)) return null;

        var bankAccount = applicationDbContext.BankAccountList.SingleOrDefault(a => a.BankAccountId == bankAccountId && a.BankBranchId == bankBranchId);

        if (bankAccount == null) return null;

        if (string.IsNullOrWhiteSpace(userId)) return bankAccount;

        if (bankAccount.ApplicationUserId != userId) return null;

        return bankAccount;
    }

Here is the method which does not return a proxy:

    public static BankAccount FindBankAccount(ApplicationDbContext applicationDbContext, string bankAccountId,
        string bankBranchId, string userId)
    {
        if (String.IsNullOrWhiteSpace(bankAccountId) || String.IsNullOrWhiteSpace(bankBranchId)) return null;

        var bankAccount = applicationDbContext.BankAccountList.Find(bankAccountId, bankBranchId);

        if (bankAccount == null) return null;

        if (string.IsNullOrWhiteSpace(userId)) return bankAccount;

        if (bankAccount.ApplicationUserId != userId) return null;

        return bankAccount;
    }

Solution

  • This could happen if your context is already tracking a non-proxy BankAccount with that key by the moment you query it.

    The strange thing is that, although First and Single always query the database, they should return the same entity as Find does.

    For example, if you have a unit test that runs this code:

            Foo nonProxy = new Foo { Id = 4, Name = "Foo 4" }; // values correspond to an existing entity in db
            ApplicationDbContext ctx = new ApplicationDbContext();          
            ctx.Foos.Attach(nonProxy);
            Assert.AreSame(nonProxy, ctx.Foos.Find(nonProxy.Id));
            Assert.AreSame(nonProxy, ctx.Foos.First(c => c.Name == "Foo 4"));
            Assert.AreSame(nonProxy, ctx.Foos.FirstOrDefault(c => c.Name == "Foo 4"));
            Assert.AreSame(nonProxy, ctx.Foos.Single(c => c.Name == "Foo 4"));
            Assert.AreSame(nonProxy, ctx.Foos.SingleOrDefault(c => c.Name == "Foo 4"));
    
            ctx = new ApplicationDbContext();
            Foo proxy = ctx.Foos.Find(nonProxy.Id);
            Assert.AreSame(proxy, ctx.Foos.Find(nonProxy.Id));
            Assert.AreSame(proxy, ctx.Foos.First(c => c.Name == "Foo 4"));
            Assert.AreSame(proxy, ctx.Foos.FirstOrDefault(c => c.Name == "Foo 4"));
            Assert.AreSame(proxy, ctx.Foos.Single(c => c.Name == "Foo 4"));
            Assert.AreSame(proxy, ctx.Foos.SingleOrDefault(c => c.Name == "Foo 4"));
    

    then it should run without errors. After an entity with the same key is being tracked by the context, they all return a reference to the same object.

    Reference Querying/Finding Entities:

    // Query for the Blog named ADO.NET Blog 
    var blog = context.Blogs.Where(b => b.Name == "ADO.NET Blog").FirstOrDefault();
    

    When results are returned from the database, objects that do not exist in the context are attached to the context. If an object is already in the context, the existing object is returned (the current and original values of the object's properties in the entry are not overwritten with database values).

    So you are probably using Find in a situation where there is already a non-proxy entity with that key tracked by the context, or you are disabling Configuration.ProxyCreationEnabled in some place in your code before using it.

    Maybe also useful: DbSet.Find Method