Showing posts with label Razor Pages. Show all posts
Showing posts with label Razor Pages. Show all posts

Saturday, December 14, 2024

.NET Aspire and Semantic Kernel AI

 Let's learn how to use the .NET Aspire Azure OpenAI client. We will familiarize ourselves with the Aspire.Azure.AI.OpenAI library, which is used to register an OpenAIClient in the dependency injection (DI) container for consuming Azure OpenAI or OpenAI functionality. In addition, it enables corresponding logging and telemetry.

Companion Video: https://youtu.be/UuLnCRdYvEI
Final Solution Code: https://github.com/medhatelmasry/AspireAI_Final

Pre-requisites:

  • .NET 9.0
  • Visual Studio Code
  • .NET Aspire Workload
  • "C# Dev Kit" extension for VS Code

Getting Started

We will start by cloning a simple C# solution that contains two projects that use Semantic Kernel - namely a console project (ConsoleAI) and a razor-pages project (RazorPagesAI). Clone the project in a working directory on your computer by executing these commands in a terminal window:

git clone https://github.com/medhatelmasry/AspireAI.git
cd AspireAI

The cloned solution contains a console application (ConsoleAI) and a razor-pages application (RazorPagesAI). They both do pretty much do the same thing. The objective of today’s exercise is to:

  • use .NET Aspire so that both projects get started from one place 
  • pass environment variables to the console and razor-pages web apps from the .AppHost project that belongs to .NET Aspire

Open the solution in VS Code and update the values in the following appsettings.json files with your access parameters for Azure OpenAI and/or OpenAI:

ConsoleAI/appsettings.json
RazorPagesAI/appsettings.json

The most important settings are the connection strings. They are identical in both projects:

"ConnectionStrings": {
  "azureOpenAi": "Endpoint=Azure-OpenAI-Endpoint-Here;Key=Azure-OpenAI-Key-Here;",
  "openAi": "Key=OpenAI-Key-Here"
}

After you update your access parameters, try each application separately to see what it does:

Here is my experience using the console application (ConsoleAI) with AzureOrOpenAI set to “OpenAI”:

cd ConsoleAI
dotnet run


I then changed the AzureOrOpenAI setting to “Azure” and ran the console application (ConsoleAI) again:

Next, try the razor pages web application (RazorPagesAI) with AzureOrOpenAI set to “OpenAI”:

cd ../RazorPagesAI
dotnet watch


In the RazorPagesAI web app’s appsettings.json file, I changed AzureOrOpenAI to “Azure”, resulting in a similar experience.


In the root folder, add .NET Aspire to the solution:

cd ..
dotnet new aspire --force

Add the previous projects to the newly created .sln file with:

dotnet sln add ./AiLibrary/AiLibrary.csproj
dotnet sln add ./RazorPagesAI/RazorPagesAI.csproj
dotnet sln add ./ConsoleAI/ConsoleAI.csproj

Add the following .NET Aspire agent packages to the client ConsoleAI and RazorPagesAI projects with:

dotnet add ./ConsoleAI/ConsoleAI.csproj package Aspire.Azure.AI.OpenAI --prerelease
dotnet add ./RazorPagesAI/RazorPagesAI.csproj package Aspire.Azure.AI.OpenAI --prerelease

To add Azure hosting support to your IDistributedApplicationBuilder, install the 📦 Aspire.Hosting.Azure.CognitiveServices NuGet package in the .AppHost project:

dotnet add ./AspireAI.AppHost/AspireAI.AppHost.csproj package Aspire.Hosting.Azure.CognitiveServices

In VS Code, add the following references:

  1. Add a reference from the .AppHost project into ConsoleAI project.
  2. Add a reference from the .AppHost project into RazorPagesAI project.
  3. Add a reference from the ConsoleAI project into .ServiceDefaults project.
  4. Add a reference from the RazorPagesAI project into .ServiceDefaults project.

Copy the AI and ConnectionStrings blocks from either the console (ConsoleAI) or web app (RazorPagesAI)  appsettings.json file into the appsettings.json file of the .AppHost project. The appsettings.json file in the .AppHost project will look similar to this:

"AI": {
  "AzureOrOpenAI": "OpenAI",
  "OpenAiChatModel": "gpt-3.5-turbo",
  "AzureChatDeploymentName": "gpt-35-turbo"
},
"ConnectionStrings": {
  "azureOpenAi": "Endpoint=Azure-OpenAI-Endpoint-Here;Key=Azure-OpenAI-Key-Here;",
  "openAi": "Key=OpenAI-Key-Here"
}

Add the following code to the Program.cs file in the .AppHost project just before builder.Build().Run()

IResourceBuilder<IResourceWithConnectionString> openai;
var AzureOrOpenAI = builder.Configuration["AI:AzureOrOpenAI"] ?? "Azure"; ;
var chatDeploymentName = builder.Configuration["AI:AzureChatDeploymentName"];
var openAiChatModel = builder.Configuration["AI:OpenAiChatModel"];
 
// Register an Azure OpenAI resource. 
// The AddAzureAIOpenAI method reads connection information
// from the app host's configuration
if (AzureOrOpenAI.ToLower() == "azure") {
    openai = builder.ExecutionContext.IsPublishMode
        ? builder.AddAzureOpenAI("azureOpenAi")
        : builder.AddConnectionString("azureOpenAi");
} else {
    openai = builder.ExecutionContext.IsPublishMode
        ? builder.AddAzureOpenAI("openAi")
        : builder.AddConnectionString("openAi");
}
 
// Register the RazorPagesAI project and pass to it environment variables.
//  WithReference method passes connection info to client project
builder.AddProject<Projects.RazorPagesAI>("razor")
    .WithReference(openai)
    .WithEnvironment("AI__AzureChatDeploymentName", chatDeploymentName)
    .WithEnvironment("AI__AzureOrOpenAI", AzureOrOpenAI)
    .WithEnvironment("AI_OpenAiChatModel", openAiChatModel);
 
 // register the ConsoleAI project and pass to it environment variables
builder.AddProject<Projects.ConsoleAI>("console")
    .WithReference(openai)
    .WithEnvironment("AI__AzureChatDeploymentName", chatDeploymentName)
    .WithEnvironment("AI__AzureOrOpenAI", AzureOrOpenAI)
    .WithEnvironment("AI_OpenAiChatModel", openAiChatModel);

We need to add .NET Aspire agents in both our console and web apps. Let us start with the web app. Add this code to the Program.cs file in the RazorPagesAI project right before “var app = builder.Build()”: 

builder.AddServiceDefaults();

In the same Program.cs of the web app (RazorPagesAI), comment out the if (azureOrOpenAi.ToLower() == "openai") { …. } else { ….. } block and replace it with this code:

if (azureOrOpenAi.ToLower() == "openai") {
    builder.AddOpenAIClient("openAi");
    builder.Services.AddKernel()
        .AddOpenAIChatCompletion(openAiChatModel);
} else {
    builder.AddAzureOpenAIClient("azureOpenAi");
    builder.Services.AddKernel()
        .AddAzureOpenAIChatCompletion(azureChatDeploymentName);
}

In the above code, we call the extension method to register an OpenAIClient for use via the dependency injection container. The method takes a connection name parameter. Also, register Semantic Kernel with the DI. 

Also, in the Program.cs file in the ConsoleAI project, add this code right below the using statements:

var hostBuilder = Host.CreateApplicationBuilder();
hostBuilder.AddServiceDefaults();

In the same Program.cs of the console app (ConsoleAI), comment out the if (azureOrOpenAi.ToLower() == "azure") { …. } else { ….. } block and replace it with this code:

if (azureOrOpenAI.ToLower() == "azure") {
    var azureChatDeploymentName = config["AI:AzureChatDeploymentName"] ?? "gpt-35-turbo";
    hostBuilder.AddAzureOpenAIClient("azureOpenAi");
    hostBuilder.Services.AddKernel()
        .AddAzureOpenAIChatCompletion(azureChatDeploymentName);
} else {
    var openAiChatModel = config["AI:OpenAiChatModel"] ?? "gpt-3.5-turbo";
    hostBuilder.AddOpenAIClient("openAi");
    hostBuilder.Services.AddKernel()
        .AddOpenAIChatCompletion(openAiChatModel);
}
var app = hostBuilder.Build();

Replace “var kernel = builder.Build();” with this code:

var kernel = app.Services.GetRequiredService<Kernel>();
app.Start();

You can now test that the .NET Aspire orchestration of both the Console and Web apps. Stop all applications, then, in a terminal window,  go to the .AppHost project and run the following command:

dotnet watch

You will see the .NET Aspire dashboard:


Click on Views under the Logs column. You will see this output indicating that the console application ran successfully:


Click on the link for the web app under the Endpoints column. It opens the razor pages web app in another tab in your browser. Test it out and verify that it works as expected.

Stop the .AppHost application, then comment out the AI and ConneectionStrings blocks in the appsettings.json files in both the console and web apps. If you run the .AppHost project again, you will discover that it works equally well because the environment variables are being passed from the .AppHost project into the console and web apps respectively.

One last refinement we can do to the console application is do away with the ConfigurationBuilder because we can get a configuration object from the ApplicationBuilder. Therefore, comment out the following code in the console application:

var config = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
    .Build();

Replace the above code with the following:

var config = hostBuilder.Configuration;

You can delete the following package from the ConsoleAI.csproj file:

<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />

Everything works just as it did before.


Thursday, February 15, 2024

Azure OpenAI Function Calling - a practical example using C# and ASP.NET Razor Pages

In as much as one can obtain valuable information by prompting the various OpenAI language models, it becomes even more valuable when these models can be integrated with custom systems and tools. In this demo, we will integrate an LLM with the Azure OpenAI Function Calling capability to query local data in the form of a products.csv file. Of course, this concept can easily be extended to more complex systems and tools. The sample application is based on ASP.NET Razor Pages. It receives a natural language prompt from the user and goes through this three-step process before responding:

  1. A call is made to a chat completions API with function definitions and the user’s prompt.
  2. The model’s response initiates calls to a custom function
  3. The chat completion API is called again with the response from the custom function, resulting in a final response.

Source Code: https://github.com/medhatelmasry/OaiFuncCall

Companion Video: https://youtu.be/3yyq3GWIj4o

Getting Started

Let’s start by creating an ASP.NET Razor pages web application. Select a suitable working folder on your computer, then enter the following terminal window commands:

dotnet new razor -o OaiFuncCall
cd OaiFuncCall

Add these packages:

dotnet add package CsvHelper
dotnet add package Azure.AI.OpenAI -v 1.0.0-beta.13

The CsvHelper package will help us load a list of products from a CSV file named products.csv and hydrate a list of Product objects. The second package is needed to work with Azure OpenAI.

Let’s Code

appsettings.json

Add this to appsettings.json:

"AzureOpenAiSettings": {
  "Endpoint": "https://YOUR_RESOURCE_NAME.openai.azure.com/",
  "Model": "gpt-35-turbo-16k",
  "ApiKey": "fake-key-fake-key-fake-key-fake-key"
}

Of course, you need to adjust the endpoint setting with the appropriate value that pertains to the Azure OpenAI service that you created. Also, enter the correct value for the ApiKey.

products.csv

Create a text file named products.csv in the wwwroot folder. Copy some sample data from  https://gist.github.com/medhatelmasry/b250023f3b4b5b14713cfc5165f1d030 and paste it into your products.csv file. 

Contents of products.csv looks like this:

ProductId,ProductName,UnitsInStock,UnitPrice
1,Aniseed Syrup,39,18
2,Chef Anton's Cajun Seasoning,17,19
3,Chef Anton's Gumbo Mix,13,10
4,Grandma's Boysenberry Spread,53,22
5,Uncle Bob's Organic Dried Pears,0,21.35
6,Northwoods Cranberry Sauce,120,25
7,Mishi Kobe Niku,15,30
8,Ikura,6,40
9,Queso Cabrales,29,97
10,Queso Manchego La Pastora,31,31
11,Konbu,22,21
12,Tofu,86,38
13,Genen Shouyu,24,6
14,Pavlova,35,23.25
15,Alice Mutton,39,15.5
16,Carnarvon Tigers,29,17.45
17,Teatime Chocolate Biscuits,0,39
18,Sir Rodney's Marmalade,42,62.5
19,Sir Rodney's Scones,25,9.2
20,Gustaf's KnŠckebršd,40,81
21,Tunnbršd,3,10
22,Guaran‡ Fant‡stica,104,21
23,NuNuCa Nu§-Nougat-Creme,61,9
24,GumbÅ r GummibÅ rchen,20,4.5
25,Schoggi Schokolade,76,14
26,Ršssle Sauerkraut,15,31.23
27,ThŸringer Rostbratwurst,49,43.9
28,Nord-Ost Matjeshering,26,45.6
29,Gorgonzola Telino,0,123.79
30,Mascarpone Fabioli,10,25.89
31,Geitost,0,12.5
32,Sasquatch Ale,9,32
33,Steeleye Stout,112,2.5
34,Inlagd Sill,111,14
35,Gravad lax,20,18
36,C™te de Blaye,112,19
37,Chartreuse verte,11,26
38,Boston Crab Meat,17,263.5
39,Jack's New England Clam Chowder,69,18
40,Singaporean Hokkien Fried Mee,123,18.4
41,Ipoh Coffee,85,9.65
42,Gula Malacca,26,14
43,Rogede sild,17,46
44,Spegesild,27,19.45
45,Zaanse koeken,5,9.5
46,Chocolade,95,12
47,Maxilaku,36,9.5
48,Valkoinen suklaa,15,12.75
49,Manjimup Dried Apples,10,20
50,Filo Mix,65,16.25
51,Perth Pasties,20,53
52,Tourtire,38,7
53,P‰tŽ chinois,0,32.8
54,Gnocchi di nonna Alice,21,7.45
55,Ravioli Angelo,115,24
56,Escargots de Bourgogne,21,38
57,Raclette Courdavault,36,19.5
58,Camembert Pierrot,62,13.25
59,Sirop d'Žrable,79,55
60,Tarte au sucre,19,34
61,Vegie-spread,113,28.5
62,Wimmers gute Semmelknšdel,17,49.3
63,Louisiana Fiery Hot Pepper Sauce,24,43.9
64,Louisiana Hot Spiced Okra,22,33.25
65,Laughing Lumberjack Lager,76,21.05
66,Scottish Longbreads,4,17
67,Gudbrandsdalsost,52,14
68,Outback Lager,6,12.5
69,Flotemysost,26,36
70,Mozzarella di Giovanni,15,15
71,Ršd Kaviar,26,21.5
72,Longlife Tofu,14,34.8
73,RhšnbrŠu Klosterbier,101,15
74,Lakkalikššri,4,10
75,Original Frankfurter grŸne So§e,125,7.75

Note that the above data was taken from the Northwind database that used to come with early versions of SQL Server.

Function Definitions

To demonstrate the power of Azure OpenAI Function Calling, we will create two separate function definitions. The first is a class named ProductAgent, which returns details about a product given the ‘Product Name’. The second is a class named MostExpensiveProductAgent, which returns the most expensive product.

Create a folder named Models and add to it three C# classes, namely: ProductProductAgent  and MostExpensiveProductAgent.

Product.cs

Add the following class to Product.cs:

public class Product{
  public int ProductId { get; set; }
  public string? ProductName { get; set; }
  public int UnitsInStock { get; set; }
  public float UnitPrice { get; set; } 
  
  // Load products from a csv file named products.csv in the wwwroot folder
  public static List<Product> LoadProducts() {
    var products = new List<Product>();
    using (var reader = new StreamReader(Path.Combine("wwwroot", "products.csv"))) {
      using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) {
          products = csv.GetRecords<Product>().ToList();
      }
  }
    return products;
  }
  
  public override string ToString() {
    return $"Product ID: {ProductId}, Product Name: {ProductName}, Units In Stock: {UnitsInStock}, Unit Price: {UnitPrice}";
  }
}

The above code declares a class named Product. The class properties match the column names in the products.csv file. The LoadProducts() static method reads contents of the CSV file and returns a hydrated list of Product objects. Note that the Product class also has a ToString() method.

ProductAgent.cs

public class ProductAgent {
  static public string Name = "get_product_details";
  static private List<Product> products = Product.LoadProducts(); 
  
  // Return the function metadata
  static public FunctionDefinition GetFunctionDefinition() {
    return new FunctionDefinition() {
        Name = Name,
        Description = "Get product details by product name",
        Parameters = BinaryData.FromObjectAsJson(
        new{
          Type = "object",
          Properties = new {
            ProductName = new {
              Type = "string",
              Description = "The product name, e.g. Pavlova",
            }
          },
          Required = new[] { "productName" },
        },
        new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }),
    };
  }
  
  static public string? GetProductDetails(string product){
    var productDetails = products.Where(p => p.ProductName == product).FirstOrDefault();
    if (productDetails == null){
      return null;
    }
    return productDetails.ToString();
  }
} 
  
// Argument for the function
public class ProductInput {
    public string ProductName { get; set; } = string.Empty;
}

The above code does the following:

  • The function definition is created as a JSON object with information about the function name, description, and required parameters.
  • The GetProductDetails() method receives a product parameter (essentially ProductName) and returns a string with product details.
  • The ProductInput class exists in the ProductAgent.cs file. It will be later used as the argument that is passed to the GetProductDetails() method.

MostExpensiveProductAgent.cs

public class MostExpensiveProductAgent {
  static public string Name = "get_most_expensive_product";
  static private List<Product> products = Product.LoadProducts(); 
  
  // Return the function metadata
  static public FunctionDefinition GetFunctionDefinition() {
    return new FunctionDefinition() {
      Name = Name,
      Description = "Get details of the most expensive product",
    };
  }
  
  static public string? GetMostExpensiveProductDetails() {
    var productDetails = products.OrderByDescending(p => p.UnitPrice).FirstOrDefault();
    if (productDetails == null) {
      return null;
    }
    return productDetails.ToString();
  }
}

T
he above code does the following:

  • Just as with the previous ProductAgent class, the function definition is created as a JSON object with information about the function name, description, and required parameters.
  •  The GetMostExpensiveProductDetails() method takes no arguments and simply returns a string with details of the most expensive product.

The User Interface

We will re-purpose the Index.cshtml and Index.cshtml.cs files so the user can enter a prompt in natural language and receive a response that comes from the OpenAI model working with our custom function. 

Index.chtml

Replace the content of Pages/Index.cshtml with:

@page
@model IndexModel
@{
    ViewData["Title"] = "OpenAI Function Calling";
}
<div class="text-center">
    <h1 class="display-4">@ViewData["Title"]</h1>
    <form method="post">
        <input type="text" name="prompt" size="80" required/>
        <input type="submit" value="Submit" />
    </form>
    <pre style="text-align: left">
        
        Example prompts:
        
        What is the id number of product: Louisiana Hot Spiced Okra?
        What is the unit price of product: Sir Rodney's Marmalade?
        How many units in stock for product: Tofu?
        What is the most expensive product?
    </pre>
    @if (Model.Reply != null) {
        <p class="alert alert-success">@Model.Reply</p>
    }
</div>

T
he above markup displays an HTML form that accepts a prompt from a user. The prompt is then submitted to the server and the response is displayed in a paragraph (<p> tag) with a green background (Bootstrap class alert-success).

Meantime, at the bottom of the page there are some suggested prompts – namely:

What is the id number of product: Louisiana Hot Spiced Okra?
What is the unit price of product: Sir Rodney's Marmalade?
How many units in stock for product: Tofu?
What is the most expensive product?

Index.chtml.cs

Replace the IndexModel class definition in Pages/Index.cshtml.cs with:

public class IndexModel : PageModel {
  private readonly ILogger<IndexModel> _logger;
  private readonly IConfiguration _config;
  
  [BindProperty]
  public string? Reply { get; set; }
  
  public IndexModel(ILogger<IndexModel> logger, IConfiguration config) {
    _logger = logger;
    _config = config;
  }
  
  public void OnGet() { }
  
  // action method that receives prompt from the form
  public async Task<IActionResult> OnPostAsync(string prompt) {
    // call the Azure Function
    var response = await CallFunction(prompt);
    Reply = response;
    return Page();
  }
  
  private async Task<string> CallFunction(string question) {
    string endpoint = _config["AzureOpenAiSettings:Endpoint"]!;
    string apiKey = _config["AzureOpenAiSettings:ApiKey"]!;
    string model = _config["AzureOpenAiSettings:Model"]!;
    
    Uri openAIUri = new(endpoint);
    
    // Instantiate OpenAIClient for Azure Open AI.
    OpenAIClient client = new(openAIUri, new AzureKeyCredential(apiKey));
    ChatCompletionsOptions chatCompletionsOptions = new();
    chatCompletionsOptions.DeploymentName = model;
    ChatChoice responseChoice;
    Response<ChatCompletions> responseWithoutStream;
    
    // Add function definitions
    FunctionDefinition getProductFunctionDefinition = ProductAgent.GetFunctionDefinition();
    FunctionDefinition getMostExpensiveProductDefinition = MostExpensiveProductAgent.GetFunctionDefinition();
    chatCompletionsOptions.Functions.Add(getProductFunctionDefinition);
    chatCompletionsOptions.Functions.Add(getMostExpensiveProductDefinition);

    chatCompletionsOptions.Messages.Add(
        new ChatRequestUserMessage(question)
    );
    responseWithoutStream =
        await client.GetChatCompletionsAsync(chatCompletionsOptions);
    responseChoice = responseWithoutStream.Value.Choices[0];
    
    while (responseChoice.FinishReason!.Value == CompletionsFinishReason.FunctionCall) {
      // Add message as a history.
      chatCompletionsOptions.Messages.Add(new ChatRequestUserMessage(responseChoice.Message.ToString()));
      if (responseChoice.Message.FunctionCall.Name == ProductAgent.Name) {
        string unvalidatedArguments = responseChoice.Message.FunctionCall.Arguments;
        ProductInput input = JsonSerializer.Deserialize<ProductInput>(unvalidatedArguments,
          new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })!;
        var functionResultData = ProductAgent.GetProductDetails(input.ProductName);
        var functionResponseMessage = new ChatRequestFunctionMessage(
          ProductAgent.Name,
          JsonSerializer.Serialize(
            functionResultData,
            new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
        chatCompletionsOptions.Messages.Add(functionResponseMessage);
      } else if (responseChoice.Message.FunctionCall.Name == MostExpensiveProductAgent.Name) {
        
        var functionResultData = MostExpensiveProductAgent.GetMostExpensiveProductDetails();
        var functionResponseMessage = new ChatRequestFunctionMessage(
          MostExpensiveProductAgent.Name,
          JsonSerializer.Serialize(
            functionResultData,
            new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }));
        chatCompletionsOptions.Messages.Add(functionResponseMessage);
      }

      // Call LLM again to generate the response.
      responseWithoutStream = await client.GetChatCompletionsAsync(chatCompletionsOptions);
      responseChoice = responseWithoutStream.Value.Choices[0];
    }
    return responseChoice.Message.Content;
  }
}

In the above code, the prompt entered by the user is posted to the OnPostAsync() method. The prompt is then passed to the CallFunction() method, which returns the final response from Azure OpenAI.

  • The CallFunction() method reads the Azure OpenAI settings from appsettings.json.
  • An OpenAIClient class is instantiated.
  • An ChatCompletionsOptions class is instantiated and the message property is set with the original prompt from the user.
  • Function definitions for ProductAgent and MostExpensiveProductAgent are obtained.
  • A GetChatCompletionsAsync call is then made to Azure Open AI. The response involves a call to a local custom function. The service is smart enough to call the correct function based on the context of the prompt. Responses from local custom function calls are added to the history of messages and sent back to OpenAI. Eventually, after no more local custom function calls are required, the final message is returned.

Trying the application

In a terminal window, at the root of the Razor Pages web application, enter the following command:

dotnet watch

The following page will display in your default browser:

After separately entering the four suggested prompts, you will receive the following responses:

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

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

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

Conclusion

The opportunities that Function Calling opens is enormous. I am simply scratching the surface of what the possibilities are. 

Resources



Sunday, October 1, 2023

Seed Users and Roles using EF Code First approach in ASP.NET Razor Pages

In this tutorial, I shall describe the steps you need to follow if you want to use Code First migration to seed both users and roles data. The seeding will be done inside the OnModelCreating() method of the Entity Framework DbContext class. To keep things simple, we will use SQLite.

In order to proceed with this tutorial you need to have the following prerequisites:

  • VS Code
  • You have installed .NET 8.0
  • You have installed the dotnet-ef tool

Getting Started

In a terminal window, execute the following command to create an ASP.NET Razor Pages application that supports database authentication using the lightweight SQLite database:

dotnet new razor --auth individual -f net8.0 -o Code1stUsersRoles

Change directory to the newly created folder then run the application:

cd Code1stUsersRoles
dotnet watch

Click on the Register link on the top-right side of your keyboard to add a new user. 


When you click on the Register button, you will receive a page that looks like this:


Click on the link “Click here to confirm your account” to simulate email confirmation. Thereafter, login with the newly created account email and password.

Click on Logout in the top-right corner.

Open the application folder in VS Code.

Create a class named SeedUsersRoles in the Data folder of your application. This will contain seed data for roles, users, and information about users that belong to roles. Below is the code for the SeedUsersRoles class:

public class SeedUsersRoles {
    private readonly List<IdentityRole> _roles;
    private readonly List<IdentityUser> _users;
    private readonly List<IdentityUserRole<string>> _userRoles; 
 
    public SeedUsersRoles() {
      _roles = GetRoles();
      _users = GetUsers();
      _userRoles = GetUserRoles(_users, _roles);
    } 
    public List<IdentityRole> Roles { get { return _roles; } }
    public List<IdentityUser> Users { get { return _users; } }
    public List<IdentityUserRole<string>> UserRoles { get { return _userRoles; } }
    private List<IdentityRole> GetRoles() {
      // Seed Roles
      var adminRole = new IdentityRole("Admin");
      adminRole.NormalizedName = adminRole.Name!.ToUpper();
      var memberRole = new IdentityRole("Member");
      memberRole.NormalizedName = memberRole.Name!.ToUpper();
      List<IdentityRole> roles = new List<IdentityRole>() {
adminRole,
memberRole
      };
      return roles;
    }
    private List<IdentityUser> GetUsers() {
      string pwd = "P@$$w0rd";
      var passwordHasher = new PasswordHasher<IdentityUser>();
      // Seed Users
      var adminUser = new IdentityUser {
        UserName = "aa@aa.aa",
        Email = "aa@aa.aa",
        EmailConfirmed = true,
      };
      adminUser.NormalizedUserName = adminUser.UserName.ToUpper();
      adminUser.NormalizedEmail = adminUser.Email.ToUpper();
      adminUser.PasswordHash = passwordHasher.HashPassword(adminUser, pwd);
      var memberUser = new IdentityUser {
        UserName = "mm@mm.mm",
        Email = "mm@mm.mm",
        EmailConfirmed = true,
      };
      memberUser.NormalizedUserName = memberUser.UserName.ToUpper();
      memberUser.NormalizedEmail = memberUser.Email.ToUpper();
      memberUser.PasswordHash = passwordHasher.HashPassword(memberUser, pwd);
      List<IdentityUser> users = new List<IdentityUser>() {
adminUser,
memberUser,
      };
      return users;
    }
    private List<IdentityUserRole<string>> GetUserRoles(List<IdentityUser> users, List<IdentityRole> roles) {
      // Seed UserRoles
      List<IdentityUserRole<string>> userRoles = new List<IdentityUserRole<string>>();
      userRoles.Add(new IdentityUserRole<string> {
        UserId = users[0].Id,
        RoleId = roles.First(q => q.Name == "Admin").Id
      });
      userRoles.Add(new IdentityUserRole<string> {
        UserId = users[1].Id,
        RoleId = roles.First(q => q.Name == "Member").Id
      });
      return userRoles;
    }
}

Open Data/ApplicationDbContext.cs in your editor. Add the following OnModelCreating() method to the class:

protected override void OnModelCreating(ModelBuilder builder) {
  base.OnModelCreating(builder);
  // Use seed method here
  SeedUsersRoles seedUsersRoles = new();
  builder.Entity<IdentityRole>().HasData(seedUsersRoles.Roles);
  builder.Entity<IdentityUser>().HasData(seedUsersRoles.Users);
  builder.Entity<IdentityUserRole<string>>().HasData(seedUsersRoles.UserRoles);
} 
 
In the Program.cs class, replace the call to builder.Services.AddDefaultIdentity statement so that it registers IdentityRole. Replace the entire builder.Services.AddDefaultIdentity statement with the following code:

builder.Services.AddIdentity<IdentityUser, IdentityRole>(
options => {
    options.Stores.MaxLengthForKeys = 128;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddRoles<IdentityRole>()
.AddDefaultUI()
.AddDefaultTokenProviders();

Delete the Data/Migrations folder and the app.db file because we will add new migrations. Thereafter, type the following from a terminal window inside of the root folder of your application:

dotnet ef migrations add M1 -o Data/Migrations

We will apply migrations and update the database with the following command:

dotnet ef database update

Now start the application. To prove that user and role data are successfully seeded, login with one of the below credentials that were previously seeded:

Email Password Role
aa@aa.aa P@$$w0rd Admin
mm@mm.mm P@$$w0rd Member

Add the following above the IndexModel class in Index.cshtml.cs to only allow users that belong to the Member role:

[Authorize (Roles="Member")]

Also, add the following above the PrivacyModel class in Privacy.cshtml.cs to only allow users that belong to the Admin role:

[Authorize (Roles="Admin")]

Only mm@mm.mm is allowed into the / home page and aa@aa.aa is allowed in the /privacy page.

We have succeeded in seeding user and role. Happy Coding.