boindiil
boindiil

Reputation: 5865

Entity Framework set navigation property to null

I have a entity framework database first project. here is a extraction of the model:

public partial class LedProject
{
    public LedProject()
    {
        this.References = new HashSet<LedProjectReference>();
        this.Results = new HashSet<LedProjectResult>();
        this.History = new HashSet<LedProjectHistory>();
    }

    public string Identifier { get; set; }
    public string Name { get; set; }
    public Nullable<System.DateTime> CompletionDate { get; set; }
    public System.DateTime CreationDate { get; set; }
    public System.Guid ProjectId { get; set; }
    public string Comment { get; set; }

    public virtual User ContactUser { get; set; }
    public virtual User CreationUser { get; set; }
    public virtual Customer Customer { get; set; }
    public virtual LedProjectAccounting Accounting { get; set; }
    public virtual LedProjectState State { get; set; }
    public virtual ICollection<LedProjectReference> References { get; set; }
    public virtual ICollection<LedProjectResult> Results { get; set; }
    public virtual User ResponsibleUser { get; set; }
    public virtual ICollection<LedProjectHistory> History { get; set; }
}
public partial class User
{
    public System.Guid UserId { get; set; }
    public string LoginName { get; set; }
    public System.DateTime CreationDate { get; set; }
    public string Firstname { get; set; }
    public string Lastname { get; set; }
    public string Email { get; set; }
}

I have a problem with setting the navigation item ResponsibleUser of the class LedProject. When I set the ResponsibleUser to a another user and afterwards save the changes of the DBContext, the changes are stored in the database.

But, when I want to delete the current ResponsibleUser of an LedProject, by setting the navigation property to null. The changes are not stored in the database.

LedProject project = db.LedProject.Find(projectId);
project.Name = string.IsNullOrEmpty(name) ? null : name;
...
project.ResponsibleUser = responsibleUser == null ? null : db.User.Find(responsibleUser.UserId);
...
db.SaveChanges();

Is there any trick for deleting navigation properties?

Upvotes: 29

Views: 21235

Answers (8)

Vincent
Vincent

Reputation: 1151

See https://learn.microsoft.com/en-us/ef/ef6/fundamentals/relationships.

The Creating and modifying relationships section explains what happens in regards to the foreign key property and the navigational property both when assigning and setting to null.

There are some changes in EF5 and forward, but the they key is to define the foreign key property so that the relationship is no longer an independant association (lacks the foreign key property).

Upvotes: 2

jonh
jonh

Reputation: 254

As another workaround, I compiled two methods into a extension method:

public static void SetToNull<TEntity, TProperty>(this TEntity entity, Expression<Func<TEntity, TProperty>> navigationProperty, DbContext context = null)
    where TEntity : class
    where TProperty : class
{
    var pi = GetPropertyInfo(entity, navigationProperty);

    if (context != null)
    {
        //If DB Context is supplied, use Entry/Reference method to null out current value
        context.Entry(entity).Reference(navigationProperty).CurrentValue = null;
    }
    else
    {
        //If no DB Context, then lazy load first
        var prevValue = (TProperty)pi.GetValue(entity);
    }

    pi.SetValue(entity, null);
}

static PropertyInfo GetPropertyInfo<TSource, TProperty>(    TSource source,    Expression<Func<TSource, TProperty>> propertyLambda)
{
    Type type = typeof(TSource);

    MemberExpression member = propertyLambda.Body as MemberExpression;
    if (member == null)
        throw new ArgumentException(string.Format(
            "Expression '{0}' refers to a method, not a property.",
            propertyLambda.ToString()));

    PropertyInfo propInfo = member.Member as PropertyInfo;
    if (propInfo == null)
        throw new ArgumentException(string.Format(
            "Expression '{0}' refers to a field, not a property.",
            propertyLambda.ToString()));

    if (type != propInfo.ReflectedType &&
        !type.IsSubclassOf(propInfo.ReflectedType))
        throw new ArgumentException(string.Format(
            "Expression '{0}' refers to a property that is not from type {1}.",
            propertyLambda.ToString(),
            type));

    return propInfo;
}

This allows you to supply a DbContext if you have one, in which case it will use the most efficient method and set the CurrentValue of the Entry Reference to null.

entity.SetToNull(e => e.ReferenceProperty, dbContext);

If no DBContext is supplied, it will lazy load first.

entity.SetToNull(e => e.ReferenceProperty);

Note, this issue is essentially a duplicate of: Entity Framework will only set related entity property to "null" if I first get the property and Setting a foreign key to null when using entity framework code first

Upvotes: -1

Xavave
Xavave

Reputation: 685

You can set all navigation properties to null like this : ( you need to have your EF Context),here : imported is IEnumerable < YourEntity>

  foreach (var entity in imported)
        {
            foreach (var np in _YourEntityRepository.GetReferenceProperties())
                entity.GetType().GetProperty(np.Name).SetValue(entity, null);
        }

with GetReferenceProperties defined as :

public IEnumerable<NavigationProperty> GetReferenceProperties()
    {

        var oc = ((IObjectContextAdapter)Context).ObjectContext;
        var entityType = oc.MetadataWorkspace.GetItems(DataSpace.OSpace)
                           .OfType<EntityType>()
                           .FirstOrDefault(et => et.Name == typeof(TEntity).Name);
        if (entityType != null)
        {
            foreach (NavigationProperty np in entityType.NavigationProperties
                    .Where(p => p.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One
                             || p.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne))
            {
                yield return np;
            }
        }
    }

Upvotes: 0

LucasM
LucasM

Reputation: 441

Starting with the Entity Framework 5.0:

db.Entry(project).Reference(p => p.ResponsibleUser).CurrentValue = null;

https://msdn.microsoft.com/en-us/data/jj713564.aspx

Upvotes: 3

NoPyGod
NoPyGod

Reputation: 5067

I ran into this problem and come up with a small "hack" that doesn't break lazy loading.

Simply define the property on your model like this -

    public int? AccountId { get; set; }

    //workaround for entity framework lazy loading problem

    Account account;

    public virtual Account Account
    {
        get
        {
            return account;
        }
        set
        {
            account = value;


            if (value == null)
            {
                AccountId = null;
            }

        }
    }

Now you don't have to eagerly load the navigation property, and setting it to null will work. More importantly, by putting the hack directly inside your entity, you won't have to remember to do explicit checks anywhere else in the codebase.

Upvotes: 0

Martin Dawson
Martin Dawson

Reputation: 7656

Figured out the best way to do this without having to eager load the navigation property so you can still use EF's Find() and not have to do a hack.

Use a primitive ID alongside the navigation property where the type is whatever the navigation properties ID type is (usually string for users), e.g:

public partial class LedProject
{
    public string ResponsibleUserId { get; set; }
    public virtual User ResponsibleUser { get; set; }
}

Update the string with the navigation property wherever you create the record and then when you want to remove the relationship just do ledProject.ResponsibleUserId = null.

If you name the id something other than navigation properties name + id at the end then you will need to use annotations or fluent api to map I think.

More info here: In what scenarios do I need foreign keys AND navigation properties in entity framework

Upvotes: 4

mheyman
mheyman

Reputation: 4325

Like boindiil said, the problem is with the lazy loading. However, you only have to load the property when you want to null it so the Entity Framework machinery will know it has changed. The code could look like:

responsibleUser = responsibleUser == null ? null : db.User.Find(responsibleUser.UserId);
if (responsibleUser == null)
{
    // load the value to assure setting it to null will cause a change
    var dummy = project.ResponsibleUser; 
}

project.ResponsibleUser = responsibleUser;
...
db.SaveChanges();

I keep thinking there should be a way to use db.ChangeTracker to force a save without the load but I haven't found it yet (and the few things I have tried seemed really hacky).

Upvotes: 6

boindiil
boindiil

Reputation: 5865

The problem lies in the lazy loading of the navigation property. It seems that the value is first set to null and afterwards loaded from the database. So the desired value (null in my case) is overridden by the currently stored value in the database.

LedProject project = db.LedProject
    .Include("ResponsibleUser")
    .Where(p => p.ProjectId == projectId)
    .FirstOrDefault();

This loads the ResponsibleUser when the Project is loaded. This finally solved my issue!

Upvotes: 53

Related Questions