private static AsyncCachePolicy <JsonDocument> GetDefaultCachePolicy(IServiceProvider serviceProvider) => Policy.CacheAsync( serviceProvider .GetRequiredService <IAsyncCacheProvider>() .AsyncFor <JsonDocument>(), // TODO: get from config / from the target service TimeSpan.FromHours(24), (ctx, key, e) => Logger.Error(e, "JsonDocument cache error for key {Key}", key));
private CachePolicy <HttpResponseMessage> CreateCachePolicy(MemoryCacheProvider provider) { return(Policy .CacheAsync( provider.AsyncFor <HttpResponseMessage>(), new ResultTtl <HttpResponseMessage>(message => new Ttl(message.Headers.CacheControl.MaxAge ?? TimeSpan.FromHours(24))) )); }
public override IAsyncPolicy GetAsyncPolicy(CallContextBase context, CacheAttribute attribute) { var methodInfo = context.Method; var cacheKeyStrategy = GetCacheKeyStrategy(methodInfo, attribute); var ttlStrategy = GetTimeToLiveStrategy(methodInfo, attribute); return(Policy.CacheAsync(_asyncCacheProvider, ttlStrategy, cacheKeyStrategy, OnCacheGet, OnCacheMiss, OnCachePut, OnCacheGetError, OnCachePutError)); }
public void Should_throw_when_cache_key_strategy_is_null() { IAsyncCacheProvider cacheProvider = new StubCacheProvider(); Func <Context, string> cacheKeyStrategy = null; Action action = () => Policy.CacheAsync <ResultPrimitive>(cacheProvider, TimeSpan.MaxValue, cacheKeyStrategy); action.Should().Throw <ArgumentNullException>().And.ParamName.Should().Be("cacheKeyStrategy"); }
public void Should_throw_when_ttl_strategy_is_null() { IAsyncCacheProvider cacheProvider = new StubCacheProvider(); ITtlStrategy ttlStrategy = null; Action action = () => Policy.CacheAsync <ResultPrimitive>(cacheProvider, ttlStrategy); action.Should().Throw <ArgumentNullException>().And.ParamName.Should().Be("ttlStrategy"); }
public (IAsyncCacheProvider <TResult>, IAsyncPolicy <TResult>) CreateAsyncCachePolicy <TCache, TResult>() { IMemoryCache memoryCache = new MemoryCache(new MemoryCacheOptions()); IAsyncCacheProvider <TResult> provider = new MemoryCacheProvider(memoryCache).AsyncFor <TResult>(); var policy = Policy.CacheAsync <TResult>(provider, TimeSpan.FromHours(1)); return(provider, policy); }
/// <inheritdoc /> public CachingCallsWrapper(ICachingStrategy cachingStrategy, IRemovableAsyncCacheProvider asyncCacheProvider) { _cacheKeys = new ConcurrentDictionary <string, object>(); _asyncCacheProvider = asyncCacheProvider; _cachingStrategy = cachingStrategy; _retryPolicy = Policy .CacheAsync(_asyncCacheProvider, new ContextualTtl()); }
public void Should_throw_informative_exception_when_sync_execute_on_an_async_policy() { IAsyncCacheProvider cacheProvider = new StubCacheProvider(); var cachePolicy = Policy.CacheAsync(cacheProvider, TimeSpan.FromMinutes(5)); Action action = () => cachePolicy.Execute(() => 0); action.ShouldThrow <InvalidOperationException>(); }
public AuthenticationApiClientCachingDecorator(IAuthenticationApiClient inner) { _inner = inner; _accessTokenResponseCachePolicy = Policy.CacheAsync( _memoryCacheProvider.AsyncFor <AccessTokenResponse>(), new ResultTtl <AccessTokenResponse>(r => new Ttl(_expiresIn(r), false))); _userInfoCachePolicy = Policy.CacheAsync( _memoryCacheProvider.AsyncFor <UserInfo>(), new ContextualTtl()); }
private IAsyncPolicy GetContextualAsyncPolicy() { return(Policy.CacheAsync( memoryCacheProvider, new ContextualTtl(), new DefaultCacheKeyStrategy(), onCachePut: (context, key) => logger.Trace($"Caching '{key}'."), onCacheGet: (context, key) => logger.Trace($"Retrieving '{key}' from the cache."), onCachePutError: (context, key, exception) => logger.ErrorJustLogIt($"Cannot add '{key}' to the cache.", exception), onCacheGetError: (context, key, exception) => logger.ErrorJustLogIt($"Cannot retrieve '{key}' from the cache.", exception), onCacheMiss: (context, key) => logger.Trace($"Cache miss for '{key}'."))); }
public void Register() { if (!registry.ContainsKey(Name)) { var policy = Policy.CacheAsync <CacheFormat>(cacheProvider, TtlStrategy, this, OnCacheGet, OnCacheMiss, OnCachePut, OnCacheGetError, OnCachePutError); registry.Add(Name, policy); } else { logger.LogError("CacheManagement {cacheName} has already been registered", Name); } }
public void ConfigureServices(ConfigurationServices configurationServices) { var keyPrefix = Environment.GetEnvironmentVariable("APP_NAME") ?? configurationServices.Environment.ApplicationName; configurationServices.Services.AddSingleton(sp => new CacheSyncManager(sp.GetRequiredService <ILogger <CacheSyncManager> >())); configurationServices.Services.AddTransient <ICache>(sp => { var cache = sp.GetRequiredService <IDistributedCache>(); var cacheSyncManager = sp.GetRequiredService <CacheSyncManager>(); var policy = Policy.CacheAsync(cache.AsAsyncCacheProvider <byte[]>(), new ContextualTtl()); return(new Cache(cache, cacheSyncManager, policy, keyPrefix)); }); }
public override void Configure(IFunctionsHostBuilder builder) { var serializerSettings = new JsonSerializerSettings(); builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = Environment.GetEnvironmentVariable("REDIS_CACHE"); options.InstanceName = "Eaglesong"; }); builder.Services.AddSingleton <Polly.Caching.IAsyncCacheProvider <HttpResponseMessage> >(sp => sp.GetRequiredService <IDistributedCache>() .AsAsyncCacheProvider <string>() .WithSerializer <HttpResponseMessage, string>( new JsonSerializer <HttpResponseMessage>(serializerSettings) ) ); builder.Services.AddSingleton <Polly.Caching.IAsyncCacheProvider <Profile> >(sp => sp.GetRequiredService <IDistributedCache>() .AsAsyncCacheProvider <string>() .WithSerializer <Profile, string>( new JsonSerializer <Profile>(serializerSettings) ) ); builder.Services.AddSingleton <Polly.Caching.IAsyncCacheProvider <Details> >(sp => sp.GetRequiredService <IDistributedCache>() .AsAsyncCacheProvider <string>() .WithSerializer <Details, string>( new JsonSerializer <Details>(serializerSettings) ) ); builder.Services.AddSingleton <IReadOnlyPolicyRegistry <string>, PolicyRegistry>((sp) => { var registry = new PolicyRegistry(); { var provider = sp.GetRequiredService <IAsyncCacheProvider <HttpResponseMessage> >(); var policy = Policy.CacheAsync(provider, TimeSpan.FromMinutes(60)); registry.Add("WindrunaPolicy", policy); } return(registry); }); builder.Services.AddHttpClient("Windrun", client => { client.BaseAddress = new Uri("https://windrun.io/api/"); }) .AddPolicyHandlerFromRegistry("WindrunaPolicy"); builder.Services.AddSingleton <IPlayerService, PlayerService>(); }
public async Task Should_execute_delegate_but_not_put_value_in_cache_if_cache_does_not_hold_value_but_ttl_indicates_not_worth_caching() { const string valueToReturn = "valueToReturn"; const string operationKey = "SomeOperationKey"; IAsyncCacheProvider stubCacheProvider = new StubCacheProvider(); CachePolicy cache = Policy.CacheAsync(stubCacheProvider, TimeSpan.Zero); ((string)await stubCacheProvider.GetAsync(operationKey, CancellationToken.None, false).ConfigureAwait(false)).Should().Be(null); (await cache.ExecuteAsync(async ctx => { await TaskHelper.EmptyTask.ConfigureAwait(false); return(valueToReturn); }, new Context(operationKey)).ConfigureAwait(false)).Should().Be(valueToReturn); ((string)await stubCacheProvider.GetAsync(operationKey, CancellationToken.None, false).ConfigureAwait(false)).Should().Be(null); }
public async Task Should_execute_delegate_and_put_value_in_cache_if_cache_does_not_hold_value() { const string valueToReturn = "valueToReturn"; const string executionKey = "SomeExecutionKey"; IAsyncCacheProvider stubCacheProvider = new StubCacheProvider(); CachePolicy cache = Policy.CacheAsync(stubCacheProvider, TimeSpan.MaxValue); ((string)await stubCacheProvider.GetAsync(executionKey, CancellationToken.None, false).ConfigureAwait(false)).Should().BeNull(); (await cache.ExecuteAsync(async() => { await TaskHelper.EmptyTask.ConfigureAwait(false); return(valueToReturn); }, new Context(executionKey)).ConfigureAwait(false)).Should().Be(valueToReturn); ((string)await stubCacheProvider.GetAsync(executionKey, CancellationToken.None, false).ConfigureAwait(false)).Should().Be(valueToReturn); }
public async Task Should_execute_delegate_and_put_value_in_cache_but_when_it_expires_execute_delegate_again() { const string valueToReturn = "valueToReturn"; const string operationKey = "SomeOperationKey"; IAsyncCacheProvider stubCacheProvider = new StubCacheProvider(); TimeSpan ttl = TimeSpan.FromMinutes(30); var cache = Policy.CacheAsync <string>(stubCacheProvider, ttl); (bool cacheHit1, object fromCache1) = await stubCacheProvider.TryGetAsync(operationKey, CancellationToken.None, false).ConfigureAwait(false); cacheHit1.Should().BeFalse(); fromCache1.Should().BeNull(); int delegateInvocations = 0; Func <Context, Task <string> > func = async ctx => { delegateInvocations++; await TaskHelper.EmptyTask.ConfigureAwait(false); return(valueToReturn); }; DateTimeOffset fixedTime = SystemClock.DateTimeOffsetUtcNow(); SystemClock.DateTimeOffsetUtcNow = () => fixedTime; // First execution should execute delegate and put result in the cache. (await cache.ExecuteAsync(func, new Context(operationKey)).ConfigureAwait(false)).Should().Be(valueToReturn); delegateInvocations.Should().Be(1); (bool cacheHit2, object fromCache2) = await stubCacheProvider.TryGetAsync(operationKey, CancellationToken.None, false).ConfigureAwait(false); cacheHit2.Should().BeTrue(); fromCache2.Should().Be(valueToReturn); // Second execution (before cache expires) should get it from the cache - no further delegate execution. // (Manipulate time so just prior cache expiry). SystemClock.DateTimeOffsetUtcNow = () => fixedTime.Add(ttl).AddSeconds(-1); (await cache.ExecuteAsync(func, new Context(operationKey)).ConfigureAwait(false)).Should().Be(valueToReturn); delegateInvocations.Should().Be(1); // Manipulate time to force cache expiry. SystemClock.DateTimeOffsetUtcNow = () => fixedTime.Add(ttl).AddSeconds(1); // Third execution (cache expired) should not get it from the cache - should cause further delegate execution. (await cache.ExecuteAsync(func, new Context(operationKey)).ConfigureAwait(false)).Should().Be(valueToReturn); delegateInvocations.Should().Be(2); }
/// <summary> /// Generates a Polly <see cref="CachePolicy{HttpResponseMessage}"/> from the configuration. /// </summary> /// <param name="logger">The <see cref="ILogger"/> instance to use for logging.</param> /// <returns>A <see cref="CachePolicy{HttpResponseMessage}"/> instance.</returns> /// <remarks> /// Currently only supports in-memory cache. /// Operation key set on the context is used as the cache key. /// </remarks> public IAsyncPolicy <HttpResponseMessage> AsTypeModel(ILogger logger) { _ = logger ?? throw new ArgumentNullException(nameof(logger)); if (Time is null || TimeSpan.Equals(Time.AsTimeSpan(), TimeSpan.Zero)) { logger.LogCritical("{PolicyConfig} : {Property} must be a valid time span", nameof(CacheConfig), "time"); throw new InvalidOperationException("time must be a valid time span"); } // Create delegates void OnCacheGet(Context context, string key) => logger.LogInformation("{PolicyKey} at {OperationKey}: Retrieving {Key} from cache", context.PolicyKey, context.OperationKey, key); void OnCacheMiss(Context context, string key) => logger.LogInformation("{PolicyKey} at {OperationKey}: {Key} was not present in cache", context.PolicyKey, context.OperationKey, key); void OnCachePut(Context context, string key) => logger.LogInformation("{PolicyKey} at {OperationKey}: Inserting {Key} into cache", context.PolicyKey, context.OperationKey, key); void OnCacheGetError(Context context, string key, Exception exception) => logger.LogError(exception, "{PolicyKey} at {OperationKey}: Error retrieving {Key} from cache", context.PolicyKey, context.OperationKey, key); void OnCachePutError(Context context, string key, Exception exception) => logger.LogError(exception, "{PolicyKey} at {OperationKey}: Error inserting {Key} into cache", context.PolicyKey, context.OperationKey, key); if (!Absolute) { // Cache strategy if it is purely time span based strategy = CreateStrategy(); } // Create policy with default cache key strategy var cache = Policy .CacheAsync(cacheProvider, ttlStrategy: new ResultTtl <HttpResponseMessage>(CacheOKResponse), onCacheGet: OnCacheGet, onCacheMiss: OnCacheMiss, onCachePut: OnCachePut, onCacheGetError: OnCacheGetError, onCachePutError: OnCachePutError); return(cache); }
public void Should_always_execute_delegate_if_execution_is_void_returning() { string executionKey = Guid.NewGuid().ToString(); CachePolicy cache = Policy.CacheAsync(new StubCacheProvider(), TimeSpan.MaxValue); int delegateInvocations = 0; Func <Task> action = async() => { delegateInvocations++; await TaskHelper.EmptyTask.ConfigureAwait(false); }; cache.ExecuteAsync(action, new Context(executionKey)); delegateInvocations.Should().Be(1); cache.ExecuteAsync(action, new Context(executionKey)); delegateInvocations.Should().Be(2); }
public void CacheHit() { var cache = new Mock <IDistributedCache>(MockBehavior.Strict); var cacheProvider = new Mock <IAsyncCacheProvider>(MockBehavior.Strict); var cachePolicy = Policy.CacheAsync(cacheProvider.Object, new Mock <ITtlStrategy>().Object); var repository = new Mock <ISecretRepository>(MockBehavior.Strict); var service = new SecretRepositoryCache(repository.Object, cachePolicy); // NB Have to use the underlying methods not the helpful extensions cache.Setup(x => x.Get("clientsecret:A")).Returns(Encoding.UTF8.GetBytes("B")); var candidate = service.ClientSecret("A"); Assert.That(candidate, Is.EqualTo("B"), "Secret differs"); }
public static void GetPolicyRegistry(IAsyncCacheProvider cacheProvider, IPolicyRegistry <string> registry) { registry.Add("thriceTriplingRetryPolicy", Policy.HandleResult <HttpResponseMessage>(r => !r.IsSuccessStatusCode) .Or <TimeoutRejectedException>() .WaitAndRetryAsync(thriceTriplingTimeSpans)); registry.Add("loginResponseRetryPolicy", Policy.HandleResult <LoginResponse>(lr => lr.LoginStatus != _successfulLoginStatus) .Or <TimeoutRejectedException>() .WaitAndRetryAsync(thriceTriplingTimeSpans)); registry.Add("thirtySecondTimeoutPolicy", Policy.TimeoutAsync(TimeSpan.FromSeconds(30))); AsyncCachePolicy <LoginResponse> cachePolicy = Policy.CacheAsync <LoginResponse>(cacheProvider, _timeToLive); registry.Add("oneMinuteLoginCachePolicy", cachePolicy); }
internal AccessTokenService(string clientId, string clientSecret, IHttpRequestSender requestSender) { _clientId = clientId; _clientSecret = clientSecret; _requestSender = requestSender; _cache = new MemoryCache(new MemoryCacheOptions()); _cachePolicy = Policy.CacheAsync( new MemoryCacheProvider(_cache).AsyncFor <AccessToken>(), new ResultTtl <AccessToken>(t => new Ttl(t.LifeTime - TimeSpan.FromHours(1))), onCacheError: (ctx, _, ex) => { Logger.ErrorException($"Could not retrieve access token: '{ex.Message}'", ex); }); }
public void Should_always_execute_delegate_if_execution_is_void_returning() { string operationKey = "SomeKey"; var cache = Policy.CacheAsync(new StubCacheProvider(), TimeSpan.MaxValue); int delegateInvocations = 0; Func <Context, Task> action = async _ => { delegateInvocations++; await TaskHelper.EmptyTask; }; cache.ExecuteAsync(action, new Context(operationKey)); delegateInvocations.Should().Be(1); cache.ExecuteAsync(action, new Context(operationKey)); delegateInvocations.Should().Be(2); }
public async Task CanCacheApiResult() { var memoryCache = new MemoryCache(new MemoryCacheOptions()); var memoryCacheProvider = new MemoryCacheProvider(memoryCache); var cachePolicy = Policy.CacheAsync(memoryCacheProvider, TimeSpan.FromMinutes(5)); // typically cache key would be dynamically generated to be unique per unique request (same for equivalent requests) var result = await cachePolicy.ExecuteAsync((ctx) => DemoHelper.DemoClient.GetStringAsync("api/demo/cache"), new Context("MyKey")); var result2 = await cachePolicy.ExecuteAsync((ctx) => DemoHelper.DemoClient.GetStringAsync("api/demo/cache"), new Context("MyKey")); var result3 = await cachePolicy.ExecuteAsync((ctx) => DemoHelper.DemoClient.GetStringAsync("api/demo/cache"), new Context("MyKey2")); Assert.AreEqual(result, result2); Assert.AreNotEqual(result, result3); }
//[Test] public async Task Foo() { var count1 = 0; var count2 = 0; var count3 = 0; var services = new ServiceCollection(); services.AddMemoryCache() .AddSingleton <Caching.IAsyncCacheProvider, MemoryCacheProvider>() .AddSingleton <Func <int, Task <Dto> > >(_ => { count1++; return(Task.FromResult(new Dto())); }) .AddSingleton <Func <Task <Dto> > >(() => { count2++; return(Task.FromResult(new Dto())); }) .AddSingleton <Func <int, Task <string> > >(_ => { count3++; return(Task.FromResult(Guid.NewGuid().ToString())); }) .AddSingleton(typeof(IService <,>), typeof(DelegatingService <,>)); // .Decorate<>(); var provider = services.BuildServiceProvider(); var policy = Policy.CacheAsync(provider.GetService <Caching.IAsyncCacheProvider>(), TimeSpan.MaxValue); var service = provider.GetRequiredService <IService <int, Dto> >(); service = service.InterceptWithPolicy(policy); // Act var result11 = await service.GetAsync(-1); var result12 = await service.GetAsync(-1); // Assert Assert.AreEqual(1, count1); Assert.AreEqual(result11, result12); Assert.DoesNotThrow(() => Guid.Parse(result11.Str)); }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, IMemoryCache memoryCache) { Polly.Caching.MemoryCache.MemoryCacheProvider memoryCacheProvider = new Polly.Caching.MemoryCache.MemoryCacheProvider(memoryCache); CachePolicy <HttpResponseMessage> cachePolicy = Policy.CacheAsync <HttpResponseMessage>(memoryCacheProvider, TimeSpan.FromMinutes(5)); _myPolicyRegistry.Add("myPollyCache", cachePolicy); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseMvc(); }
public void CacheMiss() { var cache = new Mock <IDistributedCache>(MockBehavior.Strict); var cacheProvider = new Mock <IAsyncCacheProvider>(MockBehavior.Strict); var cachePolicy = Policy.CacheAsync(cacheProvider.Object, new Mock <ITtlStrategy>().Object); var repository = new Mock <ISecretRepository>(MockBehavior.Strict); var service = new SecretRepositoryCache(repository.Object, cachePolicy); // NB Have to use the underlying methods not the helpful extensions cache.Setup(x => x.Get("clientsecret:A")).Returns((byte[])null); cache.Setup(x => x.Set("clientsecret:A", It.IsAny <byte[]>(), It.IsAny <DistributedCacheEntryOptions>())); repository.Setup(x => x.ClientSecret("A")).Returns("B"); var candidate = service.ClientSecret("A"); Assert.That(candidate, Is.EqualTo("B"), "Secret differs"); }
public async Task Should_execute_delegate_and_put_value_in_cache_for_non_nullable_types_if_cache_does_not_hold_value() { const ResultPrimitive valueToReturn = ResultPrimitive.Substitute; const string operationKey = "SomeOperationKey"; IAsyncCacheProvider stubCacheProvider = new StubCacheProvider(); CachePolicy cache = Policy.CacheAsync(stubCacheProvider, TimeSpan.MaxValue); (await stubCacheProvider.GetAsync(operationKey, CancellationToken.None, false)).Should().BeNull(); (await cache.ExecuteAsync(async ctx => { await TaskHelper.EmptyTask.ConfigureAwait(false); return(ResultPrimitive.Substitute); }, new Context(operationKey))).Should().Be(valueToReturn); (await stubCacheProvider.GetAsync(operationKey, CancellationToken.None, false)).Should().Be(valueToReturn); }
public static AsyncPolicyWrap <HttpResponseMessage> GetRequestPolicy(IMemoryCache memoryCache = null, int cacheSeconds = 0, int additionalRetries = 0, int requestTimeoutSeconds = 100) { AsyncCachePolicy cache = null; if (memoryCache != null) { var memoryCacheProvider = new MemoryCacheProvider(memoryCache); cache = Policy.CacheAsync(memoryCacheProvider, TimeSpan.FromSeconds(cacheSeconds)); } int[] httpStatusCodesWorthRetrying = { StatusCodes.Status408RequestTimeout, StatusCodes.Status429TooManyRequests, //StatusCodes.Status500InternalServerError, StatusCodes.Status502BadGateway, StatusCodes.Status503ServiceUnavailable, StatusCodes.Status504GatewayTimeout }; var waitAndRetryPolicy = Policy .Handle <HttpRequestException>() //HttpClient Timeout or CancellationToken .Or <TimeoutRejectedException>() .OrResult <HttpResponseMessage>(r => httpStatusCodesWorthRetrying.Contains((int)r.StatusCode)) .WaitAndRetryAsync(additionalRetries, retryAttempt => TimeSpan.FromSeconds(1)); //https://github.com/App-vNext/Polly/wiki/Timeout var requestTimeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(requestTimeoutSeconds)); //https://github.com/App-vNext/Polly/wiki/PolicyWrap AsyncPolicyWrap <HttpResponseMessage> policyWrap = null; if (cache != null) { policyWrap = cache.WrapAsync(waitAndRetryPolicy).WrapAsync(requestTimeout); } else { policyWrap = waitAndRetryPolicy.WrapAsync(requestTimeout); } return(policyWrap); }
public NewCombinedFeedSource(IEnumerable <IAmACommunityMember> tamarins) { EnsureHttpClient(); Tamarins = tamarins; if (retryPolicy == null) { // cache in memory for an hour var memoryCache = new MemoryCache(new MemoryCacheOptions()); var memoryCacheProvider = new MemoryCacheProvider(memoryCache); cachePolicy = Policy.CacheAsync(memoryCacheProvider, TimeSpan.FromHours(1), OnCacheError); // retry policy with max 2 retries, delay by x*x^1.2 where x is retry attempt // this will ensure we don't retry too quickly retryPolicy = Policy.Handle <FeedReadFailedException>() .WaitAndRetryAsync(2, retry => TimeSpan.FromSeconds(retry * Math.Pow(1.2, retry))); } }
public async Task Should_not_error_for_executions_on_non_nullable_types_if_cache_does_not_hold_value() { const string operationKey = "SomeOperationKey"; bool onErrorCalled = false; Action <Context, string, Exception> onError = (ctx, key, exc) => { onErrorCalled = true; }; IAsyncCacheProvider stubCacheProvider = new StubCacheProvider(); CachePolicy cache = Policy.CacheAsync(stubCacheProvider, TimeSpan.MaxValue, onError); (await stubCacheProvider.GetAsync(operationKey, CancellationToken.None, false)).Should().BeNull(); ResultPrimitive result = await cache.ExecuteAsync(async ctx => { await TaskHelper.EmptyTask.ConfigureAwait(false); return(ResultPrimitive.Substitute); }, new Context(operationKey)); onErrorCalled.Should().BeFalse(); }