public void EnsureStorageContextEquivalenceViaCreateSafe(
            string contextStringIn, bool?setMute, sc contextObjectIn,
            string contextStringOut, sc contextObjectOut)
        {
            sc sc1 = null;

            if (contextStringIn != null)
            {
                try
                {
                    sc1 = sc.CreateSafe(contextStringIn, setMute);
                }
                catch (Exception e)
                {
                    e.ShouldBeNull($"CreateSafe shouldn't get an exception, got {e.Message}");
                }

                if (contextStringOut != null)
                {
                    // confirm result via string.
                    var sc1Str = sc1.ClientRequestID;
                    contextStringOut.ShouldNotBeNullOrWhiteSpace();
                    sc1Str.ShouldNotBeNullOrWhiteSpace();
                    sc1Str.ShouldBe(contextStringOut);
                }

                if (contextObjectOut != null)
                {
                    // confirm result via object.
                    AreEquivalent(contextObjectOut, sc1, out string reason).ShouldBeTrue(reason);
                }
            }

            if (contextObjectIn != null)
            {
                sc1 = contextObjectIn;

                if (contextStringOut != null)
                {
                    // confirm result via string.
                    var sc1Str = sc1.ClientRequestID;
                    contextStringOut.ShouldNotBeNull();
                    contextStringOut.ShouldBe(sc1Str);
                }

                if (contextObjectOut != null)
                {
                    // confirm result via object.
                    contextObjectOut.ShouldBeEquivalentTo(sc1);
                    AreEquivalent(contextObjectOut, sc1, out string failedBecause).ShouldBeTrue(failedBecause);
                }
            }
        }
        /// <summary>
        /// True if the instance is one of the "read-only" special cases - None or NoneMuted.
        /// </summary>
        /// <remarks>
        /// This is used to fail property sets so as not to "corrupt" what are supposed to
        /// be immutable instances.  If other such instances are added in future, be sure to
        /// add them here.
        /// </remarks>
        private static bool IsReadOnlyInstance(StorageClientProviderContext scpc)
        {
            // Special case, don't let muting be reset for either of the static instances.
            //
            // Note: the null checks are included because the constructor uses the
            // IsMuted property which uses this method.  During static construction of
            // these two special instances, the values under construction will not yet
            // have been assigned, so those constructors using IsMuted will work as
            // hoped.
            var result = ((None != null) && object.ReferenceEquals(scpc, None)) ||
                         ((NoneMuted != null) && object.ReferenceEquals(scpc, NoneMuted));

            return(result);
        }
        /// <summary>True if two StorageContexts have the same value.</summary>
        private static bool AreEquivalent(sc lhs, sc rhs, out string reason)
        {
            reason = string.Empty;
            if (object.ReferenceEquals(lhs, rhs))
            {
                return(true);
            }

            if (lhs.ClientRequestID != rhs.ClientRequestID)
            {
                reason = $"ClientRequestID: \"{lhs.ClientRequestID}\" != \"{rhs.ClientRequestID}\"";
                return(false);
            }

            var lhsObj = lhs.ClientRequestIdAsJObject;
            var rhsObj = rhs.ClientRequestIdAsJObject;

            if (lhs == null || rhs == null)
            {
                reason = "ClientRequestIdAsJObject: one is null";
                return(false);
            }

            if (!JToken.DeepEquals(lhsObj, rhsObj))
            {
                reason = "DeepEquals failed";
                return(false);
            }

            if (lhs.IsMuted != rhs.IsMuted)
            {
                reason = $"IsMuted: {lhs.IsMuted} != {rhs.IsMuted}";
                return(false);
            }

            if (lhs.TrackingETag != lhs.TrackingETag)
            {
                reason = $"TrackingETag: {lhs.TrackingETag} != {rhs.TrackingETag}";
                return(false);
            }

            if (lhs.ETag != rhs.ETag)
            {
                reason = $"ETag: {lhs.ETag} != {rhs.ETag}";
                return(false);
            }

            return(true);
        }
        /// <summary>
        /// Reset the fields of the current instance to those in the argument context.
        /// </summary>
        /// <remarks>
        /// At first glance, this method may appear unneeded.  It is here to avoid the
        /// temptation for a user to try directly assigning context instances.
        ///
        /// That would not go well due to the way that the HTTP pipeline policy tied to the
        /// SDK client instance must have a predictable place from which to obtain desired
        /// header values.  Thus, the values must be updated within this instance. Simply
        /// "pointing" to a different StorageClientProviderContext instance wouldn't work.
        ///
        /// See Gridwich.SagaParticipants.Storage.AzureStorage/BlobClientPipelinePolicy.cs for
        /// more information on the HTTP pipeline policy used by Blob and Container SDK clients.
        ///
        /// Note: using this method to try to create equivalents of None or NoneMuted will
        /// not work exactly as one might expect.  While the result
        /// </remarks>
        /// <param name="ctx">The source context.</param>
        /// <exception cref="System.ArgumentNullException">if source instance is null.</exception>
        /// <exception cref="System.ArgumentException">if target instance is read-only.</exception>
        public void ResetTo(StorageClientProviderContext ctx)
        {
            _ = ctx ?? throw new ArgumentNullException(nameof(ctx));

            // If happens to be resetting to self
            if (object.ReferenceEquals(this, ctx))
            {
                return;
            }

            // If trying something like None.ResetTo(anotherInstance)
            if (IsReadOnlyInstance(this))
            {
                throw new ArgumentException("Cannot use ResetTo to alter read-only StorageClientProviderContext instance");
            }

            this.ETag                  = ctx.ETag;
            this.TrackingETag          = ctx.TrackingETag;
            this.contextObjectAsString = ctx.contextObjectAsString;
            this.contextObject         = ctx.contextObject?.DeepClone() as JObject;
        }
        public void EnsureStorageContextEquivalenceViaStringInits(
            string contextStringIn, bool?setMute, sc contextObjectIn,
            string contextStringOut, sc contextObjectOut,
            Type exceptionTypeExpected)
        {
            sc sc1 = null;
            sc scImplicitMuting = null;

            if (contextStringIn != null)
            {
                bool gotException = false;
                try
                {
                    sc1 = new sc(contextStringIn, setMute);
                    scImplicitMuting = new sc(contextStringIn, null);
                }
                catch (Exception e)
                {
                    e.ShouldBeOfType(exceptionTypeExpected);
                    gotException = true;
                }
                if (exceptionTypeExpected != null)
                {
                    gotException.ShouldBeTrue($"Should have gotten exception for: '{contextStringIn}'");
                }

                if (contextStringOut != null)
                {
                    // confirm result via string.
                    var sc1Str = sc1.ClientRequestID;
                    contextStringOut.ShouldNotBeNullOrWhiteSpace();
                    sc1Str.ShouldNotBeNullOrWhiteSpace();
                    sc1Str.ShouldBe(contextStringOut);
                }

                if (contextObjectOut != null)
                {
                    // confirm result via object.
                    contextObjectOut.ShouldBeEquivalentTo(sc1);
                    AreEquivalent(contextObjectOut, sc1, out string reason).ShouldBeTrue(reason);
                }
            }

            if (contextObjectIn != null)
            {
                sc1 = contextObjectIn;

                if (contextStringOut != null)
                {
                    // confirm result via string.
                    var sc1Str = sc1.ClientRequestID;
                    contextStringOut.ShouldNotBeNull();
                    contextStringOut.ShouldBe(sc1Str);
                }

                if (contextObjectOut != null)
                {
                    // confirm result via object.
                    contextObjectOut.ShouldBeEquivalentTo(sc1);
                    AreEquivalent(contextObjectOut, sc1, out string failedBecause).ShouldBeTrue(failedBecause);
                }
            }
        }
 /// <summary>
 /// Initializes a new instance of the <see cref="StorageClientProviderContext"/> class,
 /// based on another context.  i.e., like a copy constructor.
 /// </summary>
 /// <param name="ctx">The source context.</param>
 public StorageClientProviderContext(StorageClientProviderContext ctx)
 {
     ResetTo(ctx);
 }
        /// <summary>
        /// Create a context instance, being more lenient than the constructors regarding
        /// the input value.  In addition to the normal JSON input, this method will also
        /// tolerate null/empty strings, GUID strings and other non-JSON strings.  It
        /// produces a context for each.  For any of the tolerated cases, it will produce
        /// either an empty context or a wrap-up of the passed value.  Muting, etc. are
        /// processed/included in those results as dictated by the parameters.
        ///
        /// If the caller provides either logging Action argument, they will be used to
        /// records errors encountered.
        /// </summary>
        /// <remarks>
        /// This method does not throw an exception for non-JSON cases, as the constructors
        /// tend to (thus "Safe" in the name).  But, it retains a code path that will
        /// log/throw an exception on bad input.  This should never happen in real use
        /// since if the code reaches that point, it will be that the method itself constructed
        /// JSON that was unparseable.  The code remains to ensure that will be logged if
        /// logger Actions were given and the problem somehow occurs.  Generally, application
        /// code wouldn't bother using a try/catch around calls to CreateSafe - otherwise, a
        /// constructor may suffice.
        /// </remarks>
        /// If there is an error (e.g. JSON parsing or otherwise), and the corresponging log*Error
        /// argument is non-null, invoke that to let the caller log the error.
        /// </summary>
        /// <param name="operationContextString">
        /// A string representing the Operation Context as a single JSON object.
        /// e.g. { "a" : "b" }</param>
        /// <param name="muteContext">If true, context should be set as muted -- i.e., intended for
        /// internal operations whose resulting notifications (if any) are not expected to be routed
        /// to callers (e.g., Requestor).</param>
        /// <param name="trackETag">If true, send the eTag value on each request (if it's not currently
        /// empty).</param>
        /// <param name="eTag">The current eTag string.  With each response, this value is updated to
        /// reflect the value returned last from Azure Storage (regardless of trackETag's value).
        /// For each HTTP request, the eTag value is only set if trackETag is true and the value of eTag
        /// is not empty.</param>
        /// <param name="logParseError">A callback invoked (if non-null) should a JSON parse error
        /// occur in processing operationContextString. Second argument to this callback is string that
        /// was being parsed.</param>
        /// <param name="logOtherError">A callback invoked (if non-null) should an exception, other
        ///  than a JSON parse error occur in processing operationContextString.</param>
        /// <returns>An instance of StorageProviderContext.</returns>
        public static StorageClientProviderContext CreateSafe(
            string operationContextString,
            bool?muteContext = null,
            bool?trackETag   = null,
            string eTag      = null,
            Action <Exception, string> logParseError = null,
            Action <Exception> logOtherError         = null)
        {
            var muteContextSpecified = muteContext.HasValue;

            if (!trackETag.HasValue)
            {
                trackETag = false; // default, if none given
            }

            JObject contextObject         = null;
            bool    haveLoggedParseIssues = false;

            var emptyInput = string.IsNullOrWhiteSpace(operationContextString);

            // #1 -- handle case where there's some string to try as JSON
            if (!emptyInput)
            {
                try
                {
                    contextObject = JsonHelpers.DeserializeOperationContext(operationContextString);
                }
                catch (Exception ep)
                {
                    logParseError?.Invoke(ep, $"Error parsing Storage operation context from: '{operationContextString}'");
                    haveLoggedParseIssues = true;
                    // and continue...
                }
            }

            // #2 -- didn't work as JSON, check it for GUID, etc.
            if (contextObject == null)
            {
                try
                {
                    contextObject = StringContextToJObject(operationContextString, out bool _);
                }
                catch (Exception es)
                {
                    if (!haveLoggedParseIssues)
                    {
                        logParseError?.Invoke(es, $"Error converting Storage operation context from: '{operationContextString}'");
                        haveLoggedParseIssues = true;
                        // and continue...
                    }
                    // Something (really) went wrong trying to create the StorageContext instance.
                    // It could be being passed incomplete JSON  or someone sending in a random sentence of text.
                    // Rather than stop the Gridwich with an exception, wrap whatever was given into
                    // a blank OperationContext as the value of a known JSON property.
                    contextObject = new JObject();
                    contextObject.Add(GeneralPropertyName, operationContextString);
                }
            }

            // #3 -- we finally have something to use as an OperationContext, now just wrap it up in a StorageContext.
            StorageClientProviderContext result = null;

            try
            {
                result = new StorageClientProviderContext(contextObject, muteContext, trackETag, eTag);
            }
            catch (Exception eo)
            {
                // If we get here, it's not the fault of the caller.  It means that this
                // method has somehow manipulated the input string into invalid JSON.
                // This should not occur in the real world.
                logOtherError?.Invoke(eo);
                throw;
            }

            return(result);
        }