10

Im writing an web application with MVC using Entity Framework for my backend logic. My problem is that I have an entity that has certain fields that should never be changed on an update. I am not really sure what the best way to solve this problem would be. There is going to be a lot of data processed in my application, so I cant afford to just hack up a solution.

Is it possible to just define the fields as readonly in the POCO entities ? Or should I write and entity framework extension class that validates all updates. Could it be done in the mapping files between EF and the actual database?

I am relatively new with EF, so I hope some of you might be able to give me some pointers!

Thanks!

Logard
  • 1,494
  • 1
  • 13
  • 27

3 Answers3

18

If you are using .NET 4.5 and EF 5 (i.e. MVC 4), you can simply set IsModified = false on the individual properties in question. This has the benefit of sticking close to the default out-of-the-box MVC conventions.

For example, if you have a CreatedBy field that shouldn't be touched when the record is updated, use the following in your controller:

[HttpPost]
    public ActionResult Edit(Response response)
    {
        if (ModelState.IsValid)
        {
            db.Entry(response).State = EntityState.Modified;
            db.Entry(response).Property(p => p.CreatedBy).IsModified = false;
            db.SaveChanges();
            return RedirectToAction("Index");
        }
        return View(response);
    }

Note that the IsModified line is the only change from the default controller action.

You MUST put this line AFTER setting .State = EntityState.Modified (which applies to the record as a whole and adds the record into the db context).

The effect is that EF will not include this column in the SQL UPDATE statement.

I am still (very) shocked that there are no [InsertOnly] or [UpdateOnly] attributes similar to [ReadOnly]. This seems like a major oversight by the MVC team. Am I missing something?

I'm not fully satisfied with this solution because it's a hack: You're telling EF that no change was made when what you really mean to say is "HANDS OFF". It also means that you have to use this code anyplace where the field could be updated. It would be better to have an attribute on the class property.

(Apologies for posting to an older thread, but I've not see this solution anywhere else. ViewModels are robust but a lot of work, and EF was supposed to make things easier, not harder...)

Neil Laslett
  • 2,019
  • 22
  • 22
  • Did you ever find another way to handle this? The example you've used is how I've done this -- and I'm sort of surprised there wasn't some fluent way to designate "hands off", as you say, so that the properties only persist during create/update. – Rostov Mar 25 '16 at 17:40
  • As far as I know, there is still no out-of-the-box way to annotate a model property as [UpdateOnly]. In my code, I ended up NOT using the IsModified = false method. Instead, I take the incoming model, fetch the existing record from the database (second instance of the same model), and then copy values for the fields that want to update. It's another form of whitelisting. If I were doing this in a lot of places (with the same mappings) I might consider a ViewModel with AutoMapper but for most cases I think doing it manually is easier. – Neil Laslett Mar 28 '16 at 15:16
  • Darn. I've just made methods that serve as the update "gateways" in an API fashion and rather just exclude the properties that should never be updated to avoid any missed fields / mistakes with the copying over from one to the next, since in some ways, I didn't want the extra DB hit of having to read in an object to then update it. Thanks for the reply. – Rostov Mar 29 '16 at 16:23
5

Well I would advice against ever using the EF classes in the View. You're best bet is to construct ViewModel classes and use Automapper to map them from the EF classes.

When you are updating records in the database though, you can control which fields in the ViewModel are used to update the existing fields in the EF class.

The normal process would be:

  • Use the Id to get the latest version of the existing object out of the database.

  • If you are using optimistic concurrency control then check that the object has not been updated since the ViewModel was created (so check timestamp for example).

  • Update this object with the required fields from your ViewModel object.

  • Persist the updated object back to the database.

Update to include Automapper examples:

Let's say your POCO is

public class MyObject 
{
   public int Id {get;set;}
   public string Field1 {get;set;}
   public string Field2 {get;set;}
}

and Field1 is the field you don't want updating.

You should declare a view model with the same properties:

public class MyObjectModel 
{
   public int Id {get;set;}
   public string Field1 {get;set;}
   public string Field2 {get;set;}
}

and Automap between them in the constructor of your Controller.

Mapper.CreateMap<MyObject, MyObjectModel>();

you can if you wish (although I prefer to do this manually, automap the other way too:

Mapper.CreateMap<MyObjectModel, MyObject>().ForMember(dest=>dest.Field1, opt=>opt.Ignore());

When you are sending date to your website you would use:

 var myObjectModelInstance = Mapper.Map<MyObject, MyObjectModel>(myObjectInstance);

to create the viewModel.

When saving the data, you'd probably want something like:

public JsonResult SaveMyObject(MyObjectModel myModel)
{
    var poco = Mapper.Map<MyObjectModel, MyObject>(myModel);
    if(myModel.Id == 0 ) 
    {
       //New object
       poco.Field1 = myModel.Field1 //set Field1 for new creates only

    }
}

although I'd probably remove the exclusion of Field1 above and do something like:

public JsonResult SaveMyObject(MyObjectModel myModel)
{
   var poco;
   if(myModel.Id == 0)
   {
     poco = Mapper.Map<MyObjectModel, MyObject>(myModel);
   }        
   else
   {
     poco = myDataLayer.GetMyObjectById(myModel.Id);
     poco.Field2 = myModel.Field2;
   }
   myDataLayer.SaveMyObject(poco);
}

note I believe that best-practise would have you never Automap FROM the ViewModel, but to always do this manually, including for new items.

BonyT
  • 10,750
  • 5
  • 31
  • 52
  • Thanks for a great answer! I should be able to make some improvements. The data comes in json format from the clients, and are translated into EF POCO's by the controller. I could weed out the data I do not want manually there, but is there any way to automatically specify that a field should be ignored? I am afraid there will be a lot of processing if not! – Logard Jul 23 '12 at 08:49
  • Adding in the extra View-Model layer, and mapping between that and your POCO's may seem like extra overhead, but it will make your life a lot simpler in the long-run - as I've said Automapper will automate the process one-way, so you only have to manually map the items the other way. Alternatively, although I recommend the manual option for clarity you can use automapper both ways, and specify overrides in your mapping for fields you don't want persisted. - http://automapper.org/ – BonyT Jul 23 '12 at 09:10
  • Thanks! This is a great answer. I relate this to my project, and i think Ill be able to overcome the problems im working on. Cheers! – Logard Jul 23 '12 at 10:25
1

I just asked a very similar question, and I believe the answer to that one may help out a lot of folks who stumble across this one as well. The OP mentions that these are fields that should never change, and using PropertySaveBehavior.Ignore ensures this. With the existing answers to this question, you need to make custom save methods or introduce mapping where it might not make sense. By setting the AfterSave property behavior instead, you can prevent this from being possible in EF altogether.

In my project, I am generically accessing a property that is on an abstract class so I have to set it like this:

MyProperty.SetAfterSaveBehavior(PropertySaveBehavior.Ignore);

If you're accessing it directly on a known class, you'd use this:

...
  .Property(e => e.YourProperty)
  .Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Ignore);
user15716642
  • 171
  • 1
  • 12