diff --git a/zero.Core/Entities/ZeroEntity.cs b/zero.Core/Entities/ZeroEntity.cs index effbf3c9..638262a7 100644 --- a/zero.Core/Entities/ZeroEntity.cs +++ b/zero.Core/Entities/ZeroEntity.cs @@ -5,7 +5,7 @@ using System.Diagnostics; namespace zero.Core.Entities { [DebuggerDisplay("Id = {Id,nq}, Name = {Name}, Alias = {Alias}")] - public class ZeroEntity : ZeroIdEntity, IZeroDbConventions + public class ZeroEntity : ZeroIdEntity, IZeroDbConventions, IZeroRouteEntity { /// /// Full name of the entity @@ -83,4 +83,18 @@ namespace zero.Core.Entities /// public string Id { get; set; } } + + + public interface IZeroRouteEntity + { + /// + /// Id of the entity + /// + string Id { get; set; } + + /// + /// Unique hash for this entity (primarily used for routing) + /// + string Hash { get; set; } + } } diff --git a/zero.Core/Extensions/RouteExtensions.cs b/zero.Core/Extensions/RouteExtensions.cs new file mode 100644 index 00000000..1d04a79b --- /dev/null +++ b/zero.Core/Extensions/RouteExtensions.cs @@ -0,0 +1,25 @@ +using zero.Core.Routing; + +namespace zero.Core.Extensions +{ + public static class RouteExtensions + { + public static Route DependsOn(this Route route, string id) + { + route.Dependencies.Add(id); + return route; + } + + public static Route Param(this Route route, string key, object value) + { + route.Params[key] = value; + return route; + } + + public static Route Reference(this Route route, string id, string collection) + { + route.References.Add(new(id, collection)); + return route; + } + } +} diff --git a/zero.Core/Routing/IRouteProvider.cs b/zero.Core/Routing/IRouteProvider.cs index c87d84d5..239469ac 100644 --- a/zero.Core/Routing/IRouteProvider.cs +++ b/zero.Core/Routing/IRouteProvider.cs @@ -57,6 +57,11 @@ namespace zero.Core.Routing /// Task> GetAllRoutes(IAsyncDocumentSession session); + ///// + ///// Updates all affected routes on demand after an entity has been changed + ///// + //Task UpdateOnDemand(object model, object previous); + /// /// Generate unique route ID for a model /// diff --git a/zero.Core/Routing/New/INewRouteProvider.cs b/zero.Core/Routing/New/INewRouteProvider.cs new file mode 100644 index 00000000..2298358b --- /dev/null +++ b/zero.Core/Routing/New/INewRouteProvider.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using zero.Core.Entities; + +namespace zero.Core.Routing +{ + public interface INewRouteProvider where T : IZeroRouteEntity + { + string Alias { get; } + + /// + /// Generate unique route ID for a model + /// + string Id(T model); + + /// + /// Build URL for a model + /// + //string Url(T model); + + /// + /// Create route entity from a model + /// + Task Create(T model); + + /// + /// Get all models which should be provided and handled by this instance + /// + Task> All(); + + /// + /// Build a resolved route from a route response + /// + Task Resolve(RouteResponse response); + } +} diff --git a/zero.Core/Routing/New/NewAbtractRouteProvider.cs b/zero.Core/Routing/New/NewAbtractRouteProvider.cs new file mode 100644 index 00000000..bac0e343 --- /dev/null +++ b/zero.Core/Routing/New/NewAbtractRouteProvider.cs @@ -0,0 +1,105 @@ +using Raven.Client.Documents; +using Raven.Client.Documents.Session; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using zero.Core.Database; +using zero.Core.Entities; +using zero.Core.Options; + +namespace zero.Core.Routing +{ + public abstract class NewAbtractRouteProvider : INewRouteProvider where T : IZeroRouteEntity + { + public virtual string Alias { get; protected set; } + + public const string ID_PREFIX = "routes."; + + public const char SLASH = '/'; + + protected IZeroDocumentSession Session { get; set; } + + protected IZeroOptions Options { get; set; } + + + + public NewAbtractRouteProvider(string alias, IZeroDocumentSession session, IZeroOptions options) + { + Session = session; + Alias = alias; + Options = options; + } + + + public virtual string Id(T model) => ID_PREFIX + model.Hash; + + + public abstract Task Create(T model); + + + public abstract Task> All(); + + + public abstract Task Resolve(RouteResponse response); + + + + + + + + protected async Task ResolvePage() + { + IEnumerable>> resolvers = Options.Routing.PageResolvers.GetAll(typeof(T)); + + foreach (Expression> resolver in resolvers.Reverse()) + { + Page page = await Session.Query().FirstOrDefaultAsync(resolver); + + if (page != null) + { + return page; + } + } + + return null; + } + + + protected async Task> ResolvePages() + { + List pages = new(); + IEnumerable>> resolvers = Options.Routing.PageResolvers.GetAll(typeof(T)); + + foreach (Expression> resolver in resolvers.Reverse()) + { + pages.AddRange(await Session.Query().Where(resolver).ToListAsync()); + } + + return pages; + } + + + protected async Task ResolvePageRoute() + { + Page page = await ResolvePage(); + + // WARNING: we are assuming that the route id is built from the page hash but this could be altered with PageRouteProvier.GetRouteId. + // we cannot use a dependency on this provider here as we are working from the abstract route provider which is the base of the PageRouteProvider itself, + // and therefore a circular dependency. + return page == null ? null : await Session.LoadAsync(ID_PREFIX + page.Hash); + } + + + protected async Task> ResolvePageRoutes() + { + List routes = new(); + IEnumerable pages = await ResolvePages(); + + string[] ids = pages.Select(x => ID_PREFIX + x.Hash).ToArray(); + return ids.Length < 1 ? routes : (await Session.LoadAsync(ids)).Select(x => x.Value); + } + } +} diff --git a/zero.Core/Routing/New/NewTestRouteProvider.cs b/zero.Core/Routing/New/NewTestRouteProvider.cs new file mode 100644 index 00000000..24e8edd6 --- /dev/null +++ b/zero.Core/Routing/New/NewTestRouteProvider.cs @@ -0,0 +1,51 @@ +using Raven.Client.Documents; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using zero.Core.Database; +using zero.Core.Entities; +using zero.Core.Extensions; +using zero.Core.Options; + +namespace zero.Core.Routing +{ + public class NewRouteProvider : NewAbtractRouteProvider + { + protected Route PageRoute { get; set; } + + + public NewRouteProvider(IZeroDocumentSession session, IZeroOptions options) : base("new.test", session, options) + { + Options = options; + } + + + /// + public override async Task Create(Country model) + { + PageRoute ??= await ResolvePageRoute(); + + return new Route(Alias) + { + Id = Id(model), + Url = String.Join(SLASH, PageRoute.Url, "story", model.Alias) + }.DependsOn(model.Id); + } + + + /// + public override async Task Resolve(RouteResponse response) + { + PageRoute resolved = new(response.Route); + resolved.Page = await Session.LoadAsync(response.Route.Dependencies[0]); + return resolved.Page == null ? null : resolved; + } + + + /// + public override async Task> All() + { + return await Session.Query().ToListAsync(); + } + } +} diff --git a/zero.Core/Routing/Page/PageUrlBuilder.cs b/zero.Core/Routing/Page/PageUrlBuilder.cs index c30f6f01..c693f20d 100644 --- a/zero.Core/Routing/Page/PageUrlBuilder.cs +++ b/zero.Core/Routing/Page/PageUrlBuilder.cs @@ -106,7 +106,7 @@ namespace zero.Core.Routing /// /// Get the part of the URL (by querying UrlAlias and Alias) for this page /// - protected virtual string GetUrlPart(Page page) + public virtual string GetUrlPart(Page page) { string alias; diff --git a/zero.Core/Routing/Route.cs b/zero.Core/Routing/Route.cs index 6dac8050..c15e8c02 100644 --- a/zero.Core/Routing/Route.cs +++ b/zero.Core/Routing/Route.cs @@ -7,6 +7,13 @@ namespace zero.Core.Routing [Collection("Routes")] public class Route : ZeroIdEntity { + public Route() { } + + public Route(string providerAlias) + { + ProviderAlias = providerAlias; + } + /// /// Generated URL based on the URL provider /// diff --git a/zero.Core/Routing/Routes.cs b/zero.Core/Routing/Routes.cs index c00740d0..363c3147 100644 --- a/zero.Core/Routing/Routes.cs +++ b/zero.Core/Routing/Routes.cs @@ -216,30 +216,6 @@ namespace zero.Core.Routing } - ///// - //public async Task ResolveRoute(Route route) - //{ - // using IAsyncDocumentSession session = Store.OpenAsyncSession(); - // return await ResolveRouteInternal(session, null); // TODO - //} - - - /// - 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() { @@ -253,16 +229,19 @@ namespace zero.Core.Routing using IAsyncDocumentSession session = Store.OpenAsyncSession(app.Database); session.Advanced.MaxNumberOfRequestsPerSession = 1000; + // delete all routes + await Store.PurgeAsync(app.Database); + 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 } - }); + //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)) @@ -355,16 +334,6 @@ namespace zero.Core.Routing /// 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 ///