Skip to content
This repository has been archived by the owner on Jul 9, 2024. It is now read-only.

Commit

Permalink
Merge pull request #260 from kasperk81/main
Browse files Browse the repository at this point in the history
Remove all LINQ usage from product code
  • Loading branch information
andrueastman authored May 27, 2024
2 parents d714e6b + dea20d9 commit f93a5f8
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 113 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [Unreleased]

## [1.4.3] - 2024-05-24

### Changed

- Remove all LINQ usage from product code

## [1.4.2] - 2024-05-21

### Added
Expand Down
116 changes: 79 additions & 37 deletions src/HttpClientRequestAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Kiota.Abstractions;
Expand Down Expand Up @@ -388,7 +387,9 @@ private async Task ThrowIfFailedResponse(HttpResponseMessage response, Dictionar

var statusCodeAsInt = (int)response.StatusCode;
var statusCodeAsString = statusCodeAsInt.ToString();
var responseHeadersDictionary = response.Headers.ToDictionary(x => x.Key, y => y.Value, StringComparer.OrdinalIgnoreCase);
var responseHeadersDictionary = new Dictionary<string, IEnumerable<string>>(StringComparer.OrdinalIgnoreCase);
foreach (var header in response.Headers)
responseHeadersDictionary[header.Key] = header.Value;
ParsableFactory<IParsable>? errorFactory;
if(errorMapping == null ||
!errorMapping.TryGetValue(statusCodeAsString, out errorFactory) &&
Expand Down Expand Up @@ -470,51 +471,86 @@ private async Task<HttpResponseMessage> GetHttpResponseMessage(RequestInformatio
var ex = new InvalidOperationException("Could not get a response after calling the service");
throw ex;
}
if(response.Headers.TryGetValues("Content-Length", out var contentLengthValues) &&
contentLengthValues.Any() &&
contentLengthValues.First() is string firstContentLengthValue &&
int.TryParse(firstContentLengthValue, out var contentLength))
if(response.Headers.TryGetValues("Content-Length", out var contentLengthValues))
{
activityForAttributes?.SetTag("http.response_content_length", contentLength);
using var contentLengthEnumerator = contentLengthValues.GetEnumerator();
if(contentLengthEnumerator.MoveNext() && int.TryParse(contentLengthEnumerator.Current, out var contentLength))
{
activityForAttributes?.SetTag("http.response_content_length", contentLength);
}
}
if(response.Headers.TryGetValues("Content-Type", out var contentTypeValues) &&
contentTypeValues.Any() &&
contentTypeValues.First() is string firstContentTypeValue)
if(response.Headers.TryGetValues("Content-Type", out var contentTypeValues))
{
activityForAttributes?.SetTag("http.response_content_type", firstContentTypeValue);
using var contentTypeEnumerator = contentTypeValues.GetEnumerator();
if(contentTypeEnumerator.MoveNext())
{
activityForAttributes?.SetTag("http.response_content_type", contentTypeEnumerator.Current);
}
}
activityForAttributes?.SetTag("http.status_code", (int)response.StatusCode);
activityForAttributes?.SetTag("http.flavor", $"{response.Version.Major}.{response.Version.Minor}");

return await RetryCAEResponseIfRequired(response, requestInfo, cancellationToken, claims, activityForAttributes).ConfigureAwait(false);
}

private static readonly Regex caeValueRegex = new("\"([^\"]*)\"", RegexOptions.Compiled, TimeSpan.FromMilliseconds(100));

/// <summary>
/// The key for the event raised by tracing when an authentication challenge is received
/// </summary>
public const string AuthenticateChallengedEventKey = "com.microsoft.kiota.authenticate_challenge_received";

private async Task<HttpResponseMessage> RetryCAEResponseIfRequired(HttpResponseMessage response, RequestInformation requestInfo, CancellationToken cancellationToken, string? claims, Activity? activityForAttributes)
{
using var span = activitySource?.StartActivity(nameof(RetryCAEResponseIfRequired));
if(response.StatusCode == HttpStatusCode.Unauthorized &&
string.IsNullOrEmpty(claims) && // avoid infinite loop, we only retry once
(requestInfo.Content?.CanSeek ?? true) &&
response.Headers.WwwAuthenticate?.FirstOrDefault(filterAuthHeader) is AuthenticationHeaderValue authHeader &&
authHeader.Parameter?.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
.Select(static x => x.Trim())
.FirstOrDefault(static x => x.StartsWith(ClaimsKey, StringComparison.OrdinalIgnoreCase)) is string rawResponseClaims &&
caeValueRegex.Match(rawResponseClaims) is Match claimsMatch &&
claimsMatch.Groups.Count > 1 &&
claimsMatch.Groups[1].Value is string responseClaims)
(requestInfo.Content?.CanSeek ?? true))
{
span?.AddEvent(new ActivityEvent(AuthenticateChallengedEventKey));
activityForAttributes?.SetTag("http.retry_count", 1);
requestInfo.Content?.Seek(0, SeekOrigin.Begin);
await DrainAsync(response, cancellationToken).ConfigureAwait(false);
return await GetHttpResponseMessage(requestInfo, cancellationToken, activityForAttributes, responseClaims).ConfigureAwait(false);
AuthenticationHeaderValue? authHeader = null;
foreach(var header in response.Headers.WwwAuthenticate)
{
if(filterAuthHeader(header))
{
authHeader = header;
break;
}
}

if(authHeader is not null)
{
var authHeaderParameters = authHeader.Parameter?.Split(new[]{','}, StringSplitOptions.RemoveEmptyEntries);

string? rawResponseClaims = null;
if(authHeaderParameters != null)
{
foreach(var parameter in authHeaderParameters)
{
var trimmedParameter = parameter.Trim();
if(trimmedParameter.StartsWith(ClaimsKey, StringComparison.OrdinalIgnoreCase))
{
rawResponseClaims = trimmedParameter;
break;
}
}
}

if(rawResponseClaims != null &&
caeValueRegex.Match(rawResponseClaims) is Match claimsMatch &&
claimsMatch.Groups.Count > 1 &&
claimsMatch.Groups[1].Value is string responseClaims)
{
span?.AddEvent(new ActivityEvent(AuthenticateChallengedEventKey));
activityForAttributes?.SetTag("http.retry_count", 1);
requestInfo.Content?.Seek(0, SeekOrigin.Begin);
await DrainAsync(response, cancellationToken).ConfigureAwait(false);
return await GetHttpResponseMessage(requestInfo, cancellationToken, activityForAttributes, responseClaims).ConfigureAwait(false);
}
}
}
return response;
}

private void SetBaseUrlForRequestInformation(RequestInformation requestInfo)
{
IDictionaryExtensions.AddOrReplace(requestInfo.PathParameters, "baseurl", BaseUrl!);
Expand All @@ -527,6 +563,7 @@ private void SetBaseUrlForRequestInformation(RequestInformation requestInfo)
return result;
else throw new InvalidOperationException($"Could not convert the request information to a {typeof(T).Name}");
}

private HttpRequestMessage GetRequestMessageFromRequestInformation(RequestInformation requestInfo, Activity? activityForAttributes)
{
using var span = activitySource?.StartActivity(nameof(GetRequestMessageFromRequestInformation));
Expand All @@ -544,37 +581,42 @@ private HttpRequestMessage GetRequestMessageFromRequestInformation(RequestInform
Version = new Version(2, 0)
};

if(requestInfo.RequestOptions.Any())
if(requestInfo.RequestOptions != null)
#if NET5_0_OR_GREATER
{
requestInfo.RequestOptions.ToList().ForEach(x => message.Options.Set(new HttpRequestOptionsKey<IRequestOption>(x.GetType().FullName!), x));
foreach (var option in requestInfo.RequestOptions)
message.Options.Set(new HttpRequestOptionsKey<IRequestOption>(option.GetType().FullName!), option);
}
message.Options.Set(new HttpRequestOptionsKey<IRequestOption>(typeof(ObservabilityOptions).FullName!), obsOptions);
#else
{
requestInfo.RequestOptions.ToList().ForEach(x => IDictionaryExtensions.TryAdd(message.Properties, x.GetType().FullName!, x));
foreach(var option in requestInfo.RequestOptions)
IDictionaryExtensions.TryAdd(message.Properties, option.GetType().FullName!, option);
}
IDictionaryExtensions.TryAdd(message.Properties!, typeof(ObservabilityOptions).FullName, obsOptions);
#endif

if(requestInfo.Content != null && requestInfo.Content != Stream.Null)
message.Content = new StreamContent(requestInfo.Content);
if(requestInfo.Headers?.Any() ?? false)
if(requestInfo.Headers != null)
foreach(var header in requestInfo.Headers)
if(!message.Headers.TryAddWithoutValidation(header.Key, header.Value) && message.Content != null)
message.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);// Try to add the headers we couldn't add to the HttpRequestMessage before to the HttpContent

if(message.Content != null)
{
if(message.Content.Headers.TryGetValues("Content-Length", out var contentLenValues) &&
contentLenValues.Any() &&
contentLenValues.First() is string contentLenValue &&
int.TryParse(contentLenValue, out var contentLenValueInt))
activityForAttributes?.SetTag("http.request_content_length", contentLenValueInt);
if(message.Content.Headers.TryGetValues("Content-Type", out var contentTypeValues) &&
contentTypeValues.Any() &&
contentTypeValues.First() is string contentTypeValue)
activityForAttributes?.SetTag("http.request_content_type", contentTypeValue);
if(message.Content.Headers.TryGetValues("Content-Length", out var contentLenValues))
{
var contentLenEnumerator = contentLenValues.GetEnumerator();
if(contentLenEnumerator.MoveNext() && int.TryParse(contentLenEnumerator.Current, out var contentLenValueInt))
activityForAttributes?.SetTag("http.request_content_length", contentLenValueInt);
}
if(message.Content.Headers.TryGetValues("Content-Type", out var contentTypeValues))
{
var contentTypeEnumerator = contentTypeValues.GetEnumerator();
if(contentTypeEnumerator.MoveNext())
activityForAttributes?.SetTag("http.request_content_type", contentTypeEnumerator.Current);
}
}
return message;
}
Expand Down
84 changes: 52 additions & 32 deletions src/KiotaClientFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------

using System.Linq;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
Expand All @@ -26,8 +26,17 @@ public static class KiotaClientFactory
/// <returns>The <see cref="HttpClient"/> with the default middlewares.</returns>
public static HttpClient Create(HttpMessageHandler? finalHandler = null, IRequestOption[]? optionsForHandlers = null)
{
var defaultHandlers = CreateDefaultHandlers(optionsForHandlers);
var handler = ChainHandlersCollectionAndGetFirstLink(finalHandler ?? GetDefaultHttpMessageHandler(), defaultHandlers.ToArray());
var defaultHandlersEnumerable = CreateDefaultHandlers(optionsForHandlers);
int count = 0;
foreach(var _ in defaultHandlersEnumerable) count++;

var defaultHandlersArray = new DelegatingHandler[count];
int index = 0;
foreach(var handler2 in defaultHandlersEnumerable)
{
defaultHandlersArray[index++] = handler2;
}
var handler = ChainHandlersCollectionAndGetFirstLink(finalHandler ?? GetDefaultHttpMessageHandler(), defaultHandlersArray);
return handler != null ? new HttpClient(handler) : new HttpClient();
}

Expand All @@ -39,9 +48,16 @@ public static HttpClient Create(HttpMessageHandler? finalHandler = null, IReques
/// <returns>The <see cref="HttpClient"/> with the custom handlers.</returns>
public static HttpClient Create(IList<DelegatingHandler> handlers, HttpMessageHandler? finalHandler = null)
{
if(handlers == null || !handlers.Any())
if(handlers == null || handlers.Count == 0)
return Create(finalHandler);
var handler = ChainHandlersCollectionAndGetFirstLink(finalHandler ?? GetDefaultHttpMessageHandler(), handlers.ToArray());

DelegatingHandler[] handlersArray = new DelegatingHandler[handlers.Count];
for(int i = 0; i < handlers.Count; i++)
{
handlersArray[i] = handlers[i];
}

var handler = ChainHandlersCollectionAndGetFirstLink(finalHandler ?? GetDefaultHttpMessageHandler(), handlersArray);
return handler != null ? new HttpClient(handler) : new HttpClient();
}

Expand All @@ -51,35 +67,39 @@ public static HttpClient Create(IList<DelegatingHandler> handlers, HttpMessageHa
/// <returns>A list of the default handlers used by the client.</returns>
public static IList<DelegatingHandler> CreateDefaultHandlers(IRequestOption[]? optionsForHandlers = null)

Check warning on line 68 in src/KiotaClientFactory.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 20 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
optionsForHandlers ??= [];

return new List<DelegatingHandler>
{
//add the default middlewares as they are ready, and add them to the list below as well

optionsForHandlers.OfType<UriReplacementHandlerOption>().FirstOrDefault() is UriReplacementHandlerOption uriReplacementOption
? new UriReplacementHandler<UriReplacementHandlerOption>(uriReplacementOption)
: new UriReplacementHandler<UriReplacementHandlerOption>(),

optionsForHandlers.OfType<RetryHandlerOption>().FirstOrDefault() is RetryHandlerOption retryHandlerOption
? new RetryHandler(retryHandlerOption)
: new RetryHandler(),

optionsForHandlers.OfType<RedirectHandlerOption>().FirstOrDefault() is RedirectHandlerOption redirectHandlerOption
? new RedirectHandler(redirectHandlerOption)
: new RedirectHandler(),
optionsForHandlers ??= Array.Empty<IRequestOption>();

optionsForHandlers.OfType<ParametersNameDecodingOption>().FirstOrDefault() is ParametersNameDecodingOption parametersNameDecodingOption
? new ParametersNameDecodingHandler(parametersNameDecodingOption)
: new ParametersNameDecodingHandler(),
UriReplacementHandlerOption? uriReplacementOption = null;
RetryHandlerOption? retryHandlerOption = null;
RedirectHandlerOption? redirectHandlerOption = null;
ParametersNameDecodingOption? parametersNameDecodingOption = null;
UserAgentHandlerOption? userAgentHandlerOption = null;
HeadersInspectionHandlerOption? headersInspectionHandlerOption = null;

optionsForHandlers.OfType<UserAgentHandlerOption>().FirstOrDefault() is UserAgentHandlerOption userAgentHandlerOption
? new UserAgentHandler(userAgentHandlerOption)
: new UserAgentHandler(),
foreach(var option in optionsForHandlers)
{
if(uriReplacementOption == null && option is UriReplacementHandlerOption uriOption)
uriReplacementOption = uriOption;
else if(retryHandlerOption == null && option is RetryHandlerOption retryOption)
retryHandlerOption = retryOption;
else if(redirectHandlerOption == null && option is RedirectHandlerOption redirectOption)
redirectHandlerOption = redirectOption;
else if(parametersNameDecodingOption == null && option is ParametersNameDecodingOption parametersOption)
parametersNameDecodingOption = parametersOption;
else if(userAgentHandlerOption == null && option is UserAgentHandlerOption userAgentOption)
userAgentHandlerOption = userAgentOption;
else if(headersInspectionHandlerOption == null && option is HeadersInspectionHandlerOption headersOption)
headersInspectionHandlerOption = headersOption;
}

optionsForHandlers.OfType<HeadersInspectionHandlerOption>().FirstOrDefault() is HeadersInspectionHandlerOption headersInspectionHandlerOption
? new HeadersInspectionHandler(headersInspectionHandlerOption)
: new HeadersInspectionHandler(),
return new List<DelegatingHandler>
{
uriReplacementOption != null ? new UriReplacementHandler<UriReplacementHandlerOption>(uriReplacementOption) : new UriReplacementHandler<UriReplacementHandlerOption>(),
retryHandlerOption != null ? new RetryHandler(retryHandlerOption) : new RetryHandler(),
redirectHandlerOption != null ? new RedirectHandler(redirectHandlerOption) : new RedirectHandler(),
parametersNameDecodingOption != null ? new ParametersNameDecodingHandler(parametersNameDecodingOption) : new ParametersNameDecodingHandler(),
userAgentHandlerOption != null ? new UserAgentHandler(userAgentHandlerOption) : new UserAgentHandler(),
headersInspectionHandlerOption != null ? new HeadersInspectionHandler(headersInspectionHandlerOption) : new HeadersInspectionHandler(),
};
}

Expand Down Expand Up @@ -109,7 +129,7 @@ public static IList<DelegatingHandler> CreateDefaultHandlers(IRequestOption[]? o
/// <returns>The created <see cref="DelegatingHandler"/>.</returns>
public static DelegatingHandler? ChainHandlersCollectionAndGetFirstLink(HttpMessageHandler? finalHandler, params DelegatingHandler[] handlers)
{
if(handlers == null || !handlers.Any()) return default;
if(handlers == null || handlers.Length == 0) return default;
var handlersCount = handlers.Length;
for(var i = 0; i < handlersCount; i++)
{
Expand Down
Loading

0 comments on commit f93a5f8

Please sign in to comment.