Build a web app to manage a custom provider in Viva Learning with Blazor
Published: 2023-10-31 10:06:51

---
title: Build a web app to manage a custom provider in Viva Learning with Blazor
published: true
date: 2022-11-29 15:57:21 UTC
tags: 
canonical_url: 
---

[In the previous post](https://dev.to/qmatteoq/adding-a-custom-content-provider-to-viva-learning-with-microsoft-graph-3a50-temp-slug-5748805) we have learned the basic concepts behind the Viva Learning integration offered by the Microsoft Graph. However, the scenario we implemented wasn't very realistic. We have learned which APIs to use and how to use them but, in a real scenario, you won't use Postman to manage your custom catalog of learning content, but you would rely on a more robust solution.

 

In this post, we're going to reuse the concepts we have learned to build a better experience: a web application, that we can use to manage our custom learning provider and its contents. We'll focus on how to implement in a real application some of the peculiar features we have learned about these APIs, like the fact that a different set of permissions is required based on the type of content you're working with.

 

Since I'm a .NET developer, I'm going to build the web application using [Blazor](https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor), the latest addition to the .NET family which enables us to build client-side web apps using C# instead of JavaScript as a programming language. In this post, I'm going to assume you already have a basic knowledge of Blazor and basic ASP.NET Core concepts, like Razor components and dependency injection.

 

Let's start from the basics: adding authentication and authorization.

 

### Setting up the Blazor app

Blazor comes into two different flavors: [fully client side](https://learn.microsoft.com/en-us/aspnet/core/blazor/?WT.mc_id=dotnet-35129-website&view=aspnetcore-7.0#blazor-webassembly), using Web Assembly, and [server side](https://learn.microsoft.com/en-us/aspnet/core/blazor/?WT.mc_id=dotnet-35129-website&view=aspnetcore-7.0#blazor-server). The final experience is exactly the same and both approaches enables us to build client side applications using C#. In the first case, however, the application is truly client-side only, since it runs directly in the browser. In the second case, instead, the application is still client-side, but it's backed by a server side application, which takes care of "translating" the C# code into JavaScript through a Signal R channel. For our project, we're going to use the server side approach, since it enables us to fully use the Microsoft Identity platform capabilities and features.

 

Open Visual Studio, create a new project and look for the template called  **Blazor Server App**. When you reach the  **Additional information**  step, feel free to pick .NET 6 or .NET 7 based on your requirements (.NET 7 is newer, but .NET 6 is marked as LTS, so it will be supported until 2024). What's important is that, in the  **Authentication Type**  dropdown, you pick  **Microsoft Identity Platform**.

 

![new-blazor-project.png](https://techcommunity.microsoft.com/t5/image/serverpage/image-id/422030i3BDD9772CE55A75A/image-size/large?v=v2&px=999 "new-blazor-project.png")

 

After you hit the  **Create**  button, Visual Studio will scaffold the project. However, before starting to work on the code, you'll be asked first to set up the connection to the Microsoft Identity platform. As the first step, the wizard will install the required msidentity tool, which is a .NET extension that simplifies the integration of Azure Active Directory into your application. Once the tools are installed, you will be asked to pick the account where your app registration on Azure Active Directory lives (or where you want to create a new one). If you have followed the previous post, we have already had one app registration: it's the one we have used with Postman. It's essential that you use it also for the Blazor app, otherwise things won't work properly. If you remember, one of the requirements to work with the Viva Learnings APIs is that all the operations must be performed using the same application.

 

![app-registration.png](https://techcommunity.microsoft.com/t5/image/serverpage/image-id/422031iE043F968B84C000C/image-size/large?v=v2&px=999 "app-registration.png")

 

Once you hit  **Next** , you'll be able to configure Microsoft Identity not just to authenticate the user, but also to connect to additional APIs. In our case, we need to support the Microsoft Graph, so enable the  **Add Microsoft Graph**  check:

 

![microsoft-graph.png](https://techcommunity.microsoft.com/t5/image/serverpage/image-id/422033iF2BFFEDEF2AF22D7/image-size/large?v=v2&px=999 "microsoft-graph.png")

 

The next and last step is to import the client secret which is required to connect to the Azure AD app. The tool will create a key called `ClientSecret` in the `AzureAd` section of the configuration file and it will store the secret automatically retrieved from the portal. Additionally, it won't store the information in the plain configuration file, but in the local user secrets file, called `secrets.json`. [This feature](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-7.0&tabs=windows) helps you to protect your sensitive information on your development machine, since the `secrets.json` file, even if it's read at runtime and merged into the standard configuration like a regular `appsettings.json` file, lives outside the project's folder. This means that you don't risk storing the client secret in your repository when you commit the solution.

![client-secret.png](https://techcommunity.microsoft.com/t5/image/serverpage/image-id/422034i69FAF8F69250215E/image-size/large?v=v2&px=999 "client-secret.png")

Thanks to the Visual Studio integration with the Microsoft Identity platform, at the end of the wizard you will have all the required building blocks in place:

 

- Inside the `appsettings.json` file, you will find a section called `AzureAd` with all the information needed to connect to your Azure AD app, like client id, tenant id, etc.
- Inside the `Program.cs` file, you will see the code required to register the different services needed to authenticate your application and authorize the different operations.

Here is the full startup code:

```
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
using VivaLearningApp.Data;
using VivaLearningApp.Services;

var builder = Microsoft.AspNetCore.Builder.WebApplication.CreateBuilder(args);

var initialScopes = builder.Configuration["DownstreamApi:Scopes"]?.Split(' ') ?? builder.Configuration["MicrosoftGraph:Scopes"]?.Split(' ');

// Add services to the container.
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
    .AddMicrosoftGraph(builder.Configuration.GetSection("MicrosoftGraph"))
    .AddInMemoryTokenCaches();

builder.Services.AddControllersWithViews().
    AddMicrosoftIdentityUI();

builder.Services.AddAuthorization(options =>
{
    // By default, all incoming requests will be authorized according to the default policy
    options.FallbackPolicy = options.DefaultPolicy;
});

builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor()
    .AddMicrosoftIdentityConsentHandler();
builder.Services.AddSingleton<WeatherForecastService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");

app.Run();

```

This integration is made easier by the [Microsoft.Identity.Web](https://learn.microsoft.com/en-us/azure/active-directory/develop/microsoft-identity-web) library which, as the name says, simplifies the integration of the Microsoft Identity platform in a web application. In the first lines, in fact, you can see how the integration is simply enabled by the `AddMicrosoftIdentityWebApp()` method which receives, as input, the section of our configuration called `AzureAd`, which contains all the information about our Azure AD app. A few lines below you can notice also how the authorization is configured to use the default policy whenever the user isn't authenticated. This means that your application can't be used in anonymous mode. As soon as they launch the web application, they will be asked to login to move forward.

 

You can easily test this by simply launching the debugger by pressing F5. The Blazor web application will be launched locally and, as a first thing, you will be asked to login with a work account from your Microsoft 365 tenant. Only if you login, you'll land on the home page and you'll be able to see your account:

 

![user-logged-in.png](https://techcommunity.microsoft.com/t5/image/serverpage/image-id/422036i9F0F85B85AD19454/image-size/large?v=v2&px=999 "user-logged-in.png")

 

Before we start building our page to work with the custom content provider, however, we must make a change to the project's configuration. Since, during the wizard, we have enabled the integration with the Microsoft Graph, Visual Studio automatically added for us the package `Microsoft.Identity.Web.MicrosoftGraph`, which simplifies getting an authenticated Microsoft Graph client. However, in the previous post, we have learned that the Viva Learning APIs are part of the beta endpoint, as such we must make a couple of changes to our .csproj file:

 

- Replace the `Microsoft.Identity.Web.MicrosoftGraph` library with the equivalent beta version, which is `Microsoft.Identity.Web.MicrosoftGraphBeta`.

- Add the `Microsoft.Graph.Beta` package to the project, which is the beta version of the [Microsoft Graph .NET SDK](https://github.com/microsoftgraph/msgraph-sdk-dotnet):

Now we have all the basic building blocks to start the integration of the Microsoft Graph APIs.

 

### Listing the custom learning providers

As the first step, we're going to build a Razor component that displays the list of available learning providers. However, the list will contain only one item: as we have learned in the previous post, currently the Viva Learning APIs support having only a single custom provider.

Before starting to build the page, however, let's create a class we're going to use to centralize all our operations with the Microsoft Graph. Right click on your project, choose  **Add → Class**  and give it a meaningful name, like `CustomGraphService`. As the first step, we're going to add the method we need to get the list of custom providers. Thanks to the Microsoft Graph integration provided by the Microsoft Identity Web library, we don't need to create our own instance of the Graph client, but it's automatically injected inside the DI container of our Blazor web app. All we need to do is add two dependencies to the public constructor of our class, as in the following example:

```
public class CustomGraphService
{
    private readonly GraphServiceClient delegatedClient;
    private readonly MicrosoftIdentityConsentAndConditionalAccessHandler consentHandler;

    public CustomGraphService(GraphServiceClient delegatedClient, MicrosoftIdentityConsentAndConditionalAccessHandler consentHandler)
    {
        this.delegatedClient = delegatedClient;
        this.consentHandler = consentHandler;
    }
}

```

`GraphServiceClient` is the Graph client provided the Microsoft Graph .NET SDK. We can use it to perform any operation against the Microsoft Graph in a object oriented way. Instead of manually building the HTTP requests and manually parsing JSON responses, we can directly use classes and objects which represent the various endpoints and entities in the Microsoft Graph. Thanks to the Microsoft Identity Web library, the `GraphServiceClient` object we get is already authenticated: it's already using the proper access token retrieved when we logged in with our work account. `MicrosoftIdentityConsentAndConditionalAccessHandler`, instead, is a helper class provided by the Microsoft Identity Web library that we can use to wrap our Microsoft Graph operations. It makes sure that the proper consent is requested in case of issues with the access token.

Now that we have these blocks, we can create a method which returns the list of custom providers:

```
public async Task<IList<LearningProvider>> GetLearningProvidersAsync()
{
    try
    {
        var result = await delegatedClient.EmployeeExperience.LearningProviders.Request().GetAsync();
        return result.CurrentPage;
    }
    catch (Exception ex)
    {
        consentHandler.HandleException(ex);
        return null;
    }
}

```

As you can see, the Microsoft Graph .NET SDK makes it easier to consume the Graph from a .NET application. The `https://graph.microsoft.com/beta/employeeExperience/learningProviders` endpoint is mapped with the `EmployeeExperience.LearningProviders.Request()` object. By calling the `GetAsync()` method, we are performing a HTTP GET request. The response is a collection of `LearningProvider` objects, which have the same properties we have seen in the JSON response in the previous post, like id, name and logos. In this method you can also see the `MicrosoftIdentityConsentAndConditionalAccessHandler` helper in action. We wrap the Graph operation in a `try / catch` statement and, in case it fails, we use the helper to try again the request with the proper permissions.

Now we're ready to use our class. First, however, we need to register it into the Blazor dependency injection container, so that in our Razor component we can retrieve an instance with all the dependencies already satisfied. First, let's create an interface that describes our class:

```
public interface ICustomGraphService
{
    Task<IList<LearningProvider>> GetLearningProvidersAsync();
}

```

Now, let's change our class to inherit from this interface:

```
public class CustomGraphService : ICustomGraphService
{
    // class implementation
}

```

**Please note!**  During the post we're going to add additional methods to the `CustomGraphService` class. Remember to declare them also in the `ICustomGraphService` interface!

Finally, let's move to the `Program.cs` file and, before the `builder.Build()` statement, add the following line of code:

```
builder.Services.AddScoped<ICustomGraphService, CustomGraphService>();

```

Now our `CustomGraphService` class can be easily consumed by every other class and Razor component in our application. Let's put this into action immediately by creating a new component to display the list of custom providers. Right click on the  **Pages**  folder in Solution Explorer, choose  **Add → Razor Component**  and give it a meaningful name, like `LearningProviders.razor`.

As first step, let's add the following code at the top of the component:

```
page "/learningProviders"
@inject ICustomGraphService graphService
@using Microsoft.Graph

```

The first one is used to enable routing, so that we can treat this component like a page that will be available through the `/learningProviders` endpoint of the web application. The second line is used to inject our custom Microsoft Graph wrapper in the component. The third one is required to access all the types included in the Microsoft Graph .NET SDK, which belongs to the `Microsoft.Graph` namespace.

Then, in the `@code` block, let's override the `OnInitializedAsync()` method, which gets called when the component is rendered:

```
@code {
    private IList<LearningProvider> providers;

    protected async override Task OnInitializedAsync()
    {
        providers = await graphService.GetLearningProvidersAsync();
    }
}

```

We call the `GetLearningProvidersAsync()` method we have previously created in our custom class, and we store the result into a variable called `providers`, which is a collection of `LearningProvider` objects. Then, we can iterate this collection to generate a table with the list of custom providers:

```
@if (providers != null)
{
    <table class="table">
        <thead>
            <tr>
                <th>Id</th>
                <th>Name</th>
                @*<th>Contents</th>*@
            </tr>
        </thead>
        <tbody>
            @foreach (var provider in providers)
            {
                <tr>
                    <td>@provider.Id</td>
                    <td>@provider.DisplayName</td>
                </tr>
            }
        </tbody>
    </table>
}
else
{
    <h3>Loading...</h3>
}

```

For each provider returned by the Microsoft Graph, we display its `Id` and `DisplayName` properties. To test our work, press F5 to launch the debugger and then point your browser to the URL `https://localhost:7073/learningProviders`. If you did everything correctly, you should see the custom provider we have created with Postman in the previous post:

 

![blazor-learning-providers.png](https://techcommunity.microsoft.com/t5/image/serverpage/image-id/422039iA82E850757FBB98C/image-size/large?v=v2&px=999 "blazor-learning-providers.png")

Now that we have our learning provider, let's move forward and also display the list of contents available for this provider.

 

### Listing the learning content

For this demo, we're going to enhance the component we have built to automatically display the list of contents which are available right below the custom provider table. The reason of this choice is that, if you remember, we can't have more than one custom learning provider per tenant, so it wouldn't make sense to give users the options to choose which learning provider they want to see: there will be only one.

 

Let's continue working on our `CustomGraphService` class to add a new method to retrieve the list of learning contents. Before doing that, however, we have some additional work to do. If you remember the learnings from the previous posts, one of the challenges you must deal with when you work with Viva Learning is that you must use different AAD permissions based on the scenario: delegated permissions to work with learning providers, application permissions to work with learning content. The Microsoft Graph client that is injected by the Microsoft Identity Web library is authenticated with a token retrieved with the [Authorization code flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow): when we start the web app, we are asked to login with a work account from our Microsoft 365 tenant. This means that the token is valid to perform delegated operations, thus we were able to use it just fine to retrieve the list of learning providers.

When we want to get the list of learning content, instead, we must use a token which supports performing application operations. As such, we can't use the injected Microsoft Graph client, but we'll need to get a valid access token using the [client credential flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) and create a new Graph client based on it. As first step, let's create a method to perform the authentication against our AAD application but, this time, using the client credential flow:

```
using Microsoft.Graph;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web;
using System.Net.Http.Headers;

public class CustomGraphService : ICustomGraphService
{
    private readonly IConfiguration configuration;
    private readonly GraphServiceClient delegatedClient;
    private readonly MicrosoftIdentityConsentAndConditionalAccessHandler consentHandler;
    private GraphServiceClient? applicationClient;

    public CustomGraphService(IConfiguration configuration, GraphServiceClient delegatedClient, MicrosoftIdentityConsentAndConditionalAccessHandler consentHandler)
    {
        this.configuration = configuration;
        this.delegatedClient = delegatedClient;
        this.consentHandler = consentHandler;
    }

    public async Task AcquireAccessTokenAsync()
    {
        var scopes = new string[] { ".default" };

        var aadConfig = configuration.GetSection("AzureAd");
        var client = ConfidentialClientApplicationBuilder.Create(aadConfig["ClientId"])
            .WithTenantId(aadConfig["TenantId"])
            .WithClientSecret(aadConfig["ClientSecret"]).Build();

        var token = await client.AcquireTokenForClient(scopes).ExecuteAsync();

        var authProvider = new DelegateAuthenticationProvider(async (request) =>
        {
            request.Headers.Authorization =
                new AuthenticationHeaderValue("Bearer", token.AccessToken);
        });

        applicationClient = new GraphServiceClient(authProvider);
    }
}

```

Inside the `AcquireAccessTokenAsync()` method, we use the `ConfidentialClientApplicationBuilder` class included in the `Microsoft.Identity` namespace, which allows us to authenticate to our AAD application using the client credentials flow. We set it up by using the following methods:

- `Create()`, which requires the Client Id of our AAD app.
- `WithTenantId()`, which requires the Tenant Id of our AAD app.
- `WithClientSecret`, which requires the Client Secret of our AAD app.

All the information have already been stored in the `appsettings.json` file when we have connected our application to the Microsoft Identity platform. As such, we can just retrieve the section called `AzureAd` from the configuration file, using the `configuration` object. Since this object is registered inside the DI container, we just need to add the `IConfiguration` interface as parameter of the public constructor of our `CustomGraphService` class. As the last step, we call the `Build()` method to create our authentication client.

Finally, we can use the `AcquireTokenForClient()` method with the scopes we need, followed by the `ExecuteAsync()` method, to effectively retrieve the access token we need to interact with the Microsoft Graph using application permissions. Once we have it, we can use it to create a `DelegateAuthenticationProvider` object, which we can pass as parameter to create a new `GraphServiceClient` object.

If we did everything properly, now our `CustomGraphService` class should offer two different `GraphServiceClient` objects: one called `delegatedClient`, which supports delegated permissions and that we can use to work with learning providers; one called `applicationClient`, which supports application permissions and that we can use to work with learning content.

Thanks to this second client implementation, now it's quite easy to add a new method to our `CustomGraphService` class that we can use to get the list of learning contents for a given learning provider:

```
public async Task<IList<LearningContent>?> GetLearningContentAsync(string id)
{
    var response = await applicationClient.EmployeeExperience.LearningProviders[id].LearningContents.Request().GetAsync();
    return response.CurrentPage;
}

```

Now, we can go back to our Razor component and add a couple of additional statements in the `OnInitializedAsync()` method:

```
@code {
    private IList<LearningProvider> providers;
    private IList<LearningContent> contents;

    protected async override Task OnInitializedAsync()
    {
        await graphService.AcquireAccessTokenAsync();
        providers = await graphService.GetLearningProvidersAsync();
        if (providers != null)
        {
            contents = await graphService.GetLearningContentAsync(providers?.FirstOrDefault().Id);
        }
    }
}

```

First, we are calling our new `AcquireAccessTokenAsync()` to make sure to get the proper access token before performing any operation. Then, after getting the list of providers, we call the new `GetLearningContentAsync()` method to get the list of contents for the first provider in the list (which will be also the only available one). The list of contents is stored in the `contents` variable, which is a collection of `LearningContent` objects. We use it in a similar way we did for the learning providers to build a table which displays the list:

```
@if (contents != null)
{
    <table class="table">
        <thead>
            <tr>
                <th>Title</th>
                <th>Url</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var content in contents)
            {
                <tr>
                    <td>@content.Title</td>
                    <td>@content.ContentWebUrl</td>
                </tr>
            }
        </tbody>
    </table>

}
else
{
    <h3>Loading...</h3>
}

```

For each learning content, we display its `Title` and `ContentWebUrl` properties. If you have implemented everything properly, when you press F5 and you go to the `https://localhost:7073/learningProviders` page, you should see, below the customer provider, the articles from this blog we have added in the previous post with Postman:

 

![blazor-learning-contents.png](https://techcommunity.microsoft.com/t5/image/serverpage/image-id/422040i3FAD6681CC36AF3E/image-size/large?v=v2&px=999 "blazor-learning-contents.png")

 

Let's move now to the last step of our demo: providing a form to add new learning content.

 

### Adding the learning content

Unfortunately, at the time of writing, we can't use the `applicationClient` object we have created in the previous section, due to the way the API works. If you remember what we have learned in the previous post, this API has a couple of specific requirements:

 

- It must be a PATCH operation.
- You must include in the URL the external id of your content, like `https://graph.microsoft.com/beta/employeeExperience/learningProviders/303da5b7-dce3-4998-a76f-7a18849fc697/learningContents(externalId='3617543')`.

The Microsoft Graph .NET SDK, unfortunately, doesn't support this scenario. Additionally, the learning content request we have previously built using the `applicationClient.EmployeeExperience.LearningProviders[id].LearningContents.Request()` method, supports only two methods: `GetAsync()`, which is mapped with a GET request, and `AddAsync()`, which is mapped with a POST request. We don't have any `UpdateAsync()` method, which would be mapped with the PATCH request we need.

But don't worry, we can easily support our requirements by manually building our request using the standard `HttpClient` class in .NET.

First, let's change a bit our `AcquireAccessTokenAsync()` method so that we can store the access token as a global property of the `CustomGraphService` class:

```
public class CustomGraphService : ICustomGraphService
{
   private string? accessToken;

   public async Task AcquireAccessTokenAsync()
   {
       var scopes = new string[] { ".default" };

       var aadConfig = configuration.GetSection("AzureAd");
       var client = ConfidentialClientApplicationBuilder.Create(aadConfig["ClientId"])
           .WithTenantId(aadConfig["TenantId"])
           .WithClientSecret(aadConfig["ClientSecret"]).Build();

       var token = await client.AcquireTokenForClient(scopes).ExecuteAsync();
       accessToken = token.AccessToken;

       // initialization of the application Graph client
   }
}

```

Now we can use the token to create a custom authenticated `HttpClient` and submit the proper request to the Microsoft Graph:

```
public async Task AddLearningContent(string providerId, string contentId, string title, string contentUrl, string language)
{
    string baseUrl = applicationClient.EmployeeExperience.LearningProviders.RequestUrl;
            
    string url = $"{baseUrl}/{providerId}/learningContents(externalId='{contentId}')";
    HttpClient client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

    LearningContentModel content = new()
    {
        Title = title,
        ExternalId = contentId,
        ContentWebUrl = contentUrl,
        LanguageTag= language
    };

    JsonContent jsonContent = JsonContent.Create(content);

    var result = await client.PatchAsync(url, jsonContent);
}

```

First, we leverage the standard Microsoft Graph client to retrieve the base URL to work with the Viva Learning APIs. We use it to build the full URL we need to submit the request, by including the id of the learning provider and the external id. Then we create a new `HttpClient` instance, and we use the `AuthenticationHeaderValue` object with the access token to define the default `Authorization` header. Now the `HttpClient` object can make authenticated requests to the Microsoft Graph. The body of the request must be defined in JSON, so we create a new `LearningContentModel` object and we turn it into a JSON object using the `JsonContent.Create()` method. Finally, we submit the request using the `PatchAsync()` method, which is the equivalent of performing a PATCH operation.

There's only one caveat to keep in mind. As you can see, to create the JSON payload, we're using a `LearningContentModel` class, instead of the `LearningContent` one which is part of the Microsoft Graph .NET SDK. The reason is that this class contains a lot of extra properties that the standard Microsoft Graph .NET client can parse in the right way, but if we use it with a custom client (like in our case) it will generate an invalid request. As such, I've created a class called `LearningContentModel` which exposes only the properties I need:

```
public class LearningContentModel
{
    [Required]
    public string? ExternalId { get; set; }

    [Required]
    public string? Title { get; set; }

    [Required]
    public string? ContentWebUrl { get; set; }

    [Required]
    public string? LanguageTag { get; set; }
}

```

This is a quite simple implementation which exposes only the minimum set of properties which are required to create learning content. Of course, you can customize it to add any extra property you might need.

Now we have everything we need in terms of business logic. We need, however, the Razor component which will act as a submission form. Right click on the  **Pages**  folder and choose  **Add → Razor component**. Give it a meaningful name, like `NewLearningContent.razor` and click  **Add**.

Let's start to define the header of the file:

```
@page "/newLearningContent/{learningProviderId}"
@inject ICustomGraphService graphService;

```

We define the route of the page, this time however, by supporting a parameter. We're going to pass to the component, through the URL, the id of the learning provider we're going to create the content for. Then, we inject our usual `CustomGraphService` object. Now let's look at the code section:

```
@code {
    [Parameter]
    public string? LearningProviderId { get; set; }

    public LearningContentModel learningContentModel = new();

    public async Task HandleSubmit()
    {
        if (learningContentModel != null)
        {
            await graphService.AddLearningContent(LearningProviderId, learningContentModel.ExternalId, learningContentModel.Title, learningContentModel.ContentWebUrl, learningContentModel.LanguageTag);
        }
    }
}

```

The `LearningProviderId` property is decorated with the `[Parameter]` attribute, which means that it will be automatically injected with the value coming from the URL. We also define a new property of the type `LearningContentModel`: we're going to use it to build the input form. Finally, we define a method called `HandleSubmit()`, which is going to be triggered when the user clicks on the Submit button of the form. The method simply takes care of calling the `AddLearningContent()` method we have previously defined in the `CustomGraphService` class. When the user fills out the form, the `learningContentModel` object includes all the information filled in by the user. As such, we just pass its properties as inputs for the method.

Now that we have all the logic in place, we can build our input form using the `EditForm` component in Blazor:

```
<EditForm Model="@learningContentModel" OnValidSubmit="@HandleSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <p>
        <div>External Id</div>
        <div><InputText id="externalId" @bind-Value="learningContentModel.ExternalId" /></div>
    </p>
    <p>
        <div>Title</div>
        <div><InputText id="title" @bind-Value="learningContentModel.Title" /></div>
    </p>

    <p>
        <div>Content URL</div>
        <div><InputText id="contentUrl" @bind-Value="learningContentModel.ContentWebUrl" /></div>
    </p>

    <p>
        <div>Language</div>
        <div><InputText id="longLogoLight" @bind-Value="learningContentModel.LanguageTag" /></div>
    </p>

    <p>
        <button type="submit">Create</button>
    </p>

</EditForm>

```

The `EditForm` component requires two properties:

- `Model` is the object which holds the model that will be filled with the data coming from the form. It's the `learningContentModel` property we have defined in the `@code` section.
- `OnValidSubmit`, which is the method to invoke when the form is submitted. `HandleSubmit` is the name of the handler we have created in the `@code` section.

Inside the `EditForm` component, we have the freedom to define the various fields as we prefer. The only requirement is to use the `@bind-value` property to connect the field with the equivalent property in the model. We also need a `button` with `submit` as `type`, which will trigger the method defined in the `OnValidSubmit` property.

This is it. If you want to test this form, you can invoke the URL `https://localhost:7073/newLearningContent/` with the id of your custom learning provider (for example, `https://localhost:7073/newLearningContentBis/303da5b7-dce3-4998-a76f-7a18849fc697`).

 

![blazor-new-content.png](https://techcommunity.microsoft.com/t5/image/serverpage/image-id/422041i09B34B0F8436CEC6/image-size/large?v=v2&px=999 "blazor-new-content.png")

 

If you want to give the user an easy option to create new content, you can customize the `LearningProviders.razor` component and:

1. Inject the `NavigationManager` object into the component, which is provided by Blazor to manage navigation across pages:

2. Add a new button in the component:

3. Implement the `CreateNewLearningContent` method to perform the navigation:

When the user presses the button, they will be automatically redirected to the `newLearningContent` page with, as an extra parameter, the id of the learning provider.

 

### Wrapping up

It was quite a long journey, but now we have a fully working web client that we can use to manage our custom learning content in Viva Insights. In this post, we have focused on the various nuances you must keep in mind when you build such a solution, like the requirement of using different authentication types for the Microsoft Graph client to manage the different permissions required to work with the Viva Learning APIs.

 

You can find the full solution [on GitHub](https://github.com/qmatteoq/VivaLearningApp). Before using it, remember to change the configuration in the `appsettings.json` file with the information about your app registration on Azure AD.

In the same solution, you will find also another project which uses the same classes and APIs to support another common scenarios with learning providers: content syncing. In this scenario, you don't manually add custom learning content, but you sync it from another source, like an Excel file or a Web API. The project on GitHub includes an Azure Function that reads a CSV file stored on Azure Storage and uses it to import a list of contents in the custom learning provider.

 

Happy coding!