DocSpace-buildtools/thirdparty/Microsoft.Graph.Core/Requests/BaseRequest.cs

487 lines
22 KiB
C#

// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------
namespace Microsoft.Graph
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// The base request class.
/// </summary>
public class BaseRequest : IBaseRequest
{
private IResponseHandler responseHandler;
/// <summary>
/// Constructs a new <see cref="BaseRequest"/>.
/// </summary>
/// <param name="requestUrl">The URL for the request.</param>
/// <param name="client">The <see cref="IBaseClient"/> for handling requests.</param>
/// <param name="options">The header and query options for the request.</param>
public BaseRequest(
string requestUrl,
IBaseClient client,
IEnumerable<Option> options = null)
{
this.Method = "GET";
this.Client = client;
this.responseHandler = new ResponseHandler(client.HttpProvider.Serializer);
this.Headers = new List<HeaderOption>();
this.QueryOptions = new List<QueryOption>();
this.MiddlewareOptions = new Dictionary<string, IMiddlewareOption>();
this.RequestUrl = this.InitializeUrl(requestUrl);
if (options != null)
{
var headerOptions = options.OfType<HeaderOption>();
if (headerOptions != null)
{
((List<HeaderOption>)this.Headers).AddRange(headerOptions);
}
var queryOptions = options.OfType<QueryOption>();
if (queryOptions != null)
{
((List<QueryOption>)this.QueryOptions).AddRange(queryOptions);
}
}
// Adds the default authentication provider for this request.
// This can be changed can be changed by the user by calling WithPerRequestAuthProvider extension method.
this.WithDefaultAuthProvider();
}
/// <summary>
/// Gets or sets the response handler for the request.
/// </summary>
public IResponseHandler ResponseHandler { get { return responseHandler; } set { responseHandler = value; } }
/// <summary>
/// Gets or sets the content type for the request.
/// </summary>
public string ContentType { get; set; }
/// <summary>
/// Gets the <see cref="HeaderOption"/> collection for the request.
/// </summary>
public IList<HeaderOption> Headers { get; private set; }
/// <summary>
/// Gets the <see cref="IBaseClient"/> for handling requests.
/// </summary>
public IBaseClient Client { get; private set; }
/// <summary>
/// Gets or sets the HTTP method string for the request.
/// </summary>
public string Method { get; set; }
/// <summary>
/// Gets the <see cref="QueryOption"/> collection for the request.
/// </summary>
public IList<QueryOption> QueryOptions { get; set; }
/// <summary>
/// Gets the URL for the request, without query string.
/// </summary>
public string RequestUrl { get; internal set; }
/// <summary>
/// Gets or sets middleware options for the request.
/// </summary>
public IDictionary<string, IMiddlewareOption> MiddlewareOptions { get; private set; }
/// <summary>
/// Sends the request.
/// </summary>
/// <param name="serializableObject">The serializable object to send.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the request.</param>
/// <param name="completionOption">The <see cref="HttpCompletionOption"/> to pass to the <see cref="IHttpProvider"/> on send.</param>
/// <returns>The task to await.</returns>
public async Task SendAsync(
object serializableObject,
CancellationToken cancellationToken,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
using (var response = await this.SendRequestAsync(serializableObject, cancellationToken, completionOption).ConfigureAwait(false))
{
}
}
/// <summary>
/// Sends the request.
/// </summary>
/// <typeparam name="T">The expected response object type for deserialization.</typeparam>
/// <param name="serializableObject">The serializable object to send.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the request.</param>
/// <param name="completionOption">The <see cref="HttpCompletionOption"/> to pass to the <see cref="IHttpProvider"/> on send.</param>
/// <returns>The deserialized response object.</returns>
public async Task<T> SendAsync<T>(
object serializableObject,
CancellationToken cancellationToken,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
using (var response = await this.SendRequestAsync(serializableObject, cancellationToken, completionOption).ConfigureAwait(false))
{
return await this.responseHandler.HandleResponse<T>(response);
}
}
/// <summary>
/// Sends the multipart request.
/// </summary>
/// <typeparam name="T">The expected response object type for deserialization.</typeparam>
/// <param name="multipartContent">The multipart object to send.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the request.</param>
/// <param name="completionOption">The <see cref="HttpCompletionOption"/> to pass to the <see cref="IHttpProvider"/> on send.</param>
/// <returns>The deserialized response object.</returns>
public async Task<T> SendMultiPartAsync<T>(
MultipartContent multipartContent,
CancellationToken cancellationToken,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
using (var response = await this.SendMultiPartRequestAsync(multipartContent, cancellationToken, completionOption).ConfigureAwait(false))
{
return await this.responseHandler.HandleResponse<T>(response);
}
}
/// <summary>
/// Sends the request.
/// </summary>
/// <param name="serializableObject">The serializable object to send.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the request.</param>
/// <param name="completionOption">The <see cref="HttpCompletionOption"/> to pass to the <see cref="IHttpProvider"/> on send.</param>
/// <returns>The stream.</returns>
public async Task<Stream> SendStreamRequestAsync(
object serializableObject,
CancellationToken cancellationToken,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
var response = await this.SendRequestAsync(serializableObject, cancellationToken, completionOption).ConfigureAwait(false);
return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
}
/// <summary>
/// Sends the multipart request.
/// </summary>
/// <param name="multipartContent">The multipart object to send.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the request.</param>
/// <param name="completionOption">The <see cref="HttpCompletionOption"/> to pass to the <see cref="IHttpProvider"/> on send.</param>
/// <returns>The <see cref="HttpResponseMessage"/> object.</returns>
public async Task<HttpResponseMessage> SendMultiPartRequestAsync(
MultipartContent multipartContent,
CancellationToken cancellationToken,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
if (string.IsNullOrEmpty(this.RequestUrl))
{
throw new ServiceException(
new Error
{
Code = ErrorConstants.Codes.InvalidRequest,
Message = ErrorConstants.Messages.RequestUrlMissing,
});
}
if (multipartContent != null)
{
using (var request = this.GetHttpRequestMessage(cancellationToken))
{
// Only call `AuthenticateRequestAsync` when a custom IHttpProvider is used or our HttpProvider is used without an auth handler.
if (ShouldAuthenticateRequest())
await this.AuthenticateRequestAsync(request);
request.Content = multipartContent;
return await this.Client.HttpProvider.SendAsync(request, completionOption, cancellationToken).ConfigureAwait(false);
}
}
else
{
throw new Exception("The Multipart content is null. Set the multipart content.");
}
}
/// <summary>
/// Sends the request.
/// </summary>
/// <param name="serializableObject">The serializable object to send.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the request.</param>
/// <param name="completionOption">The <see cref="HttpCompletionOption"/> to pass to the <see cref="IHttpProvider"/> on send.</param>
/// <returns>The <see cref="HttpResponseMessage"/> object.</returns>
public async Task<HttpResponseMessage> SendRequestAsync(
object serializableObject,
CancellationToken cancellationToken,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
if (string.IsNullOrEmpty(this.RequestUrl))
{
throw new ServiceException(
new Error
{
Code = ErrorConstants.Codes.InvalidRequest,
Message = ErrorConstants.Messages.RequestUrlMissing,
});
}
using (var request = this.GetHttpRequestMessage(cancellationToken))
{
// Only call `AuthenticateRequestAsync` when a custom IHttpProvider is used or our HttpProvider is used without an auth handler.
if (ShouldAuthenticateRequest())
await this.AuthenticateRequestAsync(request);
if (serializableObject != null)
{
var inputStream = serializableObject as Stream;
if (inputStream != null)
{
request.Content = new StreamContent(inputStream);
}
else
{
request.Content = new StringContent(this.Client.HttpProvider.Serializer.SerializeObject(serializableObject));
}
if (!string.IsNullOrEmpty(this.ContentType))
{
request.Content.Headers.ContentType = new MediaTypeHeaderValue(this.ContentType);
}
}
return await this.Client.HttpProvider.SendAsync(request, completionOption, cancellationToken).ConfigureAwait(false);
}
}
/// <summary>
/// Gets the <see cref="HttpRequestMessage"/> representation of the request.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the request.</param>
/// <returns>The <see cref="HttpRequestMessage"/> representation of the request.</returns>
public HttpRequestMessage GetHttpRequestMessage(CancellationToken cancellationToken)
{
var queryString = this.BuildQueryString();
var request = new HttpRequestMessage(new HttpMethod(this.Method), string.Concat(this.RequestUrl, queryString));
this.AddHeadersToRequest(request);
this.AddRequestContextToRequest(request, cancellationToken);
return request;
}
/// <summary>
/// Gets the <see cref="HttpRequestMessage"/> representation of the request.
/// </summary>
/// <returns>The <see cref="HttpRequestMessage"/> representation of the request.</returns>
public HttpRequestMessage GetHttpRequestMessage()
{
return this.GetHttpRequestMessage(CancellationToken.None);
}
/// <summary>
/// Adds all of the headers from the header collection to the request.
/// </summary>
/// <param name="request">The <see cref="HttpRequestMessage"/> representation of the request.</param>
private void AddHeadersToRequest(HttpRequestMessage request)
{
if (this.Headers != null)
{
foreach (var header in this.Headers)
{
request.Headers.TryAddWithoutValidation(header.Name, header.Value);
}
}
}
/// <summary>
/// Adds a <see cref="GraphRequestContext"/> to <see cref="HttpRequestMessage"/> property bag
/// </summary>
/// <param name="httpRequestMessage">A <see cref="HttpRequestMessage"/></param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/></param>
private void AddRequestContextToRequest(HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken)
{
// Creates a request context object
var requestContext = new GraphRequestContext
{
MiddlewareOptions = MiddlewareOptions,
ClientRequestId = GetHeaderValue(httpRequestMessage, CoreConstants.Headers.ClientRequestId) ?? Guid.NewGuid().ToString(),
CancellationToken = cancellationToken,
FeatureUsage = httpRequestMessage.GetFeatureFlags()
};
httpRequestMessage.Properties.Add(typeof(GraphRequestContext).ToString(), requestContext);
}
/// <summary>
/// Gets a URL that is the request builder's request URL with the segment appended.
/// </summary>
/// <param name="urlSegment">The segment to append to the request URL.</param>
/// <returns>A URL that is the request builder's request URL with the segment appended.</returns>
public void AppendSegmentToRequestUrl(string urlSegment)
{
this.RequestUrl = string.Format("{0}/{1}", this.RequestUrl, urlSegment);
}
/// <summary>
/// Builds the query string for the request from the query option collection.
/// </summary>
/// <returns>The constructed query string.</returns>
internal string BuildQueryString()
{
if (this.QueryOptions != null)
{
var stringBuilder = new StringBuilder();
foreach (var queryOption in this.QueryOptions)
{
if (stringBuilder.Length == 0)
{
stringBuilder.AppendFormat("?{0}={1}", queryOption.Name, queryOption.Value);
}
else
{
stringBuilder.AppendFormat("&{0}={1}", queryOption.Name, queryOption.Value);
}
}
return stringBuilder.ToString();
}
return null;
}
/// <summary>
/// Adds the authentication header to the request. This is a patch to support request authentication for custom HttpProviders.
/// </summary>
/// <param name="request">The <see cref="HttpRequestMessage"/> representation of the request.</param>
/// <returns>The task to await.</returns>
private async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
if (this.Client.AuthenticationProvider == null)
{
throw new ServiceException(
new Error
{
Code = ErrorConstants.Codes.InvalidRequest,
Message = ErrorConstants.Messages.AuthenticationProviderMissing,
});
}
await Client.AuthenticationProvider.AuthenticateRequestAsync(request);
}
/// <summary>
/// Initializes the request URL for the request, breaking it into query options and base URL.
/// </summary>
/// <param name="requestUrl">The request URL.</param>
/// <returns>The request URL minus query string.</returns>
private string InitializeUrl(string requestUrl)
{
if (string.IsNullOrEmpty(requestUrl))
{
throw new ServiceException(
new Error
{
Code = ErrorConstants.Codes.InvalidRequest,
Message = ErrorConstants.Messages.BaseUrlMissing,
});
}
var uri = new Uri(requestUrl);
if (!string.IsNullOrEmpty(uri.Query))
{
var queryString = uri.Query;
if (queryString[0] == '?')
{
queryString = queryString.Substring(1);
}
var queryOptions = queryString.Split('&').Select(
queryValue =>
{
// We want to split on the first occurrence of = since there are scenarios where a query option can
// have 'sub-query' options on navigation properties for $expand scenarios. This way we can properly
// split the query option name/value into the QueryOption object. Take this for example:
// $expand=extensions($filter=Id%20eq%20'SMB'%20)
// We want to get '$expand' as the name and 'extensions($filter=Id%20eq%20'SMB'%20)' as the value
// for QueryOption object.
// OData URL conventions 5.1.2 System Query Option $expand
// http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part2-url-conventions/odata-v4.0-errata03-os-part2-url-conventions-complete.html#_Toc453752359
var segments = queryValue.Split(new[] { '=' }, 2);
return new QueryOption(
segments[0],
segments.Length > 1 ? segments[1] : string.Empty);
});
foreach(var queryOption in queryOptions)
{
this.QueryOptions.Add(queryOption);
}
}
return new UriBuilder(uri) { Query = string.Empty }.ToString();
}
/// <summary>
/// Gets a specified header value from <see cref="HttpRequestMessage"/>
/// </summary>
/// <param name="requestMessage">A <see cref="HttpRequestMessage"/></param>
/// <param name="headerName">The name, or key, of the header option.</param>
/// <returns>Header value</returns>
private string GetHeaderValue(HttpRequestMessage requestMessage, string headerName)
{
string headerValue = null;
var requestHeader = this.Headers.FirstOrDefault((h) => h.Name.Equals(headerName));
// Check request headers first
if (requestHeader != null)
{
headerValue = requestHeader.Value;
}
// If not found, check http client default headers + request headers
else if (requestMessage.Headers != null)
{
if (requestMessage.Headers.TryGetValues(headerName, out var values))
{
headerValue = values.FirstOrDefault();
}
}
return headerValue;
}
/// <summary>
/// Determines whether or not <see cref="BaseRequest"/> should authenticate the request or let <see cref="AuthenticationHandler"/> authenticate the request.
/// </summary>
/// <returns>
/// TRUE: If a CUSTOM <see cref="IHttpProvider"/> or DEFAULT <see cref="HttpProvider"/> is used WITHOUT an <see cref="AuthenticationHandler"/>.
/// FALSE: If our DEFAULT <see cref="HttpProvider"/> or <see cref="SimpleHttpProvider"/> is used WITH an <see cref="AuthenticationHandler"/>.
/// </returns>
private bool ShouldAuthenticateRequest()
{
switch (this.Client.HttpProvider)
{
case HttpProvider provider when provider.httpClient.ContainsFeatureFlag(FeatureFlag.AuthHandler):
return false; //no need to authenticate as we have an AuthHandler provided
case SimpleHttpProvider simpleHttpProvider when simpleHttpProvider.httpClient.ContainsFeatureFlag(FeatureFlag.AuthHandler):
return false; //no need to authenticate as we have an AuthHandler provided
default:
return true;
}
}
}
}