TCMS Business Services Caching
The Info Services Domain of the TCMS Application provides the data responses (Response DTOs) as the return for Web Service data requests (Request DTOs) calls.
This process is expensive as it must make calls to Cloud Services a d Data Services through their sub-domains.
The Bakery Analogy
When a request for bakery goods, such as bread, is received at the Bakery the baker does not immediately call the vendor to deliver the required ingredients for the order.
The Baker checks to see if the items exist within the bakery’s current inventory. If the items required are not on the shelves, then he calls his vendor to orders the required items.
The Baker makes a single request and expects a single package delivered to him.
The package contains the discrete boxed items ordered with a packing slip that informs him of any issues such as back orders or substituted items.
The Baker takes the requested items inside the single package and places them in inventory for consumption within the Bread order. The baker may or may not consume all of the items ordered. The remaining items are part of the Baker’s “Cache“.
Here is the work flow that TCMS Cache follows for data requests from the Info Services Domain:
-
A Single Request DTO is sent as a parameter in the Web Service call by the consuming application
-
The WebOps passes the Request DTO to the BusOps for processing into a Response DTO
-
BusOps checks its “Inventory” Cache for the requested ResponseDTO
-
If it receives a “Hit” from Cache it immediately returns the Response DTO from the Cache Inventory
-
If it does not get a Cache hit it makes the expensive call(s) to Data Services and Cloud Services as required and returns its Aggregate DTOs to BusOps for DTO Transformation into the Complex Composite Response DTO with its Error Handling Object
-
BusOps then loads the single Composite Response DTO into the Cache inventory for future use
The TCMS Info Services Request/Response process complies with the Code Design Principle:
Data Transfer Object Role Principle (DTORP)
Carry your Groceries in a Bag
… Not One Item at a Time
The BusOps Caching Code
This Cache Model uses an implementation of the Paul Glavich and Darren Boon’s “Glav Cache Adapter” from the NuGet package: .NET CacheAdapter. It is version 3.0 dated 07/21/2013.
The Info Services Caching implementation is accomplished using four classes in the BusOps.Cache assembly.
This is a demonstration of the People Domain Entity’s use of Cache for getting a list of United States States and a Complete Technologist Assessment with all of the Domain Data.
- PeopleStatesCacheManager – Implements the ICacheManage Interface to contract these Methods
- IsCachedDTO
- Checks to see if Cache is enabled: Returns passed ResponseDTO with Success false flag
- If enabled executes the CheckCacheForDTO() method
- IsCachedDTO
- CheckCacheForDTO
- Uses Glav Cache to process the Cache request
- Executes the method call Func<TRequest, TResponse> if not in cache
- Load returned DTO in cache
- Returns the new DTO to the IsCachedDTO method call
- CacheHelper – Manages the Cache Config for the Glav Adapter
- Selects the type of Cache mechanism
- Memory Cache Adapter – The Default caching model
- MemCache Adapter – A third party cache model
- Web Cache – ASP.NET Web Application Cache model
- App Fabric Cache Adapter – The Microsoft distributed cache model
- Selects the type of Cache mechanism
- CacheLogger – Implements the Glav Interface ILogging
- The Interface Methods
- WriteInfoMessage
- WriteErrorMessage
- WriteException
- Wraps the Interface methods inside Enterprise Library 6 Custom Formatted Logging Schemas
- Logs to the TCMS database
- Logs to defined sub-domains log files
- The Interface Methods
- ICacheManager – Creates the contracts for the implementing class: PeopleStatesCachemanager
- IsCachedDTO
- CheckCacheForDTO
- CreateCacheDTO
- DeleteCacheDTO
- ClealAllCaches
The consuming Client, BusOps, requires two classes to implement the Cache model:
- BusOpsPeopleServices – The Method Wrapper for the WebOps call: GetStatesSeed()
- BusinessOpsHelpers – Holds the BusOps implementation of the Onion Layer: ExecuteBusOpsRequest() using the Func<TRequest, TResponse> to pass the WebOps requested Payload Package.
- WebOps passes the RequestDTO to the BusOpsPeopleServices class method: GetStatesSeed(requestDto) for a ResponseDTO composite
- BusOpsPeopleServices class method GetStatesSeed(requestDto) wraps the WebOps call into method that handles Logging and Performance calculations:BusinessOpsHelpers.ExecuteBusOpsRequest(DataOpsPeopleServices.GetStatesSeed, requestDto)This method take the method call as a Func<TRequest, TResponse> as a method parameter. The Request DTO is the requestDto second parameter.This generic method is global for all WebOps requests as it is Typed to the required base classes: BaseRequest and Base Response. Covariance and Contravariance will understand that I am using the where clauses as a tool for Polymorphic behavior.Enterprise Library 6.0 is used as the Logging Cross Cutting Concern. The implementation is abstracted from the wrapper class with the Helper Methods: CreateSuccessLogEntry() and CreateExceptionLogEntry().This Helper Method is used to log Status and Performance for Every WebOps call for Every Domain EntityThis session stays Opened until the lower level modules, DataOps and CloudOps returns with its ResponseDTO.
- The method passed by the Func<TRequest, TResponse> is executed and sent to DataOps for the ResponseDTO If the Cache method Call IsCachedDTO() returns false
- If IsCachedDTO() returns true it carries the ResponseDTO composite with it as the payload for the WebOps call.This method checks for IsCacheEnabled DTO Property is true. It checks for the RequestDTO in the Cache
- A call to the CreateCacheDTO() method checks the Glav Memory Cache and return the object if it exists and loads the object in cache if it does not.
- The Response DTO is returned to BusOps as if had been retrieved from DataOps or CloudOps.
The Code
The IWebServices Web Service Contracts Interface
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using System.ServiceModel; using InfoServices.Infrastructure.DTOs.DTOs.People; using InfoServices.Infrastructure.DTOs.DTOs.Seed.PeopleSeed.States; using InfoServices.Infrastructure.DTOs.Seed.PeopleSeed.States; namespace InfoServices.Business.WebOps { [ServiceContract] public interface IWebServices { [OperationContract] TechnologistInfoResponseDto GetTechnologistInfo(TechnologistInfoRequestDto requestDto); [OperationContract] StatesSeedResponseDto GetStatesSeed(StatesSeedRequestDto requestDto); } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
using InfoServices.Business.BusOps.PeopleEntity; using InfoServices.Infrastructure.DTOs.DTOs.People; using InfoServices.Infrastructure.DTOs.DTOs.Seed.PeopleSeed.States; using InfoServices.Infrastructure.DTOs.Seed.PeopleSeed.States; namespace InfoServices.Business.WebOps { public class WebServices : IWebServices { public TechnologistInfoResponseDto GetTechnologistInfo(TechnologistInfoRequestDto requestDto) { return BusOpsPeopleServices.GetTechnologistInfo(requestDto); } public StatesSeedResponseDto GetStatesSeed(StatesSeedRequestDto requestDto) { return BusOpsPeopleServices.GetStatesSeed(requestDto); } } } |
The BusOps implementation of the People Ops Onion Wrapper methods
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
using InfoServices.Business.BusOps.Helpers; using InfoServices.Data.DataOps.PeopleEntity; using InfoServices.Data.DataOps.PeopleEntity.People; using InfoServices.Infrastructure.DTOs.DTOs.People; using InfoServices.Infrastructure.DTOs.DTOs.Seed.PeopleSeed.States; using InfoServices.Infrastructure.DTOs.Seed.PeopleSeed.States; namespace InfoServices.Business.BusOps.PeopleEntity { public static class BusOpsPeopleServices { private static PeopleOps _peopleOps { get; set; } static BusOpsPeopleServices() { _peopleOps = new PeopleOps(); } public static TechnologistInfoResponseDto GetTechnologistInfo(TechnologistInfoRequestDto requestDto) { return BusinessOpsHelpers.ExecuteBusOpsRequest(DataOpsPeopleServices.GetTechnologistInfo, requestDto); } public static StatesSeedResponseDto GetStatesSeed(StatesSeedRequestDto requestDto) { return BusinessOpsHelpers.ExecuteBusOpsRequest(DataOpsPeopleServices.GetStatesSeed, requestDto); } } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using InfoServices.Business.BusOps.Cache.CacheManagers; using InfoServices.Business.BusOps.Helpers.Enums; using InfoServices.Infrastructure.DTOs.DTOs.Common; using InfoServices.Infrastructure.Exceptions.Extensions; using InfoServices.Infrastructure.Logging; using InfoServices.Infrastructure.Logging.DTOs; using InfoServices.Infrastructure.Logging.Enums; using InfoServices.Infrastructure.Logging.Helpers; using InfoServices.Infrastructure.Messages.Helpers; using InfoServices.Infrastructure.Messages.ModelDtos; namespace InfoServices.Business.BusOps.Helpers { public static class BusinessOpsHelpers { private static bool _isLoggingEnabled { get; set; } private static string _title { get; set; } private static string _message { get; set; } private static int _priority { get; set; } private static TraceEventType _severity { get; set; } private static int _eventId { get; set; } private static IList<string> _categories { get; set; } private static IDictionary<string, object> _properties { get; set; } private static MessageInfoRequest _messageInfoRequest { get; set; } static BusinessOpsHelpers() { _isLoggingEnabled = true; _title = "BusinessOps Logger"; _categories = new List<string> { CategoryTypes.BusinessOps.ToString() }; _properties = new Dictionary<string, object> { { PropertyTypes.BusinessServices.ToString(), CategoryTypes.Infrastructure.ToString() } }; _messageInfoRequest = new MessageInfoRequest { ComponentName = "BusOps" }; } public static TResponse ExecuteBusOpsRequest<TRequest, TResponse>(Func<TRequest, TResponse> methodCall, TRequest request) where TRequest : BaseRequest where TResponse : BaseResponse, new() { LogResponseDTO logResponse; var peopleStatesCacheManager = new PeopleStatesCacheManager(); var response = new TResponse(); var startTime = DateTime.Now; response = (TResponse)peopleStatesCacheManager.IsCachedDTO(methodCall, request, response); if (!response.IsCached) { try { response = methodCall(request); } catch (Exception error) { logResponse = CreateExceptionLogEntry(error, startTime, DateTime.Now, response); response.IsLogSuccess = logResponse.Success; if (logResponse.ErrorList.Count > 0) response.BubbleErrors(response.ErrorList); response.ErrorList.Add(new ErrorInfo(error)); return response; } } logResponse = CreateSuccessLogEntry(startTime, DateTime.Now, response); response.TotalDuration = logResponse.ExecutionDuration; response.BusOpsDuration = (response.TotalDuration - response.DataOpsDuration); if (logResponse.ErrorList.Count > 0) { response.BubbleErrors(response.ErrorList); return response; } response.IsLogSuccess = logResponse.Success; if (response.ErrorList.Count > 0) return response; response.Success = true; return response; } #region Helpers private static LogResponseDTO CreateSuccessLogEntry(DateTime startTime, DateTime endTime, IErrorCommon response) { double duration = (endTime - startTime).TotalMilliseconds; _messageInfoRequest.MessageTag = BusOpsMessageTag.ExecutionDuration.ToString(); _message = string.Format(ServicesMessages.GetMessages(_messageInfoRequest).SystemMessage.Message, duration); string errorMessages = null; if (response.ErrorList.Count > 0) { errorMessages = response.ErrorList.Aggregate(errorMessages, (current, item) => current + (item.Message + " | ")); } _message = string.Format("{0} | {1}", _message, errorMessages); _priority = 5; _severity = TraceEventType.Information; _eventId = 202; var logRequest = LogHelpers.CreateLogRequest(_title, _message, _priority, _severity, _eventId, _categories, _properties); var logResponse = ServicesLogging.CreateLoggingEntry(logRequest, _isLoggingEnabled); logResponse.ExecutionDuration = duration; return logResponse; } private static LogResponseDTO CreateExceptionLogEntry(Exception error, DateTime startTime, DateTime endTime, IErrorCommon response) { var duration = (endTime - startTime).TotalMilliseconds; _messageInfoRequest.MessageTag = Data.DataOps.Helpers.Enums.DataOpsMessageTag.ExceptionError.ToString(); _message = string.Format(ServicesMessages.GetMessages(_messageInfoRequest).SystemMessage.Message, error.Message, duration); string errorMessages = null; if (response.ErrorList.Count > 0) { errorMessages = response.ErrorList.Aggregate(errorMessages, (current, item) => current + (item.Message + " | ")); } _message = string.Format("{0} | {1}", _message, errorMessages); _priority = 1; _severity = TraceEventType.Critical; _eventId = 502; var logRequest = LogHelpers.CreateLogRequest(_title, _message, _priority, _severity, _eventId, _categories, _properties); var logResponse = ServicesLogging.CreateLoggingEntry(logRequest, _isLoggingEnabled); logResponse.ExecutionDuration = duration; return logResponse; } #endregion } } |
The ICacheManager Contracts
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
using System; using InfoServices.Infrastructure.DTOs.DTOs.Common; namespace InfoServices.Business.BusOps.Cache.Helpers { public interface ICacheManager { BaseResponse IsCachedDTO<TRequest, TResponse>(Func<TRequest, TResponse> methodCall, TRequest request, BaseResponse responseDto) where TRequest : BaseRequest where TResponse : BaseResponse, new(); BaseResponse CheckCacheForDTO<TRequest, TResponse>(Func<TRequest, TResponse> methodCall, TRequest request) where TRequest : BaseRequest where TResponse : BaseResponse, new(); BaseResponse CreateCacheDTO(BaseResponse responseDto); BaseResponse DeleteCacheDTO(BaseResponse responseDto); BaseResponse ClealAllCaches(BaseResponse responseDto); } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
using System; using System.Collections.Generic; using System.Diagnostics; using Glav.CacheAdapter.Core.Diagnostics; using InfoServices.Infrastructure.Logging; using InfoServices.Infrastructure.Logging.DTOs; using InfoServices.Infrastructure.Logging.Enums; namespace InfoServices.Business.BusOps.Cache.Helpers { public class CacheLogger : ILogging { private string _title { get; set; } private int _priority { get; set; } private TraceEventType _severity { get; set; } private int _eventId { get; set; } private ICollection<string> _categories { get; set; } private Dictionary<string, object> _properties { get; set; } private bool _isLoggingEnabled { get; set; } public LogResponseDTO LogResponseDto { get; set; } public CacheLogger() { _title = "Cache Information Logging"; _priority = 5; _severity = TraceEventType.Information; _eventId = 601; _categories = new List<string> { CategoryTypes.BusinessOps.ToString() }; _properties = new Dictionary<string, object> { { PropertyTypes.BusinessServices.ToString(), CategoryTypes.BusinessOps.ToString() } }; _isLoggingEnabled = true; } public void WriteInfoMessage(string message) { var logRequest = CreateLogRequest(_title, message, _priority, _severity, _eventId, _categories, _properties); LogResponseDto = ServicesLogging.CreateLoggingEntry(logRequest, _isLoggingEnabled); } public void WriteErrorMessage(string message) { _title = "Cache Error Message Logging"; _priority = 2; _severity = TraceEventType.Error; _eventId = 602; var logRequest = CreateLogRequest(_title, message, _priority, _severity, _eventId, _categories, _properties); LogResponseDto = ServicesLogging.CreateLoggingEntry(logRequest, _isLoggingEnabled); } public void WriteException(Exception error) { _title = "Cache Exception Message Logging"; _priority = 1; _severity = TraceEventType.Critical; _eventId = 603; var message = string.Format("ERROR Exception: {0}", error.Message); var logRequest = CreateLogRequest(_title, message, _priority, _severity, _eventId, _categories, _properties); LogResponseDto = ServicesLogging.CreateLoggingEntry(logRequest, _isLoggingEnabled); } #region Helpers private static LogRequestDTO CreateLogRequest(string title, string message, int priority, TraceEventType severity, int eventId, ICollection<string> categories, IDictionary<string, object> properties) { return new LogRequestDTO { Title = title, Message = message, Priority = priority, Severity = severity, EventId = eventId, Categories = categories, Properties = properties }; } #endregion } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
using System; using Glav.CacheAdapter.Core; using Glav.CacheAdapter.DependencyManagement; using InfoServices.Business.BusOps.Cache.Helpers; using InfoServices.Infrastructure.DTOs.DTOs.Common; namespace InfoServices.Business.BusOps.Cache.CacheManagers { public class PeopleStatesCacheManager : ICacheManager { private static string _parentKey { get; set; } private static ICache _cache { get; set; } private static ICacheProvider _cacheProvider { get; set; } private static DateTime _absoluteExpirationDate { get; set; } private static CacheDependencyAction _cacheDependencyAction { get; set; } static PeopleStatesCacheManager() { _parentKey = "DataSeeds"; _cache = CacheHelper.GetCacheFromConfig(); _cacheProvider = CacheHelper.GetCacheProvider(); _absoluteExpirationDate = DateTime.Now.AddMinutes(60); _cacheDependencyAction = CacheDependencyAction.ClearDependentItems; _cacheProvider.InvalidateCacheItem(_parentKey); } public BaseResponse IsCachedDTO<TRequest, TResponse>(Func<TRequest, TResponse> methodCall, TRequest request, BaseResponse responseDto) where TRequest : BaseRequest where TResponse : BaseResponse, new() { if (request.IsCacheEnabled) { responseDto = CheckCacheForDTO(methodCall, request); if (responseDto.Success) { responseDto.IsCacheEnabled = true; responseDto.IsCached = true; return responseDto; } return responseDto; } return responseDto; } public BaseResponse CheckCacheForDTO<TRequest, TResponse>(Func<TRequest, TResponse> methodCall, TRequest request) where TRequest : BaseRequest where TResponse : BaseResponse, new() { var cacheKey = request.GetType().Name; var isItemInCache = _cache.Get<TResponse>(cacheKey) != null; if (!isItemInCache) { _cacheProvider.Get(_parentKey, _absoluteExpirationDate, () => _parentKey); var cacheData = _cacheProvider.Get(cacheKey, _absoluteExpirationDate, () => methodCall(request), _parentKey, _cacheDependencyAction); return cacheData; } return _cache.Get<TResponse>(cacheKey); } public BaseResponse CreateCacheDTO(BaseResponse responseDto) { responseDto.IsCached = true; return responseDto; } public BaseResponse DeleteCacheDTO(BaseResponse responseDto) { responseDto.IsCached = false; return responseDto; } public BaseResponse ClealAllCaches(BaseResponse responseDto) { responseDto.IsCached = false; return responseDto; } } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
using Glav.CacheAdapter; using Glav.CacheAdapter.Bootstrap; using Glav.CacheAdapter.Core; using Glav.CacheAdapter.DependencyManagement; using Glav.CacheAdapter.Distributed.AppFabric; using Glav.CacheAdapter.Distributed.memcached; using Glav.CacheAdapter.Web; namespace InfoServices.Business.BusOps.Cache.Helpers { public static class CacheHelper { private static readonly CacheConfig _config = new CacheConfig(); static CacheHelper() { _config = new CacheConfig(); } public static ICacheProvider GetCacheProvider() { return new CacheProvider(GetCacheFromConfig(), new CacheLogger(), GetDependencyManager()); } #region Helpers public static ICache GetCacheFromConfig() { switch (_config.CacheToUse) { case CacheTypes.MemoryCache: return new MemoryCacheAdapter(new CacheLogger()); case CacheTypes.memcached: return new memcachedAdapter(new CacheLogger()); case CacheTypes.WebCache: return new WebCacheAdapter(new CacheLogger()); case CacheTypes.AppFabricCache: return new AppFabricCacheAdapter(new CacheLogger()); default: return new MemoryCacheAdapter(new CacheLogger()); } } private static ICacheDependencyManager GetDependencyManager() { return new GenericDependencyManager(GetCacheFromConfig(), new CacheLogger()); } #endregion } } |
The Caching Model complies with the DRY Development Design Principle:
Don’t Repeat Yourself
… Repetition in Performance Tasks is the “Root of All Performance Evil”
Latest posts by Brad Huett (see all)
- DevOps: A Bridge to Your DevOps Culture - March 25, 2016
- Embracing Test Driven Development (TDD) - March 25, 2016
- DevOps: Delivering Agile Projects - March 25, 2016


