2021-10-06 11:47:39 +02:00
using Microsoft.Extensions.Logging ;
2021-11-09 16:07:43 +01:00
using Raven.Client.Documents ;
using Raven.Client.Documents.Linq ;
using System.Collections.Generic ;
using System.Linq ;
2021-10-06 11:47:39 +02:00
using System.Threading.Tasks ;
using zero.Core.Collections ;
2021-11-09 16:07:43 +01:00
using zero.Core.Database ;
using zero.Core.Database.Indexes ;
2021-10-06 11:47:39 +02:00
using zero.Core.Entities ;
namespace zero.Core.Routing
{
public class ZeroEntityRouteInterceptor : CollectionInterceptor
{
2021-11-09 16:07:43 +01:00
protected IZeroContext Context { get ; set ; }
protected IZeroStore Store { get ; set ; }
protected IEnumerable < IRouteProvider > Providers { get ; set ; }
protected IRoutes Routes { get ; set ; }
2021-11-10 15:53:19 +01:00
protected IRedirectAutomation RedirectAutomation { get ; set ; }
2021-10-06 11:47:39 +02:00
protected ILogger < ZeroEntityRouteInterceptor > Logger { get ; set ; }
2021-11-10 15:53:19 +01:00
public ZeroEntityRouteInterceptor ( IZeroContext context , IZeroStore store , IRoutes routes , ILogger < ZeroEntityRouteInterceptor > logger , IEnumerable < IRouteProvider > providers , IRedirectAutomation redirectAutomation )
2021-10-06 11:47:39 +02:00
{
2021-11-09 16:07:43 +01:00
Context = context ;
Store = store ;
Routes = routes ;
Providers = providers ;
2021-10-06 11:47:39 +02:00
Logger = logger ;
2021-11-10 15:53:19 +01:00
RedirectAutomation = redirectAutomation ;
2021-10-06 11:47:39 +02:00
}
2021-11-09 16:07:43 +01:00
/// <inheritdoc />
public override async Task Saved ( SaveParameters args )
2021-10-06 11:47:39 +02:00
{
2021-11-10 14:05:20 +01:00
// DONE [1] assume we have an update for a Product:
// this will not trigger a new seeding as the ProductRouteProvider handles <ProductRouteParams> entities, but not products itself
// we could overwrite CanHandle() to support <Product> 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>. 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.
2021-11-09 16:07:43 +01:00
2021-11-11 10:22:06 +01:00
// 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 <Channel>. 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).
2021-11-09 16:07:43 +01:00
RoutingContext context = GetContext ();
2021-11-10 14:05:20 +01:00
context . Session . Advanced . MaxNumberOfRequestsPerSession = 100_000 ;
2021-11-09 16:07:43 +01:00
2021-11-10 15:53:19 +01:00
Dictionary < string , string > urlUpdates = new ();
2021-11-10 14:05:20 +01:00
List < Route > obsoleteRoutes = await GetDependencies ( context , args . Model );
int countObsoleteRoutes = obsoleteRoutes . Count ;
int countRoutes = 0 ;
// routine to store a new or updated route
async Task StoreRoute ( Route route )
2021-11-09 16:07:43 +01:00
{
2021-11-10 14:05:20 +01:00
if ( route != null )
{
countRoutes += 1 ;
Route obsoleteRoute = obsoleteRoutes . FirstOrDefault ( x => x . Id == route . Id );
if ( obsoleteRoute != null )
{
obsoleteRoutes . Remove ( obsoleteRoute );
2021-11-10 15:53:19 +01:00
urlUpdates . Add ( obsoleteRoute . Url , route . Url );
route = obsoleteRoute . Update ( route );
2021-11-10 14:05:20 +01:00
}
await context . Session . StoreAsync ( route );
}
2021-11-09 16:07:43 +01:00
}
2021-11-10 14:05:20 +01:00
// find provider for this model
if ( Routes . TryGetProvider ( args . Model , out IRouteProvider provider ))
2021-11-09 16:07:43 +01:00
{
2021-11-10 14:05:20 +01:00
// return if the route is not stale
ZeroEntity previousModel = null ;
if ( previousModel != null && !( await provider . IsRouteStale ( context , previousModel , args . Model )))
{
return ;
}
// build and save new route
Route route = await provider . Create ( context , args . Model );
await StoreRoute ( route );
2021-11-09 16:07:43 +01:00
}
2021-11-10 14:05:20 +01:00
// find all providers which need to be informed that this route has changed
2021-11-09 16:07:43 +01:00
// 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 <Page> 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 , args . Model ))
{
2021-11-10 14:05:20 +01:00
await StoreRoute ( dependentRoute );
2021-11-09 16:07:43 +01:00
}
}
2021-11-10 14:05:20 +01:00
// 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 );
}
2021-11-09 16:07:43 +01:00
await context . Session . SaveChangesAsync ();
2021-11-10 14:05:20 +01:00
2021-11-10 15:53:19 +01:00
// 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 );
}
2021-11-10 14:05:20 +01:00
int countUpdatedRoutes = countObsoleteRoutes - obsoleteRoutes . Count ;
Logger . LogInformation ( "Route updates completed (+{added}/~{updated}/-{removed}) for {model} (id: {id})" , countRoutes - countUpdatedRoutes , countUpdatedRoutes , obsoleteRoutes . Count , args . Model . Name , args . Model . Id );
2021-11-09 16:07:43 +01:00
}
/// <inheritdoc />
public override async Task Deleted ( DeleteParameters args )
{
2021-11-10 14:05:20 +01:00
RoutingContext context = GetContext ();
string id = args . Model . Id ;
2021-11-10 15:53:19 +01:00
List < Route > dependencies = await GetDependencies ( context , args . Model );
2021-11-10 14:05:20 +01:00
await context . Store . Raven . PurgeAsync < Route >( context . Context . Application . Database , $"where c.Dependencies IN ($id)" , new Raven . Client . Parameters ()
{
{ "id" , id }
});
2021-11-10 15:53:19 +01:00
// delete associated redirects for obsolete routes
await RedirectAutomation . DeleteForRoutes ( dependencies . ToArray ());
Logger . LogInformation ( "Route deletes completed (-{removed}) for {model} (id: {id})" , dependencies . Count , args . Model . Name , args . Model . Id );
2021-11-09 16:07:43 +01:00
}
/// <summary>
2021-11-10 14:05:20 +01:00
/// Get route dependencies for an entity
2021-11-09 16:07:43 +01:00
/// </summary>
2021-11-10 14:05:20 +01:00
protected async Task < List < Route >> GetDependencies < T >( RoutingContext context , T model ) where T : IZeroRouteEntity
2021-11-09 16:07:43 +01:00
{
2021-11-10 14:05:20 +01:00
string [] ids = new [] { model . Id };
return await context . Session . Query < Route , Routes_ByDependencies >(). Where ( x => x . Dependencies . ContainsAny ( ids )). ToListAsync ();
2021-11-10 11:52:49 +01:00
}
2021-11-09 16:07:43 +01:00
/// <summary>
/// Build a new routing context
/// </summary>
protected virtual RoutingContext GetContext ()
{
return new ( Store , Context , Store . Session ());
2021-10-06 11:47:39 +02:00
}
}
}