using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Raven.Client.Documents; using Raven.Client.Documents.BulkInsert; using Raven.Client.Documents.Linq; using Raven.Client.Documents.Session; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using zero.Core.Database; using zero.Core.Database.Indexes; using zero.Core.Entities; using zero.Core.Extensions; using zero.Core.Options; namespace zero.Core.Routing { public class Routes : IRoutes { 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 Routes(IZeroStore store, ILogger logger, IEnumerable providers, IApplicationResolver appResolver, IZeroOptions options) { Store = store; Logger = logger; Providers = providers; AppResolver = appResolver; Options = options; } /// public async Task GetUrl(T model, object parameters = null) => (await GetRoute(model, parameters))?.Url; /// public async Task GetUrl(string id, object parameters = null) where T : ZeroIdEntity => (await GetRoute(id, parameters))?.Url; /// public async Task> GetUrls(IEnumerable models, object parameters = null) => (await GetRoutes(models, parameters)).ToDictionary(x => x.Key, x => x.Value?.Url); /// public async Task GetRoute(string id, object parameters = null) where T : ZeroIdEntity { if (id.IsNullOrEmpty()) { return null; } Type type = typeof(T); IRouteProvider routeProvider = Providers.FirstOrDefault(x => x.AffectedTypes.Any(t => t.IsAssignableFrom(type))); if (routeProvider == null) { return null; } using IAsyncDocumentSession session = Store.OpenAsyncSession(); T model = await session.LoadAsync(id); if (model == null) { return null; } return await routeProvider.GetRoute(session, model, parameters); } /// public async Task GetRoute(T model, object parameters = null) { if (model == null) { return null; } Type type = model.GetType(); IRouteProvider routeProvider = Providers.FirstOrDefault(x => x.AffectedTypes.Any(t => t.IsAssignableFrom(type))); if (routeProvider == null) { return null; } using IAsyncDocumentSession session = Store.OpenAsyncSession(); return await routeProvider.GetRoute(session, model, parameters); } /// public async Task> GetRoutes(IEnumerable models, object parameters = null) { if (!models.Any()) { return new(); } T firstExistingModel = models.FirstOrDefault(x => x != null); if (firstExistingModel == null) { return new(); } Type type = firstExistingModel.GetType(); IRouteProvider routeProvider = Providers.FirstOrDefault(x => x.AffectedTypes.Any(t => t.IsAssignableFrom(type))); if (routeProvider == null) { return null; } using IAsyncDocumentSession session = Store.OpenAsyncSession(); return (await routeProvider.GetRoutes(session, models.Select(x => (object)x), parameters)).ToDictionary(x => (T)x.Key, x => x.Value); } /// public async Task ResolveUrl(Application application, string path) { path = path.Length > 1 ? path.TrimEnd(PATH_SEPERATOR) : path; using IAsyncDocumentSession session = Store.OpenAsyncSession(application.Database); string[] pathParts = path.Trim(PATH_SEPERATOR).Split(PATH_SEPERATOR); string[] parts = new string[pathParts.Length]; int min = parts.Length; foreach (string pathPart in pathParts) { for (int i = 0; i < min; i++) { parts[i] += PATH_SEPERATOR + pathPart; } min -= 1; } IList routes = await session.Query() .Where(x => (!x.AllowSuffix && x.Url == path) || (x.AllowSuffix && x.Url.In(parts))) .Include("References[].Id") .Include("Dependencies") .ToListAsync(); Route route = routes.FirstOrDefault(); // try to get the best matching path when multiple routes are found // our assumption is that the best path is those with the longest path parts separated by a slash // if we still get multiple routes with equal part counts then we prefer those which do not allow a suffix if (routes.Count > 1) { int maxPathParts = routes.Max(x => x.Url.Count(u => u == PATH_SEPERATOR)); IEnumerable longestRoutes = routes.Where(x => maxPathParts == x.Url.Count(u => u == PATH_SEPERATOR)).OrderBy(x => x.AllowSuffix); if (longestRoutes.Count() > 1) { Logger.LogWarning("Multiple routes {routes} were found for {path}", longestRoutes.Select(x => x.Id), path); } route = longestRoutes.FirstOrDefault(); } if (route == null) { return null; } return await ResolveRouteInternal(session, route); } /// public async Task ResolveUrl(HttpContext context) { if (!Options.SetupCompleted || context.IsBackofficeRequest(Options.BackofficePath)) { return null; } Application app = await AppResolver.ResolveFromRequest(context); string path = context.Request.Path; if (app == null) { return null; } return await ResolveUrl(app, path); } /// public async Task ResolveRoute(Route route) { using IAsyncDocumentSession session = Store.OpenAsyncSession(); return await ResolveRouteInternal(session, route); } /// public NotFoundRoute NotFound(HttpContext context) { if (!Options.SetupCompleted || context.IsBackofficeRequest(Options.BackofficePath) || Options.Routing.NotFoundEndpoint == null) { return null; } return new NotFoundRoute(context) { Controller = Options.Routing.NotFoundEndpoint.Controller, Action = Options.Routing.NotFoundEndpoint.Action }; } /// public async Task RebuildAllRoutes() { int count = 0; using IAsyncDocumentSession coreSession = Store.OpenCoreSession(); List apps = await coreSession.Query().ToListAsync(); foreach (Application app in apps) { using IAsyncDocumentSession session = Store.OpenAsyncSession(app.Database); session.Advanced.MaxNumberOfRequestsPerSession = 1000; foreach (IRouteProvider provider in Providers) { // get all routes for this provider IList routes = await provider.GetAllRoutes(session); // delete all registered routes in the database for this provider await Store.PurgeAsync(app.Database, $"where {nameof(Route.ProviderAlias)} = $alias", new Raven.Client.Parameters() { { "alias", provider.Alias } }); // store new routes using (BulkInsertOperation bulkInsert = Store.BulkInsert(app.Database)) { foreach (Route route in routes) { await bulkInsert.StoreAsync(route, route.Id); count += 1; } } } } } /// public RouteProviderEndpoint MapEndpoint(IResolvedRoute route) { IRouteProvider routeProvider = FindProvider(route.Route.ProviderAlias); return routeProvider?.MapEndpoint(route); } /// /// Call the provider which can resolve the route /// async Task ResolveRouteInternal(IAsyncDocumentSession session, Route route) { IRouteProvider routeProvider = FindProvider(route.ProviderAlias); return await routeProvider?.ResolveRoute(session, route); } /// /// Find registered route provider for the specified alias /// IRouteProvider FindProvider(string alias) { IRouteProvider routeProvider = Providers.FirstOrDefault(x => x.Alias == alias); if (routeProvider == null) { Logger.LogWarning("Could not locate URL provider {provider}", alias); } return routeProvider; } } public interface IRoutes { /// /// Get the URL for an entity /// Task GetUrl(T model, object parameters = null); /// /// Get the URL for an entity /// Task GetUrl(string id, object parameters = null) where T : ZeroIdEntity; /// /// Get the route object for an entity /// Task GetRoute(T model, object parameters = null); /// /// Get the route object for an entity /// Task GetRoute(string id, object parameters = null) where T : ZeroIdEntity; /// /// Get URLs for multiple entities /// Task> GetUrls(IEnumerable models, object parameters = null); /// /// Get routes for multiple entities /// Task> GetRoutes(IEnumerable models, object parameters = null); /// /// Resolve an URL from the specified app and the path /// Task ResolveUrl(Application application, string path); /// /// Resolve an URL from an http context /// Task ResolveUrl(HttpContext context); /// /// Resolve a route object by passing it to the specified provider /// Task ResolveRoute(Route route); /// /// Returns the endpoint which maps 404 requests /// NotFoundRoute NotFound(HttpContext context); /// /// Purges all routes and rebuilds them by iterating over all registered providers /// Task RebuildAllRoutes(); /// /// Get endpoint the route maps to /// RouteProviderEndpoint MapEndpoint(IResolvedRoute route); } }