Skip to content

Commit 219422e

Browse files
Remove unnecessary parts from StartProject.Asp and create documentation for how to work with Reverse Proxies and make your custom UI Reverse Proxy aware.
Undo changes to appsettings.development.json and move them to appsettings.PathBase.json. Create a separate profile in launchSettings.json to start the version with PathBase enabled.
1 parent ea0a9e9 commit 219422e

File tree

11 files changed

+196
-71
lines changed

11 files changed

+196
-71
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Run behind reverse proxy
2+
3+
Running behind a reverse proxy is a common scenario for web applications.
4+
That's why there is proper support for this in ASP.NET Core and hence in MORYX.
5+
There is ample documentation for it by [Microsoft](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balance).
6+
7+
Depending on your reverse proxy setup there are different levels of awareness needed in your app.
8+
9+
A common use case for reverse proxies is to allow multiple web applications to share the same server and port.
10+
This can be done in one of two ways. Either by hostname based routing or by path based routing.
11+
12+
## Setup
13+
If you want to follow along with the examples you can use this docker-compose file together with the nginx configuration examples to setup a reverse proxy and modify the StartProject.Asp project to see if everything works as expected.
14+
The nginx configuration needs to be placed in a file named `nginx.conf` next to your `docker-compose.yml` when executing `docker compose up`.
15+
```yaml
16+
services:
17+
nginx-proxy:
18+
image: nginx:latest
19+
ports:
20+
- "8080:8080"
21+
volumes:
22+
# you can mount ./nginx-stripping.conf instead. To test the case where nginx strips the
23+
# prefix instead of removing it in asp.net
24+
- ./nginx.conf:/etc/nginx/nginx.conf:ro
25+
network_mode: host
26+
```
27+
28+
29+
30+
## Host based routing
31+
An example for host based routing would be to have a server `example.com` with two subdomains `app1.example.com` and `app2.example.com`.
32+
This is very convenient for us, because it "just works" from our perspective as an app developer.
33+
The disadvantage is the required infrastructure. We need DNS Entries for the subdomains that point to our server and if we server our content over HTTPs we need either a wildcard certificate for our subdomain or a certificate that explicitly lists all our applications.
34+
35+
## Path based routing
36+
That's why it might be more interesting to use path based routing. In this case we only have one hostname `example.com` and use the route after it to distinguish the different apps, like example.com/app1 and example.com/app2.
37+
There are two different ways to handle this scenario in our reverse proxy. The reverse proxy can either strip this prefix for passing the request along to our application or it can leave it as is. In both cases we need to enable support in our app.
38+
39+
40+
### Changes to the frontend code
41+
Regardless of the specific method, we need support not only in our backend, but also in our frontend applications. This support is already available in all MORYX Modules that are in this repository, but for your own UIs we need a few simple steps.
42+
43+
In the razor pages we usually mostly reference our script files and style sheets. It's important to not use an absolute path, but to start with a `~` instead.
44+
This is shorthand for the razor engine to automatically include the correct path. This is also required for your module eventstream.
45+
46+
```html
47+
<!--absolute-->
48+
<script src="/_content/<YourC#ProjectName>/main.js" type="module"></script>
49+
50+
<!-- base path aware -->
51+
<script src="~/_content/<YourC#ProjectName>/main.js" type="module"></script>
52+
53+
<!-- Example eventstream annotation from VisualInstructions.cshtml -- >
54+
[ModuleEventStream("~/api/moryx/instructions/stream")]
55+
```
56+
57+
Additionally we need to make sure the API Clients in the JS/TS Applications don't try to access the wrong endpoint.
58+
We usally do this using the environment for angular applications and we have a helper function in the @moryx/ngx-web-framework/environments package for this purpose. This computes the correct path for the APIs from your module path.
59+
60+
```typescript
61+
// environment.prod.ts
62+
63+
import { getPathBase } from '@moryx/ngx-web-framework/environments';
64+
65+
let path_base = getPathBase("/<Your Module Route>");
66+
67+
68+
export const environment = {
69+
production: true,
70+
assets: path_base + "/_content/<YourC#ProjectName>/",
71+
rootUrl: path_base,
72+
};
73+
74+
```
75+
76+
### Stripping proxy
77+
78+
If the proxy strips the prefix, it should set Headers to inform our application what the original request path was. An example configuration for nginx could look like this:
79+
```conf
80+
events {}
81+
# make sure "PathBase" is either empty or not present in appsettings
82+
http {
83+
server {
84+
listen 8080;
85+
server_name example.com; # Replace with your domain or IP
86+
87+
location /test/ {
88+
proxy_pass https://127.0.0.1:5000/;
89+
proxy_set_header Host $host;
90+
proxy_set_header X-Real-IP $remote_addr;
91+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
92+
proxy_set_header X-Forwarded-Prefix /test;
93+
}
94+
}
95+
}
96+
```
97+
98+
In this case we need to instruct Asp.Net to use these headers using the ForwardedHeaders Middleware like so.
99+
100+
101+
```csharp
102+
// Startup.cs or Program.cs before mapping static files, razor pages and so on.
103+
app.UseForwardedHeaders(new ForwardedHeadersOptions()
104+
{
105+
ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.All
106+
});
107+
```
108+
109+
### Route without modifying the request
110+
111+
If the proxy does not strip the prefix, we can use the `UsePathBase` middleware by adding before adding our routing.
112+
113+
```csharp
114+
app.UsePathBase(requiredPathBase);
115+
```
116+
117+
An example nginx.conf for this case:
118+
```conf
119+
events {}
120+
# make sure appsettings contains "PathBase": "/test"
121+
# this setup does only work, if docker runs on the same host as the moryx app.
122+
# running the app in windows and the container in WSL *won't* work
123+
http {
124+
server {
125+
listen 8080;
126+
server_name example.com; # Replace with your domain or IP
127+
128+
location /test/ {
129+
proxy_pass https://127.0.0.1:5000/test;
130+
proxy_set_header Host $host;
131+
proxy_set_header X-Real-IP $remote_addr;
132+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
133+
}
134+
}
135+
}
136+
137+
```

docs/tutorials/how-to-add-an-angular-application-to-your-UI.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ root to anchor the angular app into the razor page.
3737
```cshtml
3838
@* // Add the styles from the angular project *@
3939
@section Styles {
40-
<link rel="stylesheet" href="/_content/<YourC#ProjectName>/styles.css">
40+
<link rel="stylesheet" href="~/_content/<YourC#ProjectName>/styles.css">
4141
}
4242
4343
@* // Add the compiled javascript from the angular project *@
4444
@section Scripts
4545
{
46-
<script src="/_content/<YourC#ProjectName>/main.js" type="module"></script>
46+
<script src="~/_content/<YourC#ProjectName>/main.js" type="module"></script>
4747
}
4848
4949
@* // Add the application root *@

src/Moryx.VisualInstructions.Web/Pages/VisualInstructions.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
Layout = "_Layout";
1212
}
1313
@attribute [WebModule("VisualInstructions", "assignment"),
14-
ModuleEventStream("~/api/moryx/instructions/stream"),
14+
ModuleEventStream("~/api/moryx/instructions/stream"),
1515
Display(ResourceType = typeof(Strings), Name = "Module_Title", Description = "Module_Description"),
1616
Authorize(Policy = VisualInstructionsPermissions.CanView),
1717
AllowAnonymous]
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) 2026 Phoenix Contact GmbH & Co. KG
2+
// Licensed under the Apache License, Version 2.0
3+
4+
using System.Buffers;
5+
using System.Text;
6+
7+
/// <summary>
8+
/// Middleware only for testing purposes.
9+
/// </summary>
10+
/// <param name="requiredPathBase"></param>
11+
public class PathBaseTestMiddleware(string requiredPathBase)
12+
{
13+
public async Task BlockRequestsWithoutPathBase(HttpContext context, RequestDelegate next)
14+
{
15+
var pathBase = context.Request.PathBase;
16+
if (pathBase == requiredPathBase)
17+
{
18+
await next(context);
19+
}
20+
else
21+
{
22+
context.Response.StatusCode = 404;
23+
context.Response.BodyWriter.Write(Encoding.UTF8.GetBytes("Blocked of for test"));
24+
}
25+
}
26+
}

src/StartProject.Asp/Properties/launchSettings.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,21 @@
44
"commandName": "Project",
55
"dotnetRunMessages": true,
66
"launchBrowser": true,
7-
"useSSL": true,
87
"applicationUrl": "https://localhost:5000/",
9-
"launchUrl": "https://localhost:5000/test",
108
"environmentVariables": {
119
"ASPNETCORE_ENVIRONMENT": "Development"
1210
}
11+
},
12+
"StartProject.Asp - Path Base test": {
13+
"commandName": "Project",
14+
"dotnetRunMessages": true,
15+
"launchBrowser": true,
16+
"useSSL": true,
17+
"applicationUrl": "https://localhost:5000/",
18+
"launchUrl": "https://localhost:5000/test",
19+
"environmentVariables": {
20+
"ASPNETCORE_ENVIRONMENT": "PathBase"
21+
}
1322
}
1423
}
1524
}

src/StartProject.Asp/Startup.cs

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -80,33 +80,19 @@ public void ConfigureServices(IServiceCollection services)
8080
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
8181
{
8282

83-
app.UseForwardedHeaders(new ForwardedHeadersOptions()
84-
{
85-
ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.All
86-
});
87-
8883
var config = app.ApplicationServices.GetRequiredService<IConfiguration>();
84+
8985
var requiredPathBase = config.GetValue<string>("PathBase");
90-
9186
if(!string.IsNullOrEmpty(requiredPathBase))
9287
{
9388
// enable prefix striping. The path base is removed from the path and witten to
9489
// Context.Request.PathBase for later usage
9590
app.UsePathBase(requiredPathBase);
96-
// middleware to specifially block all traffic that was does not have the path base set for testing
97-
app.Use(async (context, next) =>
98-
{
99-
var pathBase = context.Request.PathBase;
100-
if (pathBase == requiredPathBase)
101-
{
102-
await next(context);
103-
}
104-
else
105-
{
106-
context.Response.StatusCode = 404;
107-
context.Response.BodyWriter.Write(Encoding.UTF8.GetBytes("Blocked of for test"));
108-
}
109-
});
91+
92+
// middleware to specifially block all traffic, that was does not have the path base set for testing.
93+
// This is not necessary for normal applications and only used for manual tests to ensure the information about the path base has been
94+
// propagated through to all the razor pages and javascript applications that request data from the server.
95+
app.Use(new PathBaseTestMiddleware(requiredPathBase).BlockRequestsWithoutPathBase);
11096
}
11197

11298
if (env.IsDevelopment())

src/StartProject.Asp/appsettings.Development.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,5 @@
77
"Microsoft.Hosting.Lifetime": "Information"
88
}
99
},
10-
"AuthUrl": "https://localhost:5001",
11-
"PathBase": "/test"
10+
"AuthUrl": "https://localhost:5001"
1211
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"DetailedErrors": true,
3+
"Logging": {
4+
"LogLevel": {
5+
"Default": "Information",
6+
"Microsoft": "Warning",
7+
"Microsoft.Hosting.Lifetime": "Information"
8+
}
9+
},
10+
"AuthUrl": "https://localhost:5001",
11+
"PathBase": "/test"
12+
}

src/Tests/ReverseProxySetup/docker-compose.yml

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/Tests/ReverseProxySetup/nginx-stripping.conf

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)