Skip to content

Commit 4a5b9e6

Browse files
Add MapSpaFallbackRoute helper. Will move into separate package shortly.
1 parent ad04dd1 commit 4a5b9e6

1 file changed

Lines changed: 68 additions & 5 deletions

File tree

samples/angular/MusicStore/Startup.cs

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
using System;
2+
using System.Collections.Generic;
13
using Microsoft.AspNet.Authorization;
24
using Microsoft.AspNet.Builder;
35
using Microsoft.AspNet.Hosting;
6+
using Microsoft.AspNet.Http;
47
using Microsoft.AspNet.Identity.EntityFramework;
8+
using Microsoft.AspNet.Routing;
59
using Microsoft.Data.Entity;
610
using Microsoft.Extensions.Configuration;
711
using Microsoft.Extensions.DependencyInjection;
@@ -108,15 +112,74 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF
108112
// Add MVC to the request pipeline.
109113
app.UseMvc(routes =>
110114
{
111-
routes.MapRoute(
112-
name: "default",
113-
template: "{controller=Home}/{action=Index}/{id?}");
114-
115-
routes.MapRoute("spa-fallback", "{*anything}", new { controller = "Home", action = "Index" });
115+
// Matches any request that doesn't appear to have a filename extension (defined as 'having a dot in the last URI segment').
116+
// This means you'll correctly get 404s for /some/dir/non-existent-image.png instead of returning the SPA HTML.
117+
// However, it means requests like /customers/isaac.newton will *not* be mapped into the SPA, so if you need to accept
118+
// URIs like that you'll need to match all URIs, e.g.:
119+
// routes.MapbackRoute("spa-fallback", "{*anything}", new { controller = "Home", action = "Index" });
120+
// (which of course will match /customers/isaac.png too - maybe that is a real customer name, not a PNG image).
121+
routes.MapSpaFallbackRoute("spa-fallback", new { controller = "Home", action = "Index" });
116122

117123
// Uncomment the following line to add a route for porting Web API 2 controllers.
118124
// routes.MapWebApiRoute("DefaultApi", "api/{controller}/{id?}");
119125
});
120126
}
121127
}
128+
internal static class SpaRouteExtensions {
129+
private const string ClientRouteTokenName = "clientRoute";
130+
131+
public static void MapSpaFallbackRoute(this IRouteBuilder routeBuilder, string name, object defaults, object constraints = null, object dataTokens = null) {
132+
MapSpaFallbackRoute(routeBuilder, name, /* templatePrefix */ (string)null, defaults, constraints, dataTokens);
133+
}
134+
135+
public static void MapSpaFallbackRoute(this IRouteBuilder routeBuilder, string name, string templatePrefix, object defaults, object constraints = null, object dataTokens = null)
136+
{
137+
var template = CreateRouteTemplate(templatePrefix);
138+
139+
var constraintsDict = ObjectToDictionary(constraints);
140+
constraintsDict.Add(ClientRouteTokenName, new SpaRouteConstraint());
141+
142+
routeBuilder.MapRoute(name, template, defaults, constraintsDict, dataTokens);
143+
}
144+
145+
private static string CreateRouteTemplate(string templatePrefix)
146+
{
147+
templatePrefix = templatePrefix ?? string.Empty;
148+
149+
if (templatePrefix.Contains("?")) {
150+
// TODO: Consider supporting this. The {*clientRoute} part should be added immediately before the '?'
151+
throw new ArgumentException("SPA fallback route templates don't support querystrings");
152+
}
153+
154+
if (templatePrefix.Contains("#")) {
155+
throw new ArgumentException("SPA fallback route templates should not include # characters. The hash part of a URI does not get sent to the server.");
156+
}
157+
158+
if (templatePrefix != string.Empty && !templatePrefix.EndsWith("/")) {
159+
templatePrefix += "/";
160+
}
161+
162+
return templatePrefix + $"{{*{ ClientRouteTokenName }}}";
163+
}
164+
165+
private static IDictionary<string, object> ObjectToDictionary(object value)
166+
{
167+
return value as IDictionary<string, object> ?? new RouteValueDictionary(value);
168+
}
169+
170+
private class SpaRouteConstraint : IRouteConstraint
171+
{
172+
public bool Match(HttpContext httpContext, IRouter route, string routeKey, IDictionary<string, object> values, RouteDirection routeDirection)
173+
{
174+
var clientRouteValue = (values[ClientRouteTokenName] as string) ?? string.Empty;
175+
return !HasDotInLastSegment(clientRouteValue);
176+
}
177+
178+
private bool HasDotInLastSegment(string uri)
179+
{
180+
var lastSegmentStartPos = uri.LastIndexOf('/');
181+
return uri.IndexOf('.', lastSegmentStartPos + 1) >= 0;
182+
}
183+
}
184+
}
122185
}

0 commit comments

Comments
 (0)