Route parsing abstraction and optimization, site router performance improvements, migrate site-based concepts (favicon, PWA support) to server for performance and prerendering benefits, move ThemeBuilder interop logic to OnAfterRenderAsync, upgrade SqlClient to release version, update installer to Bootstrap 5.1.3

This commit is contained in:
Shaun Walker 2021-12-01 08:22:59 -05:00
parent 03106526e9
commit 43d166fb7d
9 changed files with 335 additions and 246 deletions

View File

@ -158,8 +158,8 @@
if (firstRender)
{
var interop = new Interop(JSRuntime);
await interop.IncludeLink("", "stylesheet", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.0.2/css/bootstrap.min.css", "text/css", "sha512-usVBAd66/NpVNfBge19gws2j6JZinnca12rAe2l+d+QkLU9fiG02O1X8Q6hepIpr/EYKZvKx/I9WsnujJuOmBA==", "anonymous", "");
await interop.IncludeScript("", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.0.2/js/bootstrap.min.js", "sha512-a6ctI6w1kg3J4dSjknHj3aWLEbjitAXAjLDRUxo2wyYmDFRcz2RJuQr5M3Kt8O/TtUSp8n2rAyaXYy1sjoKmrQ==", "anonymous", "", "head", "");
await interop.IncludeLink("", "stylesheet", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css", "text/css", "sha512-GQGU0fMMi238uA+a/bdWJfpUGKUkBdgfFdgBm72SUQ6BeyWjoY/ton0tEjH+OSH9iP4Dfh+7HM0I9f5eR0L/4w==", "anonymous", "");
await interop.IncludeScript("", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js", "sha512-pax4MlgXjHEPfCwcJLQhigY7+N8rt6bVvWLFyUMuxShv170X53TRzGPmPkZmGBhk+jikR8WBM4yl7A9WMHHqvg==", "anonymous", "", "head", "");
}
}

View File

@ -170,7 +170,7 @@
<td><ActionLink Action="Settings" Text="Edit" ModuleId="@context.ModuleId" Security="SecurityAccessLevel.Edit" Permissions="@context.Permissions" ResourceKey="ModuleSettings" /></td>
<td><ActionDialog Header="Delete Module" Message="Are You Sure You Wish To Delete This Module?" Action="Delete" Security="SecurityAccessLevel.Edit" Permissions="@context.Permissions" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td>
<td>@context.Title</td>
<td>@context.ModuleDefinition.Name</td>
<td>@context.ModuleDefinition?.Name</td>
</Row>
</Pager>
}

View File

@ -15,78 +15,72 @@
@DynamicComponent
@code {
private string _absoluteUri;
private bool _navigationInterceptionEnabled;
private PageState _pagestate;
private string _absoluteUri;
private bool _navigationInterceptionEnabled;
private PageState _pagestate;
[Parameter]
public string Runtime { get; set; }
[Parameter]
public string Runtime { get; set; }
[Parameter]
public string RenderMode { get; set; }
[Parameter]
public string RenderMode { get; set; }
[CascadingParameter]
PageState PageState { get; set; }
[CascadingParameter]
PageState PageState { get; set; }
[Parameter]
public Action<PageState> OnStateChange { get; set; }
[Parameter]
public Action<PageState> OnStateChange { get; set; }
private RenderFragment DynamicComponent { get; set; }
private RenderFragment DynamicComponent { get; set; }
protected override void OnInitialized()
{
_absoluteUri = NavigationManager.Uri;
NavigationManager.LocationChanged += LocationChanged;
protected override void OnInitialized()
{
_absoluteUri = NavigationManager.Uri;
NavigationManager.LocationChanged += LocationChanged;
DynamicComponent = builder =>
{
if (PageState != null)
{
builder.OpenComponent(0, Type.GetType(Constants.PageComponent));
builder.CloseComponent();
}
};
}
DynamicComponent = builder =>
{
if (PageState != null)
{
builder.OpenComponent(0, Type.GetType(Constants.PageComponent));
builder.CloseComponent();
}
};
}
public void Dispose()
{
NavigationManager.LocationChanged -= LocationChanged;
}
public void Dispose()
{
NavigationManager.LocationChanged -= LocationChanged;
}
protected override async Task OnParametersSetAsync()
{
if (PageState == null)
{
await Refresh();
}
}
protected override async Task OnParametersSetAsync()
{
if (PageState == null)
{
await Refresh();
}
}
[SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")]
private async Task Refresh()
{
Site site;
List<Page> pages;
Page page;
User user = null;
List<Module> modules;
var moduleid = -1;
var action = Constants.DefaultAction;
var urlparameters = string.Empty;
var editmode = false;
var refresh = UI.Refresh.None;
var lastsyncdate = DateTime.UtcNow.AddHours(-1);
var runtime = (Shared.Runtime)Enum.Parse(typeof(Shared.Runtime), Runtime);
[SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")]
private async Task Refresh()
{
Site site;
List<Page> pages;
Page page;
User user = null;
List<Module> modules;
var editmode = false;
var refresh = UI.Refresh.None;
var lastsyncdate = DateTime.UtcNow.AddHours(-1);
var runtime = (Shared.Runtime)Enum.Parse(typeof(Shared.Runtime), Runtime);
Uri uri = new Uri(_absoluteUri);
// get path
var path = uri.LocalPath.Substring(1);
// parse querystring
var querystring = ParseQueryString(uri.Query);
Route route = new Route(_absoluteUri, SiteState.Alias.Path);
var moduleid = (int.TryParse(route.ModuleId, out int mid)) ? mid : -1;
var action = (!string.IsNullOrEmpty(route.Action)) ? route.Action : Constants.DefaultAction;
var querystring = ParseQueryString(route.Query);
// reload the client application if there is a forced reload or the user navigated to a site with a different alias
if (querystring.ContainsKey("reload") || (!path.ToLower().StartsWith(SiteState.Alias.Path.ToLower()) && !string.IsNullOrEmpty(SiteState.Alias.Path)))
if (querystring.ContainsKey("reload") || (!route.AbsolutePath.Substring(1).ToLower().StartsWith(SiteState.Alias.Path.ToLower()) && !string.IsNullOrEmpty(SiteState.Alias.Path)))
{
NavigationManager.NavigateTo(_absoluteUri.Replace("?reload", ""), true);
return;
@ -168,72 +162,9 @@
pages = PageState.Pages;
}
// format path and remove alias
path = path.Replace("//", "/"); // in case of doubleslash at end
path += (!path.EndsWith("/")) ? "/" : "";
if (SiteState.Alias.Path != "" && path.StartsWith(SiteState.Alias.Path))
{
path = path.Substring(SiteState.Alias.Path.Length + 1);
}
// extract admin route elements from path
var segments = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
int result;
int modIdPos = 0;
int actionPos = 0;
int urlParametersPos = 0;
for (int i = 0; i < segments.Length; i++)
{
if (segments[i] == Constants.UrlParametersDelimiter)
{
urlParametersPos = i + 1;
}
if (i >= urlParametersPos && urlParametersPos != 0)
{
urlparameters += "/" + segments[i];
}
if (segments[i] == Constants.ModuleDelimiter)
{
modIdPos = i + 1;
actionPos = modIdPos + 1;
if (actionPos <= segments.Length - 1)
{
action = segments[actionPos];
}
}
}
// check if path has moduleid and action specification ie. pagename/*/moduleid/action/
if (modIdPos > 0)
{
int.TryParse(segments[modIdPos], out result);
moduleid = result;
if (actionPos > segments.Length - 1)
{
path = path.Replace(segments[modIdPos - 1] + "/" + segments[modIdPos] + "/", "");
}
else
{
path = path.Replace(segments[modIdPos - 1] + "/" + segments[modIdPos] + "/" + segments[actionPos] + "/", "");
}
}
if (urlParametersPos > 0)
{
path = path.Replace(segments[urlParametersPos - 1] + urlparameters + "/", "");
}
// remove trailing slash so it can be used as a key for Pages
if (path.EndsWith("/")) path = path.Substring(0, path.Length - 1);
if (PageState == null || refresh == UI.Refresh.Site)
{
page = pages.FirstOrDefault(item => item.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
page = pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase));
}
else
{
@ -241,14 +172,13 @@
}
// get the page if the path has changed
if (page == null || page.Path != path)
if (page == null || page.Path != route.PagePath)
{
page = pages.FirstOrDefault(item => item.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
// if the home page path does not exist then use the first page in the collection (a future enhancement would allow the admin to specify a home page)
if (page == null && path == "")
page = pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase));
// if the home page path does not exist then use the first page in the collection (a future enhancement would allow the admin to specify the home page)
if (page == null && route.PagePath == "")
{
page = pages.FirstOrDefault();
path = page.Path;
}
editmode = false;
}
@ -286,7 +216,7 @@
Modules = modules,
Uri = new Uri(_absoluteUri, UriKind.Absolute),
QueryString = querystring,
UrlParameters = urlparameters,
UrlParameters = route.UrlParameters,
ModuleId = moduleid,
Action = action,
EditMode = editmode,
@ -302,12 +232,12 @@
if (user == null)
{
// redirect to login page
NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "login", "?returnurl=" + path));
NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "login", "?returnurl=" + route.AbsolutePath));
}
else
{
await LogService.Log(null, null, user.UserId, GetType().AssemblyQualifiedName, Utilities.GetTypeNameLastSegment(GetType().AssemblyQualifiedName, 1), LogFunction.Security, LogLevel.Error, null, "Page Does Not Exist Or User Is Not Authorized To View Page {Path}", path);
if (path != "")
await LogService.Log(null, null, user.UserId, GetType().AssemblyQualifiedName, Utilities.GetTypeNameLastSegment(GetType().AssemblyQualifiedName, 1), LogFunction.Security, LogLevel.Error, null, "Page Does Not Exist Or User Is Not Authorized To View Page {Path}", route.PagePath);
if (route.PagePath != "")
{
// redirect to home page
NavigationManager.NavigateTo(Utilities.NavigateUrl(SiteState.Alias.Path, "", ""));

View File

@ -5,103 +5,55 @@
@DynamicComponent
@code {
[CascadingParameter] PageState PageState { get; set; }
[CascadingParameter] PageState PageState { get; set; }
RenderFragment DynamicComponent { get; set; }
RenderFragment DynamicComponent { get; set; }
protected override async Task OnParametersSetAsync()
{
var interop = new Interop(JsRuntime);
protected override void OnParametersSet()
{
// handle page redirection
if (!string.IsNullOrEmpty(PageState.Page.Url))
{
NavigationManager.NavigateTo(PageState.Page.Url);
return;
}
// handle page redirection
if (!string.IsNullOrEmpty(PageState.Page.Url))
{
NavigationManager.NavigateTo(PageState.Page.Url);
return;
}
DynamicComponent = builder =>
{
var themeType = Type.GetType(PageState.Page.ThemeType);
builder.OpenComponent(0, themeType);
builder.CloseComponent();
};
}
// set page title
if (!string.IsNullOrEmpty(PageState.Page.Title))
{
await interop.UpdateTitle(PageState.Page.Title);
}
else
{
await interop.UpdateTitle(PageState.Site.Name + " - " + PageState.Page.Name);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
{
var interop = new Interop(JsRuntime);
// manage stylesheets for this page
string batch = DateTime.Now.ToString("yyyyMMddHHmmssfff");
var links = new List<object>();
foreach (Resource resource in PageState.Page.Resources.Where(item => item.ResourceType == ResourceType.Stylesheet && item.Declaration != ResourceDeclaration.Global))
{
links.Add(new { id = "app-stylesheet-" + batch + "-" + (links.Count + 1).ToString("00"), rel = "stylesheet", href = resource.Url, type = "text/css", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", key = "" });
}
if (links.Any())
{
await interop.IncludeLinks(links.ToArray());
}
await interop.RemoveElementsById("app-stylesheet", "", "app-stylesheet-" + batch + "-00");
// set page title
if (!string.IsNullOrEmpty(PageState.Page.Title))
{
await interop.UpdateTitle(PageState.Page.Title);
}
else
{
await interop.UpdateTitle(PageState.Site.Name + " - " + PageState.Page.Name);
}
// add favicon
if (PageState.Site.FaviconFileId != null)
{
await interop.IncludeLink("app-favicon", "shortcut icon", Utilities.ContentUrl(PageState.Alias, PageState.Site.FaviconFileId.Value), "image/x-icon", "", "", "id");
}
// add PWA support
if (PageState.Site.PwaIsEnabled && PageState.Site.PwaAppIconFileId != null && PageState.Site.PwaSplashIconFileId != null)
{
await InitializePwa(interop);
}
DynamicComponent = builder =>
{
var themeType = Type.GetType(PageState.Page.ThemeType);
builder.OpenComponent(0, themeType);
builder.CloseComponent();
};
}
private async Task InitializePwa(Interop interop)
{
string url = NavigationManager.BaseUri;
url = url.Substring(0, url.Length - 1);
// dynamically create manifest.json and add to page
string manifest = "setTimeout(() => { " +
"var manifest = { " +
"\"name\": \"" + PageState.Site.Name + "\", " +
"\"short_name\": \"" + PageState.Site.Name + "\", " +
"\"start_url\": \"" + url + "/\", " +
"\"display\": \"standalone\", " +
"\"background_color\": \"#fff\", " +
"\"description\": \"" + PageState.Site.Name + "\", " +
"\"icons\": [{ " +
"\"src\": \"" + url + Utilities.ContentUrl(PageState.Alias, PageState.Site.PwaAppIconFileId.Value) + "\", " +
"\"sizes\": \"192x192\", " +
"\"type\": \"image/png\" " +
"}, { " +
"\"src\": \"" + url + Utilities.ContentUrl(PageState.Alias, PageState.Site.PwaSplashIconFileId.Value) + "\", " +
"\"sizes\": \"512x512\", " +
"\"type\": \"image/png\" " +
"}] " +
"}; " +
"const serialized = JSON.stringify(manifest); " +
"const blob = new Blob([serialized], {type: 'application/javascript'}); " +
"const url = URL.createObjectURL(blob); " +
"document.getElementById('app-manifest').setAttribute('href', url); " +
"} " +
", 1000);";
await interop.IncludeScript("app-pwa", "", "", "", manifest, "body", "id");
// service worker must be in root of site
string serviceworker = "if ('serviceWorker' in navigator) { " +
"navigator.serviceWorker.register('/service-worker.js').then(function(registration) { " +
"console.log('ServiceWorker Registration Successful'); " +
"}).catch (function(err) { " +
"console.log('ServiceWorker Registration Failed ', err); " +
"}); " +
"}";
await interop.IncludeScript("app-serviceworker", "", "", "", serviceworker, "body", "id");
}
// manage stylesheets for this page
string batch = DateTime.Now.ToString("yyyyMMddHHmmssfff");
var links = new List<object>();
foreach (Resource resource in PageState.Page.Resources.Where(item => item.ResourceType == ResourceType.Stylesheet && item.Declaration != ResourceDeclaration.Global))
{
links.Add(new { id = "app-stylesheet-" + batch + "-" + (links.Count + 1).ToString("00"), rel = "stylesheet", href = resource.Url, type = "text/css", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", key = "" });
}
if (links.Any())
{
await interop.IncludeLinks(links.ToArray());
}
await interop.RemoveElementsById("app-stylesheet", "", "app-stylesheet-" + batch + "-00");
}
}
}

View File

@ -33,7 +33,7 @@
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="4.0.0-preview3.21293.2" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="4.0.0" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">

View File

@ -9,12 +9,14 @@
<meta name="viewport" content="width=device-width">
<title>@Model.Title</title>
<base href="~/" />
<link id="app-favicon" rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
<!-- stub the PWA manifest but defer the assignment of href -->
<link id="app-manifest" rel="manifest" />
<link rel="stylesheet" href="css/app.css" />
<link id="app-favicon" rel="shortcut icon" type="image/x-icon" href="@Model.FavIcon" />
@if (!string.IsNullOrEmpty(Model.PWAScript))
{
<link id="app-manifest" rel="manifest" />
}
<script src="js/app.js"></script>
<script src="js/loadjs.min.js"></script>
<link rel="stylesheet" href="css/app.css" />
@Html.Raw(@Model.HeadResources)
</head>
<body>
@ -44,6 +46,10 @@
{
<script src="_framework/blazor.server.js"></script>
}
@if (!string.IsNullOrEmpty(Model.PWAScript))
{
@Model.PWAScript
}
@Html.Raw(@Model.BodyResources)
</body>
</html>

View File

@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.Extensions;
namespace Oqtane.Pages
{
@ -24,8 +25,9 @@ namespace Oqtane.Pages
private readonly ILanguageRepository _languages;
private readonly IAntiforgery _antiforgery;
private readonly ISiteRepository _sites;
private readonly IPageRepository _pages;
public HostModel(IConfiguration configuration, ITenantManager tenantManager, ILocalizationManager localizationManager, ILanguageRepository languages, IAntiforgery antiforgery, ISiteRepository sites)
public HostModel(IConfiguration configuration, ITenantManager tenantManager, ILocalizationManager localizationManager, ILanguageRepository languages, IAntiforgery antiforgery, ISiteRepository sites, IPageRepository pages)
{
_configuration = configuration;
_tenantManager = tenantManager;
@ -33,6 +35,7 @@ namespace Oqtane.Pages
_languages = languages;
_antiforgery = antiforgery;
_sites = sites;
_pages = pages;
}
public string AntiForgeryToken = "";
@ -41,6 +44,9 @@ namespace Oqtane.Pages
public string HeadResources = "";
public string BodyResources = "";
public string Title = "";
public string FavIcon = "favicon.ico";
public string PWAScript = "";
public string ThemeType = "";
public void OnGet()
{
@ -55,14 +61,6 @@ namespace Oqtane.Pages
{
RenderMode = (RenderMode)Enum.Parse(typeof(RenderMode), _configuration.GetSection("RenderMode").Value, true);
}
var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies();
foreach (Assembly assembly in assemblies)
{
ProcessHostResources(assembly);
ProcessModuleControls(assembly);
ProcessThemeControls(assembly);
}
// if framework is installed
if (!string.IsNullOrEmpty(_configuration.GetConnectionString("DefaultConnection")))
@ -70,6 +68,8 @@ namespace Oqtane.Pages
var alias = _tenantManager.GetAlias();
if (alias != null)
{
Route route = new Route(HttpContext.Request.GetEncodedUrl(), alias.Path);
var site = _sites.GetSite(alias.SiteId);
if (site != null)
{
@ -81,10 +81,48 @@ namespace Oqtane.Pages
{
RenderMode = (RenderMode)Enum.Parse(typeof(RenderMode), site.RenderMode, true);
}
if (site.FaviconFileId != null)
{
FavIcon = Utilities.ContentUrl(alias, site.FaviconFileId.Value);
}
if (site.PwaIsEnabled && site.PwaAppIconFileId != null && site.PwaSplashIconFileId != null)
{
PWAScript = CreatePWAScript(alias, site, route);
}
Title = site.Name;
ThemeType = site.DefaultThemeType;
var page = _pages.GetPage(route.PagePath, site.SiteId);
if (page != null)
{
// set page title
if (!string.IsNullOrEmpty(page.Title))
{
Title = page.Title;
}
else
{
Title = Title + " - " + page.Name;
}
// include theme resources
if (!string.IsNullOrEmpty(page.ThemeType))
{
ThemeType = page.ThemeType;
}
}
}
// if culture not specified
// include global resources
var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies();
foreach (Assembly assembly in assemblies)
{
ProcessHostResources(assembly);
ProcessModuleControls(assembly);
ProcessThemeControls(assembly);
}
// set culture if not specified
if (HttpContext.Request.Cookies[CookieRequestCultureProvider.DefaultCookieName] == null)
{
// set default language for site if the culture is not supported
@ -103,6 +141,44 @@ namespace Oqtane.Pages
}
}
private string CreatePWAScript(Alias alias, Site site, Route route)
{
return
"<script>" +
"setTimeout(() => { " +
"var manifest = { " +
"\"name\": \"" + site.Name + "\", " +
"\"short_name\": \"" + site.Name + "\", " +
"\"start_url\": \"" + route.Scheme + "://" + route.Authority + "/\", " +
"\"display\": \"standalone\", " +
"\"background_color\": \"#fff\", " +
"\"description\": \"" + site.Name + "\", " +
"\"icons\": [{ " +
"\"src\": \"" + route.Scheme + "://" + route.Authority + Utilities.ContentUrl(alias, site.PwaAppIconFileId.Value) + "\", " +
"\"sizes\": \"192x192\", " +
"\"type\": \"image/png\" " +
"}, { " +
"\"src\": \"" + route.Scheme + "://" + route.Authority + Utilities.ContentUrl(alias, site.PwaSplashIconFileId.Value) + "\", " +
"\"sizes\": \"512x512\", " +
"\"type\": \"image/png\" " +
"}] " +
"}; " +
"const serialized = JSON.stringify(manifest); " +
"const blob = new Blob([serialized], {type: 'application/javascript'}); " +
"const url = URL.createObjectURL(blob); " +
"document.getElementById('app-manifest').setAttribute('href', url); " +
"} " +
", 1000);" +
"if ('serviceWorker' in navigator) { " +
"navigator.serviceWorker.register('/service-worker.js').then(function(registration) { " +
"console.log('ServiceWorker Registration Successful'); " +
"}).catch (function(err) { " +
"console.log('ServiceWorker Registration Failed ', err); " +
"}); " +
"};" +
"</script>";
}
private void ProcessHostResources(Assembly assembly)
{
var types = assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(IHostResources)));
@ -151,7 +227,7 @@ namespace Oqtane.Pages
{
foreach (var resource in obj.Resources)
{
if (resource.Declaration == ResourceDeclaration.Global)
if (resource.Declaration == ResourceDeclaration.Global || (Utilities.GetFullTypeName(type.AssemblyQualifiedName) == ThemeType && resource.ResourceType == ResourceType.Stylesheet))
{
ProcessResource(resource);
}
@ -166,7 +242,8 @@ namespace Oqtane.Pages
case ResourceType.Stylesheet:
if (!HeadResources.Contains(resource.Url, StringComparison.OrdinalIgnoreCase))
{
HeadResources += "<link rel=\"stylesheet\" href=\"" + resource.Url + "\"" + CrossOrigin(resource.CrossOrigin) + Integrity(resource.Integrity) + " />" + Environment.NewLine;
var id = (resource.Declaration == ResourceDeclaration.Global) ? "" : "id=\"app-stylesheet-" + DateTime.Now.ToString("yyyyMMddHHmmssfff") + "-00\" ";
HeadResources += "<link " + id + "rel=\"stylesheet\" href=\"" + resource.Url + "\"" + CrossOrigin(resource.CrossOrigin) + Integrity(resource.Integrity) + " />" + Environment.NewLine;
}
break;
case ResourceType.Script:

View File

@ -0,0 +1,124 @@
using System;
using Oqtane.Shared;
namespace Oqtane.Models
{
/// <summary>
/// A route is comprised of multiple components:
/// {scheme}://{hostname}/{aliaspath}/{pagepath}/*/{moduleid}/{action}/!/{urlparameters}?{query}#{fragment}
/// </summary>
public class Route
{
// default constructor accepts an absolute route url and alias
public Route(string route, string aliaspath)
{
Uri uri = new Uri(route);
Authority = uri.Authority;
Scheme = uri.Scheme;
Host = uri.Host;
Port = uri.Port.ToString();
Query = uri.Query;
Fragment = uri.Fragment;
AbsolutePath = uri.AbsolutePath;
AliasPath = aliaspath;
PagePath = AbsolutePath;
ModuleId = "";
Action = "";
UrlParameters = "";
if (AliasPath.Length != 0)
{
PagePath = PagePath.Substring(AliasPath.Length + 1);
}
int pos = PagePath.IndexOf("/" + Constants.UrlParametersDelimiter + "/");
if (pos != -1)
{
UrlParameters = PagePath.Substring(pos + 3);
PagePath = PagePath.Substring(1, pos);
}
pos = PagePath.IndexOf("/" + Constants.ModuleDelimiter + "/");
if (pos != -1)
{
ModuleId = PagePath.Substring(pos + 3);
PagePath = PagePath.Substring(1, pos);
}
if (ModuleId.Length != 0)
{
pos = ModuleId.IndexOf("/");
if (pos != -1)
{
Action = ModuleId.Substring(pos + 1);
ModuleId = ModuleId.Substring(0, pos);
}
}
if (PagePath.StartsWith("/"))
{
PagePath = (PagePath.Length == 1) ? "" : PagePath.Substring(1);
}
if (PagePath.EndsWith("/"))
{
PagePath = PagePath.Substring(0, PagePath.Length - 1);
}
}
/// <summary>
/// The host name or IP address and port number (does not include scheme or trailing slash)
/// </summary>
public string Authority { get; set; }
/// <summary>
/// A fully qualified route contains a scheme (ie. http, https )
/// </summary>
public string Scheme { get; set; }
/// <summary>
/// A fully qualified route contains a host name. The host name may include a port number.
/// </summary>
public string Host { get; set; }
/// <summary>
/// A host name may contain a port number
/// </summary>
public string Port { get; set; }
/// <summary>
/// The absolute path for the route
/// </summary>
public string AbsolutePath { get; set; }
/// <summary>
/// An absolute path may contain an alias path
/// </summary>
public string AliasPath { get; set; }
/// <summary>
/// A absolute path may contain a page path.
/// </summary>
public string PagePath { get; set; }
/// <summary>
/// A route may contain a module id (ie. when created using EditUrl) located after the module delimiter segment (/*/).
/// </summary>
public string ModuleId { get; set; }
/// <summary>
/// A route may contain an action (ie. when created using EditUrl) located after the module id.
/// </summary>
public string Action { get; set; }
/// <summary>
/// A route may contain parameters located after the url parameter delimiter segment (/!/).
/// </summary>
public string UrlParameters { get; set; }
/// <summary>
/// A route may contain querystring parameters located after the ? delimiter
/// </summary>
public string Query { get; set; }
/// <summary>
/// A route may contain a fragment located after the # delimiter
/// </summary>
public string Fragment { get; set; }
}
}

View File

@ -22,7 +22,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="6.0.0-preview.4.21253.7" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.Text.Json" Version="6.0.0" />
</ItemGroup>