Search code examples
asp.net-coremicrosoft-graph-apiblazor-webassemblymicrosoft-graph-sdks.net-7.0

Blazor WebAssembly GraphClientExtensions fails after upgrading Microsoft.Graph to version 5.0


Blazor WebAssembly GraphClientExtensions fails after upgrading Microsoft.Graph to version 5.0

Framework: Dot NET 7

Errors:

  • The type or namespace name 'IAuthenticationProvider' could not be found

  • The type or namespace name 'IHttpProvider' could not be found

  • The type or namespace name 'ISerializer' could not be found

GraphClientExtensions Code:

    internal static class GraphClientExtensions
    {
        public static IServiceCollection AddGraphClient(
            this IServiceCollection services, params string[] scopes)
        {
            services.Configure<RemoteAuthenticationOptions<MsalProviderOptions>>(
                options =>
                {
                    foreach (var scope in scopes)
                    {
                        options.ProviderOptions.AdditionalScopesToConsent.Add(scope);
                    }
                });

            services.AddScoped<IAuthenticationProvider,
                NoOpGraphAuthenticationProvider>();
            services.AddScoped<IHttpProvider, HttpClientHttpProvider>(sp =>
                new HttpClientHttpProvider(new HttpClient()));
            services.AddScoped(sp =>
            {
                return new GraphServiceClient(
                    sp.GetRequiredService<IAuthenticationProvider>(),
                    sp.GetRequiredService<IHttpProvider>());
            });

            return services;
        }

        private class NoOpGraphAuthenticationProvider : IAuthenticationProvider
        {
            public NoOpGraphAuthenticationProvider(IAccessTokenProvider tokenProvider)
            {
                TokenProvider = tokenProvider;
            }

            public IAccessTokenProvider TokenProvider { get; }

            public async Task AuthenticateRequestAsync(HttpRequestMessage request)
            {
                var result = await TokenProvider.RequestAccessToken(
                    new AccessTokenRequestOptions()
                    {
                        Scopes = new[] { "https://graph.microsoft.com/User.Read" }
                    });

                if (result.TryGetToken(out var token))
                {
                    request.Headers.Authorization ??= new AuthenticationHeaderValue(
                        "Bearer", token.Value);
                }
            }
        }

        private class HttpClientHttpProvider : IHttpProvider
        {
            private readonly HttpClient http;

            public HttpClientHttpProvider(HttpClient http)
            {
                this.http = http;
            }

            public ISerializer Serializer { get; } = new Serializer();

            public TimeSpan OverallTimeout { get; set; } = TimeSpan.FromSeconds(300);

            public void Dispose()
            {
            }

            public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
            {
                return http.SendAsync(request);
            }

            public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
                HttpCompletionOption completionOption,
                CancellationToken cancellationToken)
            {
                return http.SendAsync(request, completionOption, cancellationToken);
            }
        }
    }

Upgrading the below packages caused the error:

  • Microsoft.Graph from 4.54.0 to 5.0.0

  • Microsoft.Graph.Core from 2.0.15 to 3.0.0


Solution

  • Just attach a guide for migrating to V5 here.

    ==============================

    First, the issue comes from the changes of the package. There's no IHttpProvider and ISerializer in Microsoft.Graph V5.0, and IAuthenticationProvider changed from Microsoft.Graph.IAuthenticationProvider to Microsoft.AspNetCore.Components.WebAssembly.Authentication.IAccessTokenProvider.

    To troubleshoot this issue, I think we need to first integrate AAD into blazor wsam app. We just need to create a new application with picking up the authentication.

    enter image description here

    Then we add the Graph SDK. But the tutorial in official document hasn't updated. Using code in screenshot below will had issue like what you mentioned.

    enter image description here

    Now the construction method for GraphServiceClient is like below:

    public GraphServiceClient(IRequestAdapter requestAdapter, string baseUrl = null): base(InitializeRequestAdapterWithBaseUrl(requestAdapter,baseUrl))
    {
        this.RequestAdapter = requestAdapter;
    }
    
    public GraphServiceClient(TokenCredential tokenCredential,IEnumerable<string> scopes = null,string baseUrl = null
        ):this(new Microsoft.Graph.Authentication.AzureIdentityAuthenticationProvider(tokenCredential, null, null,scopes?.ToArray() ?? Array.Empty<string>()), baseUrl)
    {
    }
    
    public GraphServiceClient(IAuthenticationProvider authenticationProvider,string baseUrl = null
        ): this(new BaseGraphRequestAdapter(authenticationProvider, graphClientOptions),baseUrl)
    {
    }
    
    public GraphServiceClient(HttpClient httpClient, IAuthenticationProvider authenticationProvider = null,
        string baseUrl = null):this(new BaseGraphRequestAdapter(authenticationProvider ?? new AnonymousAuthenticationProvider(), graphClientOptions, httpClient: httpClient),baseUrl)
    {
    }
    

    So we need to re-implement the IAuthenticationProvider so that we can use it to inject new version GraphServiceClient. And below is what I did and it worked for me.

    In blazor webassembly, we can change to write code like this:

    Program.cs:

    using BlazorWsamNet7;
    using BlazorWsamNet7.Data;
    using Microsoft.AspNetCore.Components.Web;
    using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
    
    var builder = WebAssemblyHostBuilder.CreateDefault(args);
    builder.RootComponents.Add<App>("#app");
    builder.RootComponents.Add<HeadOutlet>("head::after");
    
    builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
    
    var baseUrl = string.Join("/",
        builder.Configuration.GetSection("MicrosoftGraph")["BaseUrl"],
        builder.Configuration.GetSection("MicrosoftGraph")["Version"]);
    var scopes = builder.Configuration.GetSection("MicrosoftGraph:Scopes")
        .Get<List<string>>();
    builder.Services.AddGraphClient(baseUrl, scopes);
    
    builder.Services.AddMsalAuthentication(options =>
    {
        builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
    });
    
    await builder.Build().RunAsync();
    

    Then GraphClientExtensions:

    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    using Microsoft.Authentication.WebAssembly.Msal.Models;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Graph;
    using Microsoft.Kiota.Abstractions;
    using Microsoft.Kiota.Abstractions.Authentication;
    using System.IdentityModel.Tokens.Jwt;
    using System.Net.Http;
    using System.Net.Http.Headers;
    
    namespace BlazorWsamNet7.Data
    {
        internal static class GraphClientExtensions
        {
            public static IServiceCollection AddGraphClient(
                this IServiceCollection services, string? baseUrl, List<string>? scopes)
            {
                services.Configure<RemoteAuthenticationOptions<MsalProviderOptions>>(
                    options =>
                    {
                        scopes?.ForEach((scope) =>
                        {
                            options.ProviderOptions.AdditionalScopesToConsent.Add(scope);
                        });
                    });
                services.AddScoped<IAuthenticationProvider, GraphAuthenticationProvider>();
                services.AddScoped(sp =>
                {
                    return new GraphServiceClient(
                        new HttpClient(),
                        sp.GetRequiredService<IAuthenticationProvider>(),
                        baseUrl);
                });
                return services;
            }
    
            private class GraphAuthenticationProvider : IAuthenticationProvider
            {
                private readonly IConfiguration config;
    
                public GraphAuthenticationProvider(Microsoft.AspNetCore.Components.WebAssembly.Authentication.IAccessTokenProvider tokenProvider,
                    IConfiguration config)
                {
                    TokenProvider = tokenProvider;
                    this.config = config;
                }
    
                public Microsoft.AspNetCore.Components.WebAssembly.Authentication.IAccessTokenProvider TokenProvider { get; }
    
                public async Task AuthenticateRequestAsync(RequestInformation request, Dictionary<string, object>? additionalAuthenticationContext = null, CancellationToken cancellationToken = default)
                {
                    var result = await TokenProvider.RequestAccessToken(
                        new AccessTokenRequestOptions()
                        {
                            Scopes = config.GetSection("MicrosoftGraph:Scopes").Get<string[]>()
                        });
    
                    if (result.TryGetToken(out var token))
                    {
                        //request.Headers.Authorization ??= new AuthenticationHeaderValue("Bearer", token.Value);
                        request.Headers.Add("Authorization", "Bearer "+token.Value);
                    }
                }
            }
        }
    }
    

    appsetting.json:

    {
      "AzureAd": {
        "Authority": "https://login.microsoftonline.com/tenant_id",
        "ClientId": "aad_client_id",
        "ValidateAuthority": true
      },
      "MicrosoftGraph": {
        "BaseUrl": "https://graph.microsoft.com",
        "Version": "v1.0",
        "Scopes": [
          "user.read"
        ]
      }
    }
    

    my View razor component:

    @page "/profile"
    @using Microsoft.AspNetCore.Authorization
    @using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    @using Microsoft.Graph;
    @using Microsoft.Graph.Models;
    @inject GraphServiceClient GraphClient
    @attribute [Authorize]
    
    <h3>UserProfile</h3>
    @{
        <table class="table">
            <thead>
                <tr>
                    <th>Property</th>
                    <th>Value</th>
                </tr>
            </thead>
            <tr>
                <td> DisplayName </td>
                <td> @_user.DisplayName </td>
            </tr>
            <tr>
                <td> UserPrincipalName </td>
                <td> @_user.UserPrincipalName </td>
            </tr>
        </table>
    }
    @code {
        protected User _user = new User();
    
        protected override async Task OnInitializedAsync()
        {
            try
            {
                _user = await GraphClient.Me.GetAsync();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }
    

    Test result:

    enter image description here enter image description here