45

I'm using the database first implementation of Entity Framework Code First as the data layer for a project, but I've run into a problem.

I need to be able to set a foreign key to null in order to remove an association in the database.

I have 2 objects. One is called Project.

public class Project
{
    public int ProjectId {get; set;}
    public Employee Employee {get;set;}
}

public class Employee
{
    public int EmployeeId {get; set;}
    public string EmployeeName {get;set;}
}

This matches what I have in the Database:

CREATE TABLE Project(
    ProjectId int IDENTITY(1,1) NOT NULL,
    EmployeeId int NULL
)

CREATE TABLE Project(
    EmployeeId int IDENTITY(1,1) NOT NULL,
    EmployeeName varchar(100) NULL
)

I can assign an Employee to a project. However, I want to be able to remove an employee from a project and have the Employee field be null. In my UI this will show as 'No EMployee Assigned'.

However, short of a direct sql query, I cannot seem to find a way to do this in the entity framework 4.1.

I've tried:

public void RemoveEmployeeFromProject(int projectId)
{
    var project = Context.Projects.FirstOrDefault(x => x.ProjectId == projectId);
    project.Employee = null;
    Context.SaveChanges();
}

But this doesn't do anything.

Does anyone have any ideas?

gunr2171
  • 16,104
  • 25
  • 61
  • 88
mccow002
  • 6,754
  • 3
  • 26
  • 36

6 Answers6

73

I think the problem is that as far as the context is concerned, you haven't actually changed anything.

You can use the lazy loading approach previously suggested by using virtual, but since you haven't requested that the Employee be loaded yet, it's still null. You could try this:

var forceLoad = project.Employee;
project.Employee = null; // Now EF knows something has changed
Context.SaveChanges();

Alternatively, explicitly include it in your original request:

var project = Context.Projects.Include(x => x.Employee).FirstOrDefault(x => x.ProjectId == projectId);
project.Employee = null;
Context.SaveChanges();

On a side note, FirstOrDefault will return null if no Project matches the given id. If you know the project exists, you can just use First. You could even use Single which will assert that there is only one such project. If you continue to use FirstOrDefault, I'd recommend checking for null before working with project.

David Ruttka
  • 14,269
  • 2
  • 44
  • 39
  • 1
    Yup, that was the issue. Thanks! – mccow002 Jun 21 '11 at 15:16
  • 2
    Had the same problem, very helpful, got my +1 – Paul Stovell Aug 28 '11 at 16:33
  • 2
    I consider this to be a bug in EF. See issue https://entityframework.codeplex.com/workitem/2074 and related SO question http://stackoverflow.com/questions/21692401/entity-framework-will-only-set-related-entity-property-to-null-if-i-first-get – Rob Kent Sep 24 '14 at 10:05
  • Lazy load was the culprit, wasted a little time, but this explained and fixed. – ransems Sep 28 '17 at 15:21
  • @David Ruttka how this will work when we use Store Procedure var list = _db.Database.SqlQuery("spGetProjectInfo @EmployeeID", sqlParams).ToListAsync() I am getting list.Employee = null how I can get employees in projects – Pritish Mar 16 '18 at 05:12
  • This is still valid on EntityFrameworkCore 2.2.2 / .NET Core 2.2 I'm using virtual field and I was not able to nulify this field if it's not included before. After adding an "xxxx.Include(a => a.myField)", then the "myEntity.myField = null" is working like a charm. – Xavierh Mar 19 '19 at 08:35
  • This fixed my problem, thanks! It would explain why my code worked in the debugger but not standalone (examining the foreign key entity in the debugger would load it). I wish they'd add intellisense for this. – Richard Moore Nov 21 '19 at 12:11
  • Took a second to grok. B/c the query to the parent entry didn't explicitly include the child, **EF was going on the assumption that the child was null**, so setting the child to null looked like no change. But if I set the child to a value -- no matter if the value looked liked what's in the database for the child or not -- EF said, "Hey, that's not null. Update!" **That is, updates would work for "truthy" child values** (any non-null value), but setting to null doesn't trigger EF _until you've pulled the child's original [non-null] value for EF to compare against_. null != [object] so update! – ruffin Feb 20 '20 at 15:40
14

You can do it this way, which means you don't have to load the related entity.

context.Entry(Project).Reference(r => r.Employee).CurrentValue = null;
Sprintstar
  • 7,938
  • 5
  • 38
  • 51
  • I like this solution. While verbose, it self-documents what's going on. The accepted answer of forcing a lazy-load or eager loading and then clearing the property is a bit hacky to me, and has the potential to carry a significant penalty due to unnecessary loads. Thanks. – Jason Tyler Mar 22 '18 at 20:02
4

The answer to this is quite simple. EF can't infer the type given the information you've provided.

Just do this instead:

public void RemoveEmployeeFromProject(int projectId)
{
    var project = Context.Projects.FirstOrDefault(x => x.ProjectId == projectId);
    project.EmployeeId = (int?)null;
    Context.SaveChanges();
}

and it will work.

0

You need to include in the linq query, the property to assign, using the same name it has in the Project class:

var project = Context.Projects.Include("Employee").FirstOrDefault(x => x.ProjectId == projectId);
ndarriulat
  • 749
  • 1
  • 9
  • 11
0

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);
jonh
  • 233
  • 1
  • 10
0

if you enable lazy loading by making the employee property virtual does it work?

public class Project
{
    public int ProjectId {get; set;}
    public virtual Employee Employee {get;set;}
}

i'd also suggest encapsulating the remove method as part of your poco class to making the meaning more clear. see this article for more details on that.

public class Project
{
    public int ProjectId {get; set;}
    public virtual Employee Employee {get;set;}
    public void RemoveEmployee()
    {
        Employee = null;
    }
}
David Wick
  • 7,055
  • 2
  • 36
  • 38
  • Unfortunately, no, that doesn't work. I think part of the reason is because with lazy loading enabled, values that you don't request are set to null. So when you call Context.SaveChanges, it must ignore null values so it doesn't replace actual values with values it didn't load. This makes sense until you want to actually set a value to null. – mccow002 Jun 21 '11 at 13:04
  • i figured it had something to do with lazy loading. – David Wick Jun 21 '11 at 19:51