using Microsoft.Extensions.Logging; using Raven.Client.Documents; using Raven.Client.Documents.Linq; namespace zero.Routing; public class ZeroEntityRouteInterceptor : Interceptor { protected IZeroContext Context { get; set; } protected IZeroStore Store { get; set; } protected IEnumerable Providers { get; set; } protected IRoutes Routes { get; set; } protected IRedirectAutomation RedirectAutomation { get; set; } protected ILogger Logger { get; set; } public ZeroEntityRouteInterceptor(IZeroContext context, IZeroStore store, IRoutes routes, ILogger logger, IEnumerable providers, IRedirectAutomation redirectAutomation) { Gravity = 100; Context = context; Store = store; Routes = routes; Providers = providers; Logger = logger; RedirectAutomation = redirectAutomation; } /// public override Task Created(InterceptorParameters args, ZeroEntity model) => Saved(args, model, false); /// public override Task Updated(InterceptorParameters args, ZeroEntity model) => Saved(args, model, true); /// /// Remove all dependent routes and redirects on route deletion /// public override async Task Deleted(InterceptorParameters args, ZeroEntity model) { RoutingContext context = GetContext(); string id = model.Id; List dependencies = await GetDependencies(context, model); if (!dependencies.Any()) { return; } await context.Store.Raven.PurgeAsync(context.Context.Application.Database, $"where c.Dependencies IN ($id)", new Raven.Client.Parameters() { { "id", id } }); // delete associated redirects for obsolete routes await RedirectAutomation.DeleteForRoutes(dependencies.ToArray()); Logger.LogInformation("Route deletes completed (-{removed}) for {model} (id: {id})", dependencies.Count, model.Name, model.Id); } /// /// Create or update a route when an entity changes and rebuild all dependent routes /// protected async Task Saved(InterceptorParameters args, ZeroEntity model, bool update = false) { // DONE [1] assume we have an update for a Product: // this will not trigger a new seeding as the ProductRouteProvider handles entities, but not products itself // we could overwrite CanHandle() to support types, but this will lead to errors when other methods are called on this provider // DONE [2] some providers have dependencies on entities which are not part of a standalone route provider. // Example: The ProductRouteProvider has a dependency on . Channel has no route provider for itself. // Therefore updates to Channels will not trigger seeding. // For deletes it's easy as we can just delete all routes with the dependency by ID (and we won't need a corresponding route provider) // DONE [3] we have to find all routes which have become obsolete after update seeding. // TODO [4] if the entity is part of a route provider it can be checked for stale-ness. // The problem is if we skip this part and only update dependencies. // E.g. ProductRouteProvider has a dependency on . As Channel has no route provider it can not be checked for stale-ness. // This has to be done in ProductRouteProvider as only the provider knows which properties of the entity it is using (therefore which properties need to be compared). RoutingContext context = GetContext(); context.Session.Advanced.MaxNumberOfRequestsPerSession = 100_000; Dictionary urlUpdates = new(); List obsoleteRoutes = await GetDependencies(context, model); int countObsoleteRoutes = obsoleteRoutes.Count; int countRoutes = 0; // routine to store a new or updated route async Task StoreRoute(Route route) { if (route != null) { countRoutes += 1; Route obsoleteRoute = obsoleteRoutes.FirstOrDefault(x => x.Id == route.Id); if (obsoleteRoute != null) { obsoleteRoutes.Remove(obsoleteRoute); urlUpdates.Add(obsoleteRoute.Url, route.Url); route = obsoleteRoute.Update(route); } await context.Session.StoreAsync(route); } } // find provider for this model if (Routes.TryGetProvider(model, out IRouteProvider provider)) { // return if the route is not stale ZeroEntity previousModel = null; if (previousModel != null && !(await provider.IsRouteStale(context, previousModel, model))) { return; } // build and save new route Route route = await provider.Create(context, model); await StoreRoute(route); } // find all providers which need to be informed that this route has changed // e.g. a Page has changed: // 1. Inform PageRouteProvider to update children // 2. Inform ProductRouteProvider to update product routes which are part of this page or of a child page // 3. ... // in a dependent route provider: // 1. subscribe to updates // 2. determine if this Page is relevant for the provider // 3. gather all updated routes from the provider foreach (IRouteProvider dependentProvider in Providers.OrderByDescending(x => x.Priority)) { await foreach (Route dependentRoute in dependentProvider.SeedOnUpdate(context, model)) { await StoreRoute(dependentRoute); } } // at first we have gathered all routes which are dependent on the entity (via GetDependencies()) // every time we insert a new route in the route table we remove this route from the gathered routes above. // what is left are routes which are obsolete and have not been updated foreach (Route obsoleteRoute in obsoleteRoutes) { context.Session.Delete(obsoleteRoute); } await context.Session.SaveChangesAsync(); // delete associated redirects for obsolete routes await RedirectAutomation.DeleteForRoutes(obsoleteRoutes.ToArray()); // update associated redirects for routes which have a new URL foreach (var kvp in urlUpdates) { await RedirectAutomation.AddForRoute(kvp.Key, kvp.Value); } if (countRoutes < 1 && countObsoleteRoutes < 1) { return; } int countUpdatedRoutes = countObsoleteRoutes - obsoleteRoutes.Count; Logger.LogInformation("Route updates completed (+{added}/~{updated}/-{removed}) for {model} (id: {id})", countRoutes - countUpdatedRoutes, countUpdatedRoutes, obsoleteRoutes.Count, model.Name, model.Id); } /// /// Get route dependencies for an entity /// protected async Task> GetDependencies(RoutingContext context, T model) where T : ISupportsRouting { string[] ids = new[] { model.Id }; return await context.Session.Query().Where(x => x.Dependencies.ContainsAny(ids)).ToListAsync(); } /// /// Build a new routing context /// protected virtual RoutingContext GetContext() { return new(Store, Context, Store.Session()); } }