Entity Framework Auditing using ObjectContext on SaveChanges - c#

For audit logging purposes, I need to get values of all the columns, including FK entities and relational entities that have been modifed for one of the table in the database. Database is basically for a website where user can upload resources (files, online document, picture etc.), I've a table called Material which has multiple many-2-many nd one-2-one relations like Material - Audience, Material - Category, 'Material-Uploader', 'Material-PermissionMaterial -Tags etc. I want to log all the changes happening to a Material. For example if someone removes a Tag from a Material, then I need to log:
[User12 - 12/12/12] - Happy tag got removed from Crappy material.
So far I got this: I can get all the ObjectStateEntries which are modified, added, deleted by using:
context.ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified)
Now, I can check whether this ObjectStateEntry is RelationShip or not using:
if (e.IsRelationship) {
HandleRelationshipEntry(e);
}
else {
HandleEntry(e);
}
Within HandleEntry method (Entry is not relationship entry), I can check the type of Entry, in my case it is Material, so I'm doing:
// We care about only Material which are modifed
if (e.State != EntityState.Modified || !(e.Entity is Material))
return;
Once, I know Entry is of type Material Entry, I can get all the columns that have changed for Material table using:
e.CurrentValues[ARCHIVE_COLUMN].ToString() != e.OriginalValues[ARCHIVE_COLUMN].ToString()
At this point, I can log all the non FK changes of Material table. But if column is FK to some other entity, I cannot resolve that FK value to corresponding Entity. I could just know that CategoryID has been changed from 42 to 76 but I cannot resolve name of the Category itself. I tried approach like casting DBDataRecord and CurrentValueRecord to EntityKey but it is just NULL. Is there any way to resolve these FKs to Entities using ObjectStateManager?
My full code for the reference:
private class SingleMaterialLogger {
MaterialAuditData auditData = new MaterialAuditData();
public void HandleEntity(ObjectStateEntry e, ObjectContext context) {
HandlePrimaryTypeChanges(e);
HandleComplexTypeChanges(e, context);
}
private void HandleComplexTypeChanges(ObjectStateEntry e, ObjectContext c) {
// Owner, Category, Contact
ChangeValueHelper(e, CONTACT_COLUMN, (k1, k2) => {
// get old value
User old = c.GetObjectByKey(k1) as User;
User current = c.GetObjectByKey(k2) as User;
});
}
public void HandlePrimaryTypeChanges(ObjectStateEntry e) {
// Name, Description, ArchiveDate, Status
// Again no reflection is used - So change them if column name changes
ChangeValueHelper<string>(e, NAME_COLUMN, (change) => auditData.Name = change);
ChangeValueHelper<string>(e, NAME_COLUMN, (change) => auditData.Description = change);
// TODO - Fix change value helper
if (e.CurrentValues[ARCHIVE_COLUMN].ToString() != e.OriginalValues[ARCHIVE_COLUMN].ToString()) {
auditData.ArchiveDate = new Change<DateTime?>(e.OriginalValues[ARCHIVE_COLUMN] as DateTime?, e.CurrentValues[ARCHIVE_COLUMN] as DateTime?);
}
}
private void ChangeValueHelper(ObjectStateEntry e, string columnName, Action<EntityKey, EntityKey> func) {
if (e.CurrentValues[columnName].ToString() != e.OriginalValues[columnName].ToString()) {
func(e.OriginalValues[columnName] as EntityKey, e.CurrentValues[columnName] as EntityKey);
}
}
private void ChangeValueHelper<T>(ObjectStateEntry e, string columnName, Action<Change<T>> func) where T : class {
if(e.CurrentValues[columnName].ToString() != e.OriginalValues[columnName].ToString()) {
func(new Change<T>(e.OriginalValues[columnName] as T, e.OriginalValues[columnName] as T));
}
}
}
Dictionary<EntityKey, SingleMaterialLogger> singleMaterialLoggerMap = new Dictionary<EntityKey, SingleMaterialLogger>();
private ObjectContext context;
public MaterialAuditLogger(ObjectContext context) {
this.context = context;
}
public void AuditMaterialChanges() {
// Grab everything thats being added/deleted/modified
foreach(var e in context.ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified)) {
if (e.IsRelationship) {
HandleRelationshipEntity(e);
}
else {
HandleEntity(e);
}
}
}
private void HandleEntity(ObjectStateEntry e) {
// We care about only Material which are modifed
if (e.State != EntityState.Modified || !(e.Entity is Material))
return;
var logger = SingleLogger(e.EntityKey);
logger.HandleEntity(e, context);
}
private void HandleRelationshipEntity(ObjectStateEntry e) {
// relations whose entity keys contains
}
private SingleMaterialLogger SingleLogger(EntityKey key) {
if(singleMaterialLoggerMap.ContainsKey(key))
return singleMaterialLoggerMap[key];
SingleMaterialLogger logger = new SingleMaterialLogger();
singleMaterialLoggerMap[key] = logger;
return logger;
}

I've run into the same problem.
It's not difficult to pull any entity type with an id value:
DbContext.Set(entityType).Find(id)
However this assumes that you have identified the entity type from the relevant navigation property in the first place. That requires some smarts, basically duplicating the EF logic by using reflection to look at property names and [ForeignKey()] attributes etc.
Some options are:
1) Add smarts to work out the FK model property from the FK ID property. Then do a lookup of the FK model on the fly during the audit log creation process, and store down the .ToString() value in the audit log.
This assumes:
You have a general utility in your DataContext/Repository to lookup any model type on the fly (eg. DbContext.Set(entityType).Find(id))
You are confident that the .ToString() implementation on all your FK models will work reliably because of one of the following:
They never rely on further navigation properties which may cause a run-time error
You can be confident the further navigation properties were properly Include()'ed in your model lookup
You have lazy loading enabled (which I strongly advise against.... but.. it would help here)
You have thought through the transaction implications (if you are using transactions beyond what EF does)
2) Store down the FK ID in the audit log. Then, when viewing the audit log, do a lookup of the FK model on the fly and render ToString() on screen.
We went with this option in our project and it works fine.
However your auditing requirements may be stricter. For example, if someone changes the name/description on the FK model, then this will appear to modify the old audit logs.

Related

Generate and auto increment the Id in Entity Framework database first

I have a table CampaignLanguage. The primary key is Id. It should be auto increment.
So I have the code:
public partial class CampaignLanguage
{
public CampaignLanguage()
{
this.CampaignLanguageQuestions = new HashSet<CampaignLanguageQuestion>();
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
Then in the controller, I want to save the generated object.
[HttpPost]
public ActionResult Save(int clientId, int campaignId)
{
var campaign = CampaignService.GetCampaignById(campaignId);
var campaignLanguage = campaign.CampaignLanguages.Where(x => x.CampaignId == campaignId).FirstOrDefault();
if (campaignLanguage != null)
{
campaignLanguage.WelcomeMessage = message;
CampaignService.Save(campaignLanguage);
}
else
{
campaignLanguage = new CampaignLanguage();
campaignLanguage.Id = 1;
CampaignService.Save(campaignLanguage);
}
return Redirect("/Campaign/Index/" + clientId);
}
However, I get the error.
{"Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. Refresh ObjectStateManager entries."}
I don't want to change my CampaignService.Save method. So how to fix it?
EDIT
public void Save(CampaignLanguage campaignLanguage)
{
_campaignLanguageRepository.Update(campaignLanguage);
_unitOfWork.Commit();
}
EDIT 1
public virtual void Add(T entity)
{
dbset.Add(entity);
}
public virtual void Update(T entity)
{
dbset.Attach(entity);
dataContext.Entry(entity).State = EntityState.Modified;
}
You should be calling Add instead of Update as this is a new instance you want to insert into the data store. Your Save method should check if the primary (auto incremented key) has a value greater than 0 to see if it is new or not. Alternatively you can also see if it is already attached. There is also no need to call Update, setting the entity to state modified does nothing except ensure that all properties will be written back to the DB but the DbContext implements change tracking on entities so this will already happen (unless you are working in a detached state).
public void Save(CampaignLanguage campaignLanguage)
{
if(campaignLanguage.Id == 0)
_campaignLanguageRepository.Add(campaignLanguage);
else
_campaignLanguageRepository.Update(campaignLanguage);
_unitOfWork.Commit();
}
On a side note: The type DbContext already implements a Unit of Work pattern and DbSet<T> is an implementation of a Repository pattern. There should not be any need to add another customer Unit of work and repository pattern around EF, you are just creating a whole abstraction layer for no reason that will problems with readability as well as issues later when you want to perform more complex operations like joining multiple tables together in a query.
Unfortunately, you need to change your CampaignService.Save. You are trying to update an inexistent campaignLanguage object.
The other problem is you are trying to force a key into an Identity column. You cannot do it with out first set insert_identy to the table.
Maybe, you need to ask for the correct method of CampaignService.

Entity Framework validation fails on unused property update

I have a problem that I can't seem to understand, let alone solve. Any help would be appreciated.
So I have an MVC application, Entity Framework, the usual. I have a request that can update one property of one entity. This request sends the Id of the entity and the new value of the property.
In order to avoid querying the database for the entity, and since I only want to do the update on that one property and then return, I attach a new entity to the context with the id and then set the CurrentValue. Something like this:
public class MyEntityHandler
{
// dbContext is my EF context, instantiated from a DI container
// ...
public void UpdateProperty<TProp>(MyEntity entity, Expression<Func<MyEntity , TProp>> property, TProp value)
{
var memberName=(property.Body as MemberExpression).Member.Name;
var temp = dbContext.ChangeTracker.Entries<MyEntity>()
.SingleOrDefault( o => o.Entity.Id == entity.Id );
if( temp == null || temp.State == EntityState.Detached)
{
dbContext.MyEntities.Attach( entity );
}
this.dbContext.Entry(entity).Property(memberName).IsModified = true;
this.dbContext.Entry(entity).Property(memberName).CurrentValue = value;
}
}
Then, this can be used like this: UpdateProperty(e, e=>e.Prop, "NewValue");
where e is of type MyEntity, and Prop is a string property in it.
When the post request comes in, I simply create a new MyEntity, I assign the id and then call this method. Something like this (there are a couple more layers, but it makes no difference for my question):
public ActionResult MyMethod(int id, string newValue)
{
var e=new MyEntity { MyEntityId=id };
new MyEntityHandler().UpdateProperty(e,e=>e.Prop,newValue);
return View();
}
My understanding is that in this case, EF should attach the entity in unchanged state. The entity in my case is not in the context and the attach succeeds. Then I update that one single property, which will put the entity into Modified state, so an update statement should be generated on SaveChanges(). Since only the one property is changed, that should be the only one in the update statement. I double checked the values in the ChangeTracker, I can see that the entity is modified and the property is modified, but every other property i not modified.
The problem is that when I call SaveChanges(), I get a DbValidationException, because one of the other properties is null, but it has a Required attribute. This is all rightfully so - since I attach a new entity and set only the id before attaching, it should be null. I just don't understand the validation error - I'm not trying to insert that value into the database (again, I checked the ChangeTracker and the state of the entity is modified and the property in question is not modified).
Why do I get this validation error? Is this by design? Is there a way to tell EF to let this save through (without disabling the validation altogerher on the context)?

NHibernate: Generate Custom Primary Key in OnPreInsert

In my OData service I have to create a custom primary key in the OnPreInsert event handler.
I know I can't use #event.Id to assign the key because it doesn't expose the setter property.
I used the reflection to set the value of this property as shown below:
public bool OnPreInsert(PreInsertEvent #event)
{
if(#event.Entity is MyEnity)
{
var myEntity = #event.Entity as MyEnity;
string newKey = GetCustomKey(...);
myEntity.myId = newKey;
var property = typeof(AbstractPreDatabaseOperationEvent).GetProperty("Id");
if (property != null)
{
property.SetValue(#event,newKey);
}
}
return false;
}
During the debug mode I can see that the value of #event.Id is initialized properly, however the key saved in the database is not the one I generated in the OnPreInsert event handler.
What am I doing wrong here?
Please, try to check this recent Q&A:
NHibernate IPreUpdateEventListener, IPreInsertEventListener not saving to DB
The point is, that as described here:
NHibernate IPreUpdateEventListener & IPreInsertEventListener
...Here comes the subtlety, however. We cannot just update the entity state. The reason for that is quite simple, the entity state was extracted from the entity and place in the entity state, any change that we make to the entity state would not be reflected in the entity itself. That may cause the database row and the entity instance to go out of sync, and make cause a whole bunch of really nasty problems that you wouldn’t know where to begin debugging.
You have to update both the entity and the entity state in these two event listeners (this is not necessarily the case in other listeners, by the way). Here is a simple example of using these event listeners...
I couldn't find some way to use the reflection to achieve what I described in my question above. I tried to use reflection because I didn't know about the Generators available in NHibernate (as I am new to NHibernate).
I have a table named sys_params which holds the next key values for different tables. My target was to fetch the next key for my table my_entity, assign it to the primary key of the new record, increment the next key value in the sys_params table and save the new record into the database.
To achieve this first I defined following classes.
public class NextIdGenerator : TableGenerator
{
}
public class NextIdGeneratorDef : IGeneratorDef
{
public string Class
{
get { return typeof(NextIdGenerator).AssemblyQualifiedName; }
}
public object Params
{
get { return null; }
}
public Type DefaultReturnType
{
get { return typeof(int); }
}
public bool SupportedAsCollectionElementId
{
get { return true; }
}
}
And then in my mapping class I defined the generator like below:
public class MyEnityMap : ClassMapping<MyEnity>
{
public MyEnityMap()
{
Table("my_entity");
Id(p => p.myId,
m=>{
m.Column("my_id");
m.Generator(new NextIdGeneratorDef(), g =>g.Params( new
{
table = "sys_params",
column = "param_nextvalue",
where = "table_name = 'my_entity'"
}));
});
.......
}
}
Hope this will help someone else. Improvements to this solution are highly appreciated.

No new many to many connections are made in the database when saving an object in EF

I have a problem with my code where I try to save a many to many connection between two objects, but for some reason it doesn't get saved.
We used the code first method to create our database, in our database we have the following entities where this problem is about:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<ProductTag> ProductTags { get; set; }
}
public class ProductTag
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
The table ProductTagProducts got automatically created, which is of course just a connection table between the two.
Now creating products works fine. We can just run the following and it will create the connnections in the ProductTagProducts table:
Product.ProductTags.Add(productTag);
To make sure no duplicate tasks are in the database, we handle the saving for it ourselves. The productTag always contains a product tag with an existing ID.
The problem occurs when we want to edit the same or another product. There are existing tags for the product. And we use the following process to save it:
List<ProductTag> productTags = new List<ProductTag>();
string[] splittedTags = productLanguagePost.TagList.Split(',');
foreach (string tag in splittedTags) {
ProductTag productTag = new ProductTag();
productTag.Name = tag;
productTags.Add(productTagRepository.InsertAndOrUse(productTag));
}
We split the tags by comma, that's how it is received from the HTML element. Then we define a new entity for it and use InsertAndOrUse to determine if the tag already existed. If the tag already existed, it returns the same entity but with the ID filled in, if it did not exist yet it adds the tag to the database, and then also returns the entity with ID. We create a new list to be sure that the product doesn't have duplicate Id's in there (I have tried it with adding it to the product's existing tag list directly, same result).
product.ProductTags = productTags;
productRepository.InsertOrUpdate(product);
productRepository.Save();
Then we set the list to ProductTags and let the repository handle the insert or update, of course, an update will be done. Just in case, this is the InsertOrUpdate function:
public void InsertOrUpdate(Product product) {
if (product.Id == default(int)) {
context.Products.Add(product);
} else {
context.Entry(product).State = EntityState.Modified;
}
}
The save method just calls the context's SaveChanges method. When I edit the product, and add another tag it doesn't save the new tag. However, when I set a breakpoint on the save function I can see that they are both there:
And when I open the newly added tag 'Oeh-la-la' I can even refer back to the product through it:
But when the save happens, which succeeds with all other values, there are no connections made in the ProductTagProducts table. Maybe it is something really simple, but I am clueless at the moment. I really hope that someone else can give a bright look.
Thanks in advance.
Edit: As requested the ProductTag's InsertAndOrUse method. The InsertOrUpdate method it calls is exactly the same as above.
public ProductTag InsertAndOrUse(ProductTag productTag)
{
ProductTag resultingdProductTag = context.ProductTags.FirstOrDefault(t => t.Name.ToLower() == productTag.Name.ToLower());
if (resultingdProductTag != null)
{
return resultingdProductTag;
}
else
{
this.InsertOrUpdate(productTag);
this.Save();
return productTag;
}
}
You have to know that this line...
context.Entry(product).State = EntityState.Modified;
...has no effect on the state of a relationship. It just marks the entity product being passed into Entry as Modified, i.e. the scalar property Product.Name is marked as modified and nothing else. The SQL UPDATE statement that is sent to the database just updates the Name property. It doesn't write anything into the many-to-many link table.
The only situation where you can change relationships with that line are foreign key associations, i.e. associations that have a foreign key exposed as property in the model.
Now, many-to-many relationships are never foreign key associations because you cannot expose a foreign key in your model since the foreign keys are in the link table that doesn't have a corresponding entity in your model. Many-to-many relationships are always independent associations.
Aside from direct manipulations of relationship state entries (which is rather advanced and requires to go down to the ObjectContext) independent associations can only be added or deleted using Entity Framework's change tracking. Moreover you have to take into account that a tag could have been removed by the user which requires that a relationship entry in the link table must be deleted. To track such a change you must load all existing related tags for the given product from the database first.
To put all this together you will have to change the InsertOrUpdate method (or introduce a new specialized method):
public void InsertOrUpdate(Product product) {
if (product.Id == default(int)) {
context.Products.Add(product);
} else {
var productInDb = context.Products.Include(p => p.ProductTags)
.SingleOrDefault(p => p.Id == product.Id);
if (productInDb != null) {
// To take changes of scalar properties like Name into account...
context.Entry(productInDb).CurrentValues.SetValues(product);
// Delete relationship
foreach (var tagInDb in productInDb.ProductTags.ToList())
if (!product.ProductTags.Any(t => t.Id == tagInDb.Id))
productInDb.ProductTags.Remove(tagInDb);
// Add relationship
foreach (var tag in product.ProductTags)
if (!productInDb.ProductTags.Any(t => t.Id == tag.Id)) {
var tagInDb = context.ProductTags.Find(tag.Id);
if (tagInDb != null)
productInDb.ProductTags.Add(tagInDb);
}
}
}
I was using Find in the code above because I am not sure from your code snippets (the exact code of InsertAndOrUse is missing) if the tags in the product.ProductTags collection are attached to the context instance or not. By using Find it should work no matter if the they are attached or not, potentially at the expense of a database roundtrip to load a tag.
If all tags in product.ProductTags are attached you can replace ...
var tagInDb = context.ProductTags.Find(tag.Id);
if (tagInDb != null)
productInDb.ProductTags.Add(tagInDb);
... just by
productInDb.ProductTags.Add(tag);
Or if it's not guaranteed that they are all attached and you want to avoid the roundtrip to the database (because you know for sure that the tags at least exist in the database, if attached or not) you can replace the code with:
var tagInDb = context.ProductTags.Local
.SingleOrDefault(t => t.Id == tag.Id);
if (tagInDb == null) {
tagInDb = tag;
context.ProductTags.Attach(tagInDb);
}
productInDb.ProductTags.Add(tagInDb);

How can I automatically filter out soft deleted entities with Entity Framework?

I am using Entity Framework Code First. I override SaveChanges in DbContext to allow me to do a "soft delete":
if (item.State == EntityState.Deleted && typeof(ISoftDelete).IsAssignableFrom(type))
{
item.State = EntityState.Modified;
item.Entity.GetType().GetMethod("Delete")
.Invoke(item.Entity, null);
continue;
}
Which is great, so the object knows how to mark itself as a soft delete (In this case it just sets IsDeleted to true).
My question is how can I make it such that when I retrieve the object it ignores any with IsDeleted? So if I said _db.Users.FirstOrDefault(UserId == id) if that user had IsDeleted == true it would ignore it. Essentially I want to filter?
Note: I do not want to just put && IsDeleted == true
That's why I am marking the classes with an interface so the remove knows how to "Just Work" and I'd like to somehow modify the retrieval to know how to "Just Work" also based on that interface being present.
Use EntityFramework.DynamicFilters. It allows you to create global filters that will be applied automatically (including against navigation properties) when queries are executed.
There is an example "IsDeleted" filter on the project page that looks like this:
modelBuilder.Filter("IsDeleted", (ISoftDelete d) => d.IsDeleted, false);
That filter will automatically inject a where clause on any query against an entity that is ISoftDelete. Filters are defined in your DbContext.OnModelCreating().
Disclaimer: I'm the author.
I've got soft delete working for all my entities and soft deleted items are not retrieved via the context using a technique suggested by this answer. That includes when you access the entity via navigation properties.
Add an IsDeleted discriminator to every entity that can be soft deleted. Unfortunately I haven't worked out how to do this bit based on the entity deriving from an abstract class or an interface (EF mapping doesn't currently support interfaces as an entity):
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Foo>().Map(m => m.Requires("IsDeleted").HasValue(false));
modelBuilder.Entity<Bar>().Map(m => m.Requires("IsDeleted").HasValue(false));
//It's more complicated if you have derived entities.
//Here 'Block' derives from 'Property'
modelBuilder.Entity<Property>()
.Map<Property>(m =>
{
m.Requires("Discriminator").HasValue("Property");
m.Requires("IsDeleted").HasValue(false);
})
.Map<Block>(m =>
{
m.Requires("Discriminator").HasValue("Block");
m.Requires("IsDeleted").HasValue(false);
});
}
Override SaveChanges and find all the entries to be deleted:
Edit
Another way to override the delete sql is to change the stored procedures generated by EF6
public override int SaveChanges()
{
foreach (var entry in ChangeTracker.Entries()
.Where(p => p.State == EntityState.Deleted
&& p.Entity is ModelBase))//I do have a base class for entities with a single
//"ID" property - all my entities derive from this,
//but you could use ISoftDelete here
SoftDelete(entry);
return base.SaveChanges();
}
The SoftDelete method runs sql directly on the database because discriminator columns cannot be included in entities:
private void SoftDelete(DbEntityEntry entry)
{
var e = entry.Entity as ModelBase;
string tableName = GetTableName(e.GetType());
Database.ExecuteSqlCommand(
String.Format("UPDATE {0} SET IsDeleted = 1 WHERE ID = #id", tableName)
, new SqlParameter("id", e.ID));
//Marking it Unchanged prevents the hard delete
//entry.State = EntityState.Unchanged;
//So does setting it to Detached:
//And that is what EF does when it deletes an item
//http://msdn.microsoft.com/en-us/data/jj592676.aspx
entry.State = EntityState.Detached;
}
GetTableName returns the table to be updated for an entity. It handles the case where the table is linked to the BaseType rather than a derived type. I suspect I should be checking the whole inheritance hierarchy....
But there are plans to improve the Metadata API and if I have to will look into EF Code First Mapping Between Types & Tables
private readonly static Dictionary<Type, EntitySetBase> _mappingCache
= new Dictionary<Type, EntitySetBase>();
private ObjectContext _ObjectContext
{
get { return (this as IObjectContextAdapter).ObjectContext; }
}
private EntitySetBase GetEntitySet(Type type)
{
type = GetObjectType(type);
if (_mappingCache.ContainsKey(type))
return _mappingCache[type];
string baseTypeName = type.BaseType.Name;
string typeName = type.Name;
ObjectContext octx = _ObjectContext;
var es = octx.MetadataWorkspace
.GetItemCollection(DataSpace.SSpace)
.GetItems<EntityContainer>()
.SelectMany(c => c.BaseEntitySets
.Where(e => e.Name == typeName
|| e.Name == baseTypeName))
.FirstOrDefault();
if (es == null)
throw new ArgumentException("Entity type not found in GetEntitySet", typeName);
_mappingCache.Add(type, es);
return es;
}
internal String GetTableName(Type type)
{
EntitySetBase es = GetEntitySet(type);
//if you are using EF6
return String.Format("[{0}].[{1}]", es.Schema, es.Table);
//if you have a version prior to EF6
//return string.Format( "[{0}].[{1}]",
// es.MetadataProperties["Schema"].Value,
// es.MetadataProperties["Table"].Value );
}
I had previously created indexes on natural keys in a migration with code that looked like this:
public override void Up()
{
CreateIndex("dbo.Organisations", "Name", unique: true, name: "IX_NaturalKey");
}
But that means that you can't create a new Organisation with the same name as a deleted Organisation. In order to allow this I changed the code to create the indexes to this:
public override void Up()
{
Sql(String.Format("CREATE UNIQUE INDEX {0} ON dbo.Organisations(Name) WHERE IsDeleted = 0", "IX_NaturalKey"));
}
And that excludes deleted items from the index
Note
While navigation properties are not populated if the related item is soft deleted, the foreign key is.
For example:
if(foo.BarID != null) //trying to avoid a database call
string name = foo.Bar.Name; //will fail because BarID is not null but Bar is
//but this works
if(foo.Bar != null) //a database call because there is a foreign key
string name = foo.Bar.Name;
P.S. Vote for global filtering here https://entityframework.codeplex.com/workitem/945?FocusElement=CommentTextBox# and filtered includes here
One option would be to encapsulate the !IsDeleted into an extension method. Something like below is just an example. Beware its just to give you an idea of an extension method, the below won't compile.
public static class EnumerableExtensions
{
public static T FirstOrDefaultExcludingDeletes<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
return source.Where(args => args != IsDeleted).FirstOrDefault(predicate);
}
}
Usage:
_db.Users.FirstOrDefaultExcludingDeletes(UserId == id)
You can use Global Query Filters on Entity Framework Core 2.0.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().Property<string>("TenantId").HasField("_tenantId");
// Configure entity filters
modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "TenantId") == _tenantId);
modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);
}
Great question.
You would need to intercept SQL query before it gets executed somehow, then add additional where clause to remove 'deleted' items from selection. Unfortunately, Entity doesn't have GetCommand that can be used to change the query.
Perhaps EF Provider Wrapper which sits in the right place could be modified to allow for query change.
Or, u can utilize QueryInterceptor but each query would have to use InterceptWith(visitor) to change the expressions...
So, I would concentrate on this approach as there is AFAIK no other option then intercepting the query and fixing it (if you want to keep code that queries unchanged).
Anyway, if you figure out something useful, let us know.

Categories

Resources