简体   繁体   中英

Entity Framework issue when saving an entity object

Scenario: Intranet app. Windows authentication. EF 6.1.3. Databases: SQL Server Compact Edition and MS Access. VS Studio 2013.

The solution has 3 projects:

EnqueteWeb.UI - ASP.NET web application; EnqueteWeb.Dominio - class library for the application domain; ControleDeAcessoGeral - class library to get data of the user logged from Active Directory, and include/update/delete/list some users that perform some special actions on the app. As the access control to the app is based on a SQL Server Compact Edition database, I have EntityFramework installed in ControleDeAcessoGeral . I want to have all the methods regarding to users in a class in this project. And so I did it.

This ControleDeAcessoGeral project is defined like this:

Aplicacao
- Corp.cs (methods to deal with Active Directory stuff)
- UsuariosApp.cs (methods to deal with the SQL Server CE database)
Contexto
- DBControleDeAcesso.cs (defines the context)
- InicializaControleDeAcesso.cs (fill in initial data to the DBControleDeAcesso database)
Entidades
- Perfil.cs (profiles that a user can have on the app)
- Usuarios.cs (users that may perform some actions on the app)
- UsuarioAD.cs (Active Directory user and its data)

The DBControleDeAcesso.cs class has the following code:

using ControleDeAcessoGeral.Models.Entidades;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;

namespace ControleDeAcessoGeral.Models.Contexto
{
    public class DBControleDeAcesso : DbContext
    {
        public DBControleDeAcesso() : base("ControleDeAcessoContext") { }

        public DbSet<Perfil> Perfis { get; set; }
        public DbSet<Usuario> Usuarios { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
        }
    }
}  

The entities classes are the following:
using System.Collections.Generic; using System.ComponentModel.DataAnnotations;

namespace ControleDeAcessoGeral.Models.Entidades
{
    public class Usuario
    {
        [Key]
        public string Logon { get; set; }
        public string Nome { get; set; }
        [Display(Name="Órgão")]
        public string Orgao { get; set; }
        public string Email { get; set; }

        [StringLength(maximumLength: 4)]
        public string Depto { get; set; }

        [Display(Name = "Perfis")]
        public virtual List<Perfil> Perfis { get; set; }

        public Usuario()
        {
            this.Perfis = new List<Perfil>();
        }
    }
}  

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ControleDeAcessoGeral.Models.Entidades
{
    public class Perfil
    {
        [Key]
        public int Id { get; set; }

        [Required(ErrorMessage = "Por favor, informe o NOME DO perfil.")]
        [StringLength(maximumLength: 25)]
        public string Nome { get; set; }

        [StringLength(maximumLength: 255)]
        [Display(Name = "Descrição")]
        public string Descricao { get; set; }

        public virtual List<Usuario> Usuarios { get; set; }

        public Perfil()
        {
            this.Usuarios = new List<Usuario>();
        }
    }
}  

And the UsuariosApp.cs class is as bellow (for the sake of brevity, I'll show only the methods that concerns to the issue):

using ControleDeAcessoGeral.Models.Contexto;
using ControleDeAcessoGeral.Models.Entidades;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;

namespace ControleDeAcessoGeral.Models.Aplicacao
{
    public class UsuariosApp
    {
        private DBControleDeAcesso db { get; set; }

        public UsuariosApp()
        {
            db = new DBControleDeAcesso();
        }

        public void SalvarUsuario(Usuario usuario)
        {
            db.Usuarios.Add(usuario);
            db.SaveChanges();
        }

        public Perfil LocalizarPerfil(int id)
        {
            return db.Perfis.Find(id);
        }
    }
}  

The action that tries to save a user (Usuarios.cs) in the SQL Server CE database is in AdministracaoController and has the following code:

using ControleDeAcessoGeral.Models.Aplicacao;
using ControleDeAcessoGeral.Models.Entidades;
using EnqueteWeb.UI.Models;
using EnqueteWeb.UI.ViewModels;
using System.Linq;
using System.Web.Mvc;

namespace EnqueteWeb.UI.Controllers
{
    public class AdministracaoController : Controller
    {

        [HttpPost]
        public ActionResult CriarUsuarioNaApp(UsuarioViewModel model)
        {
            foreach (var item in model.PerfisSelecionados)
            {
                Perfil perfil = new UsuariosApp().LocalizarPerfil(item);
                model.Usuario.Perfis.Add(perfil);
            }
            if (ModelState.IsValid)
            {
                new UsuariosApp().SalvarUsuario(model.Usuario);
                return RedirectToAction("Usuarios");
            }
            return View(model);
        }
    }
}  

So, when this action CriarUsuarioNaApp is invoked and the method SalvarUsuario(model.Usuario) runs, the following error occurs:

An entity object cannot be referenced by multiple instances of IEntityChangeTracker

I've read a few about this on web but, unfortunately, I still couldn't make it works.

Hope a wise and good soul will show me the way.

Thanks for your attention.

Paulo Ricardo Ferreira

The problem arises from the fact that you do not dispose of the first DbContext instance (from which you load the profile entities) prior to attaching said entities to the second DbContext instance.

To fix (and some additional suggestions):

  1. have UsuariosApp implement IDisposable and dispose your instance of the DbContext db when disposing UsuariosApp
  2. wrap your newly-disposable UsuariosApp instance in a using statement and use this single instance for both your Perfil loading and Usuario saving logic
  3. optimize Perfil loading by loading all values with single call
  4. validate ModelState.IsValid immediately

Something like this:

public class UsuariosApp : IDisposable
{
    private DBControleDeAcesso db { get; set; }

    public UsuariosApp()
    {
        db = new DBControleDeAcesso();
    }

    public void SalvarUsuario(Usuario usuario)
    {
        db.Usuarios.Add(usuario);
        db.SaveChanges();
    }

    public Perfil LocalizarPerfil(int id)
    {
        return db.Perfis.Find(id);
    }

    public IEnumerable<Perfil> LocalizarPerfiles( IEnumerable<int> ids )
    {
        return db.Perfils.Where( p => ids.Contains( p.Id ) )
            .ToArray();
    }

    private bool _disposed = false;

    protected virtual void Dispose( bool disposing )
    {
        if( _disposed )
        {
            return;
        }

        if( disposing )
        {
            db.Dispose();
        }

        _disposed = true;
    }

    public void Dispose()
    {
        Dispose( true );
        GC.SuppressFinalize( this );
    }
}

public ActionResult CriarUsuarioNaApp( UsuarioViewModel model )
{
    // validate model state first
    if( ModelState.IsValid )
    {
        // use single, disposable repo/uow instance
        using( var uapp = new UsuariosApp() )
        {
            // get all profiles in a single call, no loop required
            var perfils = uapp.LocalizarPerfiles( model.PerfisSelecionados );

            model.Usuario.Perfis.AddRange( perfils );

            uapp.SalvarUsuario( model.Usuario );
        }

        return RedirectToAction( "Usuarios" );
    }

    return View( model );
}

Let me know if that doesn't solve your problem.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM