Greg Trevellick
Greg Trevellick

Reputation: 1401

Entity Framework to read a column but prevent it being updated

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)

Upvotes: 13

Views: 8084

Answers (9)

yan.kun
yan.kun

Reputation: 6908

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.

Upvotes: 4

Backs
Backs

Reputation: 24913

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.

Upvotes: 0

Salah Akbari
Salah Akbari

Reputation: 39976

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

Upvotes: 4

SpaceUser7448
SpaceUser7448

Reputation: 189

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.

Upvotes: 0

vaindil
vaindil

Reputation: 7864

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;

Upvotes: 2

Phil
Phil

Reputation: 43021

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

Upvotes: 3

Svek
Svek

Reputation: 12898

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.

Upvotes: 2

user4864425
user4864425

Reputation:

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.

Upvotes: 2

Aducci
Aducci

Reputation: 26694

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.

Upvotes: 9

Related Questions