Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to select an Action with AttributeRouting in .NET MVC based on the Media Type of the Accept header?

I want to select an Action of my Controller based on the Media Type requested in the Accept header.

For example, I have a resource called a subject. Its assigned route is:

GET /subjects/{subjectId:int}

Normally, the browser is requesting text/html, which is fine. The default Media Formatter handles this great.

Now, I have custom logic I want to perform when this same route is accessed with an accept header specifying application/pdf as the accepted Media Type.

I could create a custom Media Formatter, but, to my understanding, this would mean that any route that is requested with the Accept header set to application/pdf would also run through this Media Formatter. This is unacceptable.

In Java, there is an annotation called @Produces:

The @Produces annotation is used to specify the MIME media types or representations a resource can produce and send back to the client. If @Produces is applied at the class level, all the methods in a resource can produce the specified MIME types by default. If applied at the method level, the annotation overrides any @Produces annotations applied at the class level.

This would allow me to do the following:

namespace MyNamespace
{
    [RoutePrefix("subjects")]
    public class SubjectsController : Controller
    {
        [Route("{subjectId:int}")]
        [HttpGet]
        public ActionResult GetSubject(int subjectId)
        {
        }

        [Route("{subjectId:int}")]
        [HttpGet]
        [Produces("application/pdf")]
        public ActionResult GetSubjectAsPdf(int subjectId)
        {
            //Run my custom logic here to generate a PDF.
        }
    }
}

There is no Produces Attribute in .NET that I can find, of course, so this doesn't work. I haven't been able to find a similar attribute, either.

I could of course manually check the header within the body of the action, and redirect it to another action, but that seems hackish at best.

Is there a mechanism in .NET 4.5 that I may use to pull this off that I'm overlooking or missing?

(I'm using MVC 5.2.2 from NuGet repository)

like image 896
crush Avatar asked Feb 17 '15 23:02

crush


1 Answers

After searching around the Internet for awhile, I came up with the idea that this would be best accomplished by creating an ActionMethodSelectorAttribute.

The following is a very naive, first-pass implementation of a ProducesAttribute that I wrote with the eventual intent of mimicking Java's Produces annotation:

namespace YourNamespace
{
    using System;
    using System.Collections.Generic;
    using System.Net;
    using System.Net.Mime;
    using System.Web.Mvc;

    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public sealed class ProducesAttribute : ActionMethodSelectorAttribute
    {
        private readonly ISet<ContentType> acceptableMimeTypes;

        public ProducesAttribute(params string[] acceptableMimeTypes)
        {
            this.acceptableMimeTypes = new HashSet<ContentType>();

            foreach (string acceptableMimeType in acceptableMimeTypes)
                this.acceptableMimeTypes.Add(new ContentType(acceptableMimeType));
        }

        public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
        {
            string acceptHeader = controllerContext.RequestContext.HttpContext.Request.Headers[HttpRequestHeader.Accept.ToString()];
            string[] headerMimeTypes = acceptHeader.Split(new char[] {','}, StringSplitOptions.RemoveEmptyEntries);

            foreach (var headerMimeType in headerMimeTypes)
            {
                if (this.acceptableMimeTypes.Contains(new ContentType(headerMimeType)))
                    return true;
            }

            return false;
        }
    }
}

It is meant to be used with Attribute Routing, and can be applied as follows:

public sealed class MyController : Controller
{
    [Route("subjects/{subjectId:int}")] //My route
    [Produces("application/pdf")]
    public ActionResult GetSubjectAsPdf(int subjectId)
    {
        //Here you would return the PDF representation.
    }

    [Route("subjects/{subjectId:int}")]
    public ActionResult GetSubject(int subjectId)
    {
        //Would handle all other routes.
    }
}
like image 164
crush Avatar answered Oct 26 '22 23:10

crush