1

Below is a very simple HelloWorld API Method

    [HttpGet]
    [Route("api/helloworld")]
    public string SayHello()
    {
        try
        {
            return "Hello World - API Version 1 - " + DateTime.Now.ToLongTimeString();
        }
        finally
        {
            Log("I'd like logging to not hold up the string from getting returned");
        }
    }

Unfortunately, the finally code doesn't work this way, so the log method in this case would block the string from getting returned until after the log was complete.

Is it possible to return a value in the MVC Web API, and then run code afterwards? In my particular case I'd like to log afterwards, but there is no reason for the database logging to take up time for the client to receive a response, as it will not effect the response.

moribvndvs
  • 42,191
  • 11
  • 135
  • 149
user2023918
  • 406
  • 1
  • 5
  • 12
  • [Action filters](http://www.asp.net/mvc/tutorials/hands-on-labs/aspnet-mvc-4-custom-action-filters) are the preferred mechanism for handling things like this. – moribvndvs Oct 24 '13 at 20:58
  • @HackedByChinese - Action filters are not going to help since there is no "post sending request" event. (Plus your link to regular filters, not WebAPI ones). – Alexei Levenkov Oct 24 '13 at 21:00
  • Wait; are you sure that the log message isn't written? C# language spec states that finally will always be executed unless an asynchronous exception happens on the thread. http://stackoverflow.com/questions/345091/will-code-in-a-finally-statement-fire-if-i-return-a-value-in-a-try-block – Allan Elder Oct 24 '13 at 21:04
  • Basically I'm doing this for a payment... so the order I'm really trying to do things is... 1) Open a connection to the DB 2) If connection is available, call 3rd party service to take payment 3) Return payment details 4) After api returns value, log details 5) Close DB Connection. So basically, I'd like it to never attempt making a payment if the db connection fails, because we need the logging details, however, I'd also like the logging to not take up time to get the return value to the client. – user2023918 Oct 24 '13 at 21:09
  • When I answered it I just saw the `MVC` tag, so sorry. You're right, unlike MVC there is no `OnResultExecuted`, which is frustrating. However, there is a valid (and cleaner, IMHO) approach I will add to answers below. – moribvndvs Oct 25 '13 at 04:31

2 Answers2

2

Yes, but you'd need to run it on a separate thread.

Dai
  • 141,631
  • 28
  • 261
  • 374
1

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;
     }
 }
moribvndvs
  • 42,191
  • 11
  • 135
  • 149