First let me explain the background. I am working on a project that attempts to marry a backend server that uses Web API configured via OWIN- hosted on IIS now, but potentially other OWIN-supported hosts in the future- to a frontend using AngularJS.
The AngularJS frontend is entirely static content. I completely avoid server-side technologies such as MVC/Razor, WebForms, Bundles, anything that has to do with the frontend and the assets it uses, and defer instead to the latest and greatest techniques using Node.js, Grunt/Gulp, etc. to handle CSS compilation, bundling, minification, etc. For reasons I won't go into here, I keep the frontend and server projects in separate locations within the same project (rather than stick them all in the Host project directly (see crude diagram below).
MyProject.sln
server
MyProject.Host
MyProject.Host.csproj
Startup.cs
(etc.)
frontend
MyProjectApp
app.js
index.html
MyProjectApp.njproj
(etc.)
So as far as the frontend is concerned, all I need to do is get my Host to serve my static content. In Express.js, this is trivial. With OWIN, I was able to do this easily using Microsoft.Owin.StaticFiles middleware, and it works great (it's very slick).
Here is my OwinStartup
configuration:
string dir = AppDomain.CurrentDomain.RelativeSearchPath; // get executing path
string contentPath = Path.GetFullPath(Path.Combine(dir, @"../../../frontend/MyProjectApp")); // resolve nearby frontend project directory
app.UseFileServer(new FileServerOptions
{
EnableDefaultFiles = true,
FileSystem = new PhysicalFileSystem(contentPath),
RequestPath = new PathString(string.Empty) // starts at the root of the host
});
// ensure the above occur before map handler to prevent native static content handler
app.UseStageMarker(PipelineStage.MapHandler);
Basically, it just hosts everything in frontend/MyProjectApp
as if it were right inside the root of MyProject.Host. So naturally, if you request a file that doesn't exist, IIS generates a 404 error.
Now, because this is an AngularJS app, and it supports html5mode
, I will have some routes that aren't physical files on the server, but are handled as routes in the AngularJS app. If a user were to drop onto an AngularJS (anything other than index.html
or a file that physically exists, in this example), I would get a 404 even though that route might be valid in the AngularJS app. Therefore, I need my OWIN middleware to return the index.html
file in the event a requested file does not exist, and let my AngularJS app figure out if it really is a 404.
If you're familiar with SPAs and AngularJS, this is a normal and straight-forward approach. If I were using MVC or ASP.NET routing, I could just set the default route to an MVC controller that returns my index.html
, or something along those lines. However, I've already stated I'm not using MVC and I'm trying to keep this as simple and lightweight as possible.
This user had a similar dilemma and solved it with IIS rewriting. In my case, it doesn't work because a) my content doesn't physically exist where the rewrite URL module can find it, so it always returns index.html
and b) I want something that doesn't rely on IIS, but is handled within OWIN middleware so it can be used flexibly.
Simple, how can I intercept a 404 Not Found and return the content of (note: not redirect) my FileServer
-served index.html
using OWIN middleware?
If you're using OWIN, you should be able to use this:
using AppFunc = Func<
IDictionary<string, object>, // Environment
Task>; // Done
public static class AngularServerExtension
{
public static IAppBuilder UseAngularServer(this IAppBuilder builder, string rootPath, string entryPath)
{
var options = new AngularServerOptions()
{
FileServerOptions = new FileServerOptions()
{
EnableDirectoryBrowsing = false,
FileSystem = new PhysicalFileSystem(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, rootPath))
},
EntryPath = new PathString(entryPath)
};
builder.UseDefaultFiles(options.FileServerOptions.DefaultFilesOptions);
return builder.Use(new Func<AppFunc, AppFunc>(next => new AngularServerMiddleware(next, options).Invoke));
}
}
public class AngularServerOptions
{
public FileServerOptions FileServerOptions { get; set; }
public PathString EntryPath { get; set; }
public bool Html5Mode
{
get
{
return EntryPath.HasValue;
}
}
public AngularServerOptions()
{
FileServerOptions = new FileServerOptions();
EntryPath = PathString.Empty;
}
}
public class AngularServerMiddleware
{
private readonly AngularServerOptions _options;
private readonly AppFunc _next;
private readonly StaticFileMiddleware _innerMiddleware;
public AngularServerMiddleware(AppFunc next, AngularServerOptions options)
{
_next = next;
_options = options;
_innerMiddleware = new StaticFileMiddleware(next, options.FileServerOptions.StaticFileOptions);
}
public async Task Invoke(IDictionary<string, object> arg)
{
await _innerMiddleware.Invoke(arg);
// route to root path if the status code is 404
// and need support angular html5mode
if ((int)arg["owin.ResponseStatusCode"] == 404 && _options.Html5Mode)
{
arg["owin.RequestPath"] = _options.EntryPath.Value;
await _innerMiddleware.Invoke(arg);
}
}
}
The solution that Javier Figueroa provided really works for my project. The back end of my program is an OWIN self-hosted webserver, and I use AngularJS with html5Mode enabled as the front end. I tried many different ways writing a IOwinContext middleware and none of them works till I found this one, it finally works! Thanks for sharing this solution.
solution provided by Javier Figueroa
By the way, the following is how I apply that AngularServerExtension in my OWIN startup class:
// declare the use of UseAngularServer extention
// "/" <= the rootPath
// "/index.html" <= the entryPath
app.UseAngularServer("/", "/index.html");
// Setting OWIN based web root directory
app.UseFileServer(new FileServerOptions()
{
RequestPath = PathString.Empty,
FileSystem = new PhysicalFileSystem(@staticFilesDir), // point to the root directory of my web server
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With