-
Notifications
You must be signed in to change notification settings - Fork 110
/
SpaFallbackExtensions.cs
169 lines (138 loc) · 5.72 KB
/
SpaFallbackExtensions.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using static Microsoft.AspNetCore.Http.HttpMethods;
namespace Hellang.Middleware.SpaFallback
{
public static class SpaFallbackExtensions
{
private const string UseMiddleware = nameof(IApplicationBuilder) + "." + nameof(UseSpaFallback);
private const string AddServices = nameof(IServiceCollection) + "." + nameof(AddSpaFallback);
private const string MarkerKey = "middleware.SpaFallback";
public static IServiceCollection AddSpaFallback(this IServiceCollection services)
{
return services.AddSpaFallback(configure: null);
}
public static IServiceCollection AddSpaFallback(this IServiceCollection services, PathString fallbackPath)
{
if (!fallbackPath.HasValue)
{
throw new ArgumentException("Fallback path must have a value.", nameof(fallbackPath));
}
return services.AddSpaFallback(options => options.GetFallbackPath = ctx => fallbackPath);
}
public static IServiceCollection AddSpaFallback(this IServiceCollection services, Action<SpaFallbackOptions> configure)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (configure != null)
{
services.Configure(configure);
}
// Make sure we signal that we've called AddSpaFallback.
services.TryAddSingleton<SpaFallbackMarkerService>();
// The StartupFilter is responsible for adding a marker middleware at the end of the pipeline.
services.TryAddEnumerable(ServiceDescriptor.Singleton<IStartupFilter, StartupFilter>());
return services;
}
public static IApplicationBuilder UseSpaFallback(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
var marker = app.ApplicationServices.GetService<SpaFallbackMarkerService>();
if (marker == null)
{
var message = new StringBuilder()
.AppendLine($"Unable to find the required services for the {nameof(UseSpaFallback)} middleware to function correctly.")
.AppendLine($"Make sure you call {AddServices} before calling {UseMiddleware}.")
.AppendLine("This is typically done inside the ConfigureServices method in your Startup class.")
.ToString();
throw new InvalidOperationException(message);
}
// Set the key to signal that the marker middleware should be added.
app.Properties[MarkerKey] = true;
return app.UseMiddleware<SpaFallbackMiddleware>();
}
internal static bool ShouldFallback(this HttpContext context, SpaFallbackOptions options)
{
if (context.Response.HasStarted)
{
return false;
}
if (context.Response.StatusCode != StatusCodes.Status404NotFound)
{
return false;
}
if (!IsGet(context.Request.Method))
{
return false;
}
// Fallback only on "hard" 404s, i.e. when the request reached the marker middleware.
if (!context.Items.ContainsKey(MarkerKey))
{
return false;
}
if (HasFileExtension(context.Request.Path))
{
return options.AllowFileExtensions;
}
return true;
}
internal static bool ShouldThrow(this HttpContext context, SpaFallbackOptions options)
{
return context.Response.StatusCode == StatusCodes.Status404NotFound && options.ThrowIfFallbackFails;
}
private static bool HasFileExtension(this PathString path)
{
return path.HasValue && Path.HasExtension(path.Value);
}
private class StartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
next(app);
// We only want to add the end middleware if
// UseSpaFallback has been called on the builder.
if (app.Properties.ContainsKey(MarkerKey))
{
app.UseMiddleware<MarkerMiddleware>();
}
};
}
private class MarkerMiddleware
{
public MarkerMiddleware(RequestDelegate next)
{
Next = next;
}
private RequestDelegate Next { get; }
public Task Invoke(HttpContext context)
{
// This marker is used to signal that the request wasn't
// handled and reached the end of the application pipeline.
context.Items[MarkerKey] = true;
return Next(context);
}
}
}
/// <summary>
/// A marker class used to determine if <see cref="AddSpaFallback(IServiceCollection)"/>
/// has been called before calling <see cref="UseSpaFallback"/>.
/// </summary>
private class SpaFallbackMarkerService
{
}
}
}