Implements Public Access in netcore (#10137)
* Getting new netcore PublicAccessChecker in place
* Adds full test coverage for PublicAccessChecker
* remove PublicAccessComposer
* adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller
* Implements the required methods on IMemberManager, removes old migrated code
* Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops
* adds note
* adds note
* Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling.
* Changes name to IUmbracoEndpointBuilder
* adds note
* Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect
* fixing build
* Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware.
* adds note
* removes unused filter, fixes build
* fixes WebPath and tests
* Looks up entities in one query
* remove usings
* Fix test, remove stylesheet
* Set status code before we write to response to avoid error
* Ensures that users and members are validated when logging in. Shares more code between users and members.
* Fixes RepositoryCacheKeys to ensure the keys are normalized
* oops didn't mean to commit this
* Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy
* bah, far out this keeps getting recommitted. sorry
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 15:11:45 +10:00
// Copyright (c) Umbraco.
2021-02-12 10:13:56 +01:00
// See LICENSE for more details.
2021-02-09 10:22:42 +01:00
using Umbraco.Cms.Core.Models.Entities ;
2022-01-13 17:44:11 +00:00
using Umbraco.Cms.Infrastructure.Scoping ;
2021-02-09 11:26:22 +01:00
using Umbraco.Extensions ;
2016-01-07 16:31:20 +01:00
2022-06-02 08:18:31 +02:00
namespace Umbraco.Cms.Core.Cache ;
/// <summary>
/// Represents the default cache policy.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <typeparam name="TId">The type of the identifier.</typeparam>
/// <remarks>
/// <para>The default cache policy caches entities with a 5 minutes sliding expiration.</para>
/// <para>Each entity is cached individually.</para>
/// <para>If options.GetAllCacheAllowZeroCount then a 'zero-count' array is cached when GetAll finds nothing.</para>
/// <para>If options.GetAllCacheValidateCount then we check against the db when getting many entities.</para>
/// </remarks>
public class DefaultRepositoryCachePolicy < TEntity , TId > : RepositoryCachePolicyBase < TEntity , TId >
where TEntity : class , IEntity
2016-01-07 16:31:20 +01:00
{
2022-06-02 08:18:31 +02:00
private static readonly TEntity [ ] _emptyEntities = new TEntity [ 0 ] ; // const
private readonly RepositoryCachePolicyOptions _options ;
2025-02-04 22:29:21 +11:00
private const string NullRepresentationInCache = "*NULL*" ;
2022-06-02 08:18:31 +02:00
public DefaultRepositoryCachePolicy ( IAppPolicyCache cache , IScopeAccessor scopeAccessor , RepositoryCachePolicyOptions options )
: base ( cache , scopeAccessor ) = >
_options = options ? ? throw new ArgumentNullException ( nameof ( options ) ) ;
2016-06-01 10:35:44 +02:00
2022-06-02 08:18:31 +02:00
protected string EntityTypeCacheKey { get ; } = $"uRepo_{typeof(TEntity).Name}_" ;
/// <inheritdoc />
public override void Create ( TEntity entity , Action < TEntity > persistNew )
{
if ( entity = = null )
2016-06-01 10:35:44 +02:00
{
2022-06-02 08:18:31 +02:00
throw new ArgumentNullException ( nameof ( entity ) ) ;
2017-05-12 14:49:44 +02:00
}
2022-06-02 08:18:31 +02:00
try
2016-06-01 10:35:44 +02:00
{
2022-06-02 08:18:31 +02:00
persistNew ( entity ) ;
Implements Public Access in netcore (#10137)
* Getting new netcore PublicAccessChecker in place
* Adds full test coverage for PublicAccessChecker
* remove PublicAccessComposer
* adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller
* Implements the required methods on IMemberManager, removes old migrated code
* Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops
* adds note
* adds note
* Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling.
* Changes name to IUmbracoEndpointBuilder
* adds note
* Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect
* fixing build
* Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware.
* adds note
* removes unused filter, fixes build
* fixes WebPath and tests
* Looks up entities in one query
* remove usings
* Fix test, remove stylesheet
* Set status code before we write to response to avoid error
* Ensures that users and members are validated when logging in. Shares more code between users and members.
* Fixes RepositoryCacheKeys to ensure the keys are normalized
* oops didn't mean to commit this
* Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy
* bah, far out this keeps getting recommitted. sorry
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 15:11:45 +10:00
2022-06-02 08:18:31 +02:00
// just to be safe, we cannot cache an item without an identity
if ( entity . HasIdentity )
Implements Public Access in netcore (#10137)
* Getting new netcore PublicAccessChecker in place
* Adds full test coverage for PublicAccessChecker
* remove PublicAccessComposer
* adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller
* Implements the required methods on IMemberManager, removes old migrated code
* Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops
* adds note
* adds note
* Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling.
* Changes name to IUmbracoEndpointBuilder
* adds note
* Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect
* fixing build
* Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware.
* adds note
* removes unused filter, fixes build
* fixes WebPath and tests
* Looks up entities in one query
* remove usings
* Fix test, remove stylesheet
* Set status code before we write to response to avoid error
* Ensures that users and members are validated when logging in. Shares more code between users and members.
* Fixes RepositoryCacheKeys to ensure the keys are normalized
* oops didn't mean to commit this
* Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy
* bah, far out this keeps getting recommitted. sorry
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 15:11:45 +10:00
{
2022-06-02 08:18:31 +02:00
Cache . Insert ( GetEntityCacheKey ( entity . Id ) , ( ) = > entity , TimeSpan . FromMinutes ( 5 ) , true ) ;
Implements Public Access in netcore (#10137)
* Getting new netcore PublicAccessChecker in place
* Adds full test coverage for PublicAccessChecker
* remove PublicAccessComposer
* adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller
* Implements the required methods on IMemberManager, removes old migrated code
* Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops
* adds note
* adds note
* Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling.
* Changes name to IUmbracoEndpointBuilder
* adds note
* Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect
* fixing build
* Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware.
* adds note
* removes unused filter, fixes build
* fixes WebPath and tests
* Looks up entities in one query
* remove usings
* Fix test, remove stylesheet
* Set status code before we write to response to avoid error
* Ensures that users and members are validated when logging in. Shares more code between users and members.
* Fixes RepositoryCacheKeys to ensure the keys are normalized
* oops didn't mean to commit this
* Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy
* bah, far out this keeps getting recommitted. sorry
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 15:11:45 +10:00
}
2022-06-02 08:18:31 +02:00
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
Cache . Clear ( EntityTypeCacheKey ) ;
2016-01-07 16:31:20 +01:00
}
2022-06-02 08:18:31 +02:00
catch
{
// if an exception is thrown we need to remove the entry from cache,
// this is ONLY a work around because of the way
// that we cache entities: http://issues.umbraco.org/issue/U4-4259
Cache . Clear ( GetEntityCacheKey ( entity . Id ) ) ;
2016-01-07 16:31:20 +01:00
2022-06-02 08:18:31 +02:00
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
Cache . Clear ( EntityTypeCacheKey ) ;
Implements Public Access in netcore (#10137)
* Getting new netcore PublicAccessChecker in place
* Adds full test coverage for PublicAccessChecker
* remove PublicAccessComposer
* adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller
* Implements the required methods on IMemberManager, removes old migrated code
* Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops
* adds note
* adds note
* Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling.
* Changes name to IUmbracoEndpointBuilder
* adds note
* Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect
* fixing build
* Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware.
* adds note
* removes unused filter, fixes build
* fixes WebPath and tests
* Looks up entities in one query
* remove usings
* Fix test, remove stylesheet
* Set status code before we write to response to avoid error
* Ensures that users and members are validated when logging in. Shares more code between users and members.
* Fixes RepositoryCacheKeys to ensure the keys are normalized
* oops didn't mean to commit this
* Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy
* bah, far out this keeps getting recommitted. sorry
Co-authored-by: Bjarke Berg <mail@bergmania.dk>
2021-04-20 15:11:45 +10:00
2022-06-02 08:18:31 +02:00
throw ;
}
}
2016-01-07 16:31:20 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
public override void Update ( TEntity entity , Action < TEntity > persistUpdated )
{
if ( entity = = null )
2016-01-07 16:31:20 +01:00
{
2022-06-02 08:18:31 +02:00
throw new ArgumentNullException ( nameof ( entity ) ) ;
2016-06-01 14:31:33 +02:00
}
2022-06-02 08:18:31 +02:00
try
2016-06-01 14:31:33 +02:00
{
2022-06-02 08:18:31 +02:00
persistUpdated ( entity ) ;
2016-06-01 14:31:33 +02:00
2022-06-02 08:18:31 +02:00
// just to be safe, we cannot cache an item without an identity
if ( entity . HasIdentity )
2016-06-01 14:31:33 +02:00
{
2022-06-02 08:18:31 +02:00
Cache . Insert ( GetEntityCacheKey ( entity . Id ) , ( ) = > entity , TimeSpan . FromMinutes ( 5 ) , true ) ;
2016-06-01 14:31:33 +02:00
}
2022-06-02 08:18:31 +02:00
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
Cache . Clear ( EntityTypeCacheKey ) ;
2016-06-01 10:35:44 +02:00
}
2022-06-02 08:18:31 +02:00
catch
2016-06-01 10:35:44 +02:00
{
2022-06-02 08:18:31 +02:00
// if an exception is thrown we need to remove the entry from cache,
// this is ONLY a work around because of the way
// that we cache entities: http://issues.umbraco.org/issue/U4-4259
Cache . Clear ( GetEntityCacheKey ( entity . Id ) ) ;
2016-01-07 16:31:20 +01:00
2022-06-02 08:18:31 +02:00
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
Cache . Clear ( EntityTypeCacheKey ) ;
2016-06-01 10:35:44 +02:00
2022-06-02 08:18:31 +02:00
throw ;
2016-01-07 16:31:20 +01:00
}
2022-06-02 08:18:31 +02:00
}
2016-01-07 16:31:20 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
public override void Delete ( TEntity entity , Action < TEntity > persistDeleted )
{
if ( entity = = null )
2016-01-07 16:31:20 +01:00
{
2022-06-02 08:18:31 +02:00
throw new ArgumentNullException ( nameof ( entity ) ) ;
2016-01-07 16:31:20 +01:00
}
2022-06-02 08:18:31 +02:00
try
2016-01-07 16:31:20 +01:00
{
2022-06-02 08:18:31 +02:00
persistDeleted ( entity ) ;
}
finally
{
// whatever happens, clear the cache
var cacheKey = GetEntityCacheKey ( entity . Id ) ;
2025-02-04 22:29:21 +11:00
2022-06-02 08:18:31 +02:00
Cache . Clear ( cacheKey ) ;
2016-01-07 16:31:20 +01:00
2022-06-02 08:18:31 +02:00
// if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared
Cache . Clear ( EntityTypeCacheKey ) ;
2016-01-07 16:31:20 +01:00
}
2022-06-02 08:18:31 +02:00
}
2016-01-07 16:31:20 +01:00
2022-06-02 08:18:31 +02:00
/// <inheritdoc />
public override TEntity ? Get ( TId ? id , Func < TId ? , TEntity ? > performGet , Func < TId [ ] ? , IEnumerable < TEntity > ? > performGetAll )
{
var cacheKey = GetEntityCacheKey ( id ) ;
2025-02-04 22:29:21 +11:00
2022-06-02 08:18:31 +02:00
TEntity ? fromCache = Cache . GetCacheItem < TEntity > ( cacheKey ) ;
2025-02-04 22:29:21 +11:00
// If found in cache then return immediately.
if ( fromCache is not null )
2016-01-07 16:31:20 +01:00
{
2022-06-02 08:18:31 +02:00
return fromCache ;
2016-01-07 16:31:20 +01:00
}
2025-02-04 22:29:21 +11:00
// Because TEntity can never be a string, we will never be in a position where the proxy value collides withs a real value.
// Therefore this point can only be reached if there is a proxy null value => becomes null when cast to TEntity above OR the item simply does not exist.
// If we've cached a "null" value, return null.
if ( _options . CacheNullValues & & Cache . GetCacheItem < string > ( cacheKey ) = = NullRepresentationInCache )
{
return null ;
}
// Otherwise go to the database to retrieve.
2022-06-02 08:18:31 +02:00
TEntity ? entity = performGet ( id ) ;
if ( entity ! = null & & entity . HasIdentity )
2016-01-07 16:31:20 +01:00
{
2025-02-04 22:29:21 +11:00
// If we've found an identified entity, cache it for subsequent retrieval.
2022-06-02 08:18:31 +02:00
InsertEntity ( cacheKey , entity ) ;
2016-01-07 16:31:20 +01:00
}
2025-02-04 22:29:21 +11:00
else if ( entity is null & & _options . CacheNullValues )
{
// If we've not found an entity, and we're caching null values, cache a "null" value.
InsertNull ( cacheKey ) ;
}
2016-01-07 16:31:20 +01:00
2022-06-02 08:18:31 +02:00
return entity ;
}
/// <inheritdoc />
public override TEntity ? GetCached ( TId id )
{
var cacheKey = GetEntityCacheKey ( id ) ;
return Cache . GetCacheItem < TEntity > ( cacheKey ) ;
}
/// <inheritdoc />
public override bool Exists ( TId id , Func < TId , bool > performExists , Func < TId [ ] , IEnumerable < TEntity > ? > performGetAll )
{
// if found in cache the return else check
var cacheKey = GetEntityCacheKey ( id ) ;
TEntity ? fromCache = Cache . GetCacheItem < TEntity > ( cacheKey ) ;
return fromCache ! = null | | performExists ( id ) ;
}
/// <inheritdoc />
public override TEntity [ ] GetAll ( TId [ ] ? ids , Func < TId [ ] ? , IEnumerable < TEntity > ? > performGetAll )
{
if ( ids ? . Length > 0 )
2016-01-07 16:31:20 +01:00
{
2022-06-02 08:18:31 +02:00
// try to get each entity from the cache
// if we can find all of them, return
TEntity [ ] entities = ids . Select ( GetCached ) . WhereNotNull ( ) . ToArray ( ) ;
if ( ids . Length . Equals ( entities . Length ) )
2016-01-07 16:31:20 +01:00
{
2022-06-02 08:18:31 +02:00
return entities ; // no need for null checks, we are not caching nulls
2016-01-07 16:31:20 +01:00
}
2022-06-02 08:18:31 +02:00
}
else
{
// get everything we have
TEntity ? [ ] entities = Cache . GetCacheItemsByKeySearch < TEntity > ( EntityTypeCacheKey )
. ToArray ( ) ; // no need for null checks, we are not caching nulls
2016-06-01 10:35:44 +02:00
2022-06-02 08:18:31 +02:00
if ( entities . Length > 0 )
{
// if some of them were in the cache...
if ( _options . GetAllCacheValidateCount )
2016-01-07 16:31:20 +01:00
{
2022-06-02 08:18:31 +02:00
// need to validate the count, get the actual count and return if ok
if ( _options . PerformCount is not null )
2016-01-07 16:31:20 +01:00
{
2022-06-02 08:18:31 +02:00
var totalCount = _options . PerformCount ( ) ;
if ( entities . Length = = totalCount )
2022-02-18 14:32:51 +01:00
{
2022-06-02 08:18:31 +02:00
return entities . WhereNotNull ( ) . ToArray ( ) ;
2022-02-18 14:32:51 +01:00
}
2016-01-07 16:31:20 +01:00
}
}
2022-06-02 08:18:31 +02:00
else
2016-01-07 16:31:20 +01:00
{
2022-06-02 08:18:31 +02:00
// no need to validate, just return what we have and assume it's all there is
return entities . WhereNotNull ( ) . ToArray ( ) ;
2016-01-07 16:31:20 +01:00
}
}
2022-06-02 08:18:31 +02:00
else if ( _options . GetAllCacheAllowZeroCount )
{
// if none of them were in the cache
// and we allow zero count - check for the special (empty) entry
TEntity [ ] ? empty = Cache . GetCacheItem < TEntity [ ] > ( EntityTypeCacheKey ) ;
if ( empty ! = null )
{
return empty ;
}
}
}
// cache failed, get from repo and cache
TEntity [ ] ? repoEntities = performGetAll ( ids ) ?
. WhereNotNull ( ) // exclude nulls!
. Where ( x = > x . HasIdentity ) // be safe, though would be weird...
. ToArray ( ) ;
2016-01-07 16:31:20 +01:00
2022-06-02 08:18:31 +02:00
// note: if empty & allow zero count, will cache a special (empty) entry
InsertEntities ( ids , repoEntities ) ;
2016-01-07 16:31:20 +01:00
2022-06-02 08:18:31 +02:00
return repoEntities ? ? Array . Empty < TEntity > ( ) ;
}
/// <inheritdoc />
public override void ClearAll ( ) = > Cache . ClearByKey ( EntityTypeCacheKey ) ;
protected string GetEntityCacheKey ( int id ) = > EntityTypeCacheKey + id ;
protected string GetEntityCacheKey ( TId ? id )
{
if ( EqualityComparer < TId > . Default . Equals ( id , default ) )
{
return string . Empty ;
}
2016-01-07 16:31:20 +01:00
2022-06-02 08:18:31 +02:00
if ( typeof ( TId ) . IsValueType )
{
return EntityTypeCacheKey + id ;
2016-01-07 16:31:20 +01:00
}
2016-06-01 14:31:33 +02:00
2022-06-02 08:18:31 +02:00
return EntityTypeCacheKey + id ? . ToString ( ) ? . ToUpperInvariant ( ) ;
}
protected virtual void InsertEntity ( string cacheKey , TEntity entity )
= > Cache . Insert ( cacheKey , ( ) = > entity , TimeSpan . FromMinutes ( 5 ) , true ) ;
2025-02-04 22:29:21 +11:00
protected virtual void InsertNull ( string cacheKey )
{
// We can't actually cache a null value, as in doing so wouldn't be able to distinguish between
// a value that does exist but isn't yet cached, or a value that has been explicitly cached with a null value.
// Both would return null when we retrieve from the cache and we couldn't distinguish between the two.
// So we cache a special value that represents null, and then we can check for that value when we retrieve from the cache.
Cache . Insert ( cacheKey , ( ) = > NullRepresentationInCache , TimeSpan . FromMinutes ( 5 ) , true ) ;
}
2022-06-02 08:18:31 +02:00
protected virtual void InsertEntities ( TId [ ] ? ids , TEntity [ ] ? entities )
{
if ( ids ? . Length = = 0 & & entities ? . Length = = 0 & & _options . GetAllCacheAllowZeroCount )
2016-06-01 14:31:33 +02:00
{
2022-06-02 08:18:31 +02:00
// getting all of them, and finding nothing.
// if we can cache a zero count, cache an empty array,
// for as long as the cache is not cleared (no expiration)
Cache . Insert ( EntityTypeCacheKey , ( ) = > _emptyEntities ) ;
}
else
{
if ( entities is not null )
{
// individually cache each item
foreach ( TEntity entity in entities )
{
TEntity capture = entity ;
Cache . Insert ( GetEntityCacheKey ( entity . Id ) , ( ) = > capture , TimeSpan . FromMinutes ( 5 ) , true ) ;
}
}
2016-06-01 14:31:33 +02:00
}
2016-01-07 16:31:20 +01:00
}
2017-07-20 11:21:28 +02:00
}