pnp / mgt-samples

A curated collection of community-contributed samples using the Microsoft Graph Toolkit
https://pnp.github.io/mgt-samples/
MIT License
30 stars 13 forks source link

Please provide a proxy-provider-asp-net-core sample for the new Graph SDK v5 with Kiota #22

Open electrocnic opened 2 months ago

electrocnic commented 2 months ago

Which components would you like this sample to be about?

Sample description

I am struggling to migrate the previous proxy provider from https://github.com/pnp/mgt-samples/tree/main/samples/app/proxy-provider-asp-net-core to the new Graph SDK with Kiota versions. The versions I wanted to migrate to are:

<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8" />
<PackageReference Include="Microsoft.Graph" Version="5.56.1" />
<PackageReference Include="Microsoft.Identity.Web" Version="3.1.0" />
<PackageReference Include="Microsoft.Identity.Web.GraphServiceClient" Version="3.1.0" />
<PackageReference Include="Microsoft.Identity.Web.UI" Version="3.1.0" />

I tried something like this, but the response is always null or I even get into the exception block with no helpful stacktrace in the case of $batch requests:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Graph;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Http.HttpClientLibrary;

namespace webapi.Controllers.api.v1
{
    [Authorize]
    [Route("[controller]")]
    [ApiController]
    public class GraphProxyController(GraphServiceClient graphServiceClient) : ControllerBase
    {
        private readonly GraphServiceClient _graphServiceClient = graphServiceClient;

        [HttpGet]
        [Route("{*all}")]
        public async Task<IActionResult> GetAsync(string all)
        {
            return await ProcessRequestAsync(Method.GET, all, null).ConfigureAwait(false);
        }

        [HttpPost]
        [Route("{*all}")]
        public async Task<IActionResult> PostAsync(string all, [FromBody] object body)
        {
            return await ProcessRequestAsync(Method.POST, all, body).ConfigureAwait(false);
        }

        [HttpDelete]
        [Route("{*all}")]
        public async Task<IActionResult> DeleteAsync(string all)
        {
            return await ProcessRequestAsync(Method.DELETE, all, null).ConfigureAwait(false);
        }

        [HttpPut]
        [Route("{*all}")]
        public async Task<IActionResult> PutAsync(string all, [FromBody] object body)
        {
            return await ProcessRequestAsync(Method.PUT, all, body).ConfigureAwait(false);
        }

        [HttpPatch]
        [Route("{*all}")]
        public async Task<IActionResult> PatchAsync(string all, [FromBody] object body)
        {
            return await ProcessRequestAsync(Method.PATCH, all, body).ConfigureAwait(false);
        }

        private async Task<IActionResult> ProcessRequestAsync(Method method, string all, object content)
        {
            var qs = HttpContext.Request.QueryString;
            var url = $"{GetBaseUrlWithoutVersion(_graphServiceClient)}/{all}{qs.ToUriComponent()}";

            var requestInformation = new RequestInformation
            {
                HttpMethod = method,
                UrlTemplate = url
            };

            var neededHeaders = Request.Headers
                .Where(h => h.Key.ToLower() == "if-match" || h.Key.ToLower() == "consistencylevel")
                .ToList();

            foreach (var header in neededHeaders)
            {
                requestInformation.Headers.Add(header.Key, string.Join(",", header.Value));
            }

            if (content != null)
            {
                // var jsonContent = System.Text.Json.JsonSerializer.Serialize(content);
                // var contentStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(jsonContent));
                // requestInformation.SetStreamContent(contentStream, "application/json");
                requestInformation.SetContentFromScalar<string>(_graphServiceClient
                    .RequestAdapter, "application/json", content?.ToString());
            }

            try
            {
                var response = await _graphServiceClient
                    .RequestAdapter
                    .SendPrimitiveAsync<string>(requestInformation, cancellationToken: CancellationToken.None);

                return Content(response, "application/json");
            }
            catch (ApiException ex)
            {
                return StatusCode(ex.ResponseStatusCode, ex.Message);
            }
        }

        private string GetBaseUrlWithoutVersion(GraphServiceClient graphClient)
        {
            var baseUrl = graphClient.RequestAdapter.BaseUrl;
            var index = baseUrl.LastIndexOf('/');
            return baseUrl.Substring(0, index);
        }
    }
}

Are you willing to help?

Yes

electrocnic commented 2 months ago

I found a working solution but only tested it with the mgt-react person component and the two scopes "User.Read profile". The major thing was, that Kiota is not really suited for such Proxy implementations, as it seems, according to its docs: https://learn.microsoft.com/en-us/openapi/kiota/abstractions#request-adapter

This interface is meant to support the generated code and not to be used by application developers.

Therefore, implementing the proxy with System.Net.Http was the alternative, but I now had to take care of the correct auth header manually, which was done automatically before with the old v4 graphServiceClient:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Net.Http.Headers;
using Microsoft.Extensions.Primitives;
using Microsoft.Graph;
using Microsoft.Identity.Web;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;

namespace webapi.Controllers.api.v1
{
    [Authorize]
    [Route("[controller]")]
    [ApiController]
    public class GraphProxyController : ControllerBase
    {
        private readonly HttpClient _httpClient;
        private readonly ITokenAcquisition _tokenAcquisition;

        public GraphProxyController(IHttpClientFactory httpClientFactory, GraphServiceClient graphServiceClient, ITokenAcquisition tokenAcquisition)
        {
            _httpClient = httpClientFactory.CreateClient();
            _httpClient.BaseAddress = new Uri(GetBaseUrlWithoutVersion(graphServiceClient));
            _tokenAcquisition = tokenAcquisition;
        }

        [HttpGet("{*all}")]
        public async Task<IActionResult> GetAsync(string all)
        {
            return await ProcessRequestAsync(HttpMethod.Get, all, null).ConfigureAwait(false);
        }

        [HttpPost("{*all}")]
        public async Task<IActionResult> PostAsync(string all, [FromBody] object body)
        {
            return await ProcessRequestAsync(HttpMethod.Post, all, body).ConfigureAwait(false);
        }

        [HttpDelete("{*all}")]
        public async Task<IActionResult> DeleteAsync(string all)
        {
            return await ProcessRequestAsync(HttpMethod.Delete, all, null).ConfigureAwait(false);
        }

        [HttpPut("{*all}")]
        public async Task<IActionResult> PutAsync(string all, [FromBody] object body)
        {
            return await ProcessRequestAsync(HttpMethod.Put, all, body).ConfigureAwait(false);
        }

        [HttpPatch("{*all}")]
        public async Task<IActionResult> PatchAsync(string all, [FromBody] object body)
        {
            return await ProcessRequestAsync(HttpMethod.Patch, all, body).ConfigureAwait(false);
        }

        private async Task<IActionResult> ProcessRequestAsync(HttpMethod method, string all, object content)
        {
            // Construct the full Graph API request URL with query string
            var requestUri = $"{all}{Request.QueryString.ToUriComponent()}";
            var requestMessage = new HttpRequestMessage(method, requestUri);

            var scopes = new[] { "User.Read", "profile" };
            var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes);
            requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

            var headersToForward = new[] { "If-Match", "ConsistencyLevel" };
            foreach (var headerKey in headersToForward)
            {
                if (Request.Headers.TryGetValue(headerKey, out StringValues headerValues))
                {
                    requestMessage.Headers.TryAddWithoutValidation(headerKey, headerValues.ToArray());
                }
            }

            if (content != null)
            {
                if (content is JObject jsonObject)
                {
                    // Use Newtonsoft.Json to serialize JObject
                    var jsonContent = JsonConvert.SerializeObject(jsonObject);
                    requestMessage.Content = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json");
                }
                else
                {
                    // For other content types, use System.Text.Json
                    var jsonContent = System.Text.Json.JsonSerializer.Serialize(content);
                    requestMessage.Content = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json");
                }
            }

            var response = await _httpClient.SendAsync(requestMessage);
            var responseBody = await response.Content.ReadAsStringAsync();
            var contentType = response.Content.Headers.ContentType?.ToString() ?? "application/json";
            return new ContentResult
            {
                Content = responseBody,
                ContentType = contentType,
                StatusCode = (int)response.StatusCode
            };
        }

        private string GetBaseUrlWithoutVersion(GraphServiceClient graphClient)
        {
            var baseUrl = graphClient.RequestAdapter.BaseUrl;
            var index = baseUrl.LastIndexOf('/');
            return baseUrl.Substring(0, index);
        }
    }
}
sebastienlevert commented 2 months ago

@mnickii @andrueastman can you help with answering with the questions? It's very possible that Graph v5 is a little bit less suited for this scenario but I feel it's an interesting one. Would love your perspective. Thanks!