using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Persistence;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Core.Cache;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Scoping;
namespace Umbraco.Core.Persistence.Repositories.Implement
{
///
/// Provides a base class to all based repositories.
///
/// The type of the entity's unique identifier.
/// The type of the entity managed by this repository.
public abstract class EntityRepositoryBase : RepositoryBase, IReadWriteQueryRepository
where TEntity : class, IEntity
{
private IRepositoryCachePolicy _cachePolicy;
private IQuery _hasIdQuery;
private static RepositoryCachePolicyOptions s_defaultOptions;
///
/// Initializes a new instance of the class.
///
protected EntityRepositoryBase(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger> logger)
: base(scopeAccessor, appCaches)
{
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
///
/// Gets the logger
///
protected ILogger> Logger { get; }
///
/// Gets the isolated cache for the
///
protected IAppPolicyCache GlobalIsolatedCache => AppCaches.IsolatedCaches.GetOrCreate();
///
/// Gets the isolated cache.
///
/// Depends on the ambient scope cache mode.
protected IAppPolicyCache IsolatedCache
{
get
{
switch (AmbientScope.RepositoryCacheMode)
{
case RepositoryCacheMode.Default:
return AppCaches.IsolatedCaches.GetOrCreate();
case RepositoryCacheMode.Scoped:
return AmbientScope.IsolatedCaches.GetOrCreate();
case RepositoryCacheMode.None:
return NoAppCache.Instance;
default:
throw new Exception("oops: cache mode.");
}
}
}
///
/// Gets the default
///
protected virtual RepositoryCachePolicyOptions DefaultOptions => s_defaultOptions ?? (s_defaultOptions
= new RepositoryCachePolicyOptions(() =>
{
// get count of all entities of current type (TEntity) to ensure cached result is correct
// create query once if it is needed (no need for locking here) - query is static!
IQuery query = _hasIdQuery ?? (_hasIdQuery = AmbientScope.SqlContext.Query().Where(x => x.Id != 0));
return PerformCount(query);
}));
///
/// Gets the node object type for the repository's entity
///
protected abstract Guid NodeObjectTypeId { get; }
///
/// Gets the repository cache policy
///
protected IRepositoryCachePolicy CachePolicy
{
get
{
if (AppCaches == AppCaches.NoCache)
{
return NoCacheRepositoryCachePolicy.Instance;
}
// create the cache policy using IsolatedCache which is either global
// or scoped depending on the repository cache mode for the current scope
switch (AmbientScope.RepositoryCacheMode)
{
case RepositoryCacheMode.Default:
case RepositoryCacheMode.Scoped:
// return the same cache policy in both cases - the cache policy is
// supposed to pick either the global or scope cache depending on the
// scope cache mode
return _cachePolicy ?? (_cachePolicy = CreateCachePolicy());
case RepositoryCacheMode.None:
return NoCacheRepositoryCachePolicy.Instance;
default:
throw new Exception("oops: cache mode.");
}
}
}
///
/// Get the entity id for the
///
protected virtual TId GetEntityId(TEntity entity)
=> (TId)(object)entity.Id;
///
/// Create the repository cache policy
///
protected virtual IRepositoryCachePolicy CreateCachePolicy()
=> new DefaultRepositoryCachePolicy(GlobalIsolatedCache, ScopeAccessor, DefaultOptions);
///
/// Adds or Updates an entity of type TEntity
///
/// This method is backed by an cache
public virtual void Save(TEntity entity)
{
if (entity.HasIdentity == false)
{
CachePolicy.Create(entity, PersistNewItem);
}
else
{
CachePolicy.Update(entity, PersistUpdatedItem);
}
}
///
/// Deletes the passed in entity
///
public virtual void Delete(TEntity entity)
=> CachePolicy.Delete(entity, PersistDeletedItem);
protected abstract TEntity PerformGet(TId id);
protected abstract IEnumerable PerformGetAll(params TId[] ids);
protected abstract IEnumerable PerformGetByQuery(IQuery query);
protected abstract void PersistNewItem(TEntity item);
protected abstract void PersistUpdatedItem(TEntity item);
// TODO: obsolete, use QueryType instead everywhere like GetBaseQuery(QueryType queryType);
protected abstract Sql GetBaseQuery(bool isCount);
protected abstract string GetBaseWhereClause();
protected abstract IEnumerable GetDeleteClauses();
protected virtual bool PerformExists(TId id)
{
var sql = GetBaseQuery(true);
sql.Where(GetBaseWhereClause(), new { id = id });
var count = Database.ExecuteScalar(sql);
return count == 1;
}
protected virtual int PerformCount(IQuery query)
{
var sqlClause = GetBaseQuery(true);
var translator = new SqlTranslator(sqlClause, query);
var sql = translator.Translate();
return Database.ExecuteScalar(sql);
}
protected virtual void PersistDeletedItem(TEntity entity)
{
var deletes = GetDeleteClauses();
foreach (var delete in deletes)
{
Database.Execute(delete, new { id = GetEntityId(entity) });
}
entity.DeleteDate = DateTime.Now;
}
///
/// Gets an entity by the passed in Id utilizing the repository's cache policy
///
public TEntity Get(TId id)
=> CachePolicy.Get(id, PerformGet, PerformGetAll);
///
/// Gets all entities of type TEntity or a list according to the passed in Ids
///
public IEnumerable GetMany(params TId[] ids)
{
// ensure they are de-duplicated, easy win if people don't do this as this can cause many excess queries
ids = ids.Distinct()
// don't query by anything that is a default of T (like a zero)
// TODO: I think we should enabled this in case accidental calls are made to get all with invalid ids
// .Where(x => Equals(x, default(TId)) == false)
.ToArray();
// can't query more than 2000 ids at a time... but if someone is really querying 2000+ entities,
// the additional overhead of fetching them in groups is minimal compared to the lookup time of each group
const int maxParams = 2000;
if (ids.Length <= maxParams)
{
return CachePolicy.GetAll(ids, PerformGetAll);
}
var entities = new List();
foreach (var groupOfIds in ids.InGroupsOf(maxParams))
{
entities.AddRange(CachePolicy.GetAll(groupOfIds.ToArray(), PerformGetAll));
}
return entities;
}
///
/// Gets a list of entities by the passed in query
///
public IEnumerable Get(IQuery query)
=> PerformGetByQuery(query)
.WhereNotNull(); // ensure we don't include any null refs in the returned collection!
///
/// Returns a boolean indicating whether an entity with the passed Id exists
///
public bool Exists(TId id)
=> CachePolicy.Exists(id, PerformExists, PerformGetAll);
///
/// Returns an integer with the count of entities found with the passed in query
///
public int Count(IQuery query)
=> PerformCount(query);
}
}