3

I have a status field on a class that has an ID and a Name. I'm not using an enum to model it, but rather a class with some static values, like this:

    public class MailoutStatus : IEntity
{
    public static MailoutStatus Draft = new MailoutStatus() { Id = 1, Name = "Draft" };
    public static MailoutStatus Scheduled = new MailoutStatus() { Id = 2, Name = "Scheduled" };
    public static MailoutStatus Cancelled = new MailoutStatus() { Id = 3, Name = "Cancelled" };
    public static MailoutStatus Sent = new MailoutStatus() { Id = 4, Name = "Sent" };

    public int Id { get; set; }
    public string Name { get; set; }
...
}

Now I want to set this status value on the object it describes, like so:

        var repo = new MailoutRepository();
        var mailout = repo.Get(1);
        mailout.Status = MailoutStatus.Cancelled;
        repo.Update(mailout);
        repo.CommitChanges();

However, this code will see MailoutStatus.Cancelled as a new entity and will insert a new row into the MailoutStatus table, ignoring the ID that is already on Cancelled and adding a new IDENTITY generated ID (for instance, 5). I can prevent this by adding an entityvalidation stuff, but that just makes the above blow up due to the validation failure.

I can work around the issue using this code:

    var repo = new MailoutRepository();
    var mailout = repo.Get(1);
    mailout.Status = new MailoutStatusRepository().Get(MailoutStatus.Cancelled.Id);
    repo.Update(mailout);
    repo.CommitChanges();

This works because now Entity Framework knows about the MailoutStatus that I'm fetching and is tracking its state, etc. But it's really crappy to have to write that much code just to set a status. I also don't want to use an enum for other reasons and I don't want MailoutStatus to know anything about persistence. Any ideas?

ssmith
  • 8,092
  • 6
  • 52
  • 93

3 Answers3

1

Here's how I solved it.

I defined an attribute named NotTrackedAttribute and apply that on entities like Status. Then override the SaveChanges method of the derived context as follows. Reset the tracked changes to those entities

    public override int SaveChanges()
    {
        var changedEntities = ChangeTracker.Entries();

        foreach (var changedEntity in changedEntities)
        {
             var entity = changedEntity.Entity;

             //ignore the types that are marked as NotTracked
             if (Attribute.IsDefined(entity.GetType(), typeof(NotTrackedAttribute)))
             {
                 changedEntity.State = EntityState.Unchanged;
                 continue;
             }
        }

        return base.SaveChanges();
    }

The attribute

/// <summary>
/// Indicates that a Type having this attribute should not be persisted.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class NotTrackedAttribute : Attribute
{
}

Then use it as follows

[NotTracked]
public class MailoutStatus
{

}
Eranga
  • 32,181
  • 5
  • 97
  • 96
  • You NotTrackedAttribute is just a marker - doesn't actually do anything - right? Still, can you share its implementation here? – ssmith Jul 27 '11 at 16:47
  • Unfortunately this isn't working. The attribute works as expected, but the call to set changedEntity.State to Unchanged fails: – ssmith Jul 27 '11 at 20:19
  • Here's the error: System.InvalidOperationException : AcceptChanges cannot continue because the object's key values conflict with another object in the ObjectStateManager. Make sure that the key values are unique before calling AcceptChanges. at System.Data.Objects.ObjectStateManager.FixupKey(EntityEntry entry) at System.Data.Objects.EntityEntry.AcceptChanges() at System.Data.Objects.EntityEntry.ChangeState(EntityState state) – ssmith Jul 27 '11 at 20:19
  • @ssmith my `DbContext`s are short lived(per request) so i did not have the problem of duplicates. you can try detaching the entity loaded from the database an then make the static object `Unchanged` – Eranga Jul 28 '11 at 01:04
1

You're already duplicating what's in the database. If you change your model to now just have an integer status, then you can change the MailoutStatus to a static int and it will just work.

In other words, what are you gaining by having MailoutStatus as another entity, when in fact it's just a lookup value?

Ben Scheirman
  • 40,531
  • 21
  • 102
  • 137
  • I realize that's violating DRY a bit, but for reports and other purposes it's handy to have the data in the database, and just storing the name values in the Mailout table isn't an option either, unfortunately. Given how often these values might change, I'm OK with the DRY violation - keeping them in sync is trivial. – ssmith Jul 27 '11 at 16:47
  • Indeed, so having it as an integer with a strongly-typed accessor gives you the the same option you have now, but EF will leave it alone. – Ben Scheirman Jul 27 '11 at 16:51
  • Ah, but I'm also having EF generate my DB schema, which is why it knows about it at all. I suppose I could just generate it myself for this table, since I'm getting close to the point where I'm going to be hand-updating the schema anyway. – ssmith Jul 27 '11 at 17:03
0

Now EF is supporting enums. http://blogs.msdn.com/b/efdesign/archive/2011/06/29/enumeration-support-in-entity-framework.aspx. In code first you can have a discriminater column to map enum. Or else this is a good solution Enums with EF code-first - standard method to seeding DB and then using?

Community
  • 1
  • 1
Jayantha Lal Sirisena
  • 21,216
  • 11
  • 71
  • 92