// ------------------------------------------------------------------------------
// 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;
///
/// The base request class.
///
public class BaseRequest : IBaseRequest
{
private IResponseHandler responseHandler;
///
/// Constructs a new .
///
/// The URL for the request.
/// The for handling requests.
/// The header and query options for the request.
public BaseRequest(
string requestUrl,
IBaseClient client,
IEnumerable options = null)
{
this.Method = "GET";
this.Client = client;
this.responseHandler = new ResponseHandler(client.HttpProvider.Serializer);
this.Headers = new List();
this.QueryOptions = new List();
this.MiddlewareOptions = new Dictionary();
this.RequestUrl = this.InitializeUrl(requestUrl);
if (options != null)
{
var headerOptions = options.OfType();
if (headerOptions != null)
{
((List)this.Headers).AddRange(headerOptions);
}
var queryOptions = options.OfType();
if (queryOptions != null)
{
((List)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();
}
///
/// Gets or sets the response handler for the request.
///
public IResponseHandler ResponseHandler { get { return responseHandler; } set { responseHandler = value; } }
///
/// Gets or sets the content type for the request.
///
public string ContentType { get; set; }
///
/// Gets the collection for the request.
///
public IList Headers { get; private set; }
///
/// Gets the for handling requests.
///
public IBaseClient Client { get; private set; }
///
/// Gets or sets the HTTP method string for the request.
///
public string Method { get; set; }
///
/// Gets the collection for the request.
///
public IList QueryOptions { get; set; }
///
/// Gets the URL for the request, without query string.
///
public string RequestUrl { get; internal set; }
///
/// Gets or sets middleware options for the request.
///
public IDictionary MiddlewareOptions { get; private set; }
///
/// Sends the request.
///
/// The serializable object to send.
/// The for the request.
/// The to pass to the on send.
/// The task to await.
public async Task SendAsync(
object serializableObject,
CancellationToken cancellationToken,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
using (var response = await this.SendRequestAsync(serializableObject, cancellationToken, completionOption).ConfigureAwait(false))
{
}
}
///
/// Sends the request.
///
/// The expected response object type for deserialization.
/// The serializable object to send.
/// The for the request.
/// The to pass to the on send.
/// The deserialized response object.
public async Task SendAsync(
object serializableObject,
CancellationToken cancellationToken,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
using (var response = await this.SendRequestAsync(serializableObject, cancellationToken, completionOption).ConfigureAwait(false))
{
return await this.responseHandler.HandleResponse(response);
}
}
///
/// Sends the multipart request.
///
/// The expected response object type for deserialization.
/// The multipart object to send.
/// The for the request.
/// The to pass to the on send.
/// The deserialized response object.
public async Task SendMultiPartAsync(
MultipartContent multipartContent,
CancellationToken cancellationToken,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
using (var response = await this.SendMultiPartRequestAsync(multipartContent, cancellationToken, completionOption).ConfigureAwait(false))
{
return await this.responseHandler.HandleResponse(response);
}
}
///
/// Sends the request.
///
/// The serializable object to send.
/// The for the request.
/// The to pass to the on send.
/// The stream.
public async Task 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);
}
///
/// Sends the multipart request.
///
/// The multipart object to send.
/// The for the request.
/// The to pass to the on send.
/// The object.
public async Task 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.");
}
}
///
/// Sends the request.
///
/// The serializable object to send.
/// The for the request.
/// The to pass to the on send.
/// The object.
public async Task 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);
}
}
///
/// Gets the representation of the request.
///
/// The for the request.
/// The representation of the request.
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;
}
///
/// Gets the representation of the request.
///
/// The representation of the request.
public HttpRequestMessage GetHttpRequestMessage()
{
return this.GetHttpRequestMessage(CancellationToken.None);
}
///
/// Adds all of the headers from the header collection to the request.
///
/// The representation of the request.
private void AddHeadersToRequest(HttpRequestMessage request)
{
if (this.Headers != null)
{
foreach (var header in this.Headers)
{
request.Headers.TryAddWithoutValidation(header.Name, header.Value);
}
}
}
///
/// Adds a to property bag
///
/// A
/// A
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);
}
///
/// Gets a URL that is the request builder's request URL with the segment appended.
///
/// The segment to append to the request URL.
/// A URL that is the request builder's request URL with the segment appended.
public void AppendSegmentToRequestUrl(string urlSegment)
{
this.RequestUrl = string.Format("{0}/{1}", this.RequestUrl, urlSegment);
}
///
/// Builds the query string for the request from the query option collection.
///
/// The constructed query string.
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;
}
///
/// Adds the authentication header to the request. This is a patch to support request authentication for custom HttpProviders.
///
/// The representation of the request.
/// The task to await.
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);
}
///
/// Initializes the request URL for the request, breaking it into query options and base URL.
///
/// The request URL.
/// The request URL minus query string.
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();
}
///
/// Gets a specified header value from
///
/// A
/// The name, or key, of the header option.
/// Header value
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;
}
///
/// Determines whether or not should authenticate the request or let authenticate the request.
///
///
/// TRUE: If a CUSTOM or DEFAULT is used WITHOUT an .
/// FALSE: If our DEFAULT or is used WITH an .
///
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;
}
}
}
}