Reputation: 1401
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
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
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
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
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
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
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
Upvotes: 3
Reputation: 12898
internal
access modifierYou 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 modifierThis 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 modifierYou 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; }
}
internal
member is accessible only within the same assemblyprotected
member is accessible within its class and by derived class instances.protected internal
member can be accessed from the current assembly or from types that are derived from the containing class.Upvotes: 2
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
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; }
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