📜 ⬆️ ⬇️

How to make customizable caching in the project and save colleagues from writing the same type of code

“If the essence of the programmer’s work is in automating the work of other people, then why my work is so little automated,” I thought, once again copying all the binding needed in the project to add a new entity to the database. And I decided to get rid of this routine of adding template classes, at the same time making a “good” project, unloading the database from unnecessary read operations.

A small digression about the system we are developing and its state at the time of the start of this experiment:


While thinking about the situation with the database, the thought arose: whether these 10% of requests are removed from the load on the database (and at the same time they open connections to the database, hold open transactions and template code to access the database through our repositories). At the same time, it was necessary to take into account:


An example of the implementation of one of these old caches
/// <summary> /// Base class for caches, containing rarely changed entities. Updated by subscription to nHibernate session commits. /// </summary> /// <typeparam name="T">Type, used as a base for the cache. Sessions, containing changes to any instance of this class will cause cache refresh.</typeparam> /// <typeparam name="K">Key used for cache search.</typeparam> /// <typeparam name="V">Value, stored in cache.</typeparam> public abstract class RareChangedObjectsCache<T, K, V> : EmptySessionNotificationListener, ITransactionNotificationListener, IRareChangedObjectsCache<K, V> where T : class { [NotNull] private static readonly ILog Log = IikoBizLogManager.GetLogger(typeof(RareChangedObjectsCache<T, K, V>)); [NotNull] protected readonly ReaderWriterLockSlim LockObj = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); [NotNull] protected readonly Dictionary<K, V> Cache = new Dictionary<K, V>(); [NotNull] protected abstract QueryOver<T> GetQuery(); [NotNull] private readonly ConcurrentDictionary<String, bool> changesByTransaction = new ConcurrentDictionary<string, bool>(); private DateTime lastRefreshTime = DateTime.MinValue; [NotNull] public HibernateSessionManager SessionManager { get; set; } /// <summary> /// Interval of automatic data renewal for cases of read access to cache. Also, cache is forcibly refreshed on any commit, changing base entities of this cache. /// </summary> public TimeSpan AutoRefreshInterval { get; set; } public void Reset() { lastRefreshTime = DateTime.MinValue; } protected void ReloadCacheIfNeeded(ISession session = null) { if (SystemTime.Now - lastRefreshTime <= AutoRefreshInterval) return; LockObj.EnterWriteLock(); try { if (SystemTime.Now - lastRefreshTime <= AutoRefreshInterval) return; IList<object> result; if (session == null) result = SessionManager.CallTransacted(s => GetQuery().GetExecutableQueryOver(s).List<object>()); else //TODO At that moment, the transaction may have been closed, so a new transaction opens implicitly result = GetQuery().GetExecutableQueryOver(session).List<object>(); Cache.Clear(); ProcessResult(result); lastRefreshTime = SystemTime.Now; } catch (Exception e) { Log.Error("Exception on cache invalidation: ", e); } finally { LockObj.ExitWriteLock(); } } protected abstract void ProcessResult(IList<object> result); public override void OnSaveOrUpdate(ISession session, object entity, Guid id, object[] currentState, object[] previousState, string[] propertyNames, IType[] types) { if (entity is T) { var transactionName = HibernateSessionManager.GetTransactionName(); if (!string.IsNullOrEmpty(transactionName)) { changesByTransaction.TryAdd(transactionName, true); } } } public override void OnDelete(ISession session, object entity, Guid id) { if (entity is T) { var transactionName = HibernateSessionManager.GetTransactionName(); if (!string.IsNullOrEmpty(transactionName)) { changesByTransaction.TryAdd(transactionName, true); } } } void ITransactionNotificationListener.AfterCommit(ISession session, string transactionName) { bool tmp; if (changesByTransaction.TryRemove(transactionName, out tmp)) Reset(); ReloadCacheIfNeeded(session); } void ITransactionNotificationListener.AfterRollback(ISession session, string transactionName) { } public bool Contains(K key) { ReloadCacheIfNeeded(); LockObj.EnterReadLock(); try { return Cache.ContainsKey(key); } finally { LockObj.ExitReadLock(); } } public virtual V TryGet(K key) { ReloadCacheIfNeeded(); LockObj.EnterReadLock(); try { return Cache.TryGetValue(key, out var value) ? value : default; } finally { LockObj.ExitReadLock(); } } protected TV GetOrAddEntry<TK, TV>(IDictionary<TK, TV> dictionary, TK key) where TV : new() { TV list; if (!dictionary.TryGetValue(key, out list)) dictionary[key] = (list = new TV()); return list; } } /// <summary> /// Caches all guest categories, grouped by organization or network they belong. /// </summary> [UsedImplicitly] public sealed class GuestCategoryCache : RareChangedObjectsCache<GuestCategory, Guid, HashSet<GuestCategory>> { private static readonly QueryOver<GuestCategory> SelectAllEntitiesQuery = QueryOver.Of<GuestCategory>() .Fetch(gc => gc.Organization).Eager .Fetch(gc => gc.Network).Eager; private readonly Dictionary<Guid, string> invertedCache = new Dictionary<Guid, string>(); public bool TryGetNetworkExternalId(Guid id, out string extId) { ReloadCacheIfNeeded(); LockObj.EnterReadLock(); try { return invertedCache.TryGetValue(id, out extId); } finally { LockObj.ExitReadLock(); } } protected override QueryOver<GuestCategory> GetQuery() { return SelectAllEntitiesQuery; } protected override void ProcessResult(IList<object> result) { invertedCache.Clear(); foreach (GuestCategory gc in result) { var orgOrNetworkId = gc.Organization?.Id ?? gc.Network.Id; GetOrAddEntry(Cache, orgOrNetworkId).Add(gc); } } } 


At that time, already somewhat tired.
- A new feature should not globally affect other developers, migration to a new approach should occur smoothly and gently.
')
At the same time, I wanted to make it so that adding a new entity to the caching mechanisms took a minimum of effort, the code remained readable and straightforward, and when retrieving data from the cache one could think minimally about what auxiliary code should be written. The last two points strongly cut off the range of options. In fact, one must either build something up with reflection and generic classes, or turn to the good old metaprogramming.

Since the main development tool is Visual Studio, I didn’t want to spend a lot of effort on the fact that it’s not a fact that it would bring an enormous effect - I decided to make a decision on the forehead with the most standard means and only at the stage of the finished Proof Of Concept on a couple of the most frequently used entities - present a decision to colleagues at the court.

Next was a moral dilemma. Whether to use as a source any class hung with attributes for all occasions (in the style of Fluent-mapping Nhibernate on steroids), or write cute neat XML. Remembering that laziness is the programmer’s best friend, and class description by attributes is more laborious than writing small XML, he chose the latter.

Essentially, what did I need from the entity description?


Got just this xml structure
 <class name="GuestCategory" logicallyDeletable="true" revisioned="true" organizationNetworkBased="true" basedOn="BO.GuestCategory" > <emitBo emitMapping="true"/> <emitRepository dependsOn="guestCategoryCache">IGuestCategoryRepository</emitRepository> <field name="IsActive" type="bool"/> <field name="IsDefaultForNewGuests" type="bool"/> <field name="Name" type="string" notNull="true"/> </class> 


And a big scary T4 template for its parsing and generating unsuitable, but so necessary classes:


The template itself, from a logical point of view, consists of two parts: parsing the original xml into classes that describe the desired class structure (to reduce the risk of the formation of any implicit behavior, while it was decided to do it through explicit tag parsing, and not through mapping attributes.

Classes that describe the structure
 class DalClass { public string Name { get; } public bool LogicallyDeletable { get; } public string BasedOn { get; } public string DalBOType { get; } public string[] Implements { get; } public string[] Include { get; } public bool CustomConsistencyManager { get; } public List<DalField> Fields { get; } public List<DalField> ExplicitlyDefinedFields { get; } public DalGetBy[] GetBy { get; } public DalGetBy[] GetAllBy { get; } public bool GenerateInterface { get; } public DalEmitBo EmitBo { get; } public DalEmitRepository EmitRepository { get; } public DalClass(XmlElement sourceXml) { Implements = sourceXml.SelectNodes("*[local-name()='implements']") .Cast<XmlElement>() .Select(f => f.InnerText + ",") .ToArray(); Include = sourceXml.SelectNodes("*[local-name()='include']") .Cast<XmlElement>() .Select(f => f.InnerText) .ToArray(); Name = sourceXml.GetAttribute("name"); LogicallyDeletable = sourceXml.HasAttribute("logicallyDeletable"); BasedOn = sourceXml.GetAttribute("basedOn"); DalBOType = sourceXml.HasAttribute("dalBoType") ? sourceXml.GetAttribute("dalBoType") : BasedOn; CustomConsistencyManager = sourceXml.HasAttribute("customConsistencyManager") ? sourceXml.GetAttribute("customConsistencyManager") == "true" : false; Fields = sourceXml.SelectNodes("*[local-name()='field']") .Cast<XmlElement>() .Select(f => new DalField(f)) .ToList(); ExplicitlyDefinedFields = Fields.ToList(); Fields = Fields.OrderBy(f=>f.Name).ToList(); GetBy = sourceXml.SelectNodes("*[local-name()='getBy']") .Cast<XmlElement>() .Select(f => new DalGetBy(f)) .ToArray(); GetAllBy = sourceXml.SelectNodes("*[local-name()='getAllBy']") .Cast<XmlElement>() .Select(f => new DalGetBy(f)) .ToArray(); EmitBo = sourceXml.SelectNodes("*[local-name()='emitBo']") .Cast<XmlElement>() .Select(f => new DalEmitBo(f)) .SingleOrDefault(); EmitRepository = sourceXml.SelectNodes("*[local-name()='emitRepository']") .Cast<XmlElement>() .Select(f => new DalEmitRepository(f, Name)) .SingleOrDefault(); GenerateInterface = true; } public string GetIncludedNamespaces() { return string.Join("/n", Include.Select(i => "using " + i + ";")); } public string GetBoClassDefinition() { return Name + " :\n\t\tBaseEntity,\n\t\t" + (LogicallyDeletable ? "ILogicallyDeletable,\n\t\t" : string.Empty) + (Implements.Any() ? string.Join("\n\t\t", Implements) + "\n\t\t" : string.Empty) + "I" + Name; } public string GetCachedClassDefinition() { return "Cached" + Name + " :\n\t\t" + (LogicallyDeletable ? "Deletable" : string.Empty) + "CachedEntity<" + BasedOn + ">,\n\t\t" + (Implements.Any() ? string.Join("\n\t\t", Implements) + "\n\t\t" : string.Empty) + "I" + Name; } public string TryGetIsDeletedParameter() { if (LogicallyDeletable) return ",bool getDeleted"; else return String.Empty; } public string TryGetIsDeletedFilter() { if (LogicallyDeletable) return @" if (!getDeleted) entities = entities.Where(e => !e.IsDeleted); "; else return String.Empty; } public string GetFilterParameters(DalGetBy getBy) { var filters = new List<FieldDescription>(); foreach (var filter in getBy.Filters) filters.Add(new FieldDescription { TypeName = Fields.Single(f => f.Name == filter.Key).FieldType, Alias = filter.Value }); return string.Join(", ", filters.Select(f => f.TypeName + " " + f.Alias)); } private struct FieldDescription { public string TypeName; public string Alias; } } class DalField { public string Name { get; } public string FieldType { get; } public string Source { get; } public string PropertySource { get; } public bool NotNull { get; } public DalField(string name, string type) { Name = name; FieldType = type; Source = name; } public DalField(XmlElement sourceXml) { Name = sourceXml.GetAttribute("name"); FieldType = sourceXml.GetAttribute("type"); Source = sourceXml.HasAttribute("source") ? sourceXml.GetAttribute("source") : sourceXml.GetAttribute("name"); PropertySource = sourceXml.HasAttribute("propertySource") ? sourceXml.GetAttribute("propertySource") : null; NotNull = sourceXml.HasAttribute("notNull") ? sourceXml.GetAttribute("notNull") == "true" : false; } public string GetConstructorInitValueExpression() { var fieldIsArray = FieldType.EndsWith("[]"); var fieldRealType = FieldType.Replace("[]", ""); if (PropertySource!=null && fieldRealType.StartsWith("I")) fieldRealType = "Cached" + fieldRealType.Substring(1); return UpperInitial(Name) + " = source." + Source + (fieldIsArray ? ".Select(i=>new " + fieldRealType + "(i)).ToArray()" : "") + ";"; } public string GetPropertyDefinitionExpression() { return "public " + GetType() + " " + UpperInitial(Name) + (PropertySource != null ? " => " + PropertySource + ";" : " { get; private set; }"); } public string GetBOPropertyDefinitionExpression() { return "public virtual " + GetBoType() + " " + UpperInitial(Name) + " { get; set; }"; } public string GetInterfacePropertyDefinitionExpression() { return GetNullAttribute() + GetType() + " " + UpperInitial(Name) + " { get; }"; } public string GetType() { var fieldIsArray = FieldType.EndsWith("[]"); if (fieldIsArray) return "IEnumerable<" + FieldType.Replace("[]", "") + ">"; return FieldType; } public string GetBoType() { var fieldIsArray = FieldType.EndsWith("[]"); if (fieldIsArray) return "IList<" + FieldType.Replace("[]", "") + ">"; return FieldType; } private string GetNullAttribute() => TypeCanBeNull() ? NotNull ? "[NotNull] " : "[CanBeNull] " : string.Empty; private static string[] NotNullTypes = { "Guid", "DateTime", "DateTimeOffset", "bool", "int", "long", "short", "ProgramType", "GuestSubscriptionTypes", "SenderType", "ApiClientType" }; public bool TypeCanBeNull() => !NotNullTypes.Contains(FieldType); private string UpperInitial(string name) { return name[0].ToString().ToUpperInvariant() + name.Substring(1); } } class DalGetBy { public bool IsTry { get; } public string Alias { get; } public Dictionary<string, string> Filters { get; } = new Dictionary<string, string>(); public DalGetBy(XmlElement sourceXml) { IsTry = sourceXml.HasAttribute("try"); Alias = sourceXml.GetAttribute("alias"); foreach (XmlElement filterNode in sourceXml.SelectNodes("*[local-name()='field']")) Filters.Add(filterNode.GetAttribute("field"), filterNode.GetAttribute("alias")); } public string GetConditions() { return string.Join(" && ", Filters.Select(f => $"e.{f.Key} == {f.Value}")); return string.Join(" && ", Filters.Select(f => $"e.{f.Key} == {f.Value}")); } } class DalEmitBo { public string Namespace { get; } public bool EmitMapping { get; } private XmlElement sourceXml; public DalEmitBo(XmlElement sourceXml) { Namespace = sourceXml.GetAttribute("ns"); EmitMapping = sourceXml.HasAttribute("emitMapping"); this.sourceXml = sourceXml; } public string GetMapping(DalField field) { bool notNull = !field.TypeCanBeNull() || field.NotNull; var overridenXml = sourceXml.SelectNodes("*[local-name()='column']").Cast<XmlElement>().SingleOrDefault(e => e.GetAttribute("name") == field.Name); var props = overridenXml != null ? new OverridenProperties(overridenXml) : null; switch(field.FieldType) { case "string": return $"<property name=\"{field.Name}\"><column name=\"{field.Name}\" {(notNull ? "not-null=\"true\"" : string.Empty)} {(props?.SqlType !=null ? "sql-type=\"" + props.SqlType+"\"" : string.Empty)}/></property>"; case "bool": return $"<property name=\"{field.Name}\" not-null=\"true\" type=\"boolean\"><column name=\"{field.Name}\" not-null=\"true\" default=\"{props?.DefaultValue??"0"}\" sql-type=\"bit\" /></property>"; case "DateTime": return $"<property name=\"{field.Name}\" not-null=\"true\"/>"; case "DateTime?": return $"<property name=\"{field.Name}\" />"; case "ProgramType": return $"<property name=\"{field.Name}\"><column name=\"{field.Name}\" default=\"{props?.DefaultValue??"0"}\" {(notNull ? "not-null=\"true\"" : string.Empty)}/></property>"; case "Guid?": return $"<property name=\"{field.Name}\" />"; default: throw new ArgumentOutOfRangeException($"Not supported type {field.FieldType}. Edit DAL.tt to add mapping definition."); } } private class OverridenProperties { public string DefaultValue { get; } public string SqlType { get; } public OverridenProperties(XmlElement sourceXml) { DefaultValue = sourceXml.GetAttribute("default"); SqlType = sourceXml.GetAttribute("sql-type"); } } } class DalEmitRepository { public string Interface { get; } public string DependsOn { get; } public DalEmitRepository(XmlElement sourceXml, string className) { Interface = string.IsNullOrEmpty(sourceXml.InnerText) ? "IRepository<"+className+">" : sourceXml.InnerText; DependsOn = sourceXml.GetAttribute("dependsOn"); } public string GetRepositoryClassName() => Interface.Substring(1); } 


The code of the resulting pattern
 <#@ template debug="false" hostspecific="true" language="C#" #> <#@ assembly name="System.Xml" #> <#@ assembly name="System.Core" #> <#@ assembly name="EnvDTE" #> <#@ import namespace="System.Xml" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.IO" #> <#@ import namespace="System.Linq" #> <#@ output extension="/" #> <# EnvDTE.DTE dte = (EnvDTE.DTE) ((IServiceProvider) this.Host) .GetService(typeof(EnvDTE.DTE)); XmlDocument doc = new XmlDocument(); doc.Load(System.IO.Path.Combine(dte.ActiveDocument.Path, "DAL.xml")); var classes = doc.SelectNodes("//*[local-name()='class']").Cast<XmlElement>().Select(classXml=>new DalClass(classXml)).ToArray(); //fields foreach(var classNode in classes) { #> /* The file was generated automatically and should not be edited manually. For any changes edit DAL.xml or DAL.tt. */ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using JetBrains.Annotations; <#=classNode.GetIncludedNamespaces()#> namespace Domain.DALV2 { public class <#=classNode.GetCachedClassDefinition()#> { public Cached<#=classNode.Name#>(<#=classNode.DalBOType#> source) : base(source) { <#=string.Join("\n\t\t\t", classNode.Fields.Where(f => f.PropertySource == null).Select(f=>f.GetConstructorInitValueExpression()))#> } <#=string.Join("\n\n\t\t", classNode.Fields.Select(f=>f.GetPropertyDefinitionExpression()))#> } } <#if (classNode.GenerateInterface){#> namespace Domain.DAL { public interface I<#=classNode.Name#> <#if (classNode.LogicallyDeletable){#> :ILogicallyDeletableReadonly <#}#> { Guid Id { get; } <#=string.Join("\n\n\t\t", classNode.Fields.Select(f=>f.GetInterfacePropertyDefinitionExpression()))#> } } <#SaveOutput("Entities//gen//"+classNode.Name+".g.cs");#> <#}#> /* The file was generated automatically and should not be edited manually. For any changes edit DAL.xml or DAL.tt. */ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using Resto.Framework.Common; using JetBrains.Annotations; <#=classNode.GetIncludedNamespaces()#> namespace Domain.DALV2 { public partial class <#=classNode.Name#>DAL : DAL<Cached<#=classNode.Name#>, <#=classNode.BasedOn#>, <#=classNode.DalBOType#>> { internal <#=classNode.Name#>DAL(ISessionManager sessionManager, ICacheConsistencyManager<<#=classNode.BasedOn#>, <#=classNode.DalBOType#>> consistencyManager) : base(sessionManager, Repositories.<#=classNode.Name#>, consistencyManager) { } <#foreach (var getBy in classNode.GetBy){#> <#=getBy.IsTry?"[CanBeNull]":"[NotNull]"#> public Cached<#=classNode.Name#> <#=getBy.IsTry?"Try":""#>GetBy<#=getBy.Alias#>(<#=classNode.GetFilterParameters(getBy)#>) { UpdateCacheIfNeeded(); using (ConsistencyManager.GetReadLock()) { return Cache.Values.Single<#=getBy.IsTry?"OrDefault":""#>(e => <#=getBy.GetConditions()#>); } } <#}#> <#foreach (var fieldNode in classNode.GetAllBy){#> [NotNull] [ItemNotNull] public IList<Cached<#=classNode.Name#>> GetAllBy<#=fieldNode.Alias#>(<#=classNode.GetFilterParameters(fieldNode)#>) { UpdateCacheIfNeeded(); using (ConsistencyManager.GetReadLock()) { return Cache.Values.Where(e => <#=fieldNode.GetConditions()#>).ToList(); } } <#}#> [NotNull] protected override Cached<#=classNode.Name#> Convert(<#=classNode.DalBOType#> source) { return new Cached<#=classNode.Name#>(source); } } } <#SaveOutput("Repositories//gen//"+ classNode.Name+".g.cs");#> <#if(classNode.EmitBo != null){#> <?xml version="1.0" encoding="utf-8"?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="Domain" namespace="<#=classNode.EmitBo.Namespace#>"> <class name="<#=classNode.Name#>" dynamic-update="true" dynamic-insert="true"> <id name="Id"> <generator class="assigned" /> </id> <#=string.Join("\n\t\t", classNode.ExplicitlyDefinedFields.Select(f=>classNode.EmitBo.GetMapping(f)))#> <#if (classNode.LogicallyDeletable){#> <property name="IsDeleted" not-null="true" type="boolean"> <column name="IsDeleted" not-null="true" default="0" /> </property> <property name="WhenDeleted" type="DateTime" not-null="false"/> <#}#> </class> </hibernate-mapping> <#SaveOutput("..\\..\\DAL.Hibernate\\Mapping\\gen\\"+classNode.Name+".hbm.xml");#> /* The file was generated automatically and should not be edited manually. For any changes edit DAL.xml or DAL.tt. */ using System; using Common.Domain.BO.DB; using Domain.DAL; using Domain.DALV2; using JetBrains.Annotations; <#=classNode.GetIncludedNamespaces()#> namespace <#=classNode.EmitBo.Namespace#> { public partial class <#=classNode.GetBoClassDefinition()#> { [UsedImplicitly] protected <#=classNode.Name#>(){} <#=string.Join("\n\n\t\t", classNode.ExplicitlyDefinedFields.Select(f=>f.GetBOPropertyDefinitionExpression()))#> <#if (classNode.LogicallyDeletable){#> public virtual bool IsDeleted { get; set; } public virtual DateTime? WhenDeleted { get; set; } <#}#> } } <# SaveOutput("..//BO//gen//"+ classNode.Name+".g.cs"); } } #> <!-- The file was generated automatically and should not be edited manually. For any changes edit DAL.xml or DAL.tt. --> <objects xmlns="http://www.springframework.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:db="http://www.springframework.net/database" xsi:schemaLocation="http://www.springframework.net http://www.springframework.net/xsd/spring-objects.xsd"> <# foreach(var classNode in classes.Where(c => !c.CustomConsistencyManager)) { #> <object id="<#=LowerInitial(classNode.Name)#>CacheManager" type="Domain.DALV2.TransactionSubscribedManager<<#=classNode.BasedOn#>, <#=classNode.DalBOType#>>, Domain" singleton="true"> </object> <#}#> <object id="dalGeneratedListeners" type="System.Collections.Generic.List<Common.Hibernate.DAL.ISessionNotificationListener>, mscorlib"> <constructor-arg> <list element-type="Common.Hibernate.DAL.ISessionNotificationListener, Common.Hibernate"> <# foreach(var classNode in classes) { #> <ref object="<#=LowerInitial(classNode.Name)#>CacheManager"/> <#}#> </list> </constructor-arg> </object> </objects> <#SaveOutput("..//Config//DALSpringDefinitions.g.xml");#> /* The file was generated automatically and should not be edited manually. For any changes edit DAL.xml or DAL.tt. */ using Common; using Common.Domain.DAL; using Domain.BO; using Domain.DALV2; using Spring.Context.Support; using JetBrains.Annotations; <#=string.Join("\n", classes.Select(c=>c.GetIncludedNamespaces()).Where(s=>!string.IsNullOrEmpty(s)))#> namespace Domain.DAL { public partial class DALs { private static readonly SafeLazy<DALs> instance = new SafeLazy<DALs>(() => new DALs()); private DALs() { var sessionManager = (ISessionManager)ContextRegistry.GetContext().GetObject("sessionManager"); <# foreach(var classNode in classes){ #> this.<#=LowerInitial(classNode.Name)#> = new <#=classNode.Name#>DAL(sessionManager, (ICacheConsistencyManager<<#=classNode.BasedOn#>, <#=classNode.DalBOType#>>)ContextRegistry.GetContext().GetObject("<#=LowerInitial(classNode.Name)#>CacheManager")); <# } #> } <# foreach(var classNode in classes) { #> [NotNull] private readonly <#=classNode.Name#>DAL <#=LowerInitial(classNode.Name)#>; [NotNull] public static <#=classNode.Name#>DAL <#=classNode.Name#> => instance.Value.<#=LowerInitial(classNode.Name)#>; <#}#> public static void ResetAll() => instance.Value.ResetAllImpl(); private void ResetAllImpl() { <#foreach(var classNode in classes){#> this.<#=LowerInitial(classNode.Name)#>.Reset(); <#}#> } } } <#SaveOutput("DALs.g.cs");#> /* The file was generated automatically and should not be edited manually. For any changes edit DAL.xml or DAL.tt. */ using JetBrains.Annotations; namespace Domain.DAL { public partial interface IRepositoryFactory { <#foreach(var classNode in classes.Where(c => c.EmitRepository != null)){#> [NotNull] <#=classNode.EmitRepository.Interface#> <#=classNode.Name#> { get; } <#}#> } } <#SaveOutput("IRepositoryFactory.g.cs");#> /* The file was generated automatically and should not be edited manually. For any changes edit DAL.xml or DAL.tt. */ using JetBrains.Annotations; namespace Domain.DAL { public static partial class Repositories { <#foreach(var classNode in classes.Where(c => c.EmitRepository != null)){#> [NotNull] public static <#=classNode.EmitRepository.Interface#> <#=classNode.Name#> => Instance.Value.<#=classNode.Name#>; <#}#> } } <#SaveOutput("Repositories.g.cs");#> /* The file was generated automatically and should not be edited manually. For any changes edit DAL.xml or DAL.tt. */ using Common.Domain.DAL; using Common.Hibernate.DAL; using DAL.Hibernate.Cache.CachingProviders; using Domain.BO; using Domain.DAL; using Domain.DAL.Cache; using Domain.SaveManager; using System; using System.Collections.Generic; using JetBrains.Annotations; using Engine.Main; <#=string.Join("\n", classes.Select(c=>c.GetIncludedNamespaces()).Where(s=>!string.IsNullOrEmpty(s)))#> namespace DAL.Hibernate.DAL { public partial class RepositoryFactory : IRepositoryFactory { public void Init([NotNull] IOrganizationsByNetworkCache organizationsByNetworkCache, [NotNull] INetworkCache networkCache, [NotNull] ITransactionSaveManager transactionSaveManager, [NotNull] ILoginEntryDataProvider loginEntryDataProvider, ListenerNotifier listenerNotifier) { <#foreach(var classNode in classes.Where(c => c.EmitRepository != null)){#> this.<#=classNode.Name#> = new <#=classNode.EmitRepository.GetRepositoryClassName()#>(<#=classNode.EmitRepository.DependsOn#>); <#}#> } <#foreach(var classNode in classes.Where(c => c.EmitRepository != null)){#> [NotNull] public <#=classNode.EmitRepository.Interface#> <#=classNode.Name#> { get; private set; } <#}#> } } <#SaveOutput("..\\..\\DAL.Hibernate\\DAL\\RepositoryFactory.g.cs");#> <#+ private string LowerInitial(string name) { return name[0].ToString().ToLowerInvariant() + name.Substring(1); } private string UpperInitial(string name) { return name[0].ToString().ToUpperInvariant() + name.Substring(1); } void SaveOutput(string outputFileName) { string templateDirectory = Path.GetDirectoryName(Host.TemplateFile); string outputFilePath = Path.Combine(templateDirectory, outputFileName); File.WriteAllText(outputFilePath, this.GenerationEnvironment.ToString()); this.GenerationEnvironment.Remove(0, this.GenerationEnvironment.Length); } #> 


And where without some tricky Generic-code, to subscribe to make changes to the database, update on-demand caches, and other pleasures of life. Here generic turned out to be quite enough to close all usage scenarios, coupled with those already used with

General code for data access
  public abstract class DAL<T, TBase, TCachedBo> where T : CachedEntity<TBase> where TBase : class, IEntity where TCachedBo : class, IIdEntity<Guid> { [NotNull] private readonly ILog log; [NotNull] protected readonly ISessionManager SessionManager; [NotNull] protected readonly IReadonlyRepository<TBase> BaseRepository; [NotNull] protected readonly ICacheConsistencyManager<TBase, TCachedBo> ConsistencyManager; [NotNull] protected Dictionary<Guid, T> Cache = new Dictionary<Guid, T>(); protected DAL([NotNull] ISessionManager sessionManager, [NotNull] IReadonlyRepository<TBase> baseRepository, [NotNull] ICacheConsistencyManager<TBase, TCachedBo> consistencyManager) { this.SessionManager = sessionManager; this.BaseRepository = baseRepository; this.ConsistencyManager = consistencyManager; log = LogManager.GetLogger(GetType()); } [CanBeNull] public T TryGetById(Guid id) { UpdateCacheIfNeeded(); using (ConsistencyManager.GetReadLock()) { return Cache.GetOrDefault(id); } } [NotNull] public T GetById(Guid id) { UpdateCacheIfNeeded(); using (ConsistencyManager.GetReadLock()) { return ValidateEntityFound(Cache.GetOrDefault(id), "{0} with id {1} not found", null, typeof(T), id); } } [NotNull] public TBase GetEntity([NotNull] ISession session, Guid id) { return BaseRepository.GetById(session, id); } [NotNull] public TBase GetEntity([NotNull] ISession session, [NotNull] T e) { return BaseRepository.GetById(session, e.Id); } [NotNull] public HashSet<T> GetByIds([NotNull] HashSet<Guid> ids) { UpdateCacheIfNeeded(); using (ConsistencyManager.GetReadLock()) { return ids .Select(id => Cache.GetOrDefault(id)) .ToHashSet(); } } public void Reset() { ConsistencyManager.Reset(); } protected abstract T Convert(TCachedBo source); protected void UpdateCacheIfNeeded() { if (!ConsistencyManager.UpdateRequired) return; log.Debug($"{typeof(T).Name} DAL: Update required, getting writeLock"); using (var updateScope = ConsistencyManager.GetWriteLock()) { if (updateScope.UpdateRequired ?? ConsistencyManager.UpdateRequired) SessionManager.RunTransacted(session => { try { var updatedEntities = updateScope.GetUpdatedEntities(BaseRepository, session); foreach (var updatedEntity in updatedEntities) if (updatedEntity.Value != null) Cache[updatedEntity.Key] = Convert(updatedEntity.Value); else Cache.Remove(updatedEntity.Key); Reindex(); } catch (Exception) { ConsistencyManager.Reset(); throw; } }); } } /// <summary> /// Reevaluate specific indexes, used for search in cached entities. Called after cache has been updated. /// </summary> protected virtual void Reindex() { } [NotNull] protected T1 ValidateEntityFound<T1>([CanBeNull] T1 entity, [NotNull] string errorMessage, [NotNull] string frontMessage, [NotNull] params object[] p) { if (entity == null) throw new DataAccessException(Util.GetMessage(errorMessage, p), Util.GetMessage(frontMessage, p)); return entity; } } 



Interesting, in my opinion, was the class that monitors the consistency of the cache and manages its update, the main place where I had to think about the implementation of locks so that everything was optimal and minimally blocking, but at the same time protected

Actually implementation
  [UsedImplicitly] public class TransactionSubscribedManager<TBase, TCachedBo> : EmptySessionNotificationListener, ICacheConsistencyManager<TBase, TCachedBo>, ITransactionNotificationListener where TBase : class, IEntity where TCachedBo : class, IIdEntity<Guid> { public bool UpdateRequired => needFullUpdate || !entitiesToUpdate.IsEmpty; [NotNull] protected readonly ReaderWriterLockSlim LockObj = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); [NotNull] private readonly ConcurrentDictionary<string, ConcurrentBag<Guid>> changesByTransaction = new ConcurrentDictionary<string, ConcurrentBag<Guid>>(); [NotNull] protected ConcurrentBag<Guid> entitiesToUpdate = new ConcurrentBag<Guid>(); protected bool needFullUpdate = true; public virtual CacheWriteLockScope<TBase, TCachedBo> GetWriteLock() { try { LockObj.EnterWriteLock(); if (needFullUpdate) { needFullUpdate = false; return new CacheWriteLockScope<TBase, TCachedBo>(LockObj); } return new IdBasedCacheWriteLockScope<TBase, TCachedBo>(LockObj, ChangedEntitiesIdsProvider); } catch (Exception) { LockObj.ExitWriteLock(); throw; } } protected ICollection<Guid> ChangedEntitiesIdsProvider() { var ids = new List<Guid>(); var newBag = new ConcurrentBag<Guid>(); var oldBag = Interlocked.Exchange(ref entitiesToUpdate, newBag); while (oldBag.TryTake(out var id)) ids.Add(id); return ids; } public CacheReadLockScope GetReadLock() { return new CacheReadLockScope(LockObj); } public override void OnSaveOrUpdate(ISession session, object entity, Guid id, object[] currentState, object[] previousState, string[] propertyNames, IType[] types) { if (entity is TBase) { var transactionName = HibernateSessionManager.GetTransactionName(); if (!string.IsNullOrEmpty(transactionName)) { if (changesByTransaction.TryAdd(transactionName, new ConcurrentBag<Guid>() { id })) return; changesByTransaction.TryGetValue(transactionName, out var transactionChangedEntities); // ReSharper disable once PossibleNullReferenceException // R# does not have attributes telling that value in dictionary is not null. But we know it. transactionChangedEntities.Add(id); } } } public override void OnDelete(ISession session, object entity, Guid id) { if (entity is TBase) { var transactionName = HibernateSessionManager.GetTransactionName(); if (string.IsNullOrEmpty(transactionName)) return; if (changesByTransaction.TryAdd(transactionName, new ConcurrentBag<Guid>() { id })) return; changesByTransaction.TryGetValue(transactionName, out var transactionChangedEntities); // ReSharper disable once PossibleNullReferenceException // R# does not have attributes telling that value in dictionary is not null. But we know it. transactionChangedEntities.Add(id); } } public void Reset() { needFullUpdate = true; } void ITransactionNotificationListener.AfterCommit(ISession session, string transactionName) { if (changesByTransaction.TryRemove(transactionName, out var transactionChangedEntities) && !transactionChangedEntities.IsEmpty) while (transactionChangedEntities.TryTake(out var id)) entitiesToUpdate.Add(id); } void ITransactionNotificationListener.AfterRollback(ISession session, string transactionName) { changesByTransaction.TryRemove(transactionName, out _); } } 

  public class IdBasedCacheWriteLockScope<TBase, TCachedBo> : CacheWriteLockScope<TBase, TCachedBo> where TBase : class, IEntity where TCachedBo : class, IIdEntity<Guid> { [NotNull] private readonly ICollection<Guid> changedEntitiesIds; public override bool? UpdateRequired => changedEntitiesIds.Any(); public IdBasedCacheWriteLockScope([NotNull] ReaderWriterLockSlim lockObj, [NotNull] Func<ICollection<Guid>> changedEntitiesIdsProvider) : base(lockObj) { changedEntitiesIds = changedEntitiesIdsProvider() ?? throw new InvalidOperationException(nameof(changedEntitiesIdsProvider)); } public override IDictionary<Guid, TCachedBo> GetUpdatedEntities(IReadonlyRepository<TBase> repository, ISession session) { var entities = repository.GetByIds(session, changedEntitiesIds, true).ToDictionary(e => e.Id, be => be as TCachedBo); foreach (var id in changedEntitiesIds.Where(i => !entities.ContainsKey(i))) entities.Add(id, null); return entities; } } 


Interesting shoals that came to light in the process of developing and rolling on the battle:

Source: https://habr.com/ru/post/453382/


All Articles