Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
2 of 12 tasks
electrocnic opened this issue Sep 5, 2024 · 2 comments
Open
2 of 12 tasks

Comments

@electrocnic
Copy link

Which components would you like this sample to be about?

  • mgt-login
  • mgt-person
  • mgt-person-card
  • mgt-people
  • mgt-people-picker
  • mgt-agenda
  • mgt-tasks
  • mgt-todo
  • mgt-teams-channel-picker
  • mgt-file
  • mgt-file-list
  • mgt-get

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
Copy link
Author

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
Copy link
Collaborator

sebastienlevert commented Sep 6, 2024

@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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants