diff --git a/zero.Core/Extensions/DictionaryExtensions.cs b/zero.Core/Extensions/DictionaryExtensions.cs index eefd4f2c..edb2354d 100644 --- a/zero.Core/Extensions/DictionaryExtensions.cs +++ b/zero.Core/Extensions/DictionaryExtensions.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace zero.Core.Extensions { @@ -22,5 +23,18 @@ namespace zero.Core.Extensions object value = model.GetValueOrDefault(key); return value == default || !(value is T) ? default : (T)value; } + + + public static Dictionary ToDistinctDictionary(this IEnumerable source, Func keySelector, Func elementSelector) + { + Dictionary result = new(); + + foreach (TSource sourceElement in source) + { + result.TryAdd(keySelector(sourceElement), elementSelector(sourceElement)); + } + + return result; + } } } diff --git a/zero.Core/Routing/Page/PageRouteProvider.cs b/zero.Core/Routing/Page/PageRouteProvider.cs index 3f39a719..496c83b7 100644 --- a/zero.Core/Routing/Page/PageRouteProvider.cs +++ b/zero.Core/Routing/Page/PageRouteProvider.cs @@ -12,6 +12,8 @@ namespace zero.Core.Routing { public static string PAGE_TYPE_PARAM = "pageType"; + public static string PAGE_ID_PARAM = "pageId"; + protected IPageUrlBuilder UrlBuilder { get; set; } @@ -47,6 +49,7 @@ namespace zero.Core.Routing route.DependsOn(model.Id); route.DependsOn(parents.Select(x => x.Id).ToArray()); route.Param(PAGE_TYPE_PARAM, model.PageTypeAlias); + route.Param(PAGE_ID_PARAM, model.Id); return route; } diff --git a/zero.Core/Routing/Page/PageRouteResolverHelper.cs b/zero.Core/Routing/Page/PageRouteResolverHelper.cs index b13a9dec..6ba11a80 100644 --- a/zero.Core/Routing/Page/PageRouteResolverHelper.cs +++ b/zero.Core/Routing/Page/PageRouteResolverHelper.cs @@ -20,6 +20,12 @@ namespace zero.Core.Routing } + public async Task ResolvePageFor(RoutingContext context, Func predicate = null) + { + return (await ResolveAllPagesFor(context, predicate)).FirstOrDefault(); + } + + public async Task> ResolveAllFor(RoutingContext context, Func predicate = null) { string fullKey = typeof(T).Name.ToString().ToLower() + ":" + context.Context.AppId; @@ -57,5 +63,44 @@ namespace zero.Core.Routing return routes.Select(x => x.Item2).ToHashSet(); } + + + public async Task> ResolveAllPagesFor(RoutingContext context, Func predicate = null) + { + string fullKey = typeof(T).Name.ToString().ToLower() + ":" + context.Context.AppId; + + if (!PageRouteCache.TryGetValue(fullKey, out HashSet<(Page, Route)> routes)) + { + List pages = new(); + IEnumerable>> resolvers = context.Context.Options.Routing.PageResolvers.GetAll(typeof(T)); + + foreach (Expression> resolver in resolvers.Reverse()) + { + pages.AddRange(await context.Session.Query().Where(resolver).ToListAsync()); + } + + Dictionary idToPage = pages.ToDictionary(x => context.Context.Options.Routing.PageRouteIdBuilder.Generate(x), x => x); + Dictionary idToRoute = await context.Session.LoadAsync(idToPage.Select(x => x.Key)); + + routes = new(); + + foreach ((string id, Page page) in idToPage) + { + if (idToRoute.TryGetValue(id, out Route route) && route != null) + { + routes.Add((page, route)); + } + } + + PageRouteCache.TryAdd(fullKey, routes); + } + + if (predicate != null) + { + return routes.Where(x => predicate(x.Item1)).Select(x => x.Item1).ToHashSet(); + } + + return routes.Select(x => x.Item1).ToHashSet(); + } } } diff --git a/zero.Core/Routing/RouteRefresher.cs b/zero.Core/Routing/RouteRefresher.cs index 2e477b9c..17d028ed 100644 --- a/zero.Core/Routing/RouteRefresher.cs +++ b/zero.Core/Routing/RouteRefresher.cs @@ -45,7 +45,7 @@ namespace zero.Core.Routing // delete previous route if the ID has changed, // otherwise it will just be updated - if (previousModel != null && !route.Id.Equals(GetId(provider, previousModel))) + if (previousModel != null && !route.Id.Equals(provider.Id(previousModel))) { context.Session.Delete(previousModel); } diff --git a/zero.Core/Routing/RouteResolver.cs b/zero.Core/Routing/RouteResolver.cs index 43cabd84..a9663be4 100644 --- a/zero.Core/Routing/RouteResolver.cs +++ b/zero.Core/Routing/RouteResolver.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Logging; using Raven.Client.Documents; using Raven.Client.Documents.Linq; -using Raven.Client.Documents.Session; using Raven.Client.Exceptions.Documents.Indexes; using System.Collections.Generic; using System.Linq; @@ -15,24 +14,16 @@ using zero.Core.Options; namespace zero.Core.Routing { - public class RouteResolver : IRouteResolver + public class RouteResolver : Routes, IRouteResolver { - public const char PATH_SEPERATOR = '/'; - - protected IZeroStore Store { get; set; } - protected ILogger Logger { get; set; } - protected IEnumerable Providers { get; set; } protected IApplicationResolver AppResolver { get; set; } protected IZeroOptions Options { get; set; } - public RouteResolver(IZeroStore store, ILogger logger, IEnumerable providers, IApplicationResolver appResolver, IZeroOptions options) + public RouteResolver(IZeroContext context, IZeroStore store, ILogger logger, IEnumerable providers, IApplicationResolver appResolver) : base(context, store, logger, providers) { - Store = store; - Logger = logger; - Providers = providers; AppResolver = appResolver; - Options = options; + Options = context.Options; } @@ -95,7 +86,7 @@ namespace zero.Core.Routing return null; } - return await ResolveRouteInternal(session, new RouteResponse() + return await ResolveRouteInternal(new RouteResponse() { Route = route, App = application, @@ -128,36 +119,26 @@ namespace zero.Core.Routing /// public RouteEndpoint MapEndpoint(IRouteModel route) { - IRouteProvider routeProvider = FindProvider(route.Route.ProviderAlias); + if (TryGetProvider(route.Route.ProviderAlias, out IRouteProvider provider)) + { + return provider.Map(GetContext(), route); + } + return null; - //return routeProvider?.MapEndpoint(route); } /// /// Call the provider which can resolve the route /// - Task ResolveRouteInternal(IAsyncDocumentSession session, RouteResponse response) + async Task ResolveRouteInternal(RouteResponse response) { - IRouteProvider routeProvider = FindProvider(response.Route.ProviderAlias); - return Task.FromResult(default(IRouteModel)); - //return await routeProvider?.ResolveRoute(session, response); - } - - - /// - /// Find registered route provider for the specified alias - /// - IRouteProvider FindProvider(string alias) - { - IRouteProvider routeProvider = Providers.FirstOrDefault(x => x.Alias == alias); - - if (routeProvider == null) + if (TryGetProvider(response.Route.ProviderAlias, out IRouteProvider provider)) { - Logger.LogWarning("Could not locate URL provider {provider}", alias); + return await provider.Model(GetContext(), response.Route); } - return routeProvider; + return null; } } diff --git a/zero.Core/Routing/Routes.cs b/zero.Core/Routing/Routes.cs index a8ebc396..eca7d146 100644 --- a/zero.Core/Routing/Routes.cs +++ b/zero.Core/Routing/Routes.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Threading.Tasks; using zero.Core.Database; using zero.Core.Entities; -using zero.Core.Extensions; namespace zero.Core.Routing @@ -31,47 +30,27 @@ namespace zero.Core.Routing /// - public async Task GetUrl(T model, object parameters = null) where T : IZeroRouteEntity => (await GetRoute(model, parameters))?.Url; + public async Task GetUrl(T model) where T : IZeroRouteEntity => (await GetRoute(model))?.Url; /// - public async Task GetUrl(string id, object parameters = null) where T : IZeroRouteEntity => (await GetRoute(id, parameters))?.Url; + public async Task GetUrl(string id) where T : IZeroRouteEntity => (await GetRoute(id))?.Url; /// - public async Task> GetUrls(IEnumerable models, object parameters = null) where T : IZeroRouteEntity => (await GetRoutes(models, parameters)).ToDictionary(x => x.Key, x => x.Value?.Url); + public async Task> GetUrls(params T[] models) where T : IZeroRouteEntity => (await GetRoutes(models)).ToDictionary(x => x.Key, x => x.Value?.Url); /// - public async Task GetRoute(string id, object parameters = null) where T : IZeroRouteEntity + public async Task GetRoute(string id) where T : IZeroRouteEntity { - if (id.IsNullOrEmpty()) - { - return null; - } - - Type type = typeof(T); - IRouteProvider routeProvider = Providers.FirstOrDefault(x => x.CanHandle(type)); - - if (routeProvider == null) - { - return null; - } - - RoutingContext context = GetContext(); - T model = await context.Session.LoadAsync(id); - - if (model == null) - { - return null; - } - - return await FindRoute(routeProvider, context, model); + T model = await GetContext().Session.LoadAsync(id); + return await GetRoute(model); } /// - public async Task GetRoute(T model, object parameters = null) where T : IZeroRouteEntity + public async Task GetRoute(T model) where T : IZeroRouteEntity { if (model == null) { @@ -89,9 +68,9 @@ namespace zero.Core.Routing /// - public async Task> GetRoutes(IEnumerable models, object parameters = null) where T : IZeroRouteEntity + public async Task> GetRoutes(params T[] models) where T : IZeroRouteEntity { - if (!models.Any()) + if (models.Length < 1) { return new(); } @@ -109,16 +88,7 @@ namespace zero.Core.Routing } RoutingContext context = GetContext(); - Dictionary idMap = models.ToDictionary(x => GetId(routeProvider, x), x => x); - Dictionary routes = await context.Session.LoadAsync(idMap.Select(x => x.Key)); - Dictionary result = new(); - - foreach ((string id, T model) in idMap) - { - result.Add(model, routes.GetValueOrDefault(id)); - } - - return result; + return (await routeProvider.Find(context, models.Select(x => (IZeroRouteEntity)x).ToArray())).ToDictionary(x => (T)x.Key, x => x.Value); } @@ -131,6 +101,14 @@ namespace zero.Core.Routing } + /// + public virtual bool TryGetProvider(string alias, out IRouteProvider provider) + { + provider = Providers.FirstOrDefault(x => x.Alias.Equals(alias, StringComparison.InvariantCultureIgnoreCase)); + return provider != null; + } + + /// /// Map a route to an MVC endpoint /// @@ -161,22 +139,6 @@ namespace zero.Core.Routing } - /// - /// Get ID in collection for an entity - /// - protected virtual string GetId(IRouteProvider provider, IZeroRouteEntity model) - { - string key = provider.Key(model); - - if (key.IsNullOrEmpty()) - { - return null; - } - - return "routes." + provider.Alias + "." + key; - } - - /// /// Build a new routing context /// @@ -191,36 +153,41 @@ namespace zero.Core.Routing /// /// Get the URL for an entity /// - Task GetUrl(T model, object parameters = null) where T : IZeroRouteEntity; + Task GetUrl(T model) where T : IZeroRouteEntity; /// /// Get the URL for an entity /// - Task GetUrl(string id, object parameters = null) where T : IZeroRouteEntity; + Task GetUrl(string id) where T : IZeroRouteEntity; /// /// Get URLs for multiple entities /// - Task> GetUrls(IEnumerable models, object parameters = null) where T : IZeroRouteEntity; + Task> GetUrls(params T[] models) where T : IZeroRouteEntity; /// /// Get the route object for an entity /// - Task GetRoute(T model, object parameters = null) where T : IZeroRouteEntity; + Task GetRoute(T model) where T : IZeroRouteEntity; /// /// Get the route object for an entity /// - Task GetRoute(string id, object parameters = null) where T : IZeroRouteEntity; + Task GetRoute(string id) where T : IZeroRouteEntity; /// /// Get routes for multiple entities /// - Task> GetRoutes(IEnumerable models, object parameters = null) where T : IZeroRouteEntity; + Task> GetRoutes(params T[] models) where T : IZeroRouteEntity; /// /// Find a provider for a certain entity /// bool TryGetProvider(T model, out IRouteProvider provider) where T : IZeroRouteEntity; + + /// + /// Find a provider by alias + /// + bool TryGetProvider(string alias, out IRouteProvider provider); } } diff --git a/zero.Core/Routing/ZeroRouteProvider.cs b/zero.Core/Routing/ZeroRouteProvider.cs index 52d57aae..de80c47e 100644 --- a/zero.Core/Routing/ZeroRouteProvider.cs +++ b/zero.Core/Routing/ZeroRouteProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using zero.Core.Entities; +using zero.Core.Extensions; namespace zero.Core.Routing { @@ -42,6 +43,27 @@ namespace zero.Core.Routing /// public sealed override Task Find(RoutingContext context, IZeroRouteEntity model) => Find(context, (T)model); + + /// + public virtual async Task> Find(RoutingContext context, params T[] models) + { + Dictionary idMap = models.ToDistinctDictionary(x => Id(x), x => x); + Dictionary routes = await context.Session.LoadAsync(idMap.Select(x => x.Key)); + Dictionary result = new(); + + foreach ((string id, T model) in idMap) + { + result.Add(model, routes.GetValueOrDefault(id)); + } + + return result; + } + + /// + public sealed override async Task> Find(RoutingContext context, params IZeroRouteEntity[] models) + { + return (await Find(context, models.Select(x => (T)x).ToArray())).ToDictionary(x => (IZeroRouteEntity)x.Key, x => x.Value); + } } @@ -97,10 +119,23 @@ namespace zero.Core.Routing return context.Context.Options.Routing.DefaultEndpoint; } - /// - /// Find a persisted route for an entity - /// + /// public virtual async Task Find(RoutingContext context, IZeroRouteEntity model) => await context.Session.LoadAsync(Id(model)); + + /// + public virtual async Task> Find(RoutingContext context, params IZeroRouteEntity[] models) + { + Dictionary idMap = models.ToDistinctDictionary(x => Id(x), x => x); + Dictionary routes = await context.Session.LoadAsync(idMap.Select(x => x.Key)); + Dictionary result = new(); + + foreach ((string id, IZeroRouteEntity model) in idMap) + { + result.Add(model, routes.GetValueOrDefault(id)); + } + + return result; + } } @@ -186,6 +221,11 @@ namespace zero.Core.Routing /// Find a persisted route for an entity /// Task Find(RoutingContext context, IZeroRouteEntity model); + + /// + /// Find persisted routes for entities + /// + Task> Find(RoutingContext context, params IZeroRouteEntity[] models); } } diff --git a/zero.Web/Controllers/PagesController.cs b/zero.Web/Controllers/PagesController.cs index 038c30ed..10f8bb29 100644 --- a/zero.Web/Controllers/PagesController.cs +++ b/zero.Web/Controllers/PagesController.cs @@ -38,7 +38,7 @@ namespace zero.Web.Controllers { IReadOnlyCollection pageTypes = Options.Pages.GetAllItems(); Dictionary pages = await Collection.GetByIds(ids.ToArray()); - Dictionary routes = await Routes.GetRoutes(pages.Where(x => x.Value != null).Select(x => x.Value)); + Dictionary routes = await Routes.GetRoutes(pages.Where(x => x.Value != null).Select(x => x.Value).ToArray()); return Previews(pages, item => {