Skip to content

Commit 2cf0243

Browse files
authored
Functional Lazy Router! (#27)
1 parent d0a7261 commit 2cf0243

File tree

19 files changed

+1172
-13
lines changed

19 files changed

+1172
-13
lines changed

BlazorLazyLoading.sln

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,24 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorLazyLoading.Module",
3434
EndProject
3535
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManifestGenerator", "src\ManifestGenerator\ManifestGenerator.csproj", "{DF18C6BF-F2D7-45B9-BB90-B3E6418AB36C}"
3636
EndProject
37-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorLazyLoading.Components", "nuget\BlazorLazyLoading.Components\BlazorLazyLoading.Components.csproj", "{D5DC7943-2F45-4EDE-AF78-592A6A153671}"
37+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorLazyLoading.Components", "nuget\BlazorLazyLoading.Components\BlazorLazyLoading.Components.csproj", "{D5DC7943-2F45-4EDE-AF78-592A6A153671}"
3838
EndProject
39-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LazyComponents", "src\LazyComponents\LazyComponents.csproj", "{4265A87B-BD94-413F-8883-BA3D1A3C0BC1}"
39+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LazyComponents", "src\LazyComponents\LazyComponents.csproj", "{4265A87B-BD94-413F-8883-BA3D1A3C0BC1}"
4040
EndProject
41-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManifestReader", "src\ManifestReader\ManifestReader.csproj", "{10AB33A5-80F4-4D02-9DBE-0484C3BB6954}"
41+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManifestReader", "src\ManifestReader\ManifestReader.csproj", "{10AB33A5-80F4-4D02-9DBE-0484C3BB6954}"
4242
EndProject
4343
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AssemblyLoading", "AssemblyLoading", "{9E765815-21C8-4920-B6C4-B072C5A5DEE3}"
4444
EndProject
4545
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Components", "Components", "{43806489-02DC-4E72-A983-40B4B6113D27}"
4646
EndProject
4747
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Manifest", "Manifest", "{2B3516BB-63DC-46FA-ADE9-A1A4BF05F513}"
4848
EndProject
49+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "demo", "demo", "{38184F2E-7DDA-4F92-B730-85637350E56F}"
50+
EndProject
51+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerHost", "demo\ServerHost\ServerHost.csproj", "{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}"
52+
EndProject
53+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WasmHost", "demo\WasmHost\WasmHost.csproj", "{D2DF44FE-098D-48EF-925B-F71DFDD7134D}"
54+
EndProject
4955
Global
5056
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5157
Debug|Any CPU = Debug|Any CPU
@@ -176,6 +182,30 @@ Global
176182
{10AB33A5-80F4-4D02-9DBE-0484C3BB6954}.Release|x64.Build.0 = Release|Any CPU
177183
{10AB33A5-80F4-4D02-9DBE-0484C3BB6954}.Release|x86.ActiveCfg = Release|Any CPU
178184
{10AB33A5-80F4-4D02-9DBE-0484C3BB6954}.Release|x86.Build.0 = Release|Any CPU
185+
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
186+
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Debug|Any CPU.Build.0 = Debug|Any CPU
187+
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Debug|x64.ActiveCfg = Debug|Any CPU
188+
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Debug|x64.Build.0 = Debug|Any CPU
189+
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Debug|x86.ActiveCfg = Debug|Any CPU
190+
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Debug|x86.Build.0 = Debug|Any CPU
191+
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Release|Any CPU.ActiveCfg = Release|Any CPU
192+
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Release|Any CPU.Build.0 = Release|Any CPU
193+
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Release|x64.ActiveCfg = Release|Any CPU
194+
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Release|x64.Build.0 = Release|Any CPU
195+
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Release|x86.ActiveCfg = Release|Any CPU
196+
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Release|x86.Build.0 = Release|Any CPU
197+
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
198+
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Debug|Any CPU.Build.0 = Debug|Any CPU
199+
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Debug|x64.ActiveCfg = Debug|Any CPU
200+
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Debug|x64.Build.0 = Debug|Any CPU
201+
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Debug|x86.ActiveCfg = Debug|Any CPU
202+
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Debug|x86.Build.0 = Debug|Any CPU
203+
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Release|Any CPU.ActiveCfg = Release|Any CPU
204+
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Release|Any CPU.Build.0 = Release|Any CPU
205+
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Release|x64.ActiveCfg = Release|Any CPU
206+
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Release|x64.Build.0 = Release|Any CPU
207+
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Release|x86.ActiveCfg = Release|Any CPU
208+
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Release|x86.Build.0 = Release|Any CPU
179209
EndGlobalSection
180210
GlobalSection(SolutionProperties) = preSolution
181211
HideSolutionNode = FALSE
@@ -194,6 +224,8 @@ Global
194224
{9E765815-21C8-4920-B6C4-B072C5A5DEE3} = {7A7C9856-CA0D-4655-8AF9-4BF60D997ACB}
195225
{43806489-02DC-4E72-A983-40B4B6113D27} = {7A7C9856-CA0D-4655-8AF9-4BF60D997ACB}
196226
{2B3516BB-63DC-46FA-ADE9-A1A4BF05F513} = {7A7C9856-CA0D-4655-8AF9-4BF60D997ACB}
227+
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899} = {38184F2E-7DDA-4F92-B730-85637350E56F}
228+
{D2DF44FE-098D-48EF-925B-F71DFDD7134D} = {38184F2E-7DDA-4F92-B730-85637350E56F}
197229
EndGlobalSection
198230
GlobalSection(ExtensibilityGlobals) = postSolution
199231
SolutionGuid = {FE945EF4-4832-4073-8FD8-D1517AE90E21}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@page "/page-with-param/{text}"
2+
3+
<h1>PARAMTER IS @Text!</h1>
4+
5+
@code {
6+
[Parameter]
7+
public string Text { get; set; } = "???";
8+
}
Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
11
@page "/simple-page"
22

33
<h3>SimplePage</h3>
4-
5-
@code {
6-
7-
}

demo/WasmHost/App.razor

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
<Router AppAssembly="@typeof(Program).Assembly">
1+
<LazyRouter AppAssembly="@typeof(Program).Assembly">
2+
<Loading Context="moduleName">
3+
<LayoutView Layout="@typeof(MainLayout)">
4+
<p>Loading... please wait</p>
5+
</LayoutView>
6+
</Loading>
27
<Found Context="routeData">
38
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
49
</Found>
510
<NotFound>
611
<LayoutView Layout="@typeof(MainLayout)">
7-
<p>Sorry, there's nothing at this address.</p>
12+
<p>Sorry, there's nothing at this address</p>
813
</LayoutView>
914
</NotFound>
10-
</Router>
15+
</LazyRouter>

demo/WasmHost/Shared/NavMenu.razor

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,22 @@
1919
</li>
2020
<li class="nav-item px-3">
2121
<NavLink class="nav-link" href="fetchdata">
22-
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
22+
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch Data
23+
</NavLink>
24+
</li>
25+
<li class="nav-item px-3">
26+
<NavLink class="nav-link" href="simple-page">
27+
<span class="oi oi-list-rich" aria-hidden="true"></span> (Lazy) Simple Page
28+
</NavLink>
29+
</li>
30+
<li class="nav-item px-3">
31+
<NavLink class="nav-link" href="page-with-param/hello">
32+
<span class="oi oi-list-rich" aria-hidden="true"></span> (Lazy) Page with Param
33+
</NavLink>
34+
</li>
35+
<li class="nav-item px-3">
36+
<NavLink class="nav-link" href="@Guid.NewGuid()">
37+
<span class="oi oi-list-rich" aria-hidden="true"></span> (404) Error Page
2338
</NavLink>
2439
</li>
2540
</ul>

src/AssemblyLoader/Abstractions/IAssemblyLoader.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ namespace BlazorLazyLoading.Abstractions
55
{
66
public interface IAssemblyLoader
77
{
8+
Assembly? GetLoadedAssemblyByName(AssemblyName assemblyName);
9+
810
Task<Assembly?> LoadAssemblyByNameAsync(AssemblyName assemblyName);
911
}
1012
}

src/AssemblyLoader/Services/AssemblyLoader.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ public void Dispose()
4141
}
4242
}
4343

44+
public Assembly? GetLoadedAssemblyByName(AssemblyName assemblyName)
45+
{
46+
IAssemblyComparer comparer = GetAssemblyNameComparer(assemblyName);
47+
48+
if (TryGetAlreadyLoadedAssembly(assemblyName, comparer, out var alreadyLoadedAssembly))
49+
{
50+
return alreadyLoadedAssembly;
51+
}
52+
53+
return null;
54+
}
55+
4456
public Task<Assembly?> LoadAssemblyByNameAsync(AssemblyName assemblyName)
4557
{
4658
return LoadAssemblyByNameAsync(assemblyName, null);

src/LazyComponents/LazyComponent/Lazy.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,6 @@ protected override async Task OnInitializedAsync()
9696

9797
protected override void BuildRenderTree(RenderTreeBuilder builder)
9898
{
99-
base.BuildRenderTree(builder);
100-
10199
if (Type == null)
102100
{
103101
BuildFallbackComponent(builder);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
namespace BlazorLazyLoading.LazyRoute.Internals
2+
{
3+
internal class OptionalTypeRouteConstraint<T> : RouteConstraint
4+
{
5+
public delegate bool TryParseDelegate(string str, out T result);
6+
7+
private readonly TryParseDelegate _parser;
8+
9+
public OptionalTypeRouteConstraint(TryParseDelegate parser)
10+
{
11+
_parser = parser;
12+
}
13+
14+
public override bool Match(string pathSegment, out object convertedValue)
15+
{
16+
// Unset values are set to null in the Parameters object created in
17+
// the RouteContext. To match this pattern, unset optional parmeters
18+
// are converted to null.
19+
if (string.IsNullOrEmpty(pathSegment))
20+
{
21+
convertedValue = null!;
22+
return true;
23+
}
24+
25+
if (_parser(pathSegment, out var result))
26+
{
27+
convertedValue = result!;
28+
return true;
29+
}
30+
else
31+
{
32+
convertedValue = null!;
33+
return false;
34+
}
35+
}
36+
}
37+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
4+
using System.Globalization;
5+
using System.Text;
6+
7+
namespace BlazorLazyLoading.LazyRoute.Internals
8+
{
9+
internal abstract class RouteConstraint
10+
{
11+
// note: the things that prevent this cache from growing unbounded is that
12+
// we're the only caller to this code path, and the fact that there are only
13+
// 8 possible instances that we create.
14+
//
15+
// The values passed in here for parsing are always static text defined in route attributes.
16+
private static readonly ConcurrentDictionary<string, RouteConstraint> _cachedConstraints
17+
= new ConcurrentDictionary<string, RouteConstraint>();
18+
19+
public abstract bool Match(string pathSegment, out object convertedValue);
20+
21+
public static RouteConstraint Parse(string template, string segment, string constraint)
22+
{
23+
if (string.IsNullOrEmpty(constraint))
24+
{
25+
throw new ArgumentException($"Malformed segment '{segment}' in route '{template}' contains an empty constraint.");
26+
}
27+
28+
if (_cachedConstraints.TryGetValue(constraint, out var cachedInstance))
29+
{
30+
return cachedInstance;
31+
}
32+
else
33+
{
34+
var newInstance = CreateRouteConstraint(constraint);
35+
if (newInstance != null)
36+
{
37+
// We've done to the work to create the constraint now, but it's possible
38+
// we're competing with another thread. GetOrAdd can ensure only a single
39+
// instance is returned so that any extra ones can be GC'ed.
40+
return _cachedConstraints.GetOrAdd(constraint, newInstance);
41+
}
42+
else
43+
{
44+
throw new ArgumentException($"Unsupported constraint '{constraint}' in route '{template}'.");
45+
}
46+
}
47+
}
48+
49+
/// <summary>
50+
/// Creates a structured RouteConstraint object given a string that contains
51+
/// the route constraint. A constraint is the place after the colon in a
52+
/// parameter definition, for example `{age:int?}`.
53+
///
54+
/// If the constraint denotes an optional, this method will return an
55+
/// <see cref="OptionalTypeRouteConstraint{T}" /> which handles the appropriate checks.
56+
/// </summary>
57+
/// <param name="constraint">String representation of the constraint</param>
58+
/// <returns>Type-specific RouteConstraint object</returns>
59+
private static RouteConstraint CreateRouteConstraint(string constraint)
60+
{
61+
switch (constraint)
62+
{
63+
case "bool":
64+
return new TypeRouteConstraint<bool>(bool.TryParse);
65+
case "bool?":
66+
return new OptionalTypeRouteConstraint<bool>(bool.TryParse);
67+
case "datetime":
68+
return new TypeRouteConstraint<DateTime>((string str, out DateTime result)
69+
=> DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result));
70+
case "datetime?":
71+
return new OptionalTypeRouteConstraint<DateTime>((string str, out DateTime result)
72+
=> DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result));
73+
case "decimal":
74+
return new TypeRouteConstraint<decimal>((string str, out decimal result)
75+
=> decimal.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
76+
case "decimal?":
77+
return new OptionalTypeRouteConstraint<decimal>((string str, out decimal result)
78+
=> decimal.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
79+
case "double":
80+
return new TypeRouteConstraint<double>((string str, out double result)
81+
=> double.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
82+
case "double?":
83+
return new OptionalTypeRouteConstraint<double>((string str, out double result)
84+
=> double.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
85+
case "float":
86+
return new TypeRouteConstraint<float>((string str, out float result)
87+
=> float.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
88+
case "float?":
89+
return new OptionalTypeRouteConstraint<float>((string str, out float result)
90+
=> float.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
91+
case "guid":
92+
return new TypeRouteConstraint<Guid>(Guid.TryParse);
93+
case "guid?":
94+
return new OptionalTypeRouteConstraint<Guid>(Guid.TryParse);
95+
case "int":
96+
return new TypeRouteConstraint<int>((string str, out int result)
97+
=> int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
98+
case "int?":
99+
return new OptionalTypeRouteConstraint<int>((string str, out int result)
100+
=> int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
101+
case "long":
102+
return new TypeRouteConstraint<long>((string str, out long result)
103+
=> long.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
104+
case "long?":
105+
return new OptionalTypeRouteConstraint<long>((string str, out long result)
106+
=> long.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
107+
default:
108+
return null!;
109+
}
110+
}
111+
}
112+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace BlazorLazyLoading.LazyRoute.Internals
5+
{
6+
internal class RouteContext
7+
{
8+
private static char[] Separator = new[] { '/' };
9+
10+
public RouteContext(string path)
11+
{
12+
// This is a simplification. We are assuming there are no paths like /a//b/. A proper routing
13+
// implementation would be more sophisticated.
14+
Segments = path.Trim('/').Split(Separator, StringSplitOptions.RemoveEmptyEntries);
15+
// Individual segments are URL-decoded in order to support arbitrary characters, assuming UTF-8 encoding.
16+
for (int i = 0; i < Segments.Length; i++)
17+
{
18+
Segments[i] = Uri.UnescapeDataString(Segments[i]);
19+
}
20+
}
21+
22+
public string[] Segments { get; }
23+
24+
public object Handler { get; set; } = null!;
25+
26+
public IReadOnlyDictionary<string, object> Parameters { get; set; } = null!;
27+
}
28+
}

0 commit comments

Comments
 (0)