/// <summary>
        /// Deletes an existing SharePoint web hook
        /// </summary>
        /// <param name="webUrl">Url of the site holding resource with the webhook</param>
        /// <param name="resourceType">The type of Hookable SharePoint resource</param>
        /// <param name="resourceId">Id of the resource (e.g. list id)</param>
        /// <param name="subscriptionId">Id of the web hook subscription that we need to delete</param>
        /// <param name="accessToken">Access token to authenticate against SharePoint</param>
        /// <param name="context">ClientContext instance to use for authentication</param>
        /// <returns>true if succesful, exception in case something went wrong</returns>
        internal static async Task <bool> RemoveWebhookSubscriptionAsync(string webUrl, WebHookResourceType resourceType, string resourceId, string subscriptionId, string accessToken, ClientContext context)
        {
            using (var handler = new HttpClientHandler())
            {
                if (String.IsNullOrEmpty(accessToken))
                {
                    context.Web.EnsureProperty(p => p.Url);
                    handler.Credentials = context.Credentials;
                    handler.CookieContainer.SetCookies(new Uri(context.Web.Url), (context.Credentials as SharePointOnlineCredentials).GetAuthenticationCookie(new Uri(context.Web.Url)));
                }

                using (var httpClient = new PnPHttpProvider(handler))
                {
                    string identifierUrl = GetResourceIdentifier(resourceType, webUrl, resourceId);
                    if (string.IsNullOrEmpty(identifierUrl))
                    {
                        throw new Exception("Identifier of the resource cannot be determined");
                    }

                    string requestUrl = string.Format("{0}/{1}('{2}')", identifierUrl, SubscriptionsUrlPart, subscriptionId);

                    HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, requestUrl);
                    request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

                    HttpResponseMessage response = await httpClient.SendAsync(request, new System.Threading.CancellationToken());

                    if (response.StatusCode != System.Net.HttpStatusCode.NoContent)
                    {
                        // oops...something went wrong, maybe the web hook does not exist?
                        throw new Exception(await response.Content.ReadAsStringAsync());
                    }
                    else
                    {
                        return(true);
                    }
                }
            }
        }
        /// <summary>
        /// Executes a REST Get request.
        /// </summary>
        /// <param name="web">The current web to execute the request against</param>
        /// <param name="endpoint">The full endpoint url, exluding the URL of the web, e.g. /_api/web/lists</param>
        /// <returns></returns>
        internal static async Task <string> ExecuteGetAsync(this Web web, string endpoint)
        {
            string returnObject = null;
            var    accessToken  = web.Context.GetAccessToken();

            using (var handler = new HttpClientHandler())
            {
                web.EnsureProperty(w => w.Url);

                // we're not in app-only or user + app context, so let's fall back to cookie based auth
                if (String.IsNullOrEmpty(accessToken))
                {
                    handler.SetAuthenticationCookies(web.Context as ClientContext);
                }


                using (var httpClient = new PnPHttpProvider(handler))
                {
                    var requestUrl = $"{web.Url}{endpoint}";

                    HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
                    request.Headers.Add("accept", "application/json;odata=nometadata");
                    if (!string.IsNullOrEmpty(accessToken))
                    {
                        request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
                    }
                    else
                    {
                        if (web.Context.Credentials is NetworkCredential networkCredential)
                        {
                            handler.Credentials = networkCredential;
                        }
                    }

                    var requestDigest = await(web.Context as ClientContext).GetRequestDigestAsync().ConfigureAwait(false);
                    request.Headers.Add("X-RequestDigest", requestDigest);

                    // Perform actual post operation
                    HttpResponseMessage response = await httpClient.SendAsync(request, new System.Threading.CancellationToken());

                    if (response.IsSuccessStatusCode)
                    {
                        // If value empty, URL is taken
                        var responseString = await response.Content.ReadAsStringAsync();

                        if (responseString != null)
                        {
                            try
                            {
                                returnObject = responseString;
                            }
                            catch { }
                        }
                    }
                    else
                    {
                        // Something went wrong...
                        throw new Exception(await response.Content.ReadAsStringAsync());
                    }
                }
            }
            return(await Task.Run(() => returnObject));
        }
        /// <summary>
        /// This helper method makes an HTTP request and eventually returns a result
        /// </summary>
        /// <param name="httpMethod">The HTTP method for the request</param>
        /// <param name="requestUrl">The URL of the request</param>
        /// <param name="responseHeaders">The response headers of the HTTP request (output argument)</param>
        /// <param name="accessToken">The OAuth 2.0 Access Token for the request, if authorization is required</param>
        /// <param name="accept">The content type of the accepted response</param>
        /// <param name="content">The content of the request</param>
        /// <param name="contentType">The content  type of the request</param>
        /// <param name="referer">The URL Referer for the request</param>
        /// <param name="resultPredicate">The predicate to retrieve the result, if any</param>
        /// <param name="requestHeaders">A collection of any custom request headers</param>
        /// <param name="cookies">Any request cookies values</param>
        /// <param name="retryCount">Number of times to retry the request</param>
        /// <param name="delay">Milliseconds to wait before retrying the request. The delay will be increased (doubled) every retry</param>
        /// <param name="userAgent">UserAgent string value to insert for this request. You can define this value in your app's config file using key="SharePointPnPUserAgent" value="PnPRocks"</param>
        /// <param name="spContext">An optional SharePoint client context</param>
        /// <typeparam name="TResult">The type of the result, if any</typeparam>
        /// <returns>The value of the result, if any</returns>
        private static TResult MakeHttpRequest <TResult>(
            string httpMethod,
            string requestUrl,
            out HttpResponseHeaders responseHeaders,
            string accessToken = null,
            string accept      = null,
            object content     = null,
            string contentType = null,
            string referer     = null,
            Func <HttpResponseMessage, TResult> resultPredicate = null,
            Dictionary <string, string> requestHeaders          = null,
            Dictionary <string, string> cookies = null,
            int retryCount          = 1,
            int delay               = 500,
            string userAgent        = null,
            ClientContext spContext = null)
        {
            HttpClient client = HttpHelper.httpClient;

            // Define whether to use the default HttpClient object
            // or a custom one with retry logic and/or with custom request cookies
            // or a SharePoint client context to rely on
            if (retryCount >= 1 || (cookies != null && cookies.Count > 0) || spContext != null)
            {
                // Let's create a custom HttpHandler
                var handler = new HttpClientHandler();

                // Process any SPO authentication cookies, if we have an SPO context
                if (spContext != null)
                {
                    SetAuthenticationCookies(handler, spContext);

                    if (requestHeaders == null)
                    {
                        requestHeaders = new Dictionary <string, string>();
                    }

                    if (!requestHeaders.ContainsKey("X-RequestDigest"))
                    {
                        requestHeaders.Add("X-RequestDigest", spContext.GetRequestDigest().GetAwaiter().GetResult());
                    }
                }

                // Process any other request cookies
                if (cookies != null)
                {
                    foreach (var cookie in cookies)
                    {
                        handler.CookieContainer.Add(new System.Net.Cookie(cookie.Key, cookie.Value));
                    }
                }

                // And now create the customized HttpClient object
                client = new PnPHttpProvider(handler, true, retryCount, delay, userAgent);
            }

            // Prepare the variable to hold the result, if any
            TResult result = default(TResult);

            responseHeaders = null;

            Uri requestUri = new Uri(requestUrl);

            // If we have the token, then handle the HTTP request

            // Set the Authorization Bearer token
            if (!string.IsNullOrEmpty(accessToken))
            {
                client.DefaultRequestHeaders.Authorization =
                    new AuthenticationHeaderValue("Bearer", accessToken);
            }

            if (!string.IsNullOrEmpty(referer))
            {
                client.DefaultRequestHeaders.Referrer = new Uri(referer);
            }

            // If there is an accept argument, set the corresponding HTTP header
            if (!string.IsNullOrEmpty(accept))
            {
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(
                    new MediaTypeWithQualityHeaderValue(accept));
            }

            // Process any additional custom request headers
            if (requestHeaders != null)
            {
                foreach (var requestHeader in requestHeaders)
                {
                    client.DefaultRequestHeaders.Add(requestHeader.Key, requestHeader.Value);
                }
            }

            // Prepare the content of the request, if any
            HttpContent requestContent = null;

            System.IO.Stream streamContent = content as System.IO.Stream;
            if (streamContent != null)
            {
                requestContent = new StreamContent(streamContent);
                requestContent.Headers.ContentType = new MediaTypeHeaderValue(contentType);
            }
            else if (content != null)
            {
                var jsonString = content is string
                                 ?content.ToString()
                                     : JsonConvert.SerializeObject(content, Formatting.None, new JsonSerializerSettings
                {
                    NullValueHandling = NullValueHandling.Ignore,
                    ContractResolver  = new ODataBindJsonResolver(),
                });

                requestContent = new StringContent(jsonString, Encoding.UTF8, contentType);
            }

            // Prepare the HTTP request message with the proper HTTP method
            HttpRequestMessage request = new HttpRequestMessage(
                new HttpMethod(httpMethod), requestUrl);

            // Set the request content, if any
            if (requestContent != null)
            {
                request.Content = requestContent;
            }

            // Fire the HTTP request
            HttpResponseMessage response = client.SendAsync(request).Result;

            if (response.IsSuccessStatusCode)
            {
                // If the response is Success and there is a
                // predicate to retrieve the result, invoke it
                if (resultPredicate != null)
                {
                    result = resultPredicate(response);
                }

                // Get any response header and put it in the answer
                responseHeaders = response.Headers;
            }
            else
            {
                throw new ApplicationException(
                          string.Format("Exception while invoking endpoint {0}.", requestUrl),
#if !NETSTANDARD2_0
                          new HttpException(
                              (int)response.StatusCode,
                              response.Content.ReadAsStringAsync().Result));
#else
                          new Exception(response.Content.ReadAsStringAsync().Result));
#endif
            }

            return(result);
        }
        /// <summary>
        /// Updates the expiration datetime (and notification URL) of an existing SharePoint web hook
        /// </summary>
        /// <param name="webUrl">Url of the site holding resource with the webhook</param>
        /// <param name="resourceType">The type of Hookable SharePoint resource</param>
        /// <param name="resourceId">Id of the resource (e.g. list id)</param>
        /// <param name="subscriptionId">Id of the web hook subscription that we need to delete</param>
        /// <param name="webHookEndPoint">Url of the web hook service endpoint (the one that will be called during an event)</param>
        /// <param name="expirationDateTime">New web hook expiration date</param>
        /// <param name="accessToken">Access token to authenticate against SharePoint</param>
        /// <param name="context">ClientContext instance to use for authentication</param>
        /// <exception cref="ArgumentOutOfRangeException">Thrown when expiration date is out of valid range.</exception>
        /// <returns>true if succesful, exception in case something went wrong</returns>
        public static async Task <bool> UpdateWebhookSubscriptionAsync(string webUrl, WebHookResourceType resourceType, string resourceId, string subscriptionId,
                                                                       string webHookEndPoint, DateTime expirationDateTime, string accessToken, ClientContext context)
        {
            if (!ValidateExpirationDateTime(expirationDateTime))
            {
                throw new ArgumentOutOfRangeException(nameof(expirationDateTime), "The specified expiration date is invalid. Should be greater than today and within 6 months");
            }

            await new SynchronizationContextRemover();

            using (var handler = new HttpClientHandler())
            {
                context.Web.EnsureProperty(p => p.Url);
                if (String.IsNullOrEmpty(accessToken))
                {
                    handler.SetAuthenticationCookies(context);
                }

                using (var httpClient = new PnPHttpProvider(handler))
                {
                    string identifierUrl = GetResourceIdentifier(resourceType, webUrl, resourceId);
                    if (string.IsNullOrEmpty(identifierUrl))
                    {
                        throw new Exception("Identifier of the resource cannot be determined");
                    }

                    string             requestUrl = string.Format("{0}/{1}('{2}')", identifierUrl, SubscriptionsUrlPart, subscriptionId);
                    HttpRequestMessage request    = new HttpRequestMessage(new HttpMethod("PATCH"), requestUrl);
                    request.Headers.Add("X-RequestDigest", await context.GetRequestDigestAsync());
                    request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                    if (!string.IsNullOrEmpty(accessToken))
                    {
                        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
                    }

                    WebhookSubscription webhookSubscription;

                    if (string.IsNullOrEmpty(webHookEndPoint))
                    {
                        webhookSubscription = new WebhookSubscription()
                        {
                            ExpirationDateTime = expirationDateTime
                        };
                    }
                    else
                    {
                        webhookSubscription = new WebhookSubscription()
                        {
                            NotificationUrl    = webHookEndPoint,
                            ExpirationDateTime = expirationDateTime
                        };
                    }

                    request.Content = new StringContent(JsonConvert.SerializeObject(webhookSubscription),
                                                        Encoding.UTF8, "application/json");

                    HttpResponseMessage response = await httpClient.SendAsync(request, new System.Threading.CancellationToken());

                    if (response.StatusCode != System.Net.HttpStatusCode.NoContent)
                    {
                        // oops...something went wrong, maybe the web hook does not exist?
                        throw new Exception(await response.Content.ReadAsStringAsync());
                    }
                    else
                    {
                        return(true);
                    }
                }
            }
        }