Web Apps

Web Apps are a new approach to dramatically simplify .NET Wep App development and provide the most productive development experience possible whilst maximizing reuse and component sharing. They also open up a number of new use-cases for maintaining clean isolation between front-end and back-end development with front-end developers not needing any knowledge of C#/.NET to be able to develop UIs for high-performance .NET Web Apps. Web Apps also make it easy to establish and share an approved suite of functionality amongst multiple websites all consuming the same back-end systems and data stores.

Web Apps leverages Templates to develop entire content-rich, data-driven websites without needing to write any C#, compile projects or manually refresh pages - resulting in the easiest and fastest way to develop Web Apps in .NET!

Ultimate Simplicity

Not having to write any C# code or perform any app builds dramatically reduces the cognitive overhead and conceptual knowledge required for development where the only thing front-end Web developers need to know is Template's syntax and what filters are available to call. Because of Template's high-fidelity with JavaScript, developing a Website with Templates will be instantly familiar to JavaScript devs despite calling and binding directly to .NET APIs behind the scenes.

All complexity with C#, .NET, namespaces, references, .dlls, strong naming, packages, MVC, Razor, build tools, IDE environments, etc has been eliminated leaving all Web Developers needing to do is run a cross-platform web/app.dll .NET Core 2.1 executable and configure a simple app.settings text file to specify which website folder to use, which ServiceStack features to enable, which db or redis providers to connect to, etc. Not needing to build also greatly simplifies deployments where multiple websites can be deployed with a single rsync or xcopy command or if deploying your App in a Docker Container, you just need to copy your website files, or just the app.settings if you're using an S3 or Azure Virtual File System.

Rapid Development Workflow

The iterative development experience is also unparalleled for a .NET App, no compilation is required so you can just leave the web/app.dll running whilst you add the template .html files needed to build your App and thanks to the built-in Hot Reloading support, pages will refresh automatically as you save. You'll just need to do a full page refresh when modifying external .css/.js files to bypass the browser cache and you'll need to restart web/app.dll to pick up any changes to your app.settings or added any .dlls to your /plugins folder.

Getting Started

The easiest way to get started is to from one of the project templates below. All project templates can be installed using the dotnet-new tool, which if not already can be installed with:

$ npm install -g @servicestack/cli

Bare WebApp

To start with a simple and mininal website, create a new bare-webapp project template:

$ dotnet-new bare-webapp ProjectName

This creates a multi-page Bootstrap Website with Menu navigation that's ideal for content-heavy Websites.

Parcel WebApp

For more sophisticated JavaScript Web Apps we recommended starting with the parcel-webapp project template:

$ dotnet-new parcel-webapp ProjectName

This provides a simple and powerful starting template for developing modern JavaScript .NET Core Web Apps utilizing the zero-configuration Parcel bundler to enable a rapid development workflow with access to best-of-class web technologies like TypeScript that's managed by pre-configured npm scripts to handle its entire dev workflow.

If needed, it includes an optional server component containing any C# extensions needed that's automatically built and deployed to the app's /plugins folder when started. This enables an extensibile .NET Core Web App with an integrated Parcel bundler to enable building sophisticated JavaScript Apps out-of-the-box within a live development environment.

Rockwind WebApp

To start with a working example of a data-driven and content heavy WebApp, create a new rockwind-webapp project:

$ dotnet-new rockwind-webapp ProjectName

This creates a "Kitchen Sink Project" that showcases a number of different features in a single App, including a Rockstars Content-heavy Website with a mix of content maintained in both HTML and Markdown that makes use of multiple layouts, dynamic Data-driven Web Pages to browse the Northwind Database, API Pages, Paged Based Routing, Order Report and SQL Studio use-case examples.

About WebApp Project Templates

All WebApps contains at least 2 folders, /app containing all source code and assets for your Web App and /web containing a copy of the WebApp binaries from ServiceStack/WebApp.

All projects can be run from Windows, OSX and Linux. On VS Code you can type Ctrl+Shift+B to run the configured build task, otherwise you can run from the command-line with:

dotnet web/app.dll ../app/app.settings

Once running you can view your App on http://localhost:5000/ which you can keep running whilst developing your app which will reload the page you're currently viewing on each file save using the built-in Hot Reloading.

Running multiple WebApps in a single Project

/app is an example of a single Web App, you can have multiple Web Apps in different folders and run any of them with:

dotnet web/app.dll ../<app name>/app.settings

Cloud Apps Starter Projects

If you intend to deploy your Web App on AWS or Azure you may prefer to start with one of the example Cloud Apps below which come pre-configured with deployment scripts for deploying with Travis CI and Docker:

About Bare WebApp

The Getting Started project contains a copy of the bare-webapp.web-templates.io project below which is representative of a typical Company splash Website:

Bare WebApp screenshot

The benefits over using a static website is improved maintenance as you can extract and use its common _layout.html instead of having it duplicated in each page. The menu.html partial also makes menu items easier to maintain by just adding an entry in the JavaScript object literal. The dynamic menu also takes care of highlighting the active menu item.

{{ { '/':         'Home',
     '/about':    'About',
     '/services': 'Services',
     '/contact':  'Contact',
   } | toList | assignTo: links }}

Ideal for Web Designers and Content Authors

The other primary benefit is that this is an example of a website that can be maintained by employees who don't have any programming experience as Templates in their basic form are intuitive and approachable to non-developers, e.g: The title of each page is maintained as metadata HTML comments:

<!--
title: About Us
-->

Template's syntax is also the ideal way to convey variable substitution, e.g: <title>{{ title }}</title> and even embedding a partial reads like english {{ 'menu' | partial }} which is both intuitive and works well with GUI HTML designers.

app.settings

Below is the app.settings for a Basic App, with contentRoot being the only setting required as the rest can be inferred but including the other relevant settings is both more descriptive to other developers as well making it easier to use tools like sed or powershell to replace them during deployment.

debug true
name Bare Web App
contentRoot ~/../bare
webRoot ~/../bare
debug true controls the level of internal diagnostics available and whether or not Hot Reloading is enabled.

About Parcel WebApp

The Parcel WebApp template is maintained in the following directory structure:

  • /app - Your Web App's published source code and any plugins
  • /client - The Parcel managed Client App where client source code is maintained
  • /server - Extend your Web App with an optional server.dll plugin containing additional Server functionality
  • /web - The Web App binaries

Most development will happen within /client which is automatically published to /app using parcel's CLI that's invoked from the included npm scripts.

client

The difference with templates-webapp is that the client source code is maintained in the /client folder and uses Parcel JS to automatically bundle and publish your App to /app/wwwroot which is updated with live changes during development.

The client folder also contains the npm package.json which contains all npm scripts required during development.

If this is the first time using Parcel, you will need to install its global CLI:

$ npm install -g parcel-bundler

Then you can run a watched parcel build of your Web App with:

$ npm run dev

Parcel is a zero configuration bundler which inspects your .html files to automatically transpile and bundle all your .js and .css assets and other web resources like TypeScript .ts source files into a static .html website synced at /app/wwwroot.

Then to start the ServiceStack Server to host your Web App run:

$ npm run server

Which will host your App at http://localhost:5000 which in debug mode will enable hot reloading which will automatically reload your web page as it detects any file changes made by parcel.

server

To enable even richer functionality, this Web Apps template is also pre-configured with a custom Server project where you can extend your Web App with Plugins where all Plugins, Services, Filters, etc are automatically wired and made available to your Web App.

This template includes a simple ServerPlugin.cs which contains an Empty ServerPlugin and Hello Service:

public class ServerPlugin : IPlugin
{
    public void Register(IAppHost appHost)
    {
    }
}

//[Route("/hello/{Name}")] // Handled by /hello/_name.html API page, uncomment to take over
public class Hello : IReturn<HelloResponse>
{
    public string Name { get; set; }
}

public class HelloResponse
{
    public string Result { get; set; }
}

public class MyServices : Service
{
    public object Any(Hello request)
    {
        return new HelloResponse { Result = $"Hi {request.Name} from server.dll" };
    }
}

To build the server.csproj project and copy the resulting server.dll to /app/plugins/serer.dll which will require restarting the server to load the latest plugin:

$ npm run server

This will automatically load any Plugins, Services, Filters, etc and make them available to your Web App.

One benefit of creating your APIs with C# ServiceStack Services instead of API Pages is that you can generate TypeScript DTOs with:

$ npm run dtos

Which saves generate DTOs for all your ServiceStack Services in dtos.ts which can then be accessed in your TypeScript source code.

If preferred you can instead develop Server APIs with API Pages, an example is included in /client/hello/_name.html

{{ { result: `Hi ${name} from /hello/_name.html` } | return }}

Which as it uses the same data structure as the Hello Service above, can be called with ServiceStack's JsonServiceClient and generated TypeScript DTOs.

The /client/index.ts shows an example of this where initially the App calls the C# Hello ServiceStack Service:

import { client } from "./shared";
import { Hello, HelloResponse } from "./dtos";

const result = document.querySelector("#result")!;

document.querySelector("#Name")!.addEventListener("input", async e => {
  const value = (e.target as HTMLInputElement).value;
  if (value != "") {
    const request = new Hello();
    request.name = value;
    const response = await client.get(request);
    // const response = await client.get<HelloResponse>(`/hello/${request.name}`); //call /hello/_name.html
    result.innerHTML = response.result;
  } else {
    result.innerHTML = "";
  }
});

But while your App is running you can instead toggle the uncommented the line and hit Ctrl+S to save index.ts which Parcel will automatically transpile and publish to /app/wwwroot that will be detected by Hot Reload to automatically reload the page with the latest changes. Now typing in the text field will display the response from calling the /hello/_name.html API instead.

Deployments

During development Parcel maintains a debug and source-code friendly version of your App. Before deploying you can build an optimized production version of your App with:

$ npm run build

Which will bundle and minify all .css, .js and .html assets and publish to /app/wwwroot.

Then to deploy Web Apps you just need to copy the /app and /web folders to any server with .NET Core 2.1 runtime installed. The Deploying Web Apps docs.

Example Web Apps

In addition to the templates there's a number of Web Apps to illustrate the various features available and to showcase the different kind of Web Apps that can easily be developed. The source code for each app is available either individually from github.com/NetCoreWebApps or all combined in the same repo at NetCoreWebApps/WebApp/apps. Each app runs the same unmodified Web App Binary that's also used in the Bare project above.

Running Web Apps locally

Each of the Example Apps can be run locally by clicking the download.zip link to download the project from GitHub. Extract the .zip contents and open the folder in VS Code then type Ctrl+Shift+B and run the build task. For any other text editor you can run the App with:

dotnet web/app.dll ../app/app.settings

Redis HTML

For the Redis Browser Web App, we wanted to implement an App that was an ideal candidate for a Single Page App but constrain ourselves to do all HTML rendering on the server and have each interaction request a full-page reload to see how a traditional server-generated Web App feels like with the performance of .NET Core 2.1 and Templates. We're pleasantly surprised with the result as when the App is run locally the responsiveness is effectively indistinguishable from an Ajax App. When hosted on the Internet there is a sub-second delay which causes a noticeable flicker but it still retains a pleasant UX that's faster than most websites.

The benefits of a traditional website is that it doesn't break the web where the back button and deep linking work without effort and you get to avoid the complexity train of adopting a premier JavaScript SPA Framework's configuration, dependencies, workflow and build system which has become overkill for small projects.

Redis HTML WebApp Screenshot

We've had a sordid history developing Redis UI's which we're built using the popular JavaScript frameworks that appeared dominant at the time but have since seen their ecosystem decline, starting with the Redis Admin UI (src) built using Google's Closure Library that as it works different to everything else needed a complete rewrite when creating redisreact.servicestack.net (src) using the hot new React framework, unfortunately it uses React's old deprecated ES5 syntax and Reflux which is sufficiently different from our current recommended TypeScript + React + Redux + WebPack JavaScript SPA Stack, that is going to require a significant refactor to adopt our preferred SPA tech stack.

Beautiful, succinct, declarative code

The nice thing about generating HTML is that it's the one true constant in Web development that will always be there. The entire functionality for the Redis Web App is contained in a single /redis-html/app/index.html which includes all Template and JavaScript Source Code in < 200 lines which also includes all as server logic as it doesn't rely on any back-end Services and just uses the Redis Filters to interface with Redis directly. The source code also serves as a good demonstration of the declarative coding style that Templates encourages that in addition to being highly-readable requires orders of magnitude less code than our previous Redis JavaScript SPA's with a comparable feature-set.

Having a much smaller code-base makes it much easier to maintain and enhance whilst being less susceptible to becoming obsolete by the next new JavaScript framework as it would only require rewriting 75 lines of JavaScript instead of the complete rewrite that would be required to convert the existing JavaScript Apps to a use different JavaScript fx.

app.settings

The app.settings for Redis is similar to Web App Starter above except it adds a redis.connection to configure a RedisManagerPool at the connection string provided as well as Redis Filters to give Templates access to the Redis instance.

debug true
name Redis Web App
contentRoot ~/../redis-html
webRoot ~/../redis-html
redis.connection localhost:6379

Redis Vue

Whilst the above server-generated HTML Redis UI shows how you can easily develop traditional Web Apps using Templates, we've also rewritten the Redis UI as a Single Page App which is the more suitable choice for an App like this as it provides a more optimal and responsive UX by only loading the HTML page once on Startup then utilizes Ajax to only download and update the incremental parts of the App's UI that needs changing.

Instead of using jQuery and server-side HTML this version has been rewritten to use Vue where the UI has been extracted into isolated Vue components utilizing Vue X-Templates to render the App on the client where all Redis Vue's functionality is contained within the Redis/app/index.html page.

Redis Vue WebApp Screenshot

Simple Vue App

Templates also provides a great development experience for Single Page Apps which for the most part gets out of your way letting you develop the Single Page App as if it were a static .html file, but also benefits from the flexibility of a dynamic web page when needed.

The containing _layout.html page can be separated from the index.html page that contains the App's functionality, where it's able to extract the title of the page and embed it in the HTML <head/> as well as embed the page's <script /> in its optimal location at the bottom of the HTML <body/>, after the page's blocking script dependencies:

<html>
<head>
<title>{{ title ?? 'Redis Vue WebApp' }}</title>
<i hidden>{{ '/js/hot-loader.js' | ifDebugIncludeScript }}</i>

...
<link rel="stylesheet" href="../assets/css/bootstrap.css">
<link rel="stylesheet" href="../assets/css/default.css">
</head>
<body>
    <h2 id="title"><a href="/"><img src="/assets/img/redis-logo.png" /> {{ title }}</a></h2>

    {{ page }}

    <script src="../assets/js/html-utils.js"></script>
    <script src="../assets/js/vue{{ '.min' | if(!debug) }}.js"></script>
    <script src="../assets/js/axios.min.js"></script>
    
    {{ scripts | raw }} 
</body>
</html>

Redis Vue avoids the complexity of adopting a npm build system by referencing Vue libraries as a simple script include:

<script src="../assets/js/vue{{ '.min' | if(!debug) }}.js">

Where it uses the more verbose and developer-friendly vue.js during development whilst using the production optimized vue.min.js for deployments. So despite avoiding the complexity tax of an npm-based build system it still gets some of its benefits like conditional deployments and effortless hot reloading.

Server Templates

Whilst most of index.html is a static Vue app, templates is leveraged to generate the body of the <redis-info/> Component on the initial home page render:

<script type="text/x-template" id="redis-info-template">
<div id="redis-info">
  <table class="table table-striped" style="width:450px">
    <tbody>
    {{#each toList(redisInfo) }}
        <tr>
          <th>{{ it.Key | replace('_',' ') }}</th>
          <td title="{{it.Value}}">{{ it.Value | substringWithEllipsis(32) }}</td>
        </tr>
    {{/each}}
    </tbody>
  </table>
</div>
</script>

This technique increases the time to first paint by being able to render the initial Vue page without waiting for an Ajax call response whilst benefiting from improved SEO from server-generated HTML.

Server Handling

Another area Templates is used is to handle the HTTP POST where it calls the redisChangeConnection filter to change the current Redis connection before rendering the connection-info.html partial with the current connection info:

<script type="text/x-template" id="connection-template">
<div id="connection-info" class="container">
    {{ continueExecutingFiltersOnError }}
    {{#if Request.Verb == "POST" }}
        {{ { host, port, db, password } | withoutEmptyValues | redisChangeConnection | end }}
        {{#if lastErrorMessage }}
            <div class="alert alert-danger">{{lastErrorMessage}}</div>
        {{else}}
            <div class="alert alert-success">Connection Changed</div>
        {{/if}}
    {{/if}}
</div>
</script>

Vue Ajax Server APIs

All other Server functionality is invoked by Vue using Ajax to call one of the Ajax APIs below implemented as API Pages:

search.html

Called when searching for Redis keys where the query is forwarded to the redisSearchKeys filter:

{{ limit ?? 100  | assignTo: limit }}

{{ `${q ?? ''}*` | redisSearchKeys({ limit }) 
                 | return }}
call.html

Called to execute an arbitrary Redis command on the connected instance, with the response from Redis is returned as a plain-text HTTP Response:

{{ { command } | ensureAllArgsNotEmpty }}

{{ ['flush','monitor','brpop','blpop'] | any => contains(lower(command), it)
   | assignTo: illegalCommand }}

{{ illegalCommand ? throwArgumentException('Command is not allowed.') : null }}

{{ command  | redisCall | assignTo: contents }}

{{ contents | return({ 'Content-Type': 'text/plain' }) }}

The benefits of using API Pages instead of a normal C# Service is being able to retain Web App's productive development workflow where the entire Redis Vue App is built without requiring any compilation.

Deep linking and full page reloads

The Redis Vue Single Page App also takes advantage of HTML5's history.pushState API to enable deep-linking and back-button support where most UI state changes is captured on the query string and used to initialize the Vue state on page navigation or full-page reloads where it provides transparent navigation and back-button support that functions like a traditional Web App but with the instant performance of a Single Page App.

Rockwind

The Rockwind website shows an example of combining multiple websites in a single Web App - a Rockstars Content Website and a dynamic data-driven UI for the Northwind database which can run against either SQL Server, MySql or SQLite database using just configuration. It also includes API Pages examples for rapidly developing Web APIs.

Rockstars

/rockstars is an example of a Content Website that itself maintains multiple sub sections with their own layouts - /rockstars/alive for living Rockstars and /rockstars/dead for the ones that have died. Each Rockstar maintains their own encapsulated mix of HTML, markdown content and splash image that intuitively uses the closest _layout.html, content.md and splash.jpg from the page they're referenced from. This approach makes it easy to move entire sub sections over by just moving a folder and it will automatically use the relevant layout and partials of its parent.

Rockwind WebApp screenshot

Northwind

/northwind is an example of a dynamic UI for a database containing a form to filter results, multi-nested detail pages and deep-linking for quickly navigating between referenced data. Templates is also a great solution for rapidly developing Web APIs where the /api/customers.html API Page below:

{{ limit ?? 100 | assignTo: limit }}

{{ 'select Id, CompanyName, ContactName, ContactTitle, City, Country from Customer' | assignTo: sql }}

{{#if !isEmpty(PathArgs)}}
   {{ `${sql} where Id = @id` | dbSingle({ id: PathArgs[0] }) 
      | return }}
{{/if}}

{{#if id}}      {{ 'Id = @id'           | addTo: filters }} {{/if}}
{{#if city}}    {{ 'City = @city'       | addTo: filters }} {{/if}}
{{#if country}} {{ 'Country = @country' | addTo: filters }} {{/if}}

{{#if !isEmpty(filters)}}
  {{ `${sql} WHERE ${join(filters, ' AND ')}` | assignTo: sql }}
{{/if}}

{{ `${sql} ORDER BY CompanyName ${sqlLimit(limit)}` | assignTo: sql }}

{{ sql | dbSelect({ id, city, country }) 
       | return }}

Is all the code needed to generate the following API endpoints:

/customers API
All Customers
Accept HTTP Header also supported
Alfreds Futterkiste Details
As List
Customers in Germany
Customers in London
Combination Query /api/customers?city=London&country=UK&limit=3

Multi platform configurations

In addition to being a .NET Core 2.1 App that runs flawlessly cross-platform on Windows, Linux and OSX, Web Apps can also support multiple RDBMS's and Virtual File Systems using just configuration.

web.sqlite.settings

SQLite uses a file system database letting you bundle your database with your App. So we can share the northwind.sqlite database across multiple Apps, the contentRoot is set to the /apps directory which can only be accessed by your App, whilst the webRoot is configured to use the Web Apps folder that hosts all the publicly accessible files of your App.

debug true
name Rockwind SQLite Web App
contentRoot ~/..
webRoot ~/../rockwind
db sqlite
db.connection ~/northwind.sqlite

To run the Rockwind app using the northwind.sqlite database, run the command below on Windows, Linux or OSX:

dotnet web/app.dll ../rockwind/web.sqlite.settings
web.sqlserver.settings

To switch to use the Northwind database in SQL Server we just need to update the configuration to point to a SQL Server database instance. Since the App no longer need access to the northwind.sqlite database, the contentRoot can be reverted back to the Web Apps folder:

debug true
name Rockwind SQL Server Web App
contentRoot ~/../rockwind
webRoot ~/../rockwind
db sqlserver
db.connection Server=localhost;Database=northwind;User Id=test;Password=test;

The /support/northwind-data project lets you quickly try out Rockwind against your local RDBMS by populating it with a copy of the Northwind database using the same sqlserver identifier and connection string from the App, e.g:

dotnet run sqlserver "Server=localhost;Database=northwind;User Id=test;Password=test;"
web.mysql.settings

You can run against a MySql database in the same way as SQL Server above but using a MySql db connection string:

debug true
name Rockwind MySql Web App
contentRoot ~/../rockwind
webRoot ~/../rockwind
db mysql
db.connection Server=localhost;Database=northwind;UID=root;Password=test
web.azure.settings

The example Azure configuration is also configured to use a different Virtual File System where instead of sourcing Web App files from the filesystem they're sourced from an Azure Blob Container. In this case we're not using any files from the App so we don't need to set a contentRoot or webRoot path. This also means that for deployment we're just deploying the WebApp binary with just this app.settings since both the Web App files and database are sourced remotely.

# Note: values prefixed with '$' are resolved from Environment Variables
debug false
name Azure Blob SQL Server Web App
bind *
db sqlserver
db.connection $AZURE_SQL_CONNECTION_STRING
files azure
files.config {ConnectionString:$AZURE_BLOB_CONNECTION_STRING,ContainerName:rockwind-fs}

# Reduces a Blob Storage API call, but takes longer for modified pages to appear
checkForModifiedPagesAfterSecs 60
defaultFileCacheExpirySecs     60

The /support/copy-files project lets you run Rockwind against your own Azure Blob Container by populating it with a copy of the /rockwind App's files using the same configuration above:

dotnet run azure "{ConnectionString:$AZURE_BLOB_CONNECTION_STRING,ContainerName:rockwind}"

Multi-RDBMS SQL

As Templates is unable to use a Typed ORM like OrmLite to hide the nuances of each database, we need to be a bit more diligent in Templates to use parameterized SQL that works across multiple databases by using the sql* DB Filters to avoid using RDBMS-specific SQL syntax. The /northwind/customer.html contains a good example containing a number of things to watch out for:

{{#if id}}
    {{ `select o.Id, 
            ${sqlConcat(["e.FirstName", "' '", "e.LastName"])} Employee, 
            OrderDate, ShipCountry, ShippedDate, 
            ${sqlCurrency("sum((d.Unitprice * d.Quantity) - d.discount)")} Total 
        from ${sqlQuote("Order")} o
            inner join
            OrderDetail d on o.Id = d.OrderId
            inner join 
            Employee e on o.EmployeeId = e.Id
        where CustomerId = @id
        group by o.Id, EmployeeId, FirstName, LastName, OrderDate, ShipCountry, ShippedDate`
        | dbSelect({ id }) 
        | assignTo: orders }}
{{/if}}

Use sqlConcat to concatenate strings using the RDBMS-specific SQL for the configured database. Likewise sqlCurrency utilizes RDBMS-specific SQL functions to return monetary values in a currency format, whilst sqlQuote is used for quoting tables named after a reserved word.

Of course if you don't intend on supporting multiple RDBMS's, you can ignore this and use RDBMS-specific syntax.

Rockwind VFS

/rockwind-vfs is a clone of the Rockwind Web App with 3 differences: It uses the resolveAsset filter for each .js, .css and image web asset so that it's able to generate external URLs directly to the S3 Bucket, Azure Blob Container or CDN hosting a copy of your files to both reduce the load on your Web App and maximize the responsiveness to the end user.

To maximize responsiveness when using remote storage, all embedded files utilize caching:

{{ "content.md" | includeFileWithCache | markdown }}

The other difference is that each table and column has been quoted in "double-quotes" so that it works in PostgreSQL which otherwise treats unquoted symbols as lowercase. This version of Rockwind also works with SQL Server and SQLite as they also support "Table" quotes but not MySql which uses `BackTicks` or [SquareBrackets]. It's therefore infeasible to develop Apps that support both PostgreSQL and MySql unless you're willing to use all lowercase, snake_case or the sqlQuote filter for every table and column.

Rockwind VFS WebApp screenshot

resolveAsset

If using a remote file storage like AWS S3 or Azure Blob Storage it's a good idea to use the resolveAsset filter for each external file reference. By default it returns the same path it was called with so it will continue to work locally but then ServiceStack effectively becomes a proxy where it has to call the remote Storage Service for each requested download.

<link rel="stylesheet" href="{{ 'assets/css/bootstrap.css' | resolveAsset }}" />

<img src="{{ 'splash.jpg' | resolveAsset }}" id="splash" alt="Dave Grohl" />

ServiceStack asynchronously writes each file to the Response Stream with the last Last-Modified HTTP Header to enable browser caching so it's still a workable solution but for optimal performance you can specify an args.assetsBase in your app.settings to populate the assetsBase TemplateContext Argument the resolveAsset filter uses to generate an external URL reference to the file on the remote storage service, reducing the load and improving the performance of your App, especially if it's configured to use a CDN.

Pure Cloud Apps

rockwind-aws/app.settings

The AWS settings shows an example of this where every external resource rockwind-aws.web-app.io has been replaced with a direct reference to the asset on the S3 bucket:

# Note: values prefixed with '$' are resolved from Environment Variables
debug false
name AWS S3 PostgreSQL Web App
bind *
db postgres
db.connection $AWS_RDS_POSTGRES
files s3
files.config {AccessKey:$AWS_ACCESS_KEY,SecretKey:$AWS_SECRET_KEY,Region:us-east-1,Bucket:rockwind}
args.assetsBase http://s3-postgresql.s3-website-us-east-1.amazonaws.com/

# Reduces an S3 API call, but takes longer for modified pages to appear
checkForModifiedPagesAfterSecs 60
defaultFileCacheExpirySecs     60

With all files being sourced from S3 and the App configured to use AWS RDS PostgreSQL, the AWS settings is an example of a Pure Cloud App where the entire App is hosted on managed cloud services that's decoupled from the .NET Core 2.1 binary that runs it that for the most part won't require redeploying the Web App binary unless making configuration changes or upgrading the web/app.dll as any App changes can just be uploaded straight to S3 which changes reflected within the checkForModifiedPagesAfterSecs setting, which tells the Web App how long to wait before checking for file changes whilst defaultFileCacheExpirySecs specifies how long to cache files like content.md for.

DockerFile

Deployments are also greatly simplified as all that's needed is to deploy the WebApp binary and app.settings of your Cloud App, e.g. here's the DockerFile for rockwind-aws.web-app.io - deployed to AWS ECS using the deployment scripts in rockwind-aws and following our .NET Core Docker Deployment Guideline:

FROM microsoft/dotnet:2.1-aspnetcore-runtime
COPY web /web
ADD https://raw.githubusercontent.com/NetCoreWebApps/Rockwind.Aws/master/app/app.settings /web/app.settings
WORKDIR /web
EXPOSE 5000/tcp
ENV ASPNETCORE_URLS https://*:5000
ENTRYPOINT ["dotnet", "/web/app.dll"]
rockwind-azure/app.settings

We can also create Azure Cloud Apps in the same we've done for AWS above, which runs the same /rockwind-vfs Web App but using an Azure hosted SQL Server database and its files hosted on Azure Blob Storage:

# Note: values prefixed with '$' are resolved from Environment Variables
debug false
name Azure Blob SQL Server Web App
bind *
db sqlserver
db.connection $AZURE_SQL_CONNECTION_STRING
files azure
files.config {ConnectionString:$AZURE_BLOB_CONNECTION_STRING,ContainerName:rockwind}
args.assetsBase https://servicestack.blob.core.windows.net/rockwind/

# Reduces an S3 API call, but takes longer for modified pages to appear
checkForModifiedPagesAfterSecs 60
defaultFileCacheExpirySecs     60

Plugins

Up till now the Apps above only have only used functionality built into ServiceStack, to enable even greater functionality but still retain all the benefits of developing Web Apps you can drop .dll with custom functionality into your Web App's /plugins folder. The plugins support in Web Apps is as friction-less as we could make it, there's no configuration to maintain or special interfaces to implement, you're able to drop your existing implementation .dll's as-is into the App's `/plugins` folder.

Plugins allow "no touch" sharing of ServiceStack Plugins, Services, Template Filters Template Code Pages, Validators, etc. contained within .dll's or .exe's dropped in a Web App's /plugins folder which are auto-registered on startup. The source code for all plugins used in this App were built from the .NET Core 2.1 projects in the /example-plugins folder. The plugins.web-app.io Web App below walks through examples of using Custom Filters, Services and Validators:

Plugins WebApp screenshot

Registering ServiceStack Plugins

ServiceStack Plugins can be added to your App by listing it's Type Name in the features config entry in app.settings:

debug true
name Web App Plugins
contentRoot ~/../plugins
webRoot ~/../plugins
features CustomPlugin, OpenApiFeature, PostmanFeature, CorsFeature, ValidationFeature
CustomPlugin { ShowProcessLinks: true }
ValidationFeature { ScanAppHostAssemblies: true }

All plugins listed in features will be added to your Web App's AppHost in the order they're specified. They can further customized by adding a separate config entry with the Plugin Name and a JavaScript Object literal to populate the Plugin at registration, e.g the config above is equivalent to:

Plugins.Add(new CustomPlugin { ShowProcessLinks = true });
Plugins.Add(new OpenApiFeature());
Plugins.Add(new PostmanFeature());
Plugins.Add(new CorsFeature());
Plugins.Add(new ValidationFeature { ScanAppHostAssemblies = true });

Custom Plugin

In this case it tells our CustomPlugin from /plugins/ServerInfo.dll to also show Process Links in its /metadata Page:

public class CustomPlugin : IPlugin
{
    public bool ShowDrivesLinks { get; set; } = true;
    
    public bool ShowProcessLinks { get; set; }

    public void Register(IAppHost appHost)
    {
        if (ShowDrivesLinks)
        {
            var diskFormat = Env.IsWindows ? "NTFS" : "ext2";
            appHost.GetPlugin<MetadataFeature>()
                .AddPluginLink("/drives", "All Disks")
                .AddPluginLink($"/drives?DriveFormatIn={diskFormat}", $"{diskFormat} Disks");
        }

        if (ShowProcessLinks)
        {
            appHost.GetPlugin<MetadataFeature>()
                .AddPluginLink("/processes", "All Processes")
                .AddPluginLink("/process/current", "Current Process");
        }
    }
}

Where as it was first registered in the list will appear before any links registered by other plugins:

Metadata screenshot

Built-in Plugins

It also tells the ValidationFeature to scan all Service Assemblies for Validators and to automatically register them which is how ServiceStack was able to find the ContactValidator used to validate the StoreContact request.

Other optional plugins registered in this Web App is the metadata Services required for Open API, Postman as well as support for CORS. You can check the /metadata/debug Template for all Plugins loaded in your AppHost.

.NET Extensibility

Plugins can also implement .NET Core's IStartup to be able to register any required dependencies without any coupling to any Custom AppHost.

To simplify configuration you can use the `plugins/*` wildcard in app.settings at the end of an ordered plugin list to register all remaining Plugins it finds in the apps `/plugins` folder:

features OpenApiFeature, PostmanFeature, CorsFeature, ValidationFeature, plugins/*
CustomPlugin { ShowProcessLinks: true }

Each plugin registered can continue to be furthered configured by specifying its name and a JavaScript object literal as seen above.

The /plugins2 App shows an example of this with the StartupPlugin registering a StartupDep dependency which is used by its StartupServices at runtime:

public class StartupDep
{
    public string Name { get; } = nameof(StartupDep);
}

public class StartupPlugin : IPlugin, IStartup
{
    public void Configure(IApplicationBuilder app) {}

    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton(new StartupDep());
        return null;
    }

    public void Register(IAppHost appHost)
    {
        appHost.GetPlugin<MetadataFeature>()
            .AddPluginLink("/startup-dep", "Startup Service");
    }       
}

[Route("/startup-dep")]
public class GetStartupDep : IReturn<string> {}

public class StartupServices : Service
{
    public StartupDep StartupDep { get; set; }

    [AddHeader(ContentType = MimeTypes.PlainText)]
    public object Any(GetStartupDep request) => StartupDep.Name;
}

ServiceStack Ecosystem

All Services loaded by plugins continue to benefit from ServiceStack's rich metadata services, including being listed in the /metadata page, being able to explore and interact with Services using /swagger-ui/ as well as being able to generate Typed APIs for the most popular Mobile, Web and Desktop platforms.

Chat

/chat is an example of the ultimate form of extensibility where instead of just being able to add Services, Filters and Plugins, etc. You can add your entire AppHost which Web Apps will use instead of its own. This vastly expands the use-cases that can be built with Web Apps as it gives you complete fine-grained control over how your App is configured.

Chat WebApp screenshot

Develop back-end using .NET IDE's

For chat.web-app.io we've taken a copy of the existing .NET Core 2.1 Chat App and moved its C# code to /example-plugins/Chat and its files to /apps/chat where it can be developed like any other Web App except it utilizes the Chat AppHost and implementation in the SelfHost Chat App.

Customizations from the original .NET Core Chat implementation includes removing MVC and Razor dependencies and configuration, extracting its _layout.html and converting index.html to use Templates from its original default.cshtml. It's also been enhanced with the ability to evaluate Templates from the Chat window, as seen in the screenshot above.

Chat AppHost

public class Startup
{
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole();
        var appSettings = new TextFileSettings("~/../../apps/chat/app.settings".MapProjectPath());
        app.UseServiceStack(new AppHost(appSettings));
    }
}

public class AppHost : AppHostBase
{
    public AppHost() : base("Chat Web App", typeof(ServerEventsServices).GetAssembly()) {}
    public AppHost(IAppSettings appSettings) : this() => AppSettings = appSettings;

    public override void Configure(Container container)
    {
        Plugins.AddIfNotExists(new TemplatePagesFeature()); //Already added if it's running as a Web App
        
        Plugins.Add(new ServerEventsFeature());

        SetConfig(new HostConfig
        {
            DefaultContentType = MimeTypes.Json,
            AllowSessionIdsInHttpParams = true,
        });

        this.CustomErrorHttpHandlers.Remove(HttpStatusCode.Forbidden);

        //Register all Authentication methods you want to enable for this web app.            
        Plugins.Add(new AuthFeature(
            () => new AuthUserSession(),
            new IAuthProvider[] {
                new TwitterAuthProvider(AppSettings),   //Sign-in with Twitter
                new FacebookAuthProvider(AppSettings),  //Sign-in with Facebook
                new GithubAuthProvider(AppSettings),    //Sign-in with GitHub
            }));

        container.RegisterAutoWiredAs<MemoryChatHistory, IChatHistory>();

        Plugins.Add(new CorsFeature(
            allowOriginWhitelist: new[] { "http://localhost", "http://null.jsbin.com" },
            allowCredentials: true,
            allowedHeaders: "Content-Type, Allow, Authorization"));
    }
}
Reusing Web App's web.setting and files

One nice thing from being able to reuse existing AppHost's is being able to develop all back-end C# Services and Custom Filters as a stand-alone .NET Core Project where it's more productive with access to .NET IDE tooling and debugging.

To account for these 2 modes we use AddIfNotExists to only register the TemplatePagesFeature plugin when running as a stand-alone App and add an additional constructor so it reuses the existing app.settings as its IAppSettings provider for is custom App configuration like OAuth App keys required for enabling Sign-In's via with Twitter, Facebook and GitHub when running on http://localhost:5000:

debug true
name Chat Web App
contentRoot ~/../chat
webRoot ~/../chat

oauth.RedirectUrl http://localhost:5000/
oauth.CallbackUrl http://localhost:5000/auth/{0}
oauth.twitter.ConsumerKey JvWZokH73rdghDdCFCFkJtCEU
oauth.twitter.ConsumerSecret WNeOT6YalxXDR4iWZjc4jVjFaydoDcY8jgRrGc5FVLjsVlY2Y8
oauth.facebook.Permissions email
oauth.facebook.AppId 447523305684110
oauth.facebook.AppSecret 7d8a16d5c7cbbfab4b49fd51183c93a0
oauth.github.Scopes user
oauth.github.ClientId dbe8c242e3d1099f4558
oauth.github.ClientSecret 42c8db8d0ca72a0ef202e0c197d1377670c990f4

After the back-end has been implemented we can build and copy the compiled Chat.dll into the Chat's /plugins folder where we can take advantage of the improved development experience for rapidly developing its UI.

Blog

Live Demo: blog.web-app.io

To maximize approachability the /Blog Web App has no C# source code, plugins and uses no JavaScript or CSS frameworks yielding an enjoyable development experiences as all the usual complexities of developing a C# Server and modern JS App has been dispensed and you can just focus on the App you want to create, using a plain-text editor on the left, a live updating browser on the right and nothing else to interrupt your flow.

Any infrastructure dependencies have also been avoided by using SQLite by default which is automatically created an populated on first run if no database exists, or if preferred can be changed to use any other popular RDBMS using just config.

Multi User Blogging Platform

Any number of users can Sign In via Twitter and publish content under their Twitter Username where only they'll be able to modify their own Content. Setting up Twitter is as easy as it can be which just requires modifying the Twitter Auth Config in app.settings with the URL where the blog is hosted and the OAuth Keys for the Twitter OAuth App created at apps.twitter.com.

Rich Content

Whilst most blogging platforms just edit static text, each Post content has access to full Templates language features so they can be used to create Live Documents or Render Markdown which is itself just one of the available blocks where it will render to HTML any content between the markdown blocks:

{#markdown}}
## Markdown Content
{‚Äč{/markdown}}

By default the Markdig implementation is used to render Markdown but can also be configured to use an alternate Markdown provider.

Rich Markdown Editor

To make it easy to recall Markdown features, each Content is equipped with a Rich Text editor containing the most popular formatting controls along with common short-cuts for each feature, discoverable by hovering over each button:

The Editor also adopts popular behavior in IDEs where Tab and SHIFT+Tab can be used to indent blocks of text and lines can be commented with Ctrl+/ or blocks with CTRL+SHIFT+/.

Another nice productivity win is being able to CTRL+CLICK on any Content you created to navigate to its Edit page.

Auto saved drafts

The content in each Text input and textarea is saved to localStorage on each key-press so you can freely reload pages and navigate around the site where all unpublished content will be preserved upon return.

When you want to revert back to the original published content you can clear the text boxes and reload the page which will load content from the database instead of localStorage.

Server Validation

The new.html and edit.html pages shows examples of performing server validation with ServiceStack Templates:

{{ assignErrorAndContinueExecuting: ex }}

{{ 'Title must be between 5 and 200 characters'      
   | onlyIf(length(postTitle) < 5 || length(postTitle) > 200) | assignTo: titleError }}
{{ 'Content must be between 25 and 64000 characters' 
   | onlyIf(length(content) < 25  || length(content) > 64000) | assignTo: contentError }}
{{ 'Potentially malicious characters detected'       
   | ifNotExists(contentError) | onlyIf(containsXss(content)) | assignTo: contentError }}

For more info see the docs on Error Handling.

Live Previews

Creating and Posting content benefit from Live Previews where its rendered output can be visualized in real-time before it's published.

Any textarea can easily be enhanced to enable Live Preview by including the data-livepreview attribute with the element where the output should be rendered in, e.g:

<textarea data-livepreview=".preview"></textarea>
<div class="preview"></div>

The implementation of which is surprisingly simple where the JavaScript snippet in default.js below is used to send its content on each change:

// Enable Live Preview of new Content
textAreas = document.querySelectorAll("textarea[data-livepreview]");
for (let i = 0; i < textAreas.length; i++) {
  textAreas[i].addEventListener("input", livepreview, false);
  livepreview({ target: textAreas[i] });
}

function livepreview(e) {
  let el = e.target;
  let sel = el.getAttribute("data-livepreview");

  if (el.value.trim() == "") {
    document.querySelector(sel).innerHTML = "<div>Live Preview</div>";
    return;
  }

  let formData = new FormData();
  formData.append("content", el.value);

  fetch("/preview", {
    method: "post",
    body: formData
  }).then(function(r) { return r.text(); })
    .then(function(r) { document.querySelector(sel).innerHTML = r; });
}

To the /preview.html API Page which just renders and captures any Template Content its sent and returns the output:

{{ content  | evalTemplate({use:{plugins:'MarkdownTemplatePlugin'}}) | assignTo:response }}
{{ response | return({ contentType:'text/plain' }) }}

By default the evalTemplate filter renders Templates in a new TemplateContext which can be customized to utilize any additional plugins, filters and blocks available in the configured TemplatePagesFeature, or for full access you can use {use:{context:true}} to evaluate any Template content under the same context that evalTemplate is run on.

Customizable Auth Providers

Authentication can now be configured using plain text config in your app.settings where initially you need register the AuthFeature plugin as normal by specifying it in the features list:

features AuthFeature

Then using AuthFeature.AuthProviders you can specify which Auth Providers you want to have registered, e.g:

AuthFeature.AuthProviders TwitterAuthProvider, GithubAuthProvider

Each Auth Provider checks the Web Apps app.settings for its Auth Provider specific configuration it needs, e.g. to configure both Twitter and GitHub Auth Providers you would populate it with your OAuth Apps details:

oauth.RedirectUrl http://127.0.0.1:5000/
oauth.CallbackUrl http://127.0.0.1:5000/auth/{0}

oauth.twitter.ConsumerKey {Twitter App Consumer Key}
oauth.twitter.ConsumerSecret {Twitter App Consumer Secret Key}

oauth.github.ClientId {GitHub Client Id}
oauth.github.ClientSecret {GitHub Client Secret}
oauth.github.Scopes {GitHub Auth Scopes}

Customizable Markdown Providers

By default Web Apps now utilize Markdig implementation to render its Markdown. You can also switch it back to the built-in Markdown provider that ServiceStack uses with:

markdownProvider MarkdownDeep

Rich Template Config Arguments

Any app.settings configs that are prefixed with args. are made available to Template Pages and any arguments starting with a { or [ are automatically converted into a JS object:

args.blog { name:'blog.web-app.io', href:'/' }
args.tags ['technology','marketing']

Where they can be referenced as an object or an array directly:

{{blog.name}}

{{#each tags}} {{it}} {{/each}}

The alternative approach is to give each argument value a different name:

args.blogName blog.web-app.io
args.blogHref /

made with by ServiceStack