public async Task ServiceExceptionTest()
        {
            var cacheRegion = "cacheRegion";
            var serviceInfo = new CalledServiceInfo
            {
                Name               = "Service",
                Id                 = Guid.NewGuid(),
                Endpoint           = "Endpoint",
                CircuitBreakerInfo = new CircuitBreakerInfo
                {
                    ExceptionCount = 1,
                    BreakDuration  = TimeSpan.FromMilliseconds(1)
                }
            };

            var cache             = Substitute.For <ICache>();
            var httpClientWrapper = Substitute.For <IHttpClientWrapper>();
            var tokenService      = Substitute.For <ITokenService>();

            httpClientWrapper.PostAsync(serviceInfo.Endpoint, null, CancellationToken.None).ThrowsForAnyArgs(new Exception());

            var sut = new DynamicService(cache, httpClientWrapper, tokenService);

            ServiceResponse actual = await sut.CallService(serviceInfo, cacheRegion, CancellationToken.None, null);

            Assert.AreEqual(ServiceResponseStatus.Error, actual.Status);
        }
        public void NoParametersTest()
        {
            var cacheRegion    = "cacheRegion";
            var userIdentifier = "User";
            var serviceInfo    = new CalledServiceInfo
            {
                AdditionalParameters = new ParameterInfo[0]
            };

            var cache = Substitute.For <ICache>();

            cache.Get <string>(cacheRegion, Constants.UserIdentifierCacheKey).Returns(new CacheEntry <string> {
                Value = userIdentifier
            });

            var sut = new CalledServiceBaseInstance(cache);

            IEnumerable <KeyValuePair <string, string> > actual = sut.GetPostParameters(serviceInfo, cacheRegion, null);

            cache.Received(1).Get <string>(cacheRegion, Constants.UserIdentifierCacheKey);
            IEnumerable <KeyValuePair <string, string> > keyValuePairs = actual as KeyValuePair <string, string>[] ?? actual.ToArray();

            Assert.IsTrue(keyValuePairs.Any());
            Assert.AreEqual(1, keyValuePairs.Count());

            KeyValuePair <string, string> parameter = keyValuePairs.First();

            Assert.AreEqual(Constants.UserIdentifierPostKey, parameter.Key);
            Assert.AreEqual(userIdentifier, parameter.Value);
        }
        public async Task ServiceTest()
        {
            var serviceInfo = new CalledServiceInfo
            {
                Name               = "Service",
                Id                 = Guid.NewGuid(),
                Endpoint           = "Endpoint",
                CircuitBreakerInfo = new CircuitBreakerInfo
                {
                    ExceptionCount = 1,
                    BreakDuration  = TimeSpan.FromMilliseconds(1)
                },
                Timeout = TimeSpan.FromMilliseconds(100)
            };
            var httpClientResponse = new HttpClientResponse
            {
                HttpStatusCode = HttpStatusCode.NoContent,
                Response       = ""
            };
            var cacheRegion = "cacheRegion";

            var cache             = Substitute.For <ICache>();
            var httpClientWrapper = Substitute.For <IHttpClientWrapper>();
            var tokenService      = Substitute.For <ITokenService>();

            httpClientWrapper.PostAsync(serviceInfo.Endpoint, null, CancellationToken.None).ReturnsForAnyArgs(httpClientResponse);

            var sut = new CachedService(cache, httpClientWrapper, tokenService);

            ServiceResponse actual = await sut.CallService(serviceInfo, cacheRegion, CancellationToken.None, null);

            Assert.AreEqual(ServiceResponseStatus.Success, actual.Status);
        }
        public void AdditionalParametersTest()
        {
            var  cacheRegion                   = "cacheRegion";
            var  userIdentifier                = "User";
            Guid serviceParameterId            = Guid.NewGuid();
            Guid nonExistingServiceParameterId = Guid.NewGuid();
            var  serviceParameterKey           = "Key";
            var  serviceParameterValue         = "Value";
            var  serviceInfo                   = new CalledServiceInfo
            {
                AdditionalParameters = new[]
                {
                    new ParameterInfo
                    {
                        TokenId = serviceParameterId,
                        Name    = serviceParameterKey
                    },
                    new ParameterInfo
                    {
                        TokenId = nonExistingServiceParameterId,
                        Name    = serviceParameterKey
                    }
                }
            };

            var cache = Substitute.For <ICache>();

            cache.Get <string>(cacheRegion, Constants.UserIdentifierCacheKey).Returns(new CacheEntry <string> {
                Value = userIdentifier
            });
            cache.Get <string>(cacheRegion, serviceParameterId.ToString()).Returns(new CacheEntry <string> {
                Value = serviceParameterValue
            });

            var sut = new CalledServiceBaseInstance(cache);

            IEnumerable <KeyValuePair <string, string> > actual = sut.GetPostParameters(serviceInfo, cacheRegion, null);

            cache.Received(1).Get <string>(cacheRegion, Constants.UserIdentifierCacheKey);
            IEnumerable <KeyValuePair <string, string> > keyValuePairs = actual as KeyValuePair <string, string>[] ?? actual.ToArray();

            Assert.IsTrue(keyValuePairs.Any());
            Assert.AreEqual(2, keyValuePairs.Count());

            KeyValuePair <string, string> parameter = keyValuePairs.First();

            Assert.AreEqual(Constants.UserIdentifierPostKey, parameter.Key);
            Assert.AreEqual(userIdentifier, parameter.Value);

            parameter = keyValuePairs.Skip(1).First();

            Assert.AreEqual(serviceParameterKey, parameter.Key);
            Assert.AreEqual(serviceParameterValue, parameter.Value);
        }
        public async Task SemaphoreTest()
        {
            var cacheRegion = "cacheRegion";
            var serviceInfo = new CalledServiceInfo
            {
                Name               = "Service",
                Id                 = Guid.NewGuid(),
                Endpoint           = "Endpoint",
                CircuitBreakerInfo = new CircuitBreakerInfo
                {
                    ExceptionCount = 1,
                    BreakDuration  = TimeSpan.FromMilliseconds(1)
                },
                Timeout = TimeSpan.FromMilliseconds(100)
            };
            var httpClientResponse = new HttpClientResponse
            {
                HttpStatusCode = HttpStatusCode.NoContent,
                Response       = ""
            };

            var cache             = new CacheMock();
            var httpClientWrapper = Substitute.For <IHttpClientWrapper>();
            var tokenService      = Substitute.For <ITokenService>();

            httpClientWrapper.PostAsync(serviceInfo.Endpoint, null, CancellationToken.None).ReturnsForAnyArgs(async callInfo =>
            {
                await Task.Delay(20);
                return(httpClientResponse);
            });

            var sut = new CachedService(cache, httpClientWrapper, tokenService);

            Task <ServiceResponse> task1 = Task.Run(async() => await sut.CallService(serviceInfo, cacheRegion, CancellationToken.None, null));

            Task <ServiceResponse> task2 = Task.Run(async() => await sut.CallService(serviceInfo, cacheRegion, CancellationToken.None, null));

            await Task.WhenAll(task1, task2);

            await httpClientWrapper.ReceivedWithAnyArgs(1).PostAsync(serviceInfo.Endpoint, null, CancellationToken.None);
        }
        /// <summary>
        /// Calls the dynamic service and returns the result.
        /// </summary>
        /// <param name="serviceInfo">Information about the service to call</param>
        /// <param name="cacheRegion">The cache region to look for values for post parameters under in</param>
        /// <param name="cancellationToken">Cancellation token to cancel the request</param>
        /// <param name="additionalParameters">Additional post parameters to include in the request body</param>
        /// <returns>A service response representing the result of the call to the dynamic service</returns>
        protected override async Task <ServiceResponse> CallServiceInternal(CalledServiceInfo serviceInfo, string cacheRegion,
                                                                            CancellationToken cancellationToken, IEnumerable <KeyValuePair <string, string> > additionalParameters)
        {
            var serviceResponse = new ServiceResponse
            {
                ServiceId = serviceInfo.Id
            };

            try
            {
                AsyncPolicyWrap <ServiceResponse> breaker = GetCircuitBreakerPolicy(serviceInfo);

                return(await breaker.ExecuteAsync(async (cancelToken) =>
                {
                    IEnumerable <KeyValuePair <string, string> > postParameters = GetPostParameters(serviceInfo, cacheRegion, additionalParameters);

                    HttpClientResponse response = await _httpClientWrapper.PostAsync(serviceInfo.Endpoint, postParameters, cancelToken);


                    if (response.HttpStatusCode.IsOkStatus())
                    {
                        serviceResponse.Value = response.Response;
                        serviceResponse.Status = ServiceResponseStatus.Success;
                        serviceResponse.TokenResponses = _tokenService.ParseTokens(cacheRegion, response.Response, serviceInfo.Tokens).ToArray();
                    }
                    else
                    {
                        serviceResponse.Status = ServiceResponseStatus.Error;
                    }

                    return serviceResponse;
                }, cancellationToken : cancellationToken));
            }
            catch (Exception)
            {
                serviceResponse.Status = ServiceResponseStatus.Error;
                return(serviceResponse);
            }
        }
        /// <summary>
        /// Generates a list of KeyValuePairs to use as the post body for the request to a called service
        /// </summary>
        /// <param name="serviceInfo">Information about the service to generate parameters for</param>
        /// <param name="cacheRegion">The cache region to look for values for post parameters under in</param>
        /// <param name="additionalParameters">Additional post parameters to include in the generated list in addition to the ones defined on the service info</param>
        /// <returns>List of key value pairs to use as post parameters in the body of a request</returns>
        public IEnumerable <KeyValuePair <string, string> > GetPostParameters(CalledServiceInfo serviceInfo, string cacheRegion,
                                                                              IEnumerable <KeyValuePair <string, string> > additionalParameters)
        {
            List <KeyValuePair <string, string> > pairs = additionalParameters?.ToList() ?? new List <KeyValuePair <string, string> >();

            CacheEntry <string> userIdentifier = Cache.Get <string>(cacheRegion, Common.Constants.UserIdentifierCacheKey);

            if (userIdentifier != null)
            {
                pairs.Add(new KeyValuePair <string, string>(Common.Constants.UserIdentifierPostKey, userIdentifier.Value));
            }

            if (serviceInfo.AdditionalParameters == null)
            {
                return(pairs);
            }

            if (!serviceInfo.AdditionalParameters.Any())
            {
                return(pairs);
            }

            List <KeyValuePair <string, string> > additionalUserParameters = serviceInfo.AdditionalParameters
                                                                             .Select(additionalParameter => new
            {
                additionalParameter,
                parameterValue = Cache.Get <string>(cacheRegion, additionalParameter.TokenId.ToString())?.Value
            })
                                                                             .Where(additionalParameter => !string.IsNullOrEmpty(additionalParameter.parameterValue))
                                                                             .Select(additionalParameter => new KeyValuePair <string, string>(additionalParameter.additionalParameter.Name, additionalParameter.parameterValue))
                                                                             .ToList();

            pairs.AddRange(
                additionalUserParameters
                );

            return(pairs);
        }
        /// <summary>
        /// Generates the circuit breaker policy for a service.
        /// </summary>
        /// <param name="serviceInfo">The service to generate the circuit breaker policy for</param>
        /// <returns>The circuit policy</returns>
        protected AsyncPolicyWrap <ServiceResponse> GetCircuitBreakerPolicy(CalledServiceInfo serviceInfo)
        {
            return(CircuitBreakerPolicies.GetOrAdd(serviceInfo.CacheKey, _ =>
            {
                AsyncTimeoutPolicy timeoutPolicy = Policy
                                                   .TimeoutAsync(
                    serviceInfo.Timeout,
                    TimeoutStrategy.Pessimistic
                    );

                AsyncCircuitBreakerPolicy <ServiceResponse> circuitBreakerPolicy = Policy <ServiceResponse>
                                                                                   .Handle <TimeoutRejectedException>()
                                                                                   .OrResult(resultPredicate: serviceResponse => serviceResponse.Status == ServiceResponseStatus.Error)
                                                                                   .CircuitBreakerAsync(
                    handledEventsAllowedBeforeBreaking: serviceInfo.CircuitBreakerInfo.ExceptionCount,
                    durationOfBreak: serviceInfo.CircuitBreakerInfo.BreakDuration,
                    onBreak: (__, ___) =>
                {
                    Log.Warning("Service ({ServiceName}) has reached its threshold for the circuit breaker and the circuit has been opened", serviceInfo.Name);
                },
                    onReset: () =>
                {
                    Log.Warning("Service ({ServiceName}) has been determined to be back up, circuit closed again", serviceInfo.Name);
                }
                    );

                AsyncPolicyWrap <ServiceResponse> circuitBreakerWrappingTimeout = circuitBreakerPolicy
                                                                                  .WrapAsync(timeoutPolicy);

                AsyncFallbackPolicy <ServiceResponse> timeoutFallbackPolicy = Policy <ServiceResponse>
                                                                              .Handle <TimeoutRejectedException>()
                                                                              .FallbackAsync(
                    cancellationToken =>
                {
                    return Task.FromResult(new ServiceResponse
                    {
                        Status = ServiceResponseStatus.Timeout,
                        ServiceId = serviceInfo.Id
                    });
                });
                AsyncPolicyWrap <ServiceResponse> timeoutFallbackPolicyWrappingCircuitBreaker = timeoutFallbackPolicy
                                                                                                .WrapAsync(circuitBreakerWrappingTimeout);

                AsyncFallbackPolicy <ServiceResponse> exceptionFallbackPolicy = Policy <ServiceResponse>
                                                                                .Handle <Exception>()
                                                                                .FallbackAsync(
                    cancellationToken =>
                {
                    return Task.FromResult(new ServiceResponse
                    {
                        Status = ServiceResponseStatus.Error,
                        ServiceId = serviceInfo.Id
                    });
                });
                AsyncPolicyWrap <ServiceResponse> exceptionFallbackPolicyWrappingTimeoutFallback = exceptionFallbackPolicy
                                                                                                   .WrapAsync(timeoutFallbackPolicyWrappingCircuitBreaker);

                AsyncPolicyWrap <ServiceResponse> policy = exceptionFallbackPolicyWrappingTimeoutFallback;

                return policy;
            }));
        }
 protected abstract Task <ServiceResponse> CallServiceInternal(CalledServiceInfo serviceInfo, string cacheRegion,
                                                               CancellationToken cancellationToken,
                                                               IEnumerable <KeyValuePair <string, string> > additionalParameters);
 protected override Task <ServiceResponse> CallServiceInternal(CalledServiceInfo serviceInfo, string cacheRegion, CancellationToken cancellationToken, IEnumerable <KeyValuePair <string, string> > additionalParameters)
 {
     return(Task.FromResult(new ServiceResponse()));
 }
        /// <summary>
        /// This method performs the call to the cached service if no value is available in the cache.
        /// </summary>
        /// <param name="serviceInfo">Information about the service to call</param>
        /// <param name="cacheRegion">The cache region to look for an existing response and to look for values for post parameters under in</param>
        /// <param name="cancellationToken">Cancellation token to cancel the request</param>
        /// <param name="additionalParameters">Additional post parameters to include in the request body</param>
        /// <returns>A service response representing the result of the call to the cached service</returns>
        protected override async Task <ServiceResponse> CallServiceInternal(CalledServiceInfo serviceInfo, string cacheRegion, CancellationToken cancellationToken,
                                                                            IEnumerable <KeyValuePair <string, string> > additionalParameters)
        {
            SemaphoreSlim semaphore = _semaphores.GetOrAdd($"{cacheRegion}-{serviceInfo.CacheKey}", _ => new SemaphoreSlim(1, 1));

            try
            {
                await semaphore.WaitAsync(cancellationToken);

                if (cancellationToken.IsCancellationRequested)
                {
                    return(new ServiceResponse
                    {
                        ServiceId = serviceInfo.Id,
                        Status = ServiceResponseStatus.Timeout
                    });
                }

                CacheEntry <string> cacheResult = Cache.Get <string>(cacheRegion, serviceInfo.CacheKey);

                if (cacheResult != null)
                {
                    Log.Debug("Read value for service {ServiceName} from cache. Entry has value: {HasValue}",
                              serviceInfo.Name, cacheResult.Value != null);

                    ServiceResponse serviceResponse = new ServiceResponse
                    {
                        ServiceId = serviceInfo.Id,
                        Status    = ServiceResponseStatus.Success,
                        Value     = cacheResult.Value
                    };

                    if (string.IsNullOrEmpty(serviceResponse.Value))
                    {
                        return(serviceResponse);
                    }

                    serviceResponse.TokenResponses =
                        _tokenService.ParseTokens(cacheRegion, cacheResult.Value, serviceInfo.Tokens);

                    foreach (TokenResponse token in serviceResponse.TokenResponses)
                    {
                        Cache.Set(cacheRegion, token.CacheKey, token.Value);
                    }

                    return(serviceResponse);
                }

                AsyncPolicyWrap <ServiceResponse> breaker = GetCircuitBreakerPolicy(serviceInfo);

                return(await breaker.ExecuteAsync(async (cancelToken) =>
                {
                    IEnumerable <KeyValuePair <string, string> > postParameters =
                        GetPostParameters(serviceInfo, cacheRegion, additionalParameters);

                    HttpClientResponse response =
                        await _httpClientWrapper.PostAsync(serviceInfo.Endpoint, postParameters, cancelToken);
                    var serviceResponse = new ServiceResponse
                    {
                        ServiceId = serviceInfo.Id
                    };

                    if (response.HttpStatusCode.IsOkStatus())
                    {
                        serviceResponse.Value = response.Response;
                        serviceResponse.Status = ServiceResponseStatus.Success;
                        serviceResponse.TokenResponses =
                            _tokenService.ParseTokens(cacheRegion, response.Response, serviceInfo.Tokens);

                        foreach (TokenResponse token in serviceResponse.TokenResponses)
                        {
                            Cache.Set(cacheRegion, token.CacheKey, token.Value);
                        }

                        Cache.Set(cacheRegion, serviceInfo.CacheKey, response.Response);
                    }
                    else
                    {
                        Cache.Set <string>(cacheRegion, serviceInfo.CacheKey, null);
                        serviceResponse.Status = ServiceResponseStatus.Error;
                    }

                    return serviceResponse;
                }, cancellationToken : cancellationToken));
            }
            catch (TaskCanceledException)
            {
                return(new ServiceResponse
                {
                    ServiceId = serviceInfo.Id,
                    Status = ServiceResponseStatus.Timeout
                });
            }
            finally
            {
                if (semaphore.CurrentCount == 0)
                {
                    semaphore.Release();
                }
            }
        }