RESTier
RESTier copied to clipboard
Is there a way to perform a "soft delete"?
I'm looking for guidance on overriding the default delete behavior to perform a soft delete / updating a "Deleted" flag instead of executing a DELETE
command.
There might be a better way than this, but I would think that one way to do it would be the following:
- (Optional) Hide the "Deleted" flag from the model with the ModelBuilder.
- Create an
OnDeletingXXXXX
function that does the following: - Sets the Deleted flag on the incoming object. - Saves the object to the database.- Throws an informative exception that says the operation completed.
- (Optional) Create an
OnFilterXXXXX
function that does.Where(c => c.IsDeleted == false);
Sure, the exception kinda sucks, but it could be documented in your API. to look for a message in the return saying the operation completed successfully.
I actually need to make one of my APIs do this too, so if I figure out a better way, I'll post it here. And maybe someone else from the team can jump in.
You might be able to proxy the delete calls and handle the exception. I'm thinking something like set up a generic route /api/
that routes to /odata/
, where /api/
= proxy and /odata/
= RESTier. The proxy could use something simple like Server.Transfer(toProxy)
to direct the calls to the real delete and catch the custom exception.
There might also be a simple way to trap the exceptions in Global.asax
by clearing the exception and changing the response status code.
So, I hate myself for this, but I got the exception-handling piece of this wired up.
In my DatabaseDomain.cs
file, I have something like:
protected void OnDeletingRelatedDocuments(RelatedDocument entity)
{
HttpContext.Current.Response.StatusCode = 204;
HttpContext.Current.Response.End();
throw new SoftDeleteException();
}
That SoftDeleteException
type can be added to the Visual Studio ignored exceptions list.
Then in my Global.asax
file (what is this, 2005?), I suppress the resulting HttpException
:
public void Application_Error(object sender, EventArgs e)
{
var exception = Server.GetLastError();
if (exception != null && exception is HttpException && exception.Message == "Server cannot set status after HTTP headers have been sent.")
Server.ClearError();
}
Fairly simple. Now I can implement all sorts of business rules on inserts, updates, or deletes without RESTier executing any SQL queries. Good for soft deletes or silently failing if a user tries to insert a duplicate record.
So I kept digging on this. Apparently RESTier has already deleted the entity by the time the OnDeletingEntity
method has been called, but you can revert the DbContext
's changes then add your own soft-delete logic. The revised OnDeletingEntity
method:
protected void OnDeletingRelatedDocuments(RelatedDocument entity)
{
this.RejectChanges(this.DbContext);
entity.Deleted = true;
}
RejectChanges
is an implementation borrowed from StackOverflow.
private void RejectChanges(DbContext dbContext)
{
RejectScalarChanges(dbContext);
RejectNavigationChanges(dbContext);
}
private void RejectScalarChanges(DbContext dbContext)
{
foreach (var entry in dbContext.ChangeTracker.Entries())
{
switch (entry.State)
{
case EntityState.Modified:
case EntityState.Deleted:
entry.State = EntityState.Modified; //Revert changes made to deleted entity.
entry.State = EntityState.Unchanged;
break;
case EntityState.Added:
entry.State = EntityState.Detached;
break;
}
}
}
private void RejectNavigationChanges(DbContext dbContext)
{
var objectContext = ((IObjectContextAdapter)dbContext).ObjectContext;
var deletedRelationships = objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted).Where(e => e.IsRelationship && !this.RelationshipContainsKeyEntry(e));
var addedRelationships = objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Added).Where(e => e.IsRelationship);
foreach (var relationship in addedRelationships)
relationship.Delete();
foreach (var relationship in deletedRelationships)
relationship.ChangeState(EntityState.Unchanged);
}
private bool RelationshipContainsKeyEntry(System.Data.Entity.Core.Objects.ObjectStateEntry stateEntry)
{
//prevent exception: "Cannot change state of a relationship if one of the ends of the relationship is a KeyEntry"
//I haven't been able to find the conditions under which this happens, but it sometimes does.
var objectContext = ((IObjectContextAdapter)this).ObjectContext;
var keys = new[] { stateEntry.OriginalValues[0], stateEntry.OriginalValues[1] };
return keys.Any(key => objectContext.ObjectStateManager.GetObjectStateEntry(key).Entity == null);
}
This solution has no throw exceptions, it's repeatable without a ton of domain-specific code, and it still returns the expected HTTP response codes!
I created a gist for all the reject-changes that creates an extension method on the DbContext
object, allowing a RESTier API object to be even cleaner.
AFAIK you need to be careful on that RejectNavigationChanges
method. If you're in the context of a transaction, there may be more entities in there than you'd like. I don't think you can just assume all of the entities are going back to the "unchanged" state.
Hi, you can use custom SubmitExecutor, documentation is lacking some features, but you can find something similar here.
In my case I've used a very basic, but working class, code in this gist
So I've built on @fileman's work and enhanced his version with some fluent configuration options. You can check it out here. If @fileman is OK with it, I'll publish it to NuGet ASAP.
Hi, in my project even admin execute only softdelete and there is foreach entity an action that execute harddelete of softdeleted entities in context. With your version if no "adminRole" was passed in config no hard delete is execute even if the user is admin, right? I think is a good enhancement to publish
Right, if you don't pass an admin role into the config, then admins can still soft-delete. If you DO specify a role, then admins will need to soft delete by passing a PUT and changing IsDeleted themselves, instead of a DELETE.
@robertmclaws what do you think about SoftDelete feature, built-in or add as example in documentation?
@fileman I used your initial code a while back and fleshed it out some more. You can find it here. I was planning on bringing it into the Restier codebase before RTM. Do you have any objections to that?
For first release it can be ok, but for future instead of "adminRole" why not use permissions? Since even other role can have permissions to hard delete, I can try to work on it and submit a PR but not soon… can be usefull?
Let's elaborate on this a little bit. What role would be able to hard-delete an item that wouldn't be handled already by another method?
I'll try to explain what I mean Since I'm coming from LightSwitch experience where multiple roles can have permission to hard-delete, could be better to use something similar is in your CloudNimble.RestierEssentials.Authorization Example, if the user doesn't have "HardDelete" permission SubmitExecutor use softdelete … we should check if entity has a conventional "Deleted" or "SoftDeleted" property.
I'm thinking that it may be better to make this more generic. I have some ideas... once I'm finished getting Beta 2 locked down, I'll see if I can get some ideas down and we can put a quick spec together.
Thanks!
I added this feature to RestierEssentials a number of years ago and shipped a corresponding NuGet package. I haven't updated it for ASP.NET Core, but if it's needed, just let me know.
We'll likely build this into Restier 2.0.
Thanks everyone for your contribution to the discussion!