Skip to content

Functional Lazy Router! #27

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions BlazorLazyLoading.sln
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,24 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorLazyLoading.Module",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManifestGenerator", "src\ManifestGenerator\ManifestGenerator.csproj", "{DF18C6BF-F2D7-45B9-BB90-B3E6418AB36C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorLazyLoading.Components", "nuget\BlazorLazyLoading.Components\BlazorLazyLoading.Components.csproj", "{D5DC7943-2F45-4EDE-AF78-592A6A153671}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorLazyLoading.Components", "nuget\BlazorLazyLoading.Components\BlazorLazyLoading.Components.csproj", "{D5DC7943-2F45-4EDE-AF78-592A6A153671}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LazyComponents", "src\LazyComponents\LazyComponents.csproj", "{4265A87B-BD94-413F-8883-BA3D1A3C0BC1}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LazyComponents", "src\LazyComponents\LazyComponents.csproj", "{4265A87B-BD94-413F-8883-BA3D1A3C0BC1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManifestReader", "src\ManifestReader\ManifestReader.csproj", "{10AB33A5-80F4-4D02-9DBE-0484C3BB6954}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManifestReader", "src\ManifestReader\ManifestReader.csproj", "{10AB33A5-80F4-4D02-9DBE-0484C3BB6954}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AssemblyLoading", "AssemblyLoading", "{9E765815-21C8-4920-B6C4-B072C5A5DEE3}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Components", "Components", "{43806489-02DC-4E72-A983-40B4B6113D27}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Manifest", "Manifest", "{2B3516BB-63DC-46FA-ADE9-A1A4BF05F513}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "demo", "demo", "{38184F2E-7DDA-4F92-B730-85637350E56F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerHost", "demo\ServerHost\ServerHost.csproj", "{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WasmHost", "demo\WasmHost\WasmHost.csproj", "{D2DF44FE-098D-48EF-925B-F71DFDD7134D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -176,6 +182,30 @@ Global
{10AB33A5-80F4-4D02-9DBE-0484C3BB6954}.Release|x64.Build.0 = Release|Any CPU
{10AB33A5-80F4-4D02-9DBE-0484C3BB6954}.Release|x86.ActiveCfg = Release|Any CPU
{10AB33A5-80F4-4D02-9DBE-0484C3BB6954}.Release|x86.Build.0 = Release|Any CPU
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Debug|x64.ActiveCfg = Debug|Any CPU
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Debug|x64.Build.0 = Debug|Any CPU
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Debug|x86.ActiveCfg = Debug|Any CPU
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Debug|x86.Build.0 = Debug|Any CPU
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Release|Any CPU.Build.0 = Release|Any CPU
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Release|x64.ActiveCfg = Release|Any CPU
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Release|x64.Build.0 = Release|Any CPU
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Release|x86.ActiveCfg = Release|Any CPU
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899}.Release|x86.Build.0 = Release|Any CPU
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Debug|x64.ActiveCfg = Debug|Any CPU
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Debug|x64.Build.0 = Debug|Any CPU
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Debug|x86.ActiveCfg = Debug|Any CPU
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Debug|x86.Build.0 = Debug|Any CPU
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Release|Any CPU.Build.0 = Release|Any CPU
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Release|x64.ActiveCfg = Release|Any CPU
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Release|x64.Build.0 = Release|Any CPU
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Release|x86.ActiveCfg = Release|Any CPU
{D2DF44FE-098D-48EF-925B-F71DFDD7134D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -194,6 +224,8 @@ Global
{9E765815-21C8-4920-B6C4-B072C5A5DEE3} = {7A7C9856-CA0D-4655-8AF9-4BF60D997ACB}
{43806489-02DC-4E72-A983-40B4B6113D27} = {7A7C9856-CA0D-4655-8AF9-4BF60D997ACB}
{2B3516BB-63DC-46FA-ADE9-A1A4BF05F513} = {7A7C9856-CA0D-4655-8AF9-4BF60D997ACB}
{2EF3EB2F-0E00-4554-8EDC-7CEEE5C7B899} = {38184F2E-7DDA-4F92-B730-85637350E56F}
{D2DF44FE-098D-48EF-925B-F71DFDD7134D} = {38184F2E-7DDA-4F92-B730-85637350E56F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FE945EF4-4832-4073-8FD8-D1517AE90E21}
Expand Down
8 changes: 8 additions & 0 deletions demo/ModulesHost/Components/PageWithParam.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@page "/page-with-param/{text}"

<h1>PARAMTER IS @Text!</h1>

@code {
[Parameter]
public string Text { get; set; } = "???";
}
4 changes: 0 additions & 4 deletions demo/ModulesHost/Components/SimplePage.razor
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
@page "/simple-page"

<h3>SimplePage</h3>

@code {

}
11 changes: 8 additions & 3 deletions demo/WasmHost/App.razor
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
<Router AppAssembly="@typeof(Program).Assembly">
<LazyRouter AppAssembly="@typeof(Program).Assembly">
<Loading Context="moduleName">
<LayoutView Layout="@typeof(MainLayout)">
<p>Loading... please wait</p>
</LayoutView>
</Loading>
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
<p>Sorry, there's nothing at this address</p>
</LayoutView>
</NotFound>
</Router>
</LazyRouter>
17 changes: 16 additions & 1 deletion demo/WasmHost/Shared/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,22 @@
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch Data
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="simple-page">
<span class="oi oi-list-rich" aria-hidden="true"></span> (Lazy) Simple Page
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="page-with-param/hello">
<span class="oi oi-list-rich" aria-hidden="true"></span> (Lazy) Page with Param
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="@Guid.NewGuid()">
<span class="oi oi-list-rich" aria-hidden="true"></span> (404) Error Page
</NavLink>
</li>
</ul>
Expand Down
2 changes: 2 additions & 0 deletions src/AssemblyLoader/Abstractions/IAssemblyLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ namespace BlazorLazyLoading.Abstractions
{
public interface IAssemblyLoader
{
Assembly? GetLoadedAssemblyByName(AssemblyName assemblyName);

Task<Assembly?> LoadAssemblyByNameAsync(AssemblyName assemblyName);
}
}
12 changes: 12 additions & 0 deletions src/AssemblyLoader/Services/AssemblyLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ public void Dispose()
}
}

public Assembly? GetLoadedAssemblyByName(AssemblyName assemblyName)
{
IAssemblyComparer comparer = GetAssemblyNameComparer(assemblyName);

if (TryGetAlreadyLoadedAssembly(assemblyName, comparer, out var alreadyLoadedAssembly))
{
return alreadyLoadedAssembly;
}

return null;
}

public Task<Assembly?> LoadAssemblyByNameAsync(AssemblyName assemblyName)
{
return LoadAssemblyByNameAsync(assemblyName, null);
Expand Down
2 changes: 0 additions & 2 deletions src/LazyComponents/LazyComponent/Lazy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,6 @@ protected override async Task OnInitializedAsync()

protected override void BuildRenderTree(RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);

if (Type == null)
{
BuildFallbackComponent(builder);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace BlazorLazyLoading.LazyRoute.Internals
{
internal class OptionalTypeRouteConstraint<T> : RouteConstraint
{
public delegate bool TryParseDelegate(string str, out T result);

private readonly TryParseDelegate _parser;

public OptionalTypeRouteConstraint(TryParseDelegate parser)
{
_parser = parser;
}

public override bool Match(string pathSegment, out object convertedValue)
{
// Unset values are set to null in the Parameters object created in
// the RouteContext. To match this pattern, unset optional parmeters
// are converted to null.
if (string.IsNullOrEmpty(pathSegment))
{
convertedValue = null!;
return true;
}

if (_parser(pathSegment, out var result))
{
convertedValue = result!;
return true;
}
else
{
convertedValue = null!;
return false;
}
}
}
}
112 changes: 112 additions & 0 deletions src/LazyComponents/LazyRoute/Internals/RouteConstraint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Text;

namespace BlazorLazyLoading.LazyRoute.Internals
{
internal abstract class RouteConstraint
{
// note: the things that prevent this cache from growing unbounded is that
// we're the only caller to this code path, and the fact that there are only
// 8 possible instances that we create.
//
// The values passed in here for parsing are always static text defined in route attributes.
private static readonly ConcurrentDictionary<string, RouteConstraint> _cachedConstraints
= new ConcurrentDictionary<string, RouteConstraint>();

public abstract bool Match(string pathSegment, out object convertedValue);

public static RouteConstraint Parse(string template, string segment, string constraint)
{
if (string.IsNullOrEmpty(constraint))
{
throw new ArgumentException($"Malformed segment '{segment}' in route '{template}' contains an empty constraint.");
}

if (_cachedConstraints.TryGetValue(constraint, out var cachedInstance))
{
return cachedInstance;
}
else
{
var newInstance = CreateRouteConstraint(constraint);
if (newInstance != null)
{
// We've done to the work to create the constraint now, but it's possible
// we're competing with another thread. GetOrAdd can ensure only a single
// instance is returned so that any extra ones can be GC'ed.
return _cachedConstraints.GetOrAdd(constraint, newInstance);
}
else
{
throw new ArgumentException($"Unsupported constraint '{constraint}' in route '{template}'.");
}
}
}

/// <summary>
/// Creates a structured RouteConstraint object given a string that contains
/// the route constraint. A constraint is the place after the colon in a
/// parameter definition, for example `{age:int?}`.
///
/// If the constraint denotes an optional, this method will return an
/// <see cref="OptionalTypeRouteConstraint{T}" /> which handles the appropriate checks.
/// </summary>
/// <param name="constraint">String representation of the constraint</param>
/// <returns>Type-specific RouteConstraint object</returns>
private static RouteConstraint CreateRouteConstraint(string constraint)
{
switch (constraint)
{
case "bool":
return new TypeRouteConstraint<bool>(bool.TryParse);
case "bool?":
return new OptionalTypeRouteConstraint<bool>(bool.TryParse);
case "datetime":
return new TypeRouteConstraint<DateTime>((string str, out DateTime result)
=> DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result));
case "datetime?":
return new OptionalTypeRouteConstraint<DateTime>((string str, out DateTime result)
=> DateTime.TryParse(str, CultureInfo.InvariantCulture, DateTimeStyles.None, out result));
case "decimal":
return new TypeRouteConstraint<decimal>((string str, out decimal result)
=> decimal.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
case "decimal?":
return new OptionalTypeRouteConstraint<decimal>((string str, out decimal result)
=> decimal.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
case "double":
return new TypeRouteConstraint<double>((string str, out double result)
=> double.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
case "double?":
return new OptionalTypeRouteConstraint<double>((string str, out double result)
=> double.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
case "float":
return new TypeRouteConstraint<float>((string str, out float result)
=> float.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
case "float?":
return new OptionalTypeRouteConstraint<float>((string str, out float result)
=> float.TryParse(str, NumberStyles.Number, CultureInfo.InvariantCulture, out result));
case "guid":
return new TypeRouteConstraint<Guid>(Guid.TryParse);
case "guid?":
return new OptionalTypeRouteConstraint<Guid>(Guid.TryParse);
case "int":
return new TypeRouteConstraint<int>((string str, out int result)
=> int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
case "int?":
return new OptionalTypeRouteConstraint<int>((string str, out int result)
=> int.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
case "long":
return new TypeRouteConstraint<long>((string str, out long result)
=> long.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
case "long?":
return new OptionalTypeRouteConstraint<long>((string str, out long result)
=> long.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out result));
default:
return null!;
}
}
}
}
28 changes: 28 additions & 0 deletions src/LazyComponents/LazyRoute/Internals/RouteContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;

namespace BlazorLazyLoading.LazyRoute.Internals
{
internal class RouteContext
{
private static char[] Separator = new[] { '/' };

public RouteContext(string path)
{
// This is a simplification. We are assuming there are no paths like /a//b/. A proper routing
// implementation would be more sophisticated.
Segments = path.Trim('/').Split(Separator, StringSplitOptions.RemoveEmptyEntries);
// Individual segments are URL-decoded in order to support arbitrary characters, assuming UTF-8 encoding.
for (int i = 0; i < Segments.Length; i++)
{
Segments[i] = Uri.UnescapeDataString(Segments[i]);
}
}

public string[] Segments { get; }

public object Handler { get; set; } = null!;

public IReadOnlyDictionary<string, object> Parameters { get; set; } = null!;
}
}
Loading