ASP.NET MVC Resuming Actions

(Update 2014-12-09: I’ve created a WebApi based library that leverages the System.Net.Http classes to do all the parsing. The code is very simple and much easier to support. I’d recommend trying that first. It’s a lot easier to support.)

This is to announce my new CodePlex project Resuming Action Results for ASP.NET MVC. Or MVC Resuming Actions. Or something else even more clever and catchy. Oh, and it’s already on the NuGet official feed to check out.

The History

Some time ago I started a project called Media Streaming MVC. The project was an early attempt to give ASP.NET MVC developers the ability to easily expose dynamic, routable resources as progressive-download compliant. It was just an ActionFilter and some ActionResults with code to parse HTTP Request Headers and construct the appropriate response.

That project was developed very rapidly as a proof of concept for a StackOverflow question I had tried to answer. Over the past few months I grew increasingly unhappy with the implementation. I knew I wanted to bring it more in line with the way MVC FileResult actions were called and give it a major overhaul. Unfortunately every time I sat down to take a serious look at how I could accomplish these goals, I got overwhelmed and let my attention wander to something less challenging.

Finally I made the decision that there was really no way to clean up the project, make it easier for developers to use and retain any sort of compatibility with existing code. I made the decision to cut the cord and start fresh with the lessons I’ve learned, the feedback I’d gotten and my expanded experience with the ASP.NET MVC platform.

The Modern Day

My new project is a nearly complete redesign starting from identifying the problem I was trying to solve and the design attributes I thought were necessary.

  1. Enable developers to use the library with no extra effort.
  2. Streamline the request processing to keep it far away from eyes.
  3. Build for the most common case but keep an eye toward extension.

I think I accomplished this pretty well. Many of the complaints I had with the library were solved by the simplified pipeline. Rather than parsing a request in an ActionFilter and passing it through the controller method and into the ActionResult, all the lifting is done in the specific ActionResults themselves.

I’m very happy with the way it has turned out given how little time I’ve had to tackle this project. I managed to knock it all out in about 6 hours plus another hour to configure the project for NuGet submission, create a CodePlex repository for it, update some Wiki pages on the old and new project, etc.

As I get time I’ll need to clean up the source comments that came from the re-used code in the old library and create some actual documents. I might even try my hand at creating tests again but honestly I found them such a pain last time I might still be gun shy.

The Obligatory Fluffy Statement

I really hope this new project solves the minor gripes I was hearing from the last project. I’d be very happy to receive any feedback at all about how the project is being used, how it performs, all of that.

The Shameless Beg

Now who wants to make a logo for me?

The Sign Off

-Erik

9 responses to “ASP.NET MVC Resuming Actions

  1. Hy Erik. I just found your Resuming Actions, and tried them out. They allow me to use an html5 audio tag and skip around in the track. That’s great.

    BUT on the other hand, I can not let users download files by filename anymore. Essentially, it kills (without using it) this:

    //return File(Database + Path, “application/octet-stream”, System.IO.Path.GetFileName(Path));

    instead of the file name, i always get “Get” as the filename. Even if I manually try to add
    Response.AppendHeader(“Content-Disposition”, “attachment; filename=\”” + System.IO.Path.GetFileName(Path) + “\””);

    this results in, right now, the project being unusable for me. Hope that can be fixed somehow?

    To see it in action, check out my website and click on ‘Files’. Streaming should work (only in chrome as it’s mp3 currently), file download (the Test.txt file at the bottom), doesn’t.

    • You are quite right – this is a problem. The ResumingActionResultBase attempts to write the file name as part of the response stream if the ResumingRequest has one. It seems the ResumingRequest attempts to set this value if one wasn’t specified (and I don’t see how you could easily set it yourself as the code exists now) by analyzing the HTTP request.

      if (!string.IsNullOrEmpty(request.FilePath))
      FileName = VirtualPathUtility.GetFileName(request.FilePath);

      It was my thought this would generally work but I can see the ‘FileName’ is the action name rather than some more sensible value. Good catch! I guess this is because you’re using a query string rather than a routed path. I will need to update the code to allow you to specify an alternate name. I think your explicit header append is getting overwritten by the ResumingActionResultBase itself which is why you’re unable to specify your own value right now.

      If instead of using a querystring parameter (here, Path) does it “just work” if you use a url like the below and configure the route?
      http://conesoft.net/Files/Main/Get/Test.txt

      In this case you should still be routed to Get as the controller (again, if you configure the route) but Text.txt will be the “id” type parameter. The VirtualPathProvider should pick up that up as the file name (since it’s the last part of the path) and everything should work as you expect.

      If that’s not acceptable there is an alternative though it will require some changes to the library. Looking at the code (it has been some time since I last had a chance to review it) I think a sensible action would be to add a FileName property to the ResumingActionResultBase which should let you specify your own value in the action itself. In your case something like:
      return File(Database + Path, “application/octet-stream”, System.IO.Path.GetFileName(Path) ) { FileName = “OverridenFileName.ext” };

      Or maybe it could be added as a constructor parameter to every action as well. In the ResumingActionResultBase I would try to use this value first and then, if not present (null or empty) fall back on the ResumingRequest value and again if one doesn’t exist, not setting a value. This would let those who must use a query string parameter (or have some other legitimate need) specify their own file name for the response header.

      We should really discuss this on the project board but I see there are some Issue items there already which I was never notified about(!!!) I’m so very sorry if you’ve asked this question previously but didn’t receive an answer. I was unaware anyone had posted questions or issues! I will make the time this weekend to go through the items and try to address them all. Again, my apologies!

      • Changing the Path to look like a filename worked indeed, thanks for that (I added a MapRoute that handled Path parameters.
        Works fine now.

        Glad you noted the issue items on the project board. No apologies needed, can happen to anyone 🙂

        Thanks for the quick fix (wanted to do that MapRoute later, anyways.. glad I tested it before doing the mapping). It’s a great project, will help me quite a bit in the future (I plan to host some DJ mixes of myself and friends, and not being able to skip in 1-5 hour recordings would be very annoying)

        • Yes, I’m fully subscribed to events now (as far as I can tell – discussion and issues list at least!). That sounds awesome. I’d love to know how well it works (or doesn’t). I think it’s still important for me to handle the situation you mentioned especially since one of the features I was going for was not directly exposing files if it wasn’t wanted. For instance if the request was for an item in a database and the route was /controller/rsrc/114 then the file name will try to be 114 (not 114.txt, not ‘Expenditures.txt’, etc.) and I believe the default MVC File results allow this to be specified. It’s an oversight on my part which I will try to remedy quickly.

          I wouldn’t expect such little things to be used with streaming results but it occurs to me developers might not specialize their resources into different urls like /streaming-controller/rsrc/bigfile vs /static-controller/rsrc/smallfile or /controller/stream-rsrc/bigfile vs controller/static-rsrc/smallfile and use the appropriate action results. If everything is going to be sent through the same action then the action result has to just as flexible as the MVC actions. I think I should probably check out MVC 3 and 4 as well to see if they’ve since solved this same problem and I’m no longer needed. 😀

          In the mean time, your issue is fully resolved? You’re good to go?

          • Surely, it would be nice to get the real fix (good luck with it), but yes, my issue is fully resolved. Code:

            public ActionResult Get(string Path)
            {
            if (Path.EndsWith(“mp3”))
            {
            return new VikingErik.Mvc.ResumingActionResults.ResumingFilePathResult(Database + Path, “application/octet-stream”);
            }
            return File(Database + Path, “application/octet-stream”, System.IO.Path.GetFileName(Path));
            }

            this didn’t work without the MapRoute. With the following MapRoute added, it works perfectly (as you can test on http.//conesoft.net/Files)

            context.MapRoute(“Paths”, “Files/{controller}/{action}/{Path}”, new { controller = “Main”, action = “Index”, Path = UrlParameter.Optional });

            so, I’m happy now. Sure happy to hear about the updates in the future, too. Thanks so far, you made my weekend 🙂

  2. My workaround for the file name problem:

    public class MyResumingFileStreamResult : ResumingFileStreamResult
    {
    public string OverrideFileName { get; set; }

    public MyResumingFileStreamResult(Stream fileStream, string contentType, string filename) :
    base(fileStream, contentType)
    {
    OverrideFileName = filename;
    }

    public override void ExecuteResult(ControllerContext context)
    {
    if (context == null)
    throw new ArgumentNullException(“context”);

    ResumingRequest resumingRequest = new ResumingRequest(context.HttpContext, FileContents.Length);
    resumingRequest.FileName = OverrideFileName;
    ExecuteResultBody(context, resumingRequest);
    }
    }
    Thank you polymorphism and of course Erik!

  3. Just found this and used it for streaming audio/mp3 file content that allows seeking… thanks!

  4. Man you saved my life!! One million of thanks!!! \o/

  5. Pingback: Web Api Progressive Download on GitHub and NuGet | Erik Noren's Tech Stuff

Leave a comment