5

I am building a client/server solution, using an AngularJS Single Page App as the client component and a Self-Host ServiceStack RESTful API as the server component. A single Visual Studio Console Application Project holds HTML and JavaScript files for the AngularJS component, along with C# classes for bootstrapping the ServiceStack AppHost (I have devolved Interface and Service responsibilities to separate Visual Studio Projects).

I have set all HTML and JavaScript files to have a 'Build Action' of 'None' and a 'Copy to Output Directory' of 'Copy if newer'.

Everything is working very well as long as I am prepared to put up with having a '#' in my site URLs. I would like to eliminate this by using HTML5 pushstate URLs.

Effectively this means I need to persuade ServiceStack to serve up my default Single Page App HTML shell page whenever a non-existent route is requested. There is now a FallbackRoute attribute available in ServiceStack which appears to have been added exactly for this purpose.

However, I am unsure how to use it. I have found people asking similar questions here, here and here. But the answers given were all before the new FallbackRoute attribute arrived.

Essentially, I am looking for a simple, yet complete example of how to use the FallbackRoute attribute to ensure any requests to non-existent routes are redirected to a single static HTML page.

Community
  • 1
  • 1
Holf
  • 5,605
  • 3
  • 42
  • 63

2 Answers2

4

The RazorRockstars.Web has an implementation. I'll modify it to use a wildcard path and a default view:

[FallbackRoute("/{Path*}")]
public class Fallback
{
    public string Path { get; set; }
    public string PathInfo { get; set; }
}

public class RockstarsService : Service
{
    [DefaultView("Index")]
    public object Any(Fallback request)
    {
        request.PathInfo = base.Request.PathInfo;
        return request;
    }
    // ...
}

Since this is a service it requires a View page (details here) rather than a content page.

In the RockStars example, I can't determine what view would be rendered for the FallBackResponse, but setting the view explicitly should be all you need.

The [DefaultView("Index")] attribute I added to the Any method maps the response to a Views/Index.cshtml file. The Index.cshtml file can be empty but for a template declaration, and the complete markup for your single page app can be in your template file (i.e. _Layout.cshtml)

Without Razor

Read the html into a string and return it, while setting the content type to "text/html" with an attribute, see wiki docs on service return types

public class RockstarsService : Service
{
    static string readContents;

    [AddHeader(ContentType = "text/html")]
    public string Any(Fallback request)
    {

        // check timestamp for changes for production use
        if (readContents == '') {
            using (StreamReader streamReader = new StreamReader(pathFromConfigFile, Encoding.UTF8))
            {
                 readContents = streamReader.ReadToEnd();
            }
        }
        return readContents;
    }
    // ...
}
Community
  • 1
  • 1
Pauli Price
  • 4,187
  • 3
  • 34
  • 62
  • That's all very cool. However, I would rather not have to add the Razor Plugin if I don't have to. I am using a single static HTML page and it would seem like overkill to have to add a Razor templating engine just to add a Fallback Route. – Holf Aug 20 '13 at 19:43
  • added 'without razor' option – Pauli Price Aug 20 '13 at 20:56
  • Looks very nice... I'll be trying it out later. Thanks! – Holf Aug 21 '13 at 09:00
4

It turns out it is all very simple with the FallbackRoute functionality, once you work out how to use it properly:

[FallbackRoute("/{Path*}")]
    public class Fallback
    {
        public string Path { get; set; }
    }

    public class FallBackService : Service
    {
        public object Any(Fallback request)
        {
            return new HttpResult(new FileInfo("index.html")) {ContentType = "text/html"};
        }
    }

Once this is in place, I find 'index.html' is indeed getting served up whenever I try to hit a non-existent route.

Any static files, such as JavaScript and CSS resources, get served up as normal (as long as they have a 'Copy to Output Directory' setting of 'Copy if newer', of course).

This works like a charm with the HTML5 Push-state functionality in AngularJS.

Holf
  • 5,605
  • 3
  • 42
  • 63
  • Where exactly do you put this service, is it under your web project or as part of service interface project? Do you need to do anything else in startup.cs file for this? – ShP Oct 04 '18 at 19:59
  • It's been a while and I honestly can't remember if I had different web / service interface projects, i.e. I may have treated this route as an 'HTML service'. I think for separation of concerns I would typically put this in it's own project (which may also have responsibility for serving up static JS/CSS/Image attributes). – Holf Oct 08 '18 at 14:20