13

Given a database table with a column that contains historic data but that is no longer populated, is there a way in Entity Framework to read the column but prevent it being updated when using the same model object?

For example I have an object

public class MyObject
{
    public string CurrentDataColumnName { get; set; }
    public string HistoricDataColumnName { get; set; }
}

From the documentation I don’t believe I can do either of the following, because this will stop EF reading the data as well as persisting it.

(1) Decorate the HistoricDataColumnName property with the following attribute

[NotMapped]

(2) Add the following to my EntityTypeConfiguration for MyObject

Ignore(x => x.HistoricDataColumnName)
Salah Akbari
  • 39,330
  • 10
  • 79
  • 109
Greg Trevellick
  • 1,361
  • 1
  • 16
  • 26

9 Answers9

9

You can mark the column as computed to prevent Entity Framework from updating / inserting into that column.

[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public string HistoricDataColumnName { get; set; }

DatabaseGenerated

An important database features is the ability to have computed properties. If you're mapping your Code First classes to tables that contain computed columns, you don't want Entity Framework to try to update those columns. But you do want EF to return those values from the database after you've inserted or updated data. You can use the DatabaseGenerated annotation to flag those properties in your class along with the Computed enum. Other enums are None and Identity.

Aducci
  • 26,101
  • 8
  • 63
  • 67
  • 2
    But by decorating `HistoricDataColumnName` property with `DatabaseGenerated(DatabaseGeneratedOption.Computed)` it inserts *NULL* into the `HistoricDataColumnName`. So is this a good option when using `Insert` statement? Since the post mentioned prevent it from being **updating** and not **inserting**. What if he wants to insert a value for `HistoricDataColumnName` from inputs? Regardless what he filled, it is stored as NULL in the Database. – Salah Akbari Feb 18 '17 at 07:24
  • @S.Akbari OP says that this column is no longer populated, so inserting null is fine. – Evk Feb 18 '17 at 09:48
  • @Evk But the title says: *prevent it being **updated***. I think he just wants to prevent from updating. BTW I hope the OP will clarify this. – Salah Akbari Feb 18 '17 at 09:53
  • In addition you can also manage the property with a private backing field so also the library users won't be able to set the value of the property. – bubi Feb 18 '17 at 11:54
  • 3
    The great drawback of this is that EF will read the value of the column on each insert and update. This may have a significant performance impact. – Gert Arnold Feb 19 '17 at 00:19
  • @GertArnold Of course you are right! But people don't even realize that (although it's mentioned in the pasted documentation paragraph in the post) and upvote the answer like crazy just because it's *simple* to *implement* (?!) – Ivan Stoev Feb 22 '17 at 20:12
  • 3
    @GertArnold - Calling this a _great drawback_ is a little bit of a stretch. I would be surprised if there was a noticeable impact on performance – Aducci Feb 23 '17 at 17:35
  • 3
    Well, there is. Not with only a few updates of course, but I've come across a case where I had to create a specialized context without computed column mappings to facilitate larger insert and update tasks. In this question it's about a string column of unknown size, which *may* be considerable. The least you can say is that it deserves attention. Also, the first comment, by Akbari, makes me doubt whether this is the right approach, but OK, too bad OP nor vaindil respond to that. – Gert Arnold Feb 23 '17 at 19:43
  • I intended for preventing both insert AND update, but I did not make that clear and the OP hasn't either. This is a good answer, but it appears from my reading elsewhere and from other comments that using a private setter will solve this problem. The value cannot be set, and EF is still able to map to it. – vaindil Feb 24 '17 at 20:45
4

Codewise you can set the setter simply to protected. EF useses reflection to materialize your model. I think the now hidden setter also shows to every other programmer, that the field should not be modified any longer.

Also add an [Obsolete]-attribute with further information, why the property can't be set from the public anymore.

yan.kun
  • 6,820
  • 2
  • 29
  • 38
  • This appears to be the best way to accomplish this. The value can't be set anywhere, but EF is still able to access it. Thank you! – vaindil Feb 24 '17 at 20:49
  • @vaindil This doesn't work for me, even after the set the property as Protected setter, while saving entity framework inserts null into the field. – Pரதீப் Mar 08 '23 at 07:19
4

You can simply use IsModified to check whether a specific entity property was modified or not and by this way you can still Read,Insert and Delete data:

var item = context.MyObjects.Find(id);
item.CurrentDataColumnName = "ChangedCurrentDataColumnName";
item.HistoricDataColumnName = "ChangedHistoricDataColumnName";
context.Entry(item).Property(c => c.HistoricDataColumnName).IsModified = false;
context.SaveChanges();

By using IsModified = false you are excluding the HistoricDataColumnName property from updating, so the HistoricDataColumnName column will not be updated in the database but other properties will be updated.

Setting this value to false for a modified property will revert the change by setting the current value to the original value. If the result is that no properties of the entity are marked as modified, then the entity will be marked as Unchanged. Setting this value to false for properties of Added, Unchanged, or Deleted entities is a no-op.

Check the following answer as a supplementary explanation. It might be helpful also:

https://stackoverflow.com/a/13503683/2946329

Community
  • 1
  • 1
Salah Akbari
  • 39,330
  • 10
  • 79
  • 109
  • 1
    http://stackoverflow.com/questions/11608905/what-is-the-best-way-to-prevent-updating-on-specific-fields-in-entity-framework - Just check this out – Kush Feb 23 '17 at 06:12
3

Since you say 'at the EF level or lower' a possible solution is to use a trigger to either raise an error if an attempt is made to change the column, or allow the update but ignore the change on the column of interest.

Option 1 - raise an error

CREATE TRIGGER MyTable_UpdateTriggerPreventChange
    ON dbo.Table1
    AFTER UPDATE
AS 
BEGIN
    SET NOCOUNT ON;

    if update(HistoricDataColumnName) 
    begin
        raiserror (50001, 16, 10)
    end
END

Option 2 - ignore the change

CREATE TRIGGER MyTable_UpdateTriggerIgnore
   ON dbo.Table1
   INSTEAD OF UPDATE
AS 
BEGIN
    SET NOCOUNT ON;

    update dbo.Table1 set HistoricDataColumnName=inserted.HistoricDataColumnName
    from inserted
    where inserted.Id = dbo.Table1.Id
END

You could of course do something similar for inserts if required.

Alternatively to raiserror use 'throw'

ALTER TRIGGER MyTable_UpdateTriggerPreventChange
    ON dbo.Table1
    AFTER UPDATE
AS 
BEGIN
    SET NOCOUNT ON;

    if update(HistoricDataColumnName) 
    begin
        throw 50002, 'You can''t change the historic data', 1
    end
END

either way you'll get an exception thrown. This is using LinqPad

enter image description here

Phil
  • 42,255
  • 9
  • 100
  • 100
  • Does EF catch the error of option 1 when the column is updated? I mean, is this part of the transaction? –  Feb 22 '17 at 15:03
2

For just on column this is overkill, but in general you can override SaveChanges in DbContext to have more control on the changes.

In your model:

public override int SaveChanges()
{
    var modifiedEntries = base.ChangeTracker.Entries<MyObject>()
        .Where(e => e.State == EntityState.Modified).ToList();

    foreach (var entry in modifiedEntries)
    {
         // Overwriting with the same value doesn't count as change.
         entry.CurrentValues["HistoricDataColumnName"] = entry.OriginalValues["HistoricDataColumnName"];
    }
    return base.SaveChanges();
}

But you could also undo all modifications by changing the state from modified to unchanged.

-- UPDATE --

There is one thing that worries me. As soon as a developer has the credentials to access the database you cannot prevent them from doing things you don't want. They could create their own model or query the database directly.

So I think the most important thing to do is to set the field to readonly in the database for the client. But you may not be able to lock one column.

Even if this is not an issue, I think (for design) it is better to move all historical data to other tables. Making it easy to grant readonly access only. You can map these tables 1:1. With Entity Framework you can still access the historical information quite easy.

But in that case you won't have the problem you have now and will give you other options to prevent others from changing the historical information.

2

internal access modifier

You could change the setter to internal

public class MyObject
{
    public string CurrentDataColumnName { get; internal set; }
    public string HistoricDataColumnName { get; internal set; }
}

This doesn't impose as much limitations as the other options, but depending on your requirements, this can be quite useful.

protected access modifier

This would probably be the most common usage of making a property in EF "read-only". Which essentially only allows the constructor to access the setter (and other methods within the class, and classes derived from the class).

public class MyObject
{
    public string CurrentDataColumnName { get; protected set; }
    public string HistoricDataColumnName { get; protected set; }
}

I think protected is what you're looking for.

protected internal access modifier

You can also combine the two like this, to make it protected or internal

public class MyObject
{
    public string CurrentDataColumnName { get; protected internal set; }
    public string HistoricDataColumnName { get; protected internal set; }
}

Access Modifier Refresher Course

  • A internal member is accessible only within the same assembly
  • A protected member is accessible within its class and by derived class instances.
  • A protected internal member can be accessed from the current assembly or from types that are derived from the containing class.
Svek
  • 12,350
  • 6
  • 38
  • 69
2

The question is about EF 6, but this is easily doable in EF Core with the Metadata.IsStoreGeneratedAlways property. Thanks to ajcvickers on the EF Core repo for the answer.

modelBuilder
    .Entity<Foo>()
    .Property(e => e.Bar)
    .ValueGeneratedOnAddOrUpdate()
    .Metadata.IsStoreGeneratedAlways = true;
vaindil
  • 7,536
  • 21
  • 68
  • 127
  • 1
    This [too](http://stackoverflow.com/questions/30777503/entity-framework-to-read-a-column-but-prevent-it-being-updated/42308586?noredirect=1#comment71797702_42308586) will cause EF to read the value on each insert and update. – Gert Arnold Feb 23 '17 at 19:37
  • Didn't want to down vote this answer but this is now obsolete as referenced [here](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.metadata.imutableproperty.isstoregeneratedalways?view=efcore-2.1) in favor of [BeforeSaveBehavior](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.metadata.imutableproperty.beforesavebehavior?view=efcore-2.1) and [AfterSaveBehavior](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.metadata.imutableproperty.aftersavebehavior?view=efcore-2.1) – Lance May 19 '19 at 18:05
0

Why do this in EF in the first place? Why not simply ensure that any login being used to access the database either has the rights for performing UPDATE/INSERT/DELETE revoked or even go to the extreme of setting the database to READ_ONLY in the Database options?

It seems to me that any attempt to prevent updates via EF is doomed as you can always circumvent that and, for example, just execute SQL code directly against the EF connection.

SpaceUser7448
  • 189
  • 1
  • 10
0

As for me, it's simple solution - make property setters as private:

public class MyObject
{
    public string CurrentDataColumnName { get; private set; }
    public string HistoricDataColumnName { get; private set; }
}

EF will materialize objects from database without any problem, but yout won't have any way to change value int these properties.

Backs
  • 24,430
  • 5
  • 58
  • 85