using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; using Tango.Core; using Tango.Core.DI; using Tango.FSE.BL.Connectivity; using Tango.Logging; namespace Tango.FSE.BL { /// /// Represents a cascading/fallback data retrieval mechanism. /// /// /// public class DataResolver : ExtendedObject where T : class { private static object diskLock = new object(); private static IConnectivityProvider _connectivity; private Func _onlineAction; private Func _webAction; private Func _diskCacheAction; private Func _memoryCacheAction; private Action _onError; private Action _onComplete; private List _nodes; private bool _throwOnError; private String _callingName; private bool _enableLogs = true; /// /// Prevents a default instance of the class from being created. /// private DataResolver() { if (_connectivity == null) { _connectivity = TangoIOC.Default.GetInstance(); } _nodes = new List() { DataResolverNode.Online }; _throwOnError = true; } /// /// Executes this data resolver. /// /// /// Could not execute data resolver with zero nodes. public T Execute() { if (_enableLogs) LogManager.Log($"Executing data resolver for {_callingName}..."); T result = null; var nodes = _nodes.Distinct().ToList(); if (nodes.Count == 0) { throw new InvalidOperationException("Could not execute data resolver with zero nodes."); } DataResolverContext context = new DataResolverContext(); foreach (var node in nodes) { String fallingBackText = String.Empty; if (nodes.IndexOf(node) < nodes.Count - 1) { fallingBackText = $" Falling back to {nodes[nodes.IndexOf(node) + 1]}..."; } if (node == DataResolverNode.Web) { try { if (_enableLogs) LogManager.Log($"Trying {_callingName} via web..."); result = ExecuteWebAction(context); break; } catch (Exception ex) { if (_enableLogs) LogManager.Log($"{_callingName} via web failed with exception: {ex.FlattenMessage()}{fallingBackText}", LogCategory.Warning); context.LastError = ex; } } else if (node == DataResolverNode.Online) { try { if (_enableLogs) LogManager.Log($"Trying {_callingName} via online..."); result = ExecuteOnlineAction(context); break; } catch (Exception ex) { if (_enableLogs) LogManager.Log($"{_callingName} via online failed with exception: {ex.FlattenMessage()}{fallingBackText}", LogCategory.Warning); context.LastError = ex; } } else if (node == DataResolverNode.DiskCache) { try { if (_enableLogs) LogManager.Log($"Trying {_callingName} via disk cache..."); result = _diskCacheAction.Invoke(context); break; } catch (Exception ex) { if (_enableLogs) LogManager.Log($"{_callingName} via disk cache failed with exception: {ex.FlattenMessage()}{fallingBackText}"); context.LastError = ex; } } else if (node == DataResolverNode.InMemoryCache) { try { if (_enableLogs) LogManager.Log($"Trying {_callingName} via in-memory cache..."); result = _memoryCacheAction.Invoke(context); break; } catch (Exception ex) { if (_enableLogs) LogManager.Log($"{_callingName} via in-memory cache failed with exception: {ex.FlattenMessage()}{fallingBackText}"); context.LastError = ex; } } if (context.Aborted) { if (_enableLogs) LogManager.Log($"{_callingName} aborted."); break; } } if (result != null) { if (_enableLogs) LogManager.Log($"{_callingName} completed successfully."); _onComplete?.Invoke(result); return result; } if (context.LastError == null) { context.LastError = new InvalidOperationException("No nodes were run successfully but could not retrieve the last error."); } if (_enableLogs) LogManager.Log(context.LastError, $"{_callingName} failed."); _onError?.Invoke(context.LastError); if (_throwOnError) { throw context.LastError; } else { return null; } } private T ExecuteOnlineAction(DataResolverContext context) { if (!_connectivity.CheckOnline()) { throw new InternetConnectionException(); } return _onlineAction.Invoke(context); } private T ExecuteWebAction(DataResolverContext context) { if (!_connectivity.CheckOnline()) { throw new InternetConnectionException(); } return _webAction.Invoke(context); } #region Context /// /// Represents a data resolver node context. /// /// public class DataResolverContext { internal bool Aborted { get; set; } /// /// Gets the last error, or maybe the reason that the current node is running. /// public Exception LastError { get; internal set; } internal DataResolverContext() { } /// /// Aborts the entire data resolver. /// public void AbortCascade() { Aborted = true; } } #endregion #region Builder /// /// Build a instance. /// /// public class Builder { private DataResolver _resolver; /// /// Prevents a default instance of the class from being created. /// private Builder() { _resolver = new DataResolver(); } /// /// Creates a new builder instance. /// /// public static Builder New() { var method = new StackTrace().GetFrame(1).GetMethod(); var cls = method.ReflectedType.Name; var builder = new Builder(); builder._resolver._callingName = $"'{cls}.{method.Name}'"; return builder; } /// /// Specify the method for the online node. /// /// The method. /// public Builder Online(Func func) { _resolver._onlineAction = func; return this; } /// /// Specify the method for the web client node. /// /// The method. /// public Builder Web(Func func) { _resolver._webAction = func; return this; } /// /// Specify the method for the disk cache node. /// /// The method. /// public Builder DiskCache(Func func) { _resolver._diskCacheAction = func; return this; } /// /// Specify the method for the in-memory node. /// /// The method. /// public Builder InMemoryCache(Func func) { _resolver._memoryCacheAction = func; return this; } /// /// Configure the node cascade priority. /// /// The nodes. /// public Builder ConfigureCascade(params DataResolverNode[] nodes) { _resolver._nodes = nodes.ToList(); return this; } /// /// Called when all nodes have failed. /// /// The on error. /// public Builder OnError(Action onError) { _resolver._onError = onError; return this; } /// /// Called when one of the nodes completed successful. /// /// The on complete. /// public Builder OnComplete(Action onComplete) { _resolver._onComplete = onComplete; return this; } /// /// Specify whether to throw the last exception when all the nodes have failed. /// /// if set to true [throw on error]. /// public Builder ThrowOnError(bool throwOnError) { _resolver._throwOnError = throwOnError; return this; } /// /// Specify whether to enable data resolver logging. /// /// if set to enables logging. /// public Builder EnableLogs(bool enableLogs) { _resolver._enableLogs = enableLogs; return this; } /// /// Specify a custom name for the calling service when logs are enabled. /// /// Name of the calling service. /// public Builder WithLogsCallingName(String callingName) { var method = new StackTrace().GetFrame(1).GetMethod(); _resolver._callingName = $"'{callingName}.{method.Name}'"; return this; } /// /// Builds this instance. /// /// public DataResolver Build() { return _resolver; } /// /// Builds the executes the instance. /// /// public T BuildExecute() { return Build().Execute(); } /// /// Builds the executes the instance asynchronous. /// /// public Task BuildExecuteAsync() { return Task.Factory.StartNew(() => Build().Execute()); } } #endregion } }