From 43d166fb7d3ca6124512b0b923002a4ace42173b Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Wed, 1 Dec 2021 08:22:59 -0500 Subject: [PATCH] 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 --- Oqtane.Client/Installer/Installer.razor | 4 +- Oqtane.Client/Modules/Admin/Pages/Edit.razor | 2 +- Oqtane.Client/UI/SiteRouter.razor | 216 +++++++------------ Oqtane.Client/UI/ThemeBuilder.razor | 150 +++++-------- Oqtane.Server/Oqtane.Server.csproj | 2 +- Oqtane.Server/Pages/_Host.cshtml | 14 +- Oqtane.Server/Pages/_Host.cshtml.cs | 101 +++++++-- Oqtane.Shared/Models/Route.cs | 124 +++++++++++ Oqtane.Shared/Oqtane.Shared.csproj | 2 +- 9 files changed, 352 insertions(+), 263 deletions(-) create mode 100644 Oqtane.Shared/Models/Route.cs diff --git a/Oqtane.Client/Installer/Installer.razor b/Oqtane.Client/Installer/Installer.razor index 1ced68d10..75343a490 100644 --- a/Oqtane.Client/Installer/Installer.razor +++ b/Oqtane.Client/Installer/Installer.razor @@ -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", ""); } } diff --git a/Oqtane.Client/Modules/Admin/Pages/Edit.razor b/Oqtane.Client/Modules/Admin/Pages/Edit.razor index 6a118a62e..5f2a85360 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Edit.razor @@ -170,7 +170,7 @@ @context.Title - @context.ModuleDefinition.Name + @context.ModuleDefinition?.Name } diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index b1570c9f8..876f1ead4 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -15,78 +15,72 @@ @DynamicComponent @code { - private string _absoluteUri; - private bool _navigationInterceptionEnabled; - private PageState _pagestate; - - [Parameter] - public string Runtime { get; set; } - - [Parameter] - public string RenderMode { get; set; } - - [CascadingParameter] - PageState PageState { get; set; } - - [Parameter] - public Action OnStateChange { get; set; } - - private RenderFragment DynamicComponent { get; set; } - - protected override void OnInitialized() - { - _absoluteUri = NavigationManager.Uri; - NavigationManager.LocationChanged += LocationChanged; - - DynamicComponent = builder => - { - if (PageState != null) - { - builder.OpenComponent(0, Type.GetType(Constants.PageComponent)); - builder.CloseComponent(); - } - }; - } - - public void Dispose() - { - NavigationManager.LocationChanged -= LocationChanged; - } - - protected override async Task OnParametersSetAsync() - { - if (PageState == null) - { - await Refresh(); - } - } - - [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] - private async Task Refresh() - { - Site site; - List pages; - Page page; - User user = null; - List 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); - - Uri uri = new Uri(_absoluteUri); - - // get path - var path = uri.LocalPath.Substring(1); - - // parse querystring - var querystring = ParseQueryString(uri.Query); + private string _absoluteUri; + private bool _navigationInterceptionEnabled; + private PageState _pagestate; + + [Parameter] + public string Runtime { get; set; } + + [Parameter] + public string RenderMode { get; set; } + + [CascadingParameter] + PageState PageState { get; set; } + + [Parameter] + public Action OnStateChange { get; set; } + + private RenderFragment DynamicComponent { get; set; } + + protected override void OnInitialized() + { + _absoluteUri = NavigationManager.Uri; + NavigationManager.LocationChanged += LocationChanged; + + DynamicComponent = builder => + { + if (PageState != null) + { + builder.OpenComponent(0, Type.GetType(Constants.PageComponent)); + builder.CloseComponent(); + } + }; + } + + public void Dispose() + { + NavigationManager.LocationChanged -= LocationChanged; + } + + protected override async Task OnParametersSetAsync() + { + if (PageState == null) + { + await Refresh(); + } + } + + [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")] + private async Task Refresh() + { + Site site; + List pages; + Page page; + User user = null; + List 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); + + 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, "", "")); diff --git a/Oqtane.Client/UI/ThemeBuilder.razor b/Oqtane.Client/UI/ThemeBuilder.razor index e1bec7370..0f96323ee 100644 --- a/Oqtane.Client/UI/ThemeBuilder.razor +++ b/Oqtane.Client/UI/ThemeBuilder.razor @@ -5,103 +5,55 @@ @DynamicComponent @code { - [CascadingParameter] PageState PageState { get; set; } - - RenderFragment DynamicComponent { get; set; } - - protected override async Task OnParametersSetAsync() - { - var interop = new Interop(JsRuntime); - - // handle page redirection - if (!string.IsNullOrEmpty(PageState.Page.Url)) - { - NavigationManager.NavigateTo(PageState.Page.Url); - return; - } - - // 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); - } - - // manage stylesheets for this page - string batch = DateTime.Now.ToString("yyyyMMddHHmmssfff"); - var links = new List(); - 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"); - - // 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"); - } + [CascadingParameter] PageState PageState { get; set; } + + RenderFragment DynamicComponent { get; set; } + + protected override void OnParametersSet() + { + // 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(); + }; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (!firstRender) + { + var interop = new Interop(JsRuntime); + + // 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); + } + + // manage stylesheets for this page + string batch = DateTime.Now.ToString("yyyyMMddHHmmssfff"); + var links = new List(); + 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"); + } + } } diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index b171ea56b..6ac2caaf6 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -33,7 +33,7 @@ - + diff --git a/Oqtane.Server/Pages/_Host.cshtml b/Oqtane.Server/Pages/_Host.cshtml index 1800176c7..8e0980ec1 100644 --- a/Oqtane.Server/Pages/_Host.cshtml +++ b/Oqtane.Server/Pages/_Host.cshtml @@ -9,12 +9,14 @@ @Model.Title - - - - + + @if (!string.IsNullOrEmpty(Model.PWAScript)) + { + + } + @Html.Raw(@Model.HeadResources) @@ -44,6 +46,10 @@ { } + @if (!string.IsNullOrEmpty(Model.PWAScript)) + { + @Model.PWAScript + } @Html.Raw(@Model.BodyResources) diff --git a/Oqtane.Server/Pages/_Host.cshtml.cs b/Oqtane.Server/Pages/_Host.cshtml.cs index bb3f75c47..cb878e2ee 100644 --- a/Oqtane.Server/Pages/_Host.cshtml.cs +++ b/Oqtane.Server/Pages/_Host.cshtml.cs @@ -13,6 +13,7 @@ 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 @@ public class HostModel : PageModel 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 @@ public HostModel(IConfiguration configuration, ITenantManager tenantManager, ILo _languages = languages; _antiforgery = antiforgery; _sites = sites; + _pages = pages; } public string AntiForgeryToken = ""; @@ -41,6 +44,9 @@ public HostModel(IConfiguration configuration, ITenantManager tenantManager, ILo 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 @@ public void OnGet() { 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 @@ public void OnGet() 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 @@ public void OnGet() { 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; + } + } + } + + // include global resources + var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies(); + foreach (Assembly assembly in assemblies) + { + ProcessHostResources(assembly); + ProcessModuleControls(assembly); + ProcessThemeControls(assembly); } - // if culture not specified + // 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 @@ public void OnGet() } } + private string CreatePWAScript(Alias alias, Site site, Route route) + { + return + ""; + } + private void ProcessHostResources(Assembly assembly) { var types = assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(IHostResources))); @@ -151,7 +227,7 @@ private void ProcessThemeControls(Assembly assembly) { 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 @@ private void ProcessResource(Resource resource) case ResourceType.Stylesheet: if (!HeadResources.Contains(resource.Url, StringComparison.OrdinalIgnoreCase)) { - HeadResources += "" + Environment.NewLine; + var id = (resource.Declaration == ResourceDeclaration.Global) ? "" : "id=\"app-stylesheet-" + DateTime.Now.ToString("yyyyMMddHHmmssfff") + "-00\" "; + HeadResources += "" + Environment.NewLine; } break; case ResourceType.Script: diff --git a/Oqtane.Shared/Models/Route.cs b/Oqtane.Shared/Models/Route.cs new file mode 100644 index 000000000..6f02538a4 --- /dev/null +++ b/Oqtane.Shared/Models/Route.cs @@ -0,0 +1,124 @@ +using System; +using Oqtane.Shared; + +namespace Oqtane.Models +{ + /// + /// A route is comprised of multiple components: + /// {scheme}://{hostname}/{aliaspath}/{pagepath}/*/{moduleid}/{action}/!/{urlparameters}?{query}#{fragment} + /// + 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); + } + } + + /// + /// The host name or IP address and port number (does not include scheme or trailing slash) + /// + public string Authority { get; set; } + + /// + /// A fully qualified route contains a scheme (ie. http, https ) + /// + public string Scheme { get; set; } + + /// + /// A fully qualified route contains a host name. The host name may include a port number. + /// + public string Host { get; set; } + + /// + /// A host name may contain a port number + /// + public string Port { get; set; } + + /// + /// The absolute path for the route + /// + public string AbsolutePath { get; set; } + + /// + /// An absolute path may contain an alias path + /// + public string AliasPath { get; set; } + + /// + /// A absolute path may contain a page path. + /// + public string PagePath { get; set; } + + /// + /// A route may contain a module id (ie. when created using EditUrl) located after the module delimiter segment (/*/). + /// + public string ModuleId { get; set; } + + /// + /// A route may contain an action (ie. when created using EditUrl) located after the module id. + /// + public string Action { get; set; } + + /// + /// A route may contain parameters located after the url parameter delimiter segment (/!/). + /// + public string UrlParameters { get; set; } + + /// + /// A route may contain querystring parameters located after the ? delimiter + /// + public string Query { get; set; } + + /// + /// A route may contain a fragment located after the # delimiter + /// + public string Fragment { get; set; } + } +} diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index 44e525ab2..bd43767fe 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -22,7 +22,7 @@ - +