While WebAPI does not have a OnRequestExecuted
method on filters, which is what you are probably looking for it, I think filters are still the correct approach.
What you will need is a filter combined with a derivative ObjectContent
class that defers your post-request logic to after the response is written. I use this approach to automatically create NHibernate session and transaction at the beginning of a request, and commit them when the request is complete, which is similar to what you described in a comment. Please keep in mind this is greatly simplified to illustrate my answer.
public class TransactionAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
// create your connection and transaction. in this example, I have the dependency resolver create an NHibernate ISession, which manages my connection and transaction. you don't have to use the dependency scope (you could, for example, stuff a connection in the request properties and retrieve it in the controller), but it's the best way to coordinate the same instance of a required service for the duration of a request
var session = actionContext.Request.GetDependencyScope().GetService(typeof (ISession));
// make sure to create a transaction unless there is already one active.
if (!session.Transaction.IsActive) session.BeginTransaction();
// now i have a valid session and transaction that will be injected into the controller and usable in the action.
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
var session = actionExecutedContext.Request.GetDependecyScope().GetService(typeof(ISession));
var response = actionExecutedContext.Response;
if (actionExecutedContext.Exception == null)
{
var content = response.Content as ObjectContent;
if (content != null)
{
// here's the real trick; if there is content that needs to be sent to the client, we need to swap the content with an object that will clean up the connection and transaction AFTER the response is written.
response.Content = new TransactionObjectContent(content.ObjectType, content.Value, content.Formatter, session, content.Headers);
}
else
{
// there is no content to send to the client, so commit and clean up immediately (in this example, the container cleans up session, so it is omitted below)
if (session.Transaction.IsActive) session.Transaction.Commit();
}
}
else
{
// an exception was encountered, so immediately rollback the transaction, let the content return unmolested, and clean up your session (in this example, the container cleans up the session for me, so I omitted it)
if (session.Transaction.IsActive) session.Transaction.Rollback();
}
}
}
And the magic happens in this ObjectContent
derivative. Its responsibility is to stream the object to the response, supporting async actions, and do something after the response is sent down. You can add your logging, db, whatever in here. In this example, it merely commits a transaction after successfully writing the response.
public class TransactionObjectContent : ObjectContent
{
private ISession _session;
public TransactionObjectContent(Type type, object value, MediaTypeFormatter formatter, ISession session, HttpContentHeaders headers)
: base(type, value, formatter)
{
_session = session;
foreach (var header in headers)
{
response.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}
protected async override Task SerializeToStreamAsync(Stream stream, TransportContext context)
{
await base.SerializeToStreamAsync(stream, context); // let it write the response to the client
// here's the meat and potatoes. you'd add anything here that you need done after the response is written.
if (_session.Transaction.IsActive) _session.Transaction.Commit();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
if (_session != null)
{
// if transaction is still active by now, we need to roll it back because it means an error occurred while serializing to stream above.
if (_session.Transaction.IsActive) _session.Transaction.Rollback();
_session = null;
}
}
}
}
Now you can either register the filter in your global filters, or add it directly to actions or controllers. You don't have to keep copying and pasting redundant code for executing your logic in each action in another thread; the logic just gets applied automagically to every action you target with the filter. Much cleaner, much easier, and DRY.
Example controller:
[Transaction] // will apply the TransactionFilter to each action in this controller
public DoAllTheThingsController : ApiController
{
private ISession _session;
public DoAllTheThingsController(ISession session)
{
_session = session; // we're assuming here you've set up an IoC to inject the Isession from the dependency scope, which will be the same as the one we saw in the filter
}
[HttpPost]
public TheThing Post(TheThingModel model)
{
var thing = new TheThing();
// omitted: map model to the thing.
// the filter will have created a session and ensured a transaction, so this all nice and safe, no need to add logic to fart around with the session or transaction. if an error occurs while saving, the filter will roll it back.
_session.Save(thing);
return thing;
}
}