23

I'm working on moving an API project from raw http handlers where I'm using periods in the paths:

http://server/collection/id.format

I would like to follow the same URL schema in a Web Api (self-hosted) version, and tried this:

var c = new HttpSelfHostConfiguration(b);
c.Routes.MapHttpRoute(
    name: "DefaultApiRoute",
    routeTemplate: "{controller}/{id}.{format}",
    defaults: new { id = RouteParameter.Optional, format = RouteParameter.Optional },
    constraints: null
);

Unfortunately, that doesn't seem to resolve (consistent 404's on /foo, /foo/bar and /foo/bar.txt). A similar pattern using a slash before 'format' works fine:

var c = new HttpSelfHostConfiguration(b);
c.Routes.MapHttpRoute(
    name: "DefaultApiRoute",
    routeTemplate: "{controller}/{id}/{format}",
    defaults: new { id = RouteParameter.Optional, format = RouteParameter.Optional },
    constraints: null
);

I haven't yet delved into the code for the Web Api, and before I do thought I'd ask here to see if this is a known, or perhaps even justified limitation in Web Api.

UPDATE: I neglected to mention that "id" and "format" are strings, which turns out to be important for the solution to this question. Adding a constraint to exclude periods from the "id" token solves the 404 problem.

Lee
  • 801
  • 1
  • 6
  • 20
  • Facing a **very** similar problem myself: http://stackoverflow.com/questions/11487181/when-my-seo-text-slug-in-the-url-ends-with-a-dot-symbol-i-get-a-404-error | As far as I can tell, a dot cannot be used in a URL. I hope I'm proven wrong however. – Only Bolivian Here Jul 15 '12 at 17:49
  • Have you tried inheriting from `Controller` and override whatever comes through the `Initialize`? – Silvermind Jul 15 '12 at 18:08
  • Other similar answers, [here (if getting a 404)](https://stackoverflow.com/questions/13298542/apicontroller-returns-404-when-id-contains-period), [or here](https://stackoverflow.com/questions/11728846/dots-in-url-causes-404-with-asp-net-mvc-and-iis), and [here](https://stackoverflow.com/questions/20998816/dot-character-in-mvc-web-api-2-for-request-such-as-api-people-staff-45287) – Nate Anderson Feb 07 '22 at 21:44

6 Answers6

30

I was able to achieve this by doing the following: replace "*." with "*" in system.webServer.handlers in web.config, i.e. remove the period.

<add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
and... break
  • 478
  • 4
  • 5
17

Be careful to set runAllManagedModulesForAllRequests option in modules attribute in your web.config

<modules runAllManagedModulesForAllRequests="true">..</modules>

Otherwise it will not work in IIS (probably it would be handled by non-managed handlers).

pvasek
  • 1,086
  • 11
  • 11
16

I am unable to reproduce the problem. This should work. Here's my setup:

  1. Create a new .NET 4.0 Console Application
  2. Switch to .NET Framework 4.0 profile
  3. Install the Microsoft.AspNet.WebApi.SelfHost NuGet
  4. Define a Product

    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    
  5. A corresponding API Controller:

    public class ProductsController : ApiController
    {
        public Product Get(int id)
        {
            return new Product
            {
                Id = id,
                Name = "prd " + id
            };
        }
    }
    
  6. And a host:

    class Program
    {
        static void Main(string[] args)
        {
            var config = new HttpSelfHostConfiguration("http://localhost:8080");
    
            config.Routes.MapHttpRoute(
                name: "DefaultApiRoute",
                routeTemplate: "{controller}/{id}.{format}",
                defaults: new { id = RouteParameter.Optional, format = RouteParameter.Optional },
                constraints: null
            );
    
            using (var server = new HttpSelfHostServer(config))
            {
                server.OpenAsync().Wait();
                Console.WriteLine("Press Enter to quit.");
                Console.ReadLine();
            }
        }
    }
    

Now when you run this console application you could navigate to http://localhost:8080/products/123.xml. But of course you could navigate to http://localhost:8080/products/123.json and you will still get XML. So the question is: How to enable content negotiation using a route parameter?

You could do the following:

    class Program
    {
        static void Main(string[] args)
        {
            var config = new HttpSelfHostConfiguration("http://localhost:8080");
            config.Formatters.XmlFormatter.AddUriPathExtensionMapping("xml", "text/html");
            config.Formatters.JsonFormatter.AddUriPathExtensionMapping("json", "application/json");

            config.Routes.MapHttpRoute(
                name: "DefaultApiRoute",
                routeTemplate: "{controller}/{id}.{ext}",
                defaults: new { id = RouteParameter.Optional, formatter = RouteParameter.Optional },
                constraints: null
            );

            using (var server = new HttpSelfHostServer(config))
            {
                server.OpenAsync().Wait();
                Console.WriteLine("Press Enter to quit.");
                Console.ReadLine();
            }
        }
    }

and now you can use the following urls:

http://localhost:8080/products/123.xml
http://localhost:8080/products/123.json

Now you might be wondering what's the relation between the {ext} route parameter that we used in our route definition and the AddUriPathExtensionMapping method because nowhere we did not specify it. Well, guess what: it's hardcoded in the UriPathExtensionMapping class to ext and you cannot modify it because it is readonly:

public class UriPathExtensionMapping
{
    public static readonly string UriPathExtensionKey;

    static UriPathExtensionMapping()
    {
        UriPathExtensionKey = "ext";
    }

    ...
}

All this to answer your question:

Can periods be used in Asp.Net Web Api Routes?

Yes.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • Your answer is specifically correct, but I forgot to mention that "id" and "format" are strings. I will update the question, but can I accept your answer and then add another informational answer of my own? – Lee Jul 16 '12 at 18:01
  • formatter as defaults parameter name, and ext in the routetemplate, that can't be correct? – Frode Nilsen Jun 24 '15 at 08:47
12

I'm accepting Darin's answer (yes, periods can be used in route urls) because it was specifically correct to my example, yet unhelpful to me. This is my fault for not specifically indicating that "id" is a string, not an integer.

To use a period following a string parameter the routing engine needs hints in the form of a constraint:

var c = new HttpSelfHostConfiguration(b);
c.Routes.MapHttpRoute(
    name: "DefaultApiRoute",
    routeTemplate: "{controller}/{id}.{format}",
    defaults: new { id = RouteParameter.Optional, format = RouteParameter.Optional },
    constraints: new { id = "[^\\.]+" } // anything but a period
);

Adding the constraint to the preceding token allows inbound URLs to be correctly decomposed and processed. Without the hint, the "id" token can be interpreted to match the remaining extent of the URL. This is just a specific case of needing constraints to delineate boundaries between string parameters in general.

Yes, periods can be used in URL routes in the Asp.Net Web API, but if they are to follow a string parameter be sure to apply the correct constraint to the route.

Lee
  • 801
  • 1
  • 6
  • 20
0

IIS intercepts requests with a period as file downloads. In your web.config, you can configure IIS to ignore specific URL paths because the webapi will handle the requests instead. If you want IIS to handle file downloads as well as process webapi calls, you can add a ManagedDllExtension configuration to system.webServer.handlers in web.config.

      <add name="ManagedDllExtension" path="collection/*.*" verb="GET" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
  • 1
    This is not correct, and the change noted is not necessary under most circumstances. If you have all managed modules set to handle, IIS does not 'intercept' requests simply for having a period in the path. That would be a silly thing to do, anyway; file names don't need to have a period in them, and directory/route names can. Read the OP's own answer for what was actually going on here. – Andrew Barber Jul 31 '14 at 15:02
  • @AndrewBarber This is not true. IIS can intercept request for simply having a period in the path depending on how you have the security configured. If you go into Request Filtering -> Edit Feature Settings, there's an option for Allow Unlisted File Name Extensions. If you uncheck that option and attempt to pass a value in a route with a period, you will get a 404.7 error. – DerHaifisch Jun 07 '23 at 15:49
0

Suprotim Agarwal provided a web.config rewrite solution here: https://www.devcurry.com/2015/07/allowing-dots-in-aspnet-mvc-paths.html - this rewrite interactively adds a slash to the end of a URL that contains a period when it is not a request for a directory or file:

<system.webServer>

    <rewrite>
        <rules>
            <clear />
            <rule name="AddTrailingSlashRule1" stopProcessing="true">
                <match url="(.*[^/])$" />
                <conditions>
                    <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
                    <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
                </conditions>
                <action type="Redirect" url="{R:1}/" />
            </rule>
        </rules>
    </rewrite>

</system.webServer>

EDIT 2023-03-17: Unfortunately I had to abandon this approach because when I used this, my Angular app would produce the following error:

Failed to load module script: Expected a Javascript module script but the server responded with a MIME type of "text/html".  Strict MIME type checking is enforced for module scripts per HTML spec.

It was hard to be sure this rewrite was causing the error because when I removed it from web.config the error persisted, even if I recycled my IIS App Pool. Something persists on a per-browser basis - if I started up a different browser the state of web.config (no-longer having the rewrite) would mean that app worked again.

StackOverflowUser
  • 945
  • 12
  • 10