diff --git a/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2.cs b/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2.cs
new file mode 100644
index 0000000000..65722390e9
--- /dev/null
+++ b/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerFx.Connectors
+{
+ ///
+ /// Delegate for providing a bearer token for authentication.
+ ///
+ /// Cancellation token.
+ /// Token without "Bearer" scheme as the prefix.
+ public delegate Task PowerPlatformConnectorClient2BearerTokenProvider(
+ CancellationToken cancellationToken);
+
+ public interface IPowerPlatformConnectorClient2
+ {
+ ///
+ /// Sends an HTTP request to the Power Platform connector.
+ ///
+ /// HTTP method.
+ /// Operation path and query string.
+ /// Headers.
+ /// Content.
+ /// Diagnostic options.
+ /// Cancellation token.
+ /// .
+ Task SendAsync(
+ HttpMethod method,
+ string operationPathAndQuery,
+ IEnumerable>> headers,
+ HttpContent content,
+ PowerPlatformConnectorClient2DiagnosticOptions diagnosticOptions,
+ CancellationToken cancellationToken);
+ }
+}
diff --git a/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2Extensions.cs b/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2Extensions.cs
new file mode 100644
index 0000000000..bdd9561351
--- /dev/null
+++ b/src/libraries/Microsoft.PowerFx.Connectors/IPowerPlatformConnectorClient2Extensions.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+using System;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.PowerFx.Connectors
+{
+ public static class IPowerPlatformConnectorClient2Extensions
+ {
+ ///
+ /// Sends an HTTP request to the Power Platform connector.
+ ///
+ /// Client.
+ /// HTTP request message. URI path will be used as a relative path for the connector request.
+ /// Diagnostic options.
+ /// Cancellation token.
+ /// .
+ public static Task SendAsync(
+ this IPowerPlatformConnectorClient2 client,
+ HttpRequestMessage requestMessage,
+ PowerPlatformConnectorClient2DiagnosticOptions diagnosticOptions,
+ CancellationToken cancellationToken)
+ {
+ if (client is null)
+ {
+ throw new ArgumentNullException(nameof(client));
+ }
+
+ if (requestMessage is null)
+ {
+ throw new ArgumentNullException(nameof(requestMessage));
+ }
+
+ return client.SendAsync(
+ requestMessage.Method,
+ requestMessage.RequestUri.PathAndQuery,
+ requestMessage.Headers,
+ requestMessage.Content,
+ diagnosticOptions,
+ cancellationToken);
+ }
+ }
+}
diff --git a/src/libraries/Microsoft.PowerFx.Connectors/OpenApiExtensions.cs b/src/libraries/Microsoft.PowerFx.Connectors/OpenApiExtensions.cs
index 4d24231d31..7922d46efc 100644
--- a/src/libraries/Microsoft.PowerFx.Connectors/OpenApiExtensions.cs
+++ b/src/libraries/Microsoft.PowerFx.Connectors/OpenApiExtensions.cs
@@ -51,6 +51,17 @@ public static class OpenApiExtensions
public static string GetAuthority(this OpenApiDocument openApiDocument, SupportsConnectorErrors errors) => GetUriElement(openApiDocument, (uri) => uri.Authority, errors);
private static string GetUriElement(this OpenApiDocument openApiDocument, Func getElement, SupportsConnectorErrors errors)
+ {
+ var uri = GetFirstServerUri(openApiDocument, errors);
+ if (uri is null)
+ {
+ return null;
+ }
+
+ return getElement(uri);
+ }
+
+ internal static Uri GetFirstServerUri(this OpenApiDocument openApiDocument, SupportsConnectorErrors errors)
{
if (openApiDocument?.Servers == null)
{
@@ -72,8 +83,7 @@ private static string GetUriElement(this OpenApiDocument openApiDocument, Func> list, bool isNumber) = openApiParameter.GetEnumValues();
if (list != null && list.Any())
@@ -1205,7 +1215,7 @@ internal static Dictionary GetParameterMap(thi
{
// https://github.com/microsoft/OpenAPI.NET/issues/533
// https://github.com/microsoft/Power-Fx/pull/1987 - https://github.com/microsoft/Power-Fx/issues/1982
- // api-version, x-ms-api-version, X-GitHub-Api-Version...
+ // api-version, x-ms-api-version, X-GitHub-Api-Version...
if (prm.Key.EndsWith("api-version", StringComparison.OrdinalIgnoreCase))
{
fv = FormulaValue.New(dtv.GetConvertedValue(TimeZoneInfo.Utc).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
diff --git a/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs b/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs
new file mode 100644
index 0000000000..3836f3dad9
--- /dev/null
+++ b/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2.cs
@@ -0,0 +1,197 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.OpenApi.Models;
+
+namespace Microsoft.PowerFx.Connectors
+{
+ ///
+ /// Client for invoking operations of Power Platform connectors.
+ ///
+ public class PowerPlatformConnectorClient2 : IPowerPlatformConnectorClient2
+ {
+ private readonly string _baseUrlStr;
+ private readonly HttpMessageInvoker _httpMessageInvoker;
+ private readonly PowerPlatformConnectorClient2BearerTokenProvider _tokenProvider;
+ private readonly string _environmentId;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Document used for extracting the base URL.
+ /// HTTP message invoker.
+ /// Bearer token provider.
+ /// Environment ID.
+ public PowerPlatformConnectorClient2(
+ OpenApiDocument document,
+ HttpMessageInvoker httpMessageInvoker,
+ PowerPlatformConnectorClient2BearerTokenProvider tokenProvider,
+ string environmentId)
+ : this(GetBaseUrlFromOpenApiDocument(document), httpMessageInvoker, tokenProvider, environmentId)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Base URL for requests.
+ /// HTTP message invoker.
+ /// Bearer token provider.
+ /// Environment ID.
+ public PowerPlatformConnectorClient2(
+ Uri baseUrl,
+ HttpMessageInvoker httpMessageInvoker,
+ PowerPlatformConnectorClient2BearerTokenProvider tokenProvider,
+ string environmentId)
+ {
+ this._baseUrlStr = GetBaseUrlStr(baseUrl ?? throw new ArgumentNullException(nameof(baseUrl)));
+ this._httpMessageInvoker = httpMessageInvoker ?? throw new ArgumentNullException(nameof(httpMessageInvoker));
+ this._tokenProvider = tokenProvider ?? throw new ArgumentNullException(nameof(tokenProvider));
+ this._environmentId = environmentId ?? throw new ArgumentNullException(nameof(environmentId));
+
+ static string GetBaseUrlStr(Uri uri)
+ {
+ var str = uri.GetLeftPart(UriPartial.Path);
+
+ // Note (shgogna): Ensure the base URL does NOT end with "/".
+ // This will allow us to concatenate the operation path to the base URL
+ // without worrying about the "/".
+ str = str.TrimEnd('/');
+ return str;
+ }
+ }
+
+ //
+ public Task SendAsync(
+ HttpMethod method,
+ string operationPathAndQuery,
+ IEnumerable>> headers,
+ HttpContent content,
+ PowerPlatformConnectorClient2DiagnosticOptions diagnosticOptions,
+ CancellationToken cancellationToken)
+ {
+ if (operationPathAndQuery is null)
+ {
+ throw new ArgumentNullException(nameof(operationPathAndQuery));
+ }
+
+ var uri = this.CombineBaseUrlWithOperationPathAndQuery(operationPathAndQuery);
+
+ if (!uri.AbsoluteUri.StartsWith(this._baseUrlStr, StringComparison.Ordinal))
+ {
+ throw new ArgumentException("Path traversal detected during combination of base URL path and operation path.", nameof(operationPathAndQuery));
+ }
+
+ return this.InternalSendAsync(
+ method,
+ uri,
+ headers,
+ content,
+ diagnosticOptions,
+ cancellationToken);
+ }
+
+ private async Task InternalSendAsync(
+ HttpMethod method,
+ Uri uri,
+ IEnumerable>> headers,
+ HttpContent content,
+ PowerPlatformConnectorClient2DiagnosticOptions diagnosticOptions,
+ CancellationToken cancellationToken)
+ {
+ var authToken = await this._tokenProvider(cancellationToken).ConfigureAwait(false);
+
+ using (var req = new HttpRequestMessage(method, uri))
+ {
+ req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
+
+ this.AddDiagnosticHeaders(diagnosticOptions, req);
+
+ foreach (var header in headers)
+ {
+ req.Headers.Add(header.Key, header.Value);
+ }
+
+ req.Content = content;
+ return await this._httpMessageInvoker.SendAsync(req, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ private Uri CombineBaseUrlWithOperationPathAndQuery(string operationPathAndQuery)
+ {
+ if (operationPathAndQuery.StartsWith("/", StringComparison.Ordinal))
+ {
+ return new Uri(this._baseUrlStr + operationPathAndQuery);
+ }
+ else
+ {
+ return new Uri(this._baseUrlStr + "/" + operationPathAndQuery);
+ }
+ }
+
+ private void AddDiagnosticHeaders(
+ PowerPlatformConnectorClient2DiagnosticOptions diagnosticOptions,
+ HttpRequestMessage req)
+ {
+ var userAgent = string.IsNullOrWhiteSpace(diagnosticOptions?.UserAgent)
+ ? $"PowerFx/{PowerPlatformConnectorClient.Version}"
+ : $"{diagnosticOptions.UserAgent} PowerFx/{PowerPlatformConnectorClient.Version}";
+
+ var clientRequestId = string.IsNullOrWhiteSpace(diagnosticOptions?.ClientRequestId)
+ ? Guid.NewGuid().ToString()
+ : diagnosticOptions.ClientRequestId;
+
+ // CorrelationID can be the same as ClientRequestID
+ var correlationId = string.IsNullOrWhiteSpace(diagnosticOptions?.CorrelationId)
+ ? clientRequestId
+ : diagnosticOptions.CorrelationId;
+
+ req.Headers.Add("User-Agent", userAgent);
+ req.Headers.Add("x-ms-user-agent", userAgent);
+ req.Headers.Add("x-ms-client-environment-id", $"/providers/Microsoft.PowerApps/environments/{this._environmentId}");
+ req.Headers.Add("x-ms-client-request-id", clientRequestId);
+ req.Headers.Add("x-ms-correlation-id", correlationId);
+
+ if (!string.IsNullOrWhiteSpace(diagnosticOptions?.ClientSessionId))
+ {
+ req.Headers.Add("x-ms-client-session-id", diagnosticOptions.ClientSessionId);
+ }
+
+ if (!string.IsNullOrWhiteSpace(diagnosticOptions?.ClientTenantId))
+ {
+ req.Headers.Add("x-ms-client-tenant-id", diagnosticOptions.ClientTenantId);
+ }
+
+ if (!string.IsNullOrWhiteSpace(diagnosticOptions?.ClientObjectId))
+ {
+ req.Headers.Add("x-ms-client-object-id", diagnosticOptions.ClientObjectId);
+ }
+ }
+
+ private static Uri GetBaseUrlFromOpenApiDocument(OpenApiDocument document)
+ {
+ ConnectorErrors errors = new ConnectorErrors();
+ Uri uri = document.GetFirstServerUri(errors);
+
+ if (uri is null)
+ {
+ errors.AddError("Swagger document doesn't contain an endpoint");
+ }
+
+ if (errors.HasErrors)
+ {
+ throw new PowerFxConnectorException(string.Join(", ", errors.Errors));
+ }
+
+ return uri;
+ }
+ }
+}
diff --git a/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2DiagnosticOptions.cs b/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2DiagnosticOptions.cs
new file mode 100644
index 0000000000..a4b29564e5
--- /dev/null
+++ b/src/libraries/Microsoft.PowerFx.Connectors/PowerPlatformConnectorClient2DiagnosticOptions.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+namespace Microsoft.PowerFx.Connectors
+{
+ public class PowerPlatformConnectorClient2DiagnosticOptions
+ {
+ public string ClientSessionId { get; set; }
+
+ public string ClientRequestId { get; set; }
+
+ public string ClientTenantId { get; set; }
+
+ public string ClientObjectId { get; set; }
+
+ public string CorrelationId { get; set; }
+
+ public string UserAgent { get; set; }
+ }
+}