/// <summary> /// Records/playbacks the <see cref="HttpResponseMessage" /> to specified <see cref="HttpRequestMessage" />. /// </summary> /// <param name="request">The request.</param> /// <param name="getResponseAsync">The function to call if a recording is needed.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <param name="callerFilePath">The caller file path; set automatically.</param> /// <param name="callerMemberName">Name of the caller member; set automatically.</param> /// <param name="callerLineNumber">The caller line number; set automatically.</param> /// <returns> /// The <see cref="HttpResponseMessage" />. /// </returns> /// <exception cref="ArgumentNullException">request /// or /// request</exception> /// <exception cref="ArgumentOutOfRangeException">mode - null</exception> public Task <HttpResponseMessage> RecordAsync( HttpRequestMessage request, GetResponseAsync getResponseAsync, CancellationToken cancellationToken, [CallerFilePath] string callerFilePath = "", [CallerMemberName] string callerMemberName = "", [CallerLineNumber] int callerLineNumber = 0) => RecordAsync(request, getResponseAsync, null, cancellationToken, callerFilePath, callerMemberName, callerLineNumber);
#pragma warning restore DF0000 // Marks undisposed anonymous objects from object creations. #pragma warning restore DF0001 // Marks undisposed anonymous objects from method invocations. // ReSharper restore ExplicitCallerInfoArgument /// <summary> /// Records/playbacks the <see cref="HttpResponseMessage" /> to specified <see cref="HttpRequestMessage" />. /// </summary> /// <param name="request">The request.</param> /// <param name="getResponseAsync">The function to call if a recording is needed.</param> /// <param name="options">The options.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <param name="callerFilePath">The caller file path; set automatically.</param> /// <param name="callerMemberName">Name of the caller member; set automatically.</param> /// <param name="callerLineNumber">The caller line number; set automatically.</param> /// <returns> /// The <see cref="HttpResponseMessage" />. /// </returns> /// <exception cref="ArgumentNullException">request /// or /// request</exception> /// <exception cref="ArgumentOutOfRangeException">mode - null</exception> public async Task <HttpResponseMessage> RecordAsync( HttpRequestMessage request, GetResponseAsync getResponseAsync, CassetteOptions options = null, CancellationToken cancellationToken = default(CancellationToken), [CallerFilePath] string callerFilePath = "", [CallerMemberName] string callerMemberName = "", [CallerLineNumber] int callerLineNumber = 0) { if (request is null) { throw new ArgumentNullException(nameof(request)); } if (getResponseAsync is null) { throw new ArgumentNullException(nameof(request)); } // Overwrite defaults with options options = DefaultOptions & options; // ReSharper disable once PossibleInvalidOperationException RecordMode mode = options.Mode.Value; // If we're in 'none' mode skip recording. if (mode == RecordMode.None) { return(await getResponseAsync(request, cancellationToken).ConfigureAwait(false)); } // Ensure request has been completed before serialization attempts if (!(request.Content is null)) { await request.Content.LoadIntoBufferAsync().ConfigureAwait(false); } // Get key data. byte[] key = await KeyGenerator.Generate(request, cancellationToken); // Get key data hash. string hash = key.GetKeyHash(); /* * Lock based on hash - so only one operation is allowed for the same hash at the same time. */ IDisposable @lock = await _keyedSemaphoreSlim.WaitAsync(hash, cancellationToken); try { // Try to get recording from the store. Recording recording; bool found = false; HttpResponseMessage response = null; byte[] recordingData = null; /* * Unless we're in overwrite mode, try to get a response from the store. */ if (mode != RecordMode.Overwrite) { // Logs an error // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Local void Error(string message, Exception exception = null, bool @throw = false) { CassetteException cassetteException = new CassetteException( message, Store.Name, callerFilePath, callerMemberName, callerLineNumber, exception); if (@throw) { Logger.LogCritical(cassetteException); throw cassetteException; } Logger.LogError(cassetteException); recording = null; found = false; } try { recordingData = await Store.GetAsync(hash, cancellationToken); } catch (Exception e) { Error("The underlying store threw an exception when attempting to retrieve a recording.", e); } // If we got a response and it has more than 0 bytes consider it fond! if (!(recordingData is null) && recordingData.Length > 0) { found = true; // If we're in recording mode don't bother to deserialize it as we're not going to use it if (mode != RecordMode.Record) { // Deserialize recording try { recording = MessagePackSerializer.Deserialize <Recording>(recordingData, RecorderResolver.Instance); } catch (Exception e) { Error("The recording could not be deserialized.", e); } // Validate key if (found && !string.Equals(hash, recording.Hash)) { Error("The recording's hash did not match, ignoring."); } if (found && !string.Equals(KeyGenerator.Name, recording.KeyGeneratorName)) { Error("The recording's key generator name did not match, ignoring."); } /* * If we're in playback or auto mode we need to replay response */ if (found && (mode == RecordMode.Playback || mode == RecordMode.Auto)) { if (recording.ResponseData is null) { Error("No response data found in recording, ignoring."); } // Deserialize response if (found) { try { response = MessagePackSerializer.Deserialize <HttpResponseMessage>( recording.ResponseData, RecorderResolver.Instance); } catch (Exception e) { Error("Failed to deserialize the response from the store, ignoring.", e); } } // ReSharper disable once PossibleInvalidOperationException RequestPlaybackMode requestPlaybackMode = options.RequestPlaybackMode.Value; if (found) { if (recording.RequestData is null || requestPlaybackMode == RequestPlaybackMode.IgnoreRecorded) { if (requestPlaybackMode == RequestPlaybackMode.UseRecorded) { Error( "No request found in the recording, and in RequestPlaybackMode UseRecorded.", null, true); } // ReSharper disable once PossibleNullReferenceException response.RequestMessage = request; } else { // Deserialize request try { #pragma warning disable DF0023 // Marks undisposed objects assinged to a property, originated from a method invocation. // ReSharper disable once PossibleNullReferenceException response.RequestMessage = MessagePackSerializer.Deserialize <HttpRequestMessage>( recording.RequestData, RecorderResolver.Instance); #pragma warning restore DF0023 // Marks undisposed objects assinged to a property, originated from a method invocation. } catch (Exception e) { if (requestPlaybackMode == RequestPlaybackMode.UseRecorded) { Error( "Failed to deserialize the request from the store, and in RequestPlaybackMode UseRecorded.", e, true); } else { Error("Failed to deserialize the request from the store, ignoring.", e); } } } } // ReSharper disable once PossibleInvalidOperationException if (found) { TimeSpan simulateDelay = options.SimulateDelay.Value; if (simulateDelay != default(TimeSpan)) { int delay = simulateDelay < TimeSpan.Zero ? recording.DurationMs : (int)simulateDelay.TotalMilliseconds; Logger.LogInformation( $"Responding with matching recording from '{recording.RecordedUtc.ToLocalTime()}' after {delay}ms simulated delay.", Store.Name, callerFilePath, callerMemberName, callerLineNumber); await Task.Delay(delay, cancellationToken); } else { Logger.LogInformation( $"Responding with matching recording from '{recording.RecordedUtc.ToLocalTime()}'.", Store.Name, callerFilePath, callerMemberName, callerLineNumber); } return(response); } } } } } // If we're in playback mode we've failed to get a recording so error if (mode == RecordMode.Playback) { // Recording not found so error in playback mode. CassetteNotFoundException exception = new CassetteNotFoundException( Store.Name, callerFilePath, callerMemberName, callerLineNumber); Logger.LogError(exception); throw exception; } /* * Record original request to detect changes if options set to RequestRecordMode.RecordIfChanged */ byte[] requestData; // ReSharper disable once PossibleInvalidOperationException RequestRecordMode requestRecordMode = options.RequestRecordMode.Value; if (!found && requestRecordMode == RequestRecordMode.RecordIfChanged) { // If the key was generated with the FullRequestKeyGenerator.Instance then the request is already serialized. if (ReferenceEquals(KeyGenerator, FullRequestKeyGenerator.Instance)) { requestData = key; } else { try { requestData = MessagePackSerializer.Serialize(request, RecorderResolver.Instance); } catch (Exception e) { CassetteException ce = new CassetteException( "Failed to serialize the request.", Store.Name, callerFilePath, callerMemberName, callerLineNumber, e); Logger.LogCritical(ce); throw ce; } } } else { requestData = null; } /* * Retrieve response from endpoint. */ int durationMs; DateTime recordedUtc; try { // Use stopwatch to record how long it takes to get a response. Stopwatch stopwatch = Stopwatch.StartNew(); #pragma warning disable DF0010 // Marks undisposed local variables. response = await getResponseAsync(request, cancellationToken).ConfigureAwait(false); #pragma warning restore DF0010 // Marks undisposed local variables. durationMs = (int)stopwatch.ElapsedMilliseconds; recordedUtc = DateTime.UtcNow; } catch (Exception e) { // TODO We could save the exception an repeat on playback, useful for testing handlers // Unfortunately MessagePack-CSharp doesn't support exception serialization normally so would need to be // handled in a custom way. CassetteException re = new CassetteException("Fatal error occured retrieving the response.", Store.Name, callerFilePath, callerMemberName, callerLineNumber, e); Logger.LogError(re); throw re; } // If we have a recording, don't overwrite just return the new response here. if (found) { Logger.LogInformation( "Existing recording found so not overwriting it.", Store.Name, callerFilePath, callerMemberName, callerLineNumber); return(response); } // Serialize response byte[] responseData; try { responseData = MessagePackSerializer.Serialize(response, RecorderResolver.Instance); } catch (Exception e) { CassetteException re = new CassetteException( "Failed to serialize response, not storing.", Store.Name, callerFilePath, callerMemberName, callerLineNumber, e); Logger.LogError(re); return(response); } if (requestRecordMode != RequestRecordMode.Ignore) { byte[] oldRequestData = requestData; // Serialize the request try { requestData = MessagePackSerializer.Serialize(response.RequestMessage, RecorderResolver.Instance); // If we're only recording requests on change, check for changes if (requestRecordMode == RequestRecordMode.RecordIfChanged && // ReSharper disable once AssignNullToNotNullAttribute requestData.SequenceEqual(oldRequestData)) { requestData = null; } } catch (Exception e) { CassetteException re = new CassetteException( "Failed to serialize response's request message, so ignoring check.", Store.Name, callerFilePath, callerMemberName, callerLineNumber, e); Logger.LogError(re); requestData = null; } } // Create new recording recording = new Recording( hash, KeyGenerator.Name, recordedUtc, durationMs, responseData, requestData); // Finally serialize the recording try { recordingData = MessagePackSerializer.Serialize(recording, RecorderResolver.Instance); } catch (Exception e) { CassetteException re = new CassetteException( "Failed to serialize recording, not storing.", Store.Name, callerFilePath, callerMemberName, callerLineNumber, e); Logger.LogError(re); return(response); } // Set the response Logger.LogInformation( $"Recording response at '{recordedUtc.ToLocalTime()}' (took {durationMs} ms).", Store.Name, callerFilePath, callerMemberName, callerLineNumber); if (options.WaitForSave == true) { try { await Store.StoreAsync(hash, recordingData); } catch (Exception e) { // Just log the error. CassetteException re = new CassetteException( "Failed to store recording.", Store.Name, callerFilePath, callerMemberName, callerLineNumber, e); Logger.LogError(re); } // We can now dispose the lock safely. @lock.Dispose(); } else { // Store the recording asynchronously, and don't wait the result (errors will be logged and suppressed). StoreAsync(hash, recordingData, @lock, callerFilePath, callerMemberName, callerLineNumber); } // Return the response. return(response); }