实体框架6.1.13延迟加载和虚拟对象处理

时间:2016-10-12 16:16:13

标签: c# entity-framework dao

我已经使用Entity Framework大约8个月了,我最近从版本4或5更新到6.1.13。

我编写了以下通用DAO类:

using System;
using System.Linq;
using System.Reflection;
using System.Data.Entity;
using System.Linq.Expressions;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using Entidades;
using Repositorio.Context;

namespace Repositorio.DAO
{
    /// <summary>
    /// Classe generica para operacoes (CRUD) relacionadas com o banco de dados.
    /// Para cada entidade do tipo TBean, um DAO devera ser criado.
    /// </summary>
    public class DAO<TBean> : IDisposable where TBean : Bean
    {
        /// <summary>
        /// Set de entidades
        /// </summary>
        protected DbSet<TBean> set;

        /// <summary>
        /// Contexto das entidades
        /// </summary>
        protected DatabaseContext context;

        /// <summary>
        /// Construtor. Inicializa atributos internos.
        /// </summary>
        public DAO()
        {
            context = new DatabaseContext();
            set = context.Set<TBean>();
        }

        /// <summary>
        /// Destroi este DAO.
        /// </summary>
        public void Dispose()
        {
            if (context != null)
            {
                context.Dispose();
            }
        }

        /// <summary>
        /// Metodo de commit para transacoes com auto-commit desabilitado
        /// </summary>
        public void SaveChanges()
        {
            context.SaveChanges();
        }

        /// <summary>
        /// Insere ou atualiza um objeto do tipo TBean. Em atualizacoes, todos os atributos sao sobrepostos (atualizados).
        /// </summary>
        public void CreateUpdate(TBean bean, bool commit = true)
        {
            // Se entidade nao tiver um id, ela sera adicionada
            if (bean.Id.Equals(default(int)))
            {
                Create(bean, commit);
            }
            else
            {
                Update(bean, commit);
            }
        }

        /// <summary>
        /// Cria uma entidade do tipo TBean.
        /// </summary>
        public virtual void Create(TBean bean, bool commit = true, int committer = 0)
        {
            SetBeanState(EntityState.Added, bean, commit);
        }

        /// <summary>
        /// Atualiza/sobescreve uma entidade do tipo TBean.
        /// </summary>
        public virtual void Update(TBean bean, bool commit = true, int committer = 0)
        {
            SetBeanState(EntityState.Modified, bean, commit);
        }

        /// <summary>
        /// Deleta uma entidade do tipo TBean.
        /// </summary>
        public virtual void Delete(TBean bean, bool commit = true, int committer = 0)
        {
            SetBeanState(EntityState.Deleted, bean, commit);
        }

        /// <summary>
        /// Carrega, de forma lazy (preguicosa), todos os resultados de um objeto do tipo TBean.
        /// </summary>
        public virtual IQueryable<TBean> List()
        {
            return ListEager(bean => true, null);
        }

        /// <summary>
        /// Carrega, de forma lazy (preguicosa), todos os resultados de um objeto do tipo TBean, dado um filtro.
        /// </summary>
        public virtual IQueryable<TBean> List(Expression<Func<TBean, bool>> filter)
        {
            return ListEager(filter, null);
        }

        /// <summary>
        /// Carrega, de forma eager (gulosa), todos os resultados de um objeto do tipo TBean.
        /// </summary>
        public IQueryable<TBean> ListEager(Expression<Func<TBean, object>> include)
        {
            return ListEager(bean => true, include);
        }

        /// <summary>
        /// Carrega, de forma eager (gulosa), todos os resultados de um objeto do tipo TBean.
        /// </summary>
        public IQueryable<TBean> ListEager(Expression<Func<TBean, bool>> filter, Expression<Func<TBean, object>> include)
        {
            IQueryable<TBean> query = set.AsQueryable();

            // Se houver includes, valida e adiciona
            if (include != null)
            {
                foreach (var member in RetrieveForeignKeyMembers(include))
                {
                    query = query.Include(member.Name);
                }
            }

            return query.Where(filter);
        }

        /// <summary>
        /// Procura por um objeto do tipo TBean, dado um identificador
        /// </summary>
        public virtual TBean Search(int id)
        {
            return SearchEager(bean => bean.Id == id, null);
        }

        /// <summary>
        /// Procura por um objeto do tipo TBean, dado um filtro
        /// </summary>
        public virtual TBean Search(Expression<Func<TBean, bool>> filter)
        {
            return SearchEager(filter, null);
        }

        /// <summary>
        /// Procura por um objeto do tipo TBean, usando eager loading, dado um identificador.
        /// </summary>
        public TBean SearchEager(int id, Expression<Func<TBean, object>> include)
        {
            return SearchEager(bean => bean.Id == id, include);
        }

        /// <summary>
        /// Procura por um objeto do tipo TBean, usando eager loading, dado um filtro.
        /// </summary>
        public TBean SearchEager(Expression<Func<TBean, bool>> filter, Expression<Func<TBean, object>> include)
        {
            IQueryable<TBean> query = set.AsQueryable();

            // Se houver includes, valida e adiciona
            if (include != null)
            {
                foreach (var member in RetrieveForeignKeyMembers(include))
                {
                    query = query.Include(member.Name);
                }
            }

            return query.FirstOrDefault(filter);
        }

        /// <summary>
        /// Retorna todos os membros definidos como chave estrangeira, a partir de uma Expression.
        /// Ex.: 
        ///     1. attr => attr.AlgumaCoisa
        ///     2. attrs => new { attr.AlgumaCoisa, attr.AlgumaOutraCoisa, attr.MaisCoisa }
        /// </summary>
        protected List<MemberInfo> RetrieveForeignKeyMembers(Expression<Func<TBean, object>> expression)
        {
            List<MemberInfo> members = new List<MemberInfo>();

            // Apenas expressoes lambdas sao validas para definicao de uma ForeignKey
            if (expression.NodeType.Equals(ExpressionType.Lambda))
            {
                // Captura membros. Em new expressions, Members nao contem CustomAttributes!
                switch (expression.Body.NodeType)
                {
                    case ExpressionType.MemberAccess:
                        members.Add((expression.Body as MemberExpression).Member);
                        break;
                    case ExpressionType.New:
                        members.AddRange((expression.Body as NewExpression).Arguments.Select(arg => (arg as MemberExpression).Member));
                        break;
                }
            }

            // Para cada membro, checa se este possui um ForeignKeyAttribute
            if (members.Any(member => member.GetCustomAttributes<ForeignKeyAttribute>().Count() == 0))
            {
                throw new ArgumentException("Include inválido!", "expression");
            }

            return members;
        }

        protected void SetBeanState(EntityState state, TBean bean, bool commit = true)
        {
            // Atributos correspondentes a chave primaria nao podem ser invalidos!
            context.Entry(bean).State = state;

            if (commit)
            {
                context.SaveChanges();
            }
        }
    }
}

我认为它按预期工作,直到昨天。我注意到即使将'LazyLoadingEnabled'设置为true(默认)并且没有Include()调用,也会加载虚拟字段。

让我们以这个bean类为例(不幸的是我不能粘贴所有东西,比如Bean,IAjustepreco,IAuditable接口等)。另请注意,所有bean配置都在Configuration类中完成。 ForeignKey属性仅用于标记导航属性,因此忽略它们的“NAVIGATION_PROPERTY”值,因为它们不能为空或为空。这是:

using System;
using System.ComponentModel.DataAnnotations.Schema;
using Entidades.Enums;
using Entidades.Interfaces;
using Entidades.Interfaces.Auditoria;

namespace Entidades
{
    /// <summary>
    /// Ajustes de preco de um determinado material.
    /// Assessores poderao propor novos precos para seus clientes,
    /// submetendo-os a um workflow de aprovacao por parte dos seus gerentes,
    /// superintendente, diretores comerciais e diretor de logistica.
    /// </summary>
    [Table("TB_AJUSTE_PRECO", Schema = "dbo")]
    public class AjustePreco : Bean, IAjustePreco, IAuditable<AjustePrecoAudition>
    {
        /// <summary>
        /// Identificador deste objeto.
        /// </summary>
        [Column("ID")]
        public override int Id { get; set; }

        /// <summary>
        /// ID do usuario que solicitou o ajuste (FK).
        /// </summary>
        [Column("ID_USUARIO_SOLICITANTE")]
        public int UsuarioCriador { get; set; }

        /// <summary>
        /// Data em que este ajuste foi solicitado.
        /// </summary>
        [Column("DT_SOLICITACAO")]
        public DateTime DataCriacao { get; set; }

        /// <summary>
        /// ID do motivo pelo qual este ajuste foi submetido (FK).
        /// </summary>
        [Column("ID_MOTIVO_SUBMISSAO")]
        public int MotivoSubmissaoId { get; set; }

        /// <summary>
        /// ID do perfil ao qual este ajuste se encontra pendente.
        /// </summary>
        [Column("TP_PERFIL")]
        public PerfilAcesso Perfil { get; set; }

        /// <summary>
        /// Estado atual deste ajuste.
        /// </summary>
        [Column("STATUS")]
        public AjustePrecoStatus Status { get; set; }

        /// <summary>
        /// Data de validade (vingencia) desse ajuste.
        /// </summary>
        [Column("DT_VALIDADE")]
        public DateTime DataValidade { get; set; }

        /// <summary>
        /// Valor do preco final proposto por alguem (assessor).
        /// Este valor sera sugerido como um novo valor para algum material de algum cliente.
        /// </summary>
        [Column("PRECO_FINAL_NOVO")]
        public decimal PrecoFinalNovo { get; set; }

        /// <summary>
        /// Valor do preco final do material no momento em que este ajuste foi solicitado (valor atual).
        /// Este valor e utilizado apenas para validacao da atualizacao deste ajuste.
        /// </summary>
        [Column("PRECO_FINAL_ATUAL")]
        public decimal PrecoFinalAtual { get; set; }

        /// <summary>
        /// Valor do acrescimo/desconto do material no momento em que este ajuste foi solicitado (valor atual).
        /// </summary>
        [Column("VL_AJUSTE_ATUAL")]
        public decimal ValorAjusteAtual { get; set; }

        /// <summary>
        /// Valor do novo ajuste. Em casos onde nao ha imposto ICMS ST, o calculo desta 
        /// variavel sera (PrecoFinalNovo - PrecoFinalAtual) + ValorAjusteAtual.
        /// Caso contrario, havera um webservice para calcular este valor.
        /// Ao concluir o workflow (todas as aprovacoes), este valor sera enviado
        /// para o SAP para substituir o ajuste corrente de um usuario.
        /// </summary>
        [Column("VL_AJUSTE_NOVO")]
        public decimal ValorAjusteNovo { get; set; }

        /// <summary>
        /// Nome do cliente favorecido.
        /// </summary>
        [Column("NM_CLIENTE")]
        public string NomeCliente { get; set; }

        /// <summary>
        /// Nome do material ao qual este ajuste se refere.
        /// </summary>
        [Column("NM_MATERIAL")]
        public string NomeMaterial { get; set; }

        /// <summary>
        /// Nome do centro
        /// </summary>
        [Column("NM_CENTRO")]
        public string NomeCentro { get; set; }

        /// <summary>
        /// Nome da condição de pagamento.
        /// </summary>
        [Column("NM_CONDICAO_PAGAMENTO")]
        public string NomeCondicaoPagamento { get; set; }

        /// <summary>
        /// Nome da bandeira do cliente.
        /// </summary>
        [Column("NM_BANDEIRA")]
        public string NomeBandeira { get; set; }

        /// <summary>
        /// Nome do frete utilizado.
        /// </summary>
        [Column("NM_FRETE")]
        public string NomeFrete { get; set; }

        /// <summary>
        /// Id do cliente, no SAP, o qual sera atribuido o ajuste
        /// </summary>
        [Column("CD_CLIENTE")]
        public string CodigoCliente { get; set; }

        /// <summary>
        /// Codigo do material, em string, com padding left zeros.
        /// </summary>
        [Column("CD_MATERIAL")]
        public string CodigoMaterial { get; set; }

        /// <summary>
        /// Codigo do Centro, em string.
        /// Apesar de nao conter letras em seus codigos, algum dia o tipo de
        /// dados podera ser alterado. Isto explica o tipo 'string'.
        /// </summary>
        [Column("CD_CENTRO")]
        public string CodigoCentro { get; set; }

        /// <summary>
        /// Codigo da condição de pagamento: C000, C001 e etc.
        /// </summary>
        [Column("CD_CONDICAO_PAGAMENTO")]
        public string CodigoCondicaoPagamento { get; set; }

        /// <summary>
        /// Codigo da bandeira do cliente favorecido.
        /// </summary>
        [Column("CD_BANDEIRA")]
        public string CodigoBandeira { get; set; }

        /// <summary>
        /// Codigo do frete utilizado.
        /// </summary>
        [Column("CD_FRETE")]
        public string CodigoFrete { get; set; }

        /// <summary>
        /// Distancia entre o cliente e a base mais proxima
        /// </summary>
        [Column("DISTANCIA_ATUAL")]
        public decimal DistanciaAtual { get; set; }

        /// <summary>
        /// [NotMapped]
        /// Usuario que solicitou este ajuste (Navigation property).
        /// </summary>
        [ForeignKey("NAVIGATION_PROPERTY")]
        public Usuario Solicitante { get; private set; }

        /// <summary>
        /// [NotMapped]
        /// Motivo de submissao deste ajuste (Navigation property).
        /// </summary>
        [ForeignKey("NAVIGATION_PROPERTY")]
        public AjustePrecoMotivo MotivoSubmissao { get; private set; }

        /// <summary>
        /// [NotMapped]
        /// Preco de custo do material ao qual este ajuste se refere.
        /// Esta diretamente relacionada a ZFMSD_CALCULA_PRECO_PORTAL.E_VL_CUSTO_MEDIO_MOVEL.
        /// </summary>
        //[Column("PRECO_CUSTO_MEDIA_MOVEL")]
        [NotMapped]
        public decimal PrecoCustoMediaMovel { get; set; }

        /// <summary>
        /// [NotMapped]
        /// </summary>
        [NotMapped]
        public decimal PrecoCustoUtilizado
        {
            get { return PrecoBase - ValorMargem - ValorOverhead; }
        }

        /// <summary>
        /// [NotMapped]
        /// Valor da diferenca entre o preco proposto e o preco atual.
        /// </summary>
        [NotMapped]
        public decimal PrecoFinalDelta
        {
            get { return PrecoFinalAtual - PrecoFinalNovo; }
        }

        /// <summary>
        /// [NotMapped]
        /// Preco base do material ao qual este ajuste se refere.
        /// Este dado e obtido atraves de webservice.
        /// </summary>
        [NotMapped]
        public decimal PrecoBase { get; set; }

        /// <summary>
        /// [NotMapped]
        /// Valor do juros aplicado sobre a condicao de pagamento do material 
        /// ao qual este ajuste se refere.
        /// Este dado e obtido atraves de webservice.
        /// </summary>
        [NotMapped]
        public decimal ValorFinanceiro { get; set; }

        /// <summary>
        /// [NotMapped]
        /// Valor do frete do material ao qual este ajuste se refere.
        /// Este dado e obtido atraves de webservice.
        /// </summary>
        [NotMapped]
        public decimal ValorFrete { get; set; }

        /// <summary>
        /// [NotMapped]
        /// Valor do investimento (ROI) do material ao qual este ajuste se refere.
        /// Este dado e obtido atraves de webservice.
        /// </summary>
        [NotMapped]
        public decimal ValorInvestimento { get; set; }

        /// <summary>
        /// [NotMapped]
        /// Valor do rateio de todas as despesas da empresa incluso ao preco 
        /// de um material, o qual este ajuste se refere.
        /// Este dado e obtido atraves de webservice.
        /// </summary>
        [NotMapped]
        public decimal ValorOverhead { get; set; }

        /// <summary>
        /// [NotMapped]
        /// Margem de lucro de um material ao qual este ajuste se refere.
        /// Este dado e obtido atraves de webservice.
        /// </summary>
        [NotMapped]
        public decimal ValorMargem { get; set; }

        /// <summary>
        /// [NotMapped]
        /// Formula: ValorMargem + ValorInvestimento + ValorAjusteNovo.
        /// </summary>
        [NotMapped]
        public decimal ValorMargemAjustada
        {
            get { return ValorMargem + ValorInvestimento + ValorAjusteNovo; }
        }

        /// <summary>
        /// [NotMapped]
        /// Formula: PrecoFinalAtual - ValorCustoUtilizado - ValorInvestimento - ValorFrete - ValorFinanceiro.
        /// </summary>
        [NotMapped]
        public decimal ValorMargemBruta
        {
            get { return PrecoFinalAtual - ValorInvestimento - ValorFrete - ValorFinanceiro; } // - ValorCustoUtilizado
        }

        /// <summary>
        /// [NotMapped]
        /// Identifica se este ajuste e um acrescimo ou um desconto.
        /// </summary>
        [NotMapped]
        public bool IsAcrescimo
        {
            get { return PrecoFinalNovo - PrecoFinalAtual > 0M; }
        }

        /// <summary>
        /// Construtor. Configura valores default.
        /// </summary>
        /// <remarks>
        /// Atributos decimais devem ter as casas decimais configuradas,
        /// caso contrario os valores persistidos serao "0" ao inves de "0.0000".
        /// </remarks>
        public AjustePreco()
        {
            PrecoFinalNovo = 0.0000M;
            PrecoFinalAtual = 0.0000M;
            ValorAjusteAtual = 0.0000M;
        }

        /// <summary>
        /// Cria seu objeto de auditoria a partir do ponteiro para este objeto
        /// </summary>
        public AjustePrecoAudition CreateAudition()
        {
            return new AjustePrecoAudition(this);
        }
    }
}

现在,有缺陷的部分。这段代码按预期工作:

public static IEnumerable<AjustePreco> Listar(bool eager = true)
{
    using (var dao = new AjustePrecoDAO())
    {
        if (eager)
        {
            return new List<AjustePreco>(dao.ListEager(ap => new { ap.Solicitante, ap.MotivoSubmissao }));
        }
        else
        {
            //var x = new List<AjustePreco>(dao.List()); return x; // Navigation properties are set (unexpected)
            return new List<AjustePreco>(dao.List()); // Navigation properties are disposed (expected)
        }
    }
}

虽然这个没有(注意:下面真的很奇怪的行为):

public static void Test(Usuario usuario)
{
    IEnumerable<AjustePreco> x0 = ManterAjustePreco.Listar(false);
    IEnumerable<AjustePreco> x1 = null;
    IEnumerable<AjustePreco> x2 = null;
    IEnumerable<AjustePreco> x3 = null;
    IEnumerable<AjustePreco> x4 = null;

    using (var dao = new AjustePrecoDAO())
    {
        x1 = dao.List().ToList();
        x2 = new List<AjustePreco>(dao.List(u => u.Perfil == PerfilAcesso.Gerente));
        x3 = new List<AjustePreco>(dao.ListEager(i => i.Solicitante));
        x4 = new List<AjustePreco>(dao.ListEager(a => a.Perfil == PerfilAcesso.Gerente, i => i.Solicitante));
    }

    // THIS IS THE WEIRDEST PART:
    // Sometimes, running this same function multiple times would cause an exception in any of the next 3 lines.
    var f = x0.ElementAt(0).MotivoSubmissao;
    var g = x1.ElementAt(0).MotivoSubmissao;
    var h = x2.ElementAt(0).MotivoSubmissao;

    System.Diagnostics.Debug.WriteLine("Done");
}

这太奇怪了,我甚至不确定我是否可以清楚地解释它。如果不清楚,请告诉我。我很绝望。

非常感谢!

修改

在与Ivan进行简短讨论后,我从导航属性中删除了virtual关键字。这导致我期待的真实行为,虽然我面临着另一个奇怪的事情,如下:

IEnumerable<AjustePreco> x1 = null;
IEnumerable<AjustePreco> x3 = null;

using (var dao = new AjustePrecoDAO())
{
    x1 = dao.List().ToList(); // Gets modified
    x1 = new List<AjustePreco>(dao.List()); // Gets modified
    x1 = new List<AjustePreco>(dao.List()).AsReadOnly(); // Gets modified
    x3 = new List<AjustePreco>(dao.ListEager(i => i.MotivoSubmissao));
}

所有3 x1装载都可以。确定,我的意思是所有导航属性都为null(正如预期的那样)。但是,当它加载x3(包括MotivoSubmissao导航属性)时,它会更改x1元素MotivoSubmissao值(从null到其他值)。我知道每个选择都会更新DbSet缓存,但如果它是一个sepparate var,它如何更改x1列表?!

0 个答案:

没有答案