Showing posts with label Development. Show all posts
Showing posts with label Development. Show all posts

Monday, October 12, 2026

Seed & Extend Identity Users & Roles in ASP.NET with Scaffold tool

In this tutorial, we will learn how to add more data fields to the standard ASP.NET users & roles schema using the scaffold tool. We will also learn how to seed users and roles.

Prerequisites

To proceed with this tutorial, you need the following:

  • VS Code
  • .NET: The current version of .NET is 10.0
  • the dotnet-ef tool
  • Tthe Microsoft.dotnet-scaffold tool

Getting Started

Create a new razor pages application with the following terminal window command:

dotnet new razor --auth individual -o XtendUsersRoles
cd XtendUsersRoles
mkdir Models

This creates an ASP.NET app that uses SQLite database for authentication.

Open the application folder in VS Code.

Suppose we want to capture more data about the user, in addition to email and password. Keeping it simple, lwe will extend user data with FirstName & LastName.

An easy way to do this is to create a new class that extends IdentityUser and adds FirstName and LastName properties. In the Models folder, add a new class named XUser to it with the following class code: 

public class XUser : IdentityUser {
  public XUser() : base() { }

  public string? FirstName { get; set; }
  public string? LastName { get; set; }
}

We may also wish to extend the standard roles table with a CreatedDate property.

Just as we did with users, we will create another class for roles that inherits from IdentityRole. In the Models folder, add another class named XRole and add to it the following code:

public class XRole: IdentityRole {

  public XRole() : base() { }

  public XRole(string roleName) : base(roleName) { }

  public DateTime CreatedDate { get; set; }
}

Add the following to Pages/_ViewImports.cshtml:

@using XtendUsersRoles.Models

Edit Data/ApplicationDbContext.cs file and make ApplicationDbContext  inherit from IdentityDbContext<XUser, XRole, string>(options). The ApplicationDbContext class code will look like this:

public class ApplicationDbContext(
    DbContextOptions<ApplicationDbContext> options)
    : IdentityDbContext<XUser, XRole, string>(options)
{

}

Add these passwords to the appsettings.Development.json file:

"Passwords": {
  "Admin": "P@$$w0rd",
  "Member": "P@$$w0rd"
}

We will seed the database with two roles and two users. Add Data/IdentitySeeder.cs with the following code:

public static class IdentitySeeder {
  public static async Task SeedAsync(
      UserManager<XUser> userManager,
      RoleManager<XRole> roleManager,
      IConfiguration configuration) {

      ArgumentNullException.ThrowIfNull(userManager);
      ArgumentNullException.ThrowIfNull(roleManager);
      ArgumentNullException.ThrowIfNull(configuration);

      await EnsureRoleAsync(roleManager, "Admin");
      await EnsureRoleAsync(roleManager, "Member");

      await EnsureUserAsync(
          userManager, configuration["Passwords:Admin"]!, "aa@aa.aa", "Adam", "Atkins", "Admin");

      await EnsureUserAsync(

          userManager, configuration["Passwords:Member"]!, "mm@mm.mm", "Mike", "Moore", "Member");
  }

  private static async Task EnsureRoleAsync(RoleManager<XRole> roleManager, string roleName) {
      if (!await roleManager.RoleExistsAsync(roleName)) {
          var result = await roleManager.CreateAsync(new XRole(roleName) {
              CreatedDate = DateTime.UtcNow
          });

          EnsureSucceeded(result, $"creating role '{roleName}'");
      }
  }

  private static async Task EnsureUserAsync(
      UserManager<XUser> userManager,
      string password,
      string email,
      string firstName,
      string lastName,
      string roleName) {

      var user = await userManager.FindByEmailAsync(email);

      if (user is null) {
          user = new XUser {
              UserName = email,
              Email = email,
              EmailConfirmed = true,
              FirstName = firstName,
              LastName = lastName
          };

          var result = await userManager.CreateAsync(user, password);
          EnsureSucceeded(result, $"creating user '{email}'");
      }

      if (!await userManager.IsInRoleAsync(user, roleName)) {
          var result = await userManager.AddToRoleAsync(user, roleName);
          EnsureSucceeded(result, $"adding user '{email}' to role '{roleName}'");
      }

  }

  private static void EnsureSucceeded(IdentityResult result, string operation) {
      if (!result.Succeeded) {
          var errors = string.Join("; ", result.Errors.Select(error => $"{error.Code}: {error.Description}"));

          throw new InvalidOperationException($"Identity seeding failed while {operation}: {errors}");
      }
  }
}

In the Program.cs, replace the builder.Services.AddDefaultIdentity. . .  statement with this:

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

We need to call the identity seed logic from somewhere. In Program.cs, Add the following code to the “if (app.Environment.IsDevelopment())” block, right before “app.UseMigrationsEndPoint()”:

await using var scope = app.Services.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await dbContext.Database.MigrateAsync();

await IdentitySeeder.SeedAsync(
    scope.ServiceProvider.GetRequiredService<UserManager<XUser>>(),
    scope.ServiceProvider.GetRequiredService<RoleManager<XRole>>(),
    app.Configuration);

Edit Pages/Shared/_LoginPartial.cshtml and change:

@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

TO

@inject SignInManager<XUser> SignInManager
@inject UserManager<XUser> UserManager

Let us start with a clean database and migration. Therefore, delete app.db and the Data/Migrations folder. 

If you do not already have the .NET entity framework tool, execute the following command from within a terminal window:

dotnet tool install -g dotnet-ef

Then, execute the following entity framework migration commands from within a terminal window in the root folder of the application:

dotnet ef migrations add M1 -o Data/Migrations
dotnet ef database update

To seed the database with sample roles and users, start the application with:

dotnet watch

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

EmailPassword
aa@aa.aa P@$$w0rd
mm@mm.mm P@$$w0rd

The next task we need to accomplish is to modify the registration page so that the application can capture extended data such as FirstName & LastName. ASP.NET provides ASP.NET Core Identity as a Razor Class Library. This means that the registration UI is baked into the assemblies and is surfaced with the .AddDefaultUI() option with the services.AddIdentity() command in Program.cs.

If you do not already have the .NET scaffolding tool, execute the following command from within a terminal window:

dotnet tool install -g Microsoft.dotnet-scaffold

To expose the identity pages (like LOgin, Register, Logout, etc) and change them, we will use the Scaffold tool. Run the following command from within a terminal window at the root of your project:

dotnet scaffold

Follow these steps to generate the Identity razor pages:


The result of the scaffolding process is that we now have a new folder /Areas/Identity/Pages/Account with many pages that pertains to Identity:

There are, however, some things that the scaffold tool did incorrectly:

  1. It added an ApplicationDbContext connection string in appsettings.json file. Go ahead and delete it.
  2. The scaffold tool also added this line of code into Program.cs, that also needs to be deleted:

builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true).AddEntityFrameworkStores<ApplicationDbContext>();

Edit the code-behind file Areas/Identity/Pages/Account/Register.cshtml.cs. Add the following properties to the InputModel class: 

[Required]
[DataType(DataType.Text)]
[StringLength(50, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 2)]
[Display(Name = "First Name")]
public string? FirstName { get; set; }

[Required]
[DataType(DataType.Text)]
[StringLength(50, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 2)]
[Display(Name = "Last Name")]
public string? LastName { get; set; }

In the same file, delete the CreateUser() method.

In the OnPostAsync() method at around line 128, replace:

var user = CreateUser();

with: 

var user = new XUser {
  UserName = Input.Email,
  Email = Input.Email,
  FirstName = Input.FirstName,
  LastName = Input.LastName
};

In the same Register.cshtml.cs file, replace all instances of ApplicationUser with XUser. Do this also in files Login.cshtml.cs and Logout.cshtml.cs.

Next, let us update the UI. Edit razor page Areas\Identity\Pages\Account\Register.cshtml. Add the following markup to the form right before the email/username block: 

<div class="form-floating mb-3">
    <input asp-for="Input.FirstName" class="form-control" aria-required="true" placeholder="first name" />
    <label asp-for="Input.FirstName">First Name</label>
    <span asp-validation-for="Input.FirstName" class="text-danger"></span>
</div>

<div class="form-floating mb-3">
    <input asp-for="Input.LastName" class="form-control" aria-required="true" placeholder="last name" />
    <label asp-for="Input.LastName">Last Name</label>
    <span asp-validation-for="Input.LastName" class="text-danger"></span>
</div>

Run the web application and click on the Register button on the top-right side.


After you click on Register, all user data will be saved in the database. We can capture first name and last name as expected. 

We have succeeded in updating the registration page so that additional user data can be captured. Mission accomplished.

Monday, October 5, 2026

ASP.NET 10.0 Minimal WebAPI with SQLite & Swagger

This tutorial is about using ASP.NET 10.0 Minimal WebAPI. We will build a simple application with SQLite to save students data. We will then consume the API from an HTML page and add a Swagger interface to test the API endpoints.

Requirements:

  • .NET 10.0
  • C# Dev Kit VS Code Extenstion

In a suitable working directory, create a Web API web application in the terminal window with:

dotnet new webapi -o StudentsMinApi 
cd StudentsMinApi
mkdir Data
mkdir Models
mkdir wwwroot code . dotnet watch

This will appear in your terminal window:

Point your browser to the URL displayed in the terminal window, followed by /weatherforecast.

In the above case the address would be http://localhost:5130/weatherforecast. This would display the following in your browser:

Look at the API code in Program.cs:

app.MapGet("/weatherforecast", () => {
    var forecast =  Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast");

Add this Entity Framework tool if you do not already have it: 

dotnet tool install --global dotnet-ef 

Let us add these packages that provide support for SQLite and Swagger: 

dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SQLite
dotnet add package Microsoft.EntityFrameworkCore.SQLite.Design
dotnet add package Swashbuckle.AspNetCore

Inside the Models folder, add the following Student class: 

public class Student {
    public int StudentId { get; set; }
    public string? LastName { get; set; }
    public string? FirstName { get; set; }
    public string? School { get; set; }
    public string? Gender { get; set; }
    public DateTime? DateOfBirth { get; set; }
}

Developers prefer having sample data when building data driven applications. Therefore, we will create some sample data to ensure that our application behaves as expected. Copy the following data from https://gist.github.com/medhatelmasry/2ae7a36d5392f862b0d1039b2a52b9b4 and save it into a text file wwwroot/students.csv.

Add the following connection string to your  appsettings.json file:

"ConnectionStrings": {
  "DefaultConnection": "DataSource=school.db;cache=shared;"
},

Next, we need to add an Entity Framework context class. Inside the Data folder, add a class file named ApplicationDbContext with the following content: 

public class ApplicationDbContext : DbContext {

    public DbSet<tudent> Students => Set<Student>();

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder) {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Entity<Student>().HasData(GetStudents());
    }

    private static IEnumerable<Student> GetStudents() {
        string[] p = { Directory.GetCurrentDirectory(), "wwwroot", "students.json" };
        var filePath = Path.Combine(p);

        var json = File.ReadAllText(filePath);
        var data = System.Text.Json.JsonSerializer.Deserialize<List<Student>>(json) ?? new List<Student>();

        return data;
    }

}

In the above code, student data is being seeded in the OnModelCreating() method by reading contents of students.json file.

We need to register the context class (ApplicationDbContext) with dependency injection in Program.cs. Add the following code right before “var app = builder.Build();” in Program.cs: 

var connStr = builder.Configuration.GetConnectionString("DefaultConnection")
    ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");

builder.Services.AddDbContext<ApplicationDbContext>(option => option.UseSqlite(connStr));

Let us add a migration and subsequently update the database. Execute the following CLI commands in a terminal window:

dotnet ef migrations add M1 -o Data/Migrations
dotnet ef database update

At this point the database and tables are created.

Students API

Let us add API endpoints that:

  • Read all the students
  • Read student data by id
  • Add student data
  • Update student data
  • Delete student data

This can easily be done using the scaffold tool. Make sure you install the tool globally with this terminal window command:

dotnet tool install --global Microsoft.dotnet-scaffold

Once you have installed the scaffold tool, enter the following command to start the tool at the root of your project:

dotnet scaffold

Make the choices as shown in the various steps below:

 

A file named StudentsApi.cs gets created in the root folder of your project with this code:

using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
using StudentsMinApi.Data;
using StudentsMinApi.Models;

public static class StudentsApi {
    public static void MapStudentEndpoints(this IEndpointRouteBuilder routes) {
        var group = routes.MapGroup("/api/Student").WithTags(nameof(Student));

        group.MapGet("/", async (ApplicationDbContext db) => {
            return await db.Students.ToListAsync();
        })
        .WithName("GetAllStudents");

        group.MapGet("/{id}", async Task<Results<Ok<Student>, NotFound>> (int studentid, ApplicationDbContext db) => {
            return await db.Students.AsNoTracking()
                .FirstOrDefaultAsync(model => model.StudentId == studentid)
                is Student model
                    ? TypedResults.Ok(model)
                    : TypedResults.NotFound();
        })
        .WithName("GetStudentById");

        group.MapPut("/{id}", async Task<Results<Ok, NotFound>> (int studentid, Student student, ApplicationDbContext db) => {
            var affected = await db.Students
                .Where(model => model.StudentId == studentid)
                .ExecuteUpdateAsync(setters => setters
                .SetProperty(m => m.StudentId, student.StudentId)
                .SetProperty(m => m.LastName, student.LastName)
                .SetProperty(m => m.FirstName, student.FirstName)
                .SetProperty(m => m.School, student.School)
                .SetProperty(m => m.Gender, student.Gender)
                .SetProperty(m => m.DateOfBirth, student.DateOfBirth)
        );

            return affected == 1 ? TypedResults.Ok() : TypedResults.NotFound();
        })
        .WithName("UpdateStudent");

        group.MapPost("/", async (Student student, ApplicationDbContext db) => {
            db.Students.Add(student);
            await db.SaveChangesAsync();
            return TypedResults.Created($"/api/Student/{student.StudentId}",student);
        })
        .WithName("CreateStudent");

        group.MapDelete("/{id}", async Task<Results<Ok, NotFound>> (int studentid, ApplicationDbContext db) => {
            var affected = await db.Students
                .Where(model => model.StudentId == studentid)
                .ExecuteDeleteAsync();

            return affected == 1 ? TypedResults.Ok() : TypedResults.NotFound();
        })
        .WithName("DeleteStudent");
    }
}

Note the following mapping code in your Program.cs file:

app.MapStudentEndpoints();

You can start the WebAPI app and view all students at endpoint: /api/student.

OPTIONAL: If you want migrations to be applied automatically, add the following code to Program.cs right before the last “app.Run()” statement

using (var scope = app.Services.CreateScope()) {
    var services = scope.ServiceProvider;

    var context = services.GetRequiredService<ApplicationDbContext>();    
    context.Database.Migrate();
}

Adding Swagger Support

It is very easy to add Swagger support to our application so that we can test the various endpoints that our WebAPI application has to offer. 

Add the following code right under app.MapOpenApi():

app.UseSwaggerUI(options =>
{
        options.SwaggerEndpoint("/openapi/v1.json", "My WebAPI");
});

In file Properties/launchSettings.json, set the value of launchBrowser to true in two places:

"launchBrowser": true

Also, add the following property to both the http and https JSON blocks:

"launchUrl": "swagger"

Restart the app by typing dotnet watch in the terminal window. The app will automatically load into your default browser in a Swagger interfact that looks like this:

Go ahead and test all the available student endpoints.

You may encounter the following error when you try any of the endpoints that require an ID parameter - example: /api/Student/{id} endpoint:

This error can easily be resolved by targeting OpenAPI version 3. In Program.cs, replace "builder.Services.AddOpenApi();" with:

builder.Services.AddOpenApi(options => {
    options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_0;
});

CORS (Cross-Origin Resource Sharing)

In wwwroot folder, create a file named show.html and add to it this HTML/JavaScript code:

<!DOCTYPE html>
<html>
  <html>
    <head>
      <meta charset="utf-8" />
      <title>Test API</title>
    </head>
    <body>
      <h3>Test API</h3>
      <button id="btnGetData">Get Data</button>
      <pre id="preOutput"></pre>
      <script>
        const url = "PUT-API-URL-HERE";

        var showResponse = function (object) {
          document.querySelector("#preOutput").innerHTML = JSON.stringify(
            object,
            null,
            4
          );
        };

        const button = document.querySelector("#btnGetData");
        button.addEventListener("click", (e) => {
          getData();
        });

        var getData = async function () {
          await fetch(url)
            .then((response) => {
              return response.json();
            })
            .then((data) => {
              showResponse(data);
            });

          return false;
        };
      </script>
    </body>
  </html>
</html>

Replace PUT-API-URL-HERE with the URL that gets all the students (Example: http://localhost:5130/api/students). 

From the file system, double-click on the wwwroot/show.html file. You will see the following page:


When you click on the “Get Data” button, nothing will appear because there is a JavaScript error. To understand where this error is coming from, hit F12 in your browser and check the console. This error will appear:

We need to enable CORS in the WebAPI project. This is done by adding the following code in Program.cs just before “var app = builder.Build();”:
 
// Add Cors
builder.Services.AddCors(o => o.AddPolicy("Policy", builder => {
  builder.AllowAnyOrigin()
    .AllowAnyMethod()
    .AllowAnyHeader();
}));

Also, in the same Program.cs file, add this code just after “var app = builder.Build();”: 

app.UseCors("Policy");

Save your code then make a new request for data from show.html. This time you should be successful:


The application should work as expected.

Congrats for coming this far.

Thursday, March 12, 2026

Function Calling with Microsoft Agent Framework, C#, & Entity Framework

In this article, we will create a Microsoft Agentic Framework plugin that contains four functions that interact with live SQLite data. Entity Framework will be used to access the SQLite database. The end result is to use the powers of the OpenAI natural language models to ask questions and get answers about our custom data.

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

Pre-requisites

  • You will be using AI models hosted on GitHub. Therefore, you will need to obtain a personal access token from GitHub.
  • .NET Framework 10.0+

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 --auth individual -o EfFuncCallMAF
cd EfFuncCallMAF

Te above creates a Razor Pages app with support for Entity Framework and SQLite.

Add these packages:

dotnet add package CsvHelper
dotnet add package Microsoft.Agents.AI.OpenAI --prerelease 
dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Identity.UI
dotnet add package Microsoft.EntityFrameworkCore.Sqlite

The CsvHelper package will help us load a list of products from a CSV file named students.csv and hydrate a list of Student objects. The second package is needed to work with Microsoft Agent Framework. The rest of the packages support Identity, Entity Framework and SQLite.

Let’s Code

appsettings.json

Add these to appsettings.json:

"GitHub": {
  "Token": "PUT-GITHUB-PERSONAL-ACCESS-TOKEN-HERE",
  "ApiEndpoint": "https://models.github.ai/inference",
  "Model": "openai/gpt-4o-mini"
}

Of course, you need to adjust the Token setting with your GitHub personal access token.

Data

Create a folder named Models. Inside the Models folder, add the following Student class: 

public class Student {
   public int StudentId { get; set; }

   [Display(Name = "First Name")]
   [Required]
   public string? FirstName { get; set; }

   [Display(Name = "Last Name")]
   [Required]
   public string? LastName { get; set; }

   [Required]
   public string? School { get; set; }
 
   public override string ToString() {
      return $"Student ID: {StudentId}, First Name: {FirstName}, Last Name: {LastName}, School: {School}";
   }
}

Developers like having sample data when building data driven applications. Therefore, we will create sample data to ensure that our application behaves as expected. Copy CSV data from this link and save it to a text file wwwroot/students.csv.

Add the following code inside the Data/ApplicationDbContext class located inside the Data folder:

public DbSet<Student> Students => Set<Student>();    
 
protected override void OnModelCreating(ModelBuilder modelBuilder) {
    base.OnModelCreating(modelBuilder);
    modelBuilder.Entity<Student>().HasData(LoadStudents());
}  
 
// Load students from a csv file named students.csv in the wwwroot folder
public static List<Student> LoadStudents() {
    var students = new List<Student>();
    using (var reader = new StreamReader(Path.Combine("wwwroot", "students.csv"))) {
        using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
        students = csv.GetRecords<Student>().ToList();
    }
    return students;
}

Let us add a migration and subsequently update the database. Execute the following CLI commands in a terminal window.

dotnet ef migrations add M1 -o Data/Migrations
dotnet ef database update

At this point the database and tables are created in a SQLite database named app.db.

Helper Methods

We need a couple of static helper methods to assist us along the way. In the Models folder, add a class named Utils and add to it the following class definition:

public class Utils {
  public static string GetConfigValue(string config) {
    IConfigurationBuilder builder = new ConfigurationBuilder();
    if (System.IO.File.Exists("appsettings.json"))
      builder.AddJsonFile("appsettings.json", false, true);
    if (System.IO.File.Exists("appsettings.Development.json"))
      builder.AddJsonFile("appsettings.Development.json", false, true);
    IConfigurationRoot root = builder.Build();
    return root[config]!;
  }
 
  public static ApplicationDbContext GetDbContext() {
    var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
    var connStr = Utils.GetConfigValue("ConnectionStrings:DefaultConnection");
    optionsBuilder.UseSqlite(connStr);
    ApplicationDbContext db = new ApplicationDbContext(optionsBuilder.Options);
    return db;
  }
}

Method GetConfigValue() will read values in appsettings.json from any static method. The second GetDbContext() method gets an instance of the ApplicationDbContext class, also from any static method.

Plugins

Create a folder named Plugins and add to it the following class file named StudentPlugin.cs with this code:

public class StudentPlugin {
  [Description("Get student details by first name and last name")]
  public static string? GetStudentDetails(
    [Description("student first name, e.g. Kim")]
    string firstName,
    [Description("student last name, e.g. Ash")]
    string lastName
  ) {
      var db = Utils.GetDbContext();
      var studentDetails = db.Students
        .Where(s => s.FirstName == firstName && s.LastName == lastName).FirstOrDefault();
      if (studentDetails == null)
          return null;
      return studentDetails.ToString();
  }

  [Description("Get students in a school given the school name")]
  public static string? GetStudentsBySchool(
  [Description("The school name, e.g. Nursing")]
  string school
  ) {
      var studentsBySchool = Utils.GetDbContext().Students
        .Where(s => s.School == school).ToList();
      if (studentsBySchool.Count == 0)
          return null;
      return JsonSerializer.Serialize(studentsBySchool);
  }


  [Description("Get the school with most or least students. Takes boolean argument with true for most and false for least.")]
  static public string? GetSchoolWithMostOrLeastStudents(
  [Description("isMost is a boolean argument with true for most and false for least. Default is true.")]
  bool isMost = true
  ) {
      var students = Utils.GetDbContext().Students.ToList();
      IGrouping<string, Student>? schoolGroup = null;
      if (isMost)
          schoolGroup = students.GroupBy(s => s.School)
              .OrderByDescending(g => g.Count()).FirstOrDefault()!;
      else
          schoolGroup = students.GroupBy(s => s.School)
              .OrderBy(g => g.Count()).FirstOrDefault()!;
      if (schoolGroup != null)
          return $"{schoolGroup.Key} has {schoolGroup.Count()} students";
      else
          return null;
  }

  [Description("Get students grouped by school.")]
  static public string? GetStudentsInSchool() {
      var students = Utils.GetDbContext().Students.ToList().GroupBy(s => s.School)
        .OrderByDescending(g => g.Count());
      if (students == null)
          return null;
      else
          return JsonSerializer.Serialize(students);
  }
}

 In the above code, there are four methods with these purposes:

GetStudentDetails()Gets student details given first and last names
GetStudentsBySchool()Gets students in a school given the name of the school
GetSchoolWithMostOrLeastStudents()Takes a Boolean value isMost – true returns school with most students and false returns school with least students.
GetStudentsInSchool()Takes no arguments and returns a count of students by school.

Registering the Chat Client

In the Program.cs file, add the following code to register the MAF chat client so it is available for dependency injecton. The code goes before the "var app = builder.Build();" statement.

string? apiKey = builder.Configuration["GitHub:Token"];
string? model = builder.Configuration["GitHub:Model"] ?? "openai/gpt-4o-mini";
string? endpoint = builder.Configuration["GitHub:ApiEndpoint"] ?? "https://models.github.ai/inference";

builder.Services.AddSingleton<IChatClient>(_ =>
    new OpenAIClient(
        new ApiKeyCredential(apiKey!),
        new OpenAIClientOptions { Endpoint = new Uri(endpoint!) }
    ).GetChatClient(model!).AsIChatClient()
);

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 Microsoft Agent Framework plugin. 

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 IChatClient _chatClient;

  [BindProperty]
  public string? Reply { get; set; }

  public IndexModel(ILogger<IndexModel> logger, IChatClient chatClient) {
    _logger = logger;
    _chatClient = chatClient;
  }
  public void OnGet() { }
  // action method that receives prompt from the form
  public async Task<IActionResult> OnPostAsync(string prompt) {
    var response = await CallFunction(prompt);
    Reply = response;
    return Page();
  }

  private async Task<string> CallFunction(string question) {
    // Create tools from StudentPlugin methods
    var tools = new List<AITool> {
      AIFunctionFactory.Create(StudentPlugin.GetStudentDetails),
      AIFunctionFactory.Create(StudentPlugin.GetStudentsBySchool),
      AIFunctionFactory.Create(StudentPlugin.GetSchoolWithMostOrLeastStudents),
      AIFunctionFactory.Create(StudentPlugin.GetStudentsInSchool),
    };

    // Create the AI agent with tools
    var agent = _chatClient.AsAIAgent(
      instructions: "You are a helpful assistant that can look up student information.",
      name: "StudentAgent",
      tools: tools
    );

    // Run streaming and collect the response
    string fullMessage = "";
    await foreach (var update in agent.RunStreamingAsync(question)) {
      if (!string.IsNullOrEmpty(update.Text)) {
        fullMessage += update.Text;
      }
    }
    return fullMessage;
  }
}

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 sets up the AI agent with tools.

Note that the IChatClient object is available through dependency injection

All the tools (or plugins) are loaded into a list of AITool objects.

Index.chtml

Replace the content of Pages/Index.cshtml with:

@page
@model IndexModel
@{
    ViewData["Title"] = "Function Calling with Microsoft Agent Framework";
}
<div class="text-center">
    <h3 class="display-6">@ViewData["Title"]</h3>
    <form method="post">
        <input type="text" name="prompt" size="80" required />
        <input type="submit" value="Submit" />
    </form>
    <div style="text-align: left">
        <h5>Example prompts:</h5>
        <p>Which school does Mat Tan go to?</p>
        <p>Which school has the most students?</p>
        <p>Which school has the least students?</p>
        <p>Get the count of students in each school.</p>
        <p>How many students are there in the school of Mining?</p>
        <p>What is the ID of Jan Fry and which school does she go to?</p>
        <p>Which students belong to the school of Business? Respond only in JSON format.</p>
        <p>Which students in the school of Nursing have their first or last name start with the letter 'J'?</p>
    </div>
    @if (Model.Reply != null)
    {
        <p class="alert alert-success" id="reply">@Model.Reply</p>
    }
</div>

The 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 to facilitate testing – namely:

Which school does Mat Tan go to?
Which school has the most students?
Which school has the least students?
Get the count of students in each school.
How many students are there in the school of Mining?
What is the ID of Jan Fry and which school does she go to?
Which students belong to the school of Business? Respond only in JSON format.
Which students in the school of Nursing have their first or last name start with the letter 'J'?

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:

You can enter any of the suggested prompts to ensure we are getting the proper results. I entered the last prompt and got these results:


Conclusion

We have seen how The Micrsoft Agenr Framework and Function Calling can be used with data coming from a database. In this example we are using SQLite. However, any other database can be used using the same technique.

Sunday, October 19, 2025

Small Language Models with AI Toolkit Extension in VS Code

In this article, we will see how we can work with small language models (SLM) from the AI Toolkit extension in VS Code. Though the toolkit can do other things, our focus is to consume an ONNX SLM hosted on Visual Studio Code from a C# application. We will first look at an example that is based on OpenAI packages. We will later use a similar example based on the Sematic Kernal approach.

Companion Video: https://youtu.be/V_eWAM2fxJg

Prerequisites

You will need:

  • The latest version of VS Code
  • .NET version 9.0 or higher

What are small language models (SLMs)?

Small Language Models (SLMs) are compact versions of large language models (LLMs), designed to deliver strong performance in natural language tasks while using significantly fewer computational resources.

What is the AI Toolkit Extension in VS Code?

The AI Toolkit Extension for Visual Studio Code is a powerful, all-in-one environment for building, testing, and deploying generative AI applications—especially useful for developers working with small language models (SLMs).

Getting Started

Install the following Visual Studio Code extension:


Click on the three dots (...) in the left navigation of VS Code, and choose "AI Toolkit".

Click on "Model Catalog".

Scroll down down the list until you find “Local Models” >> ONNX >> Minstral 7B – (CPU – Small, Standard) >> + Add Model.

Once the model is fully downloaded, it will appear under Models >> ONNX.

Right-click on the model and select “Copy Model Name”.

I copied the following name for the "Minstral 7B" model: 

mistral-7b-v02-int4-cpu

Using OpenAI packages

Create a C# console application named AIToolkitOpenAI and add to it required packages with the following terminal window commands:

dotnet new console -n AIToolkitOpenAI
cd AIToolkitOpenAI
dotnet add package OpenAI

Start VS Code with:

code .

Click on the "AI Toolkit" tab in VS Code and make sure that the "Minstral 7B" model is running.

Replace content of Program.cs with this code:

using OpenAI;
using OpenAI.Chat;
using System.ClientModel;
using System.Text;

var model = "mistral-7b-v02-int4-cpu";
var baseUrl = "http://localhost:5272/v1/"; // root URL for local OpenAI-like server
var apikey = "unused";

OpenAIClientOptions options = new OpenAIClientOptions();
options.Endpoint = new Uri(baseUrl);
ApiKeyCredential credential = new ApiKeyCredential(apikey);
ChatClient client = new OpenAIClient(credential, options).GetChatClient(model);

// Build the prompt
StringBuilder prompt = new StringBuilder();
prompt.AppendLine("You will analyze the sentiment of the following product reviews.");
prompt.AppendLine("Each line is its own review. Output the sentiment of each review in");
prompt.AppendLine("a bulleted list and then provide a general sentiment of all reviews.");
prompt.AppendLine();
prompt.AppendLine("I bought this product and it's amazing. I love it!");
prompt.AppendLine("This product is terrible. I hate it.");
prompt.AppendLine("I'm not sure about this product. It's okay.");
prompt.AppendLine("I found this product based on the other reviews. It worked");

// send the prompt to the model and wait for the text completion
var response = await client.CompleteChatAsync(prompt.ToString());
// display the response
Console.WriteLine(response.Value.Content[0].Text);

Run the application with:

dotnet run

The application does sentiment analysis on what customers think of the product.

This is a sample of the output:

* I bought this product and it's amazing. I love it!: Positive sentiment
* This product is terrible. I hate it.: Negative sentiment
* I'm not sure about this product. It's okay.: Neutral sentiment
* I found this product based on the other reviews. It worked for me.: Positive sentiment

General sentiment: The reviews contain both positive and negative sentiments. Some customers expressed their love for the product, while others expressed their dislike. Neutral sentiment was also expressed by one customer. Overall, the reviews suggest that the product has the potential to elicit strong feelings from customers, both positive and negative.

Sematic Kernel packages

Create a C# console application named AIToolkitSK and add to it required packages with the following terminal window commands:

dotnet new console -n AIToolkitSK
cd AIToolkitSK
dotnet add package Microsoft.SemanticKernel

Start VS Code with:

code .

Click on the "AI Toolkit" tab in VS Code and make sure that the "Minstral 7B" model is running.

Replace content of Program.cs with this code:

using System.Text;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;

var model = "mistral-7b-v02-int4-cpu";
var baseUrl = "http://localhost:5272/v1/";
var apikey = "unused";

// Create a chat completion service
var kernel = Kernel.CreateBuilder()
    .AddOpenAIChatCompletion(modelId: model, apiKey: apikey, endpoint: new Uri(baseUrl))
    .Build();
var chat = kernel.GetRequiredService<IChatCompletionService>();
var history = new ChatHistory();
history.AddSystemMessage("You are a useful chatbot. Always reply in a funny way with short answers.");
var settings = new OpenAIPromptExecutionSettings
{
    MaxTokens = 500,
    Temperature = 1,
};

while (true)
{
    Console.Write("\nUser: ");
    var userInput = Console.ReadLine();
    if (string.IsNullOrWhiteSpace(userInput)) break;

    history.AddUserMessage(userInput);

    var responseBuilder = new StringBuilder();
    Console.Write("\nAI: ");
    await foreach (var message in chat.GetStreamingChatMessageContentsAsync(userInput, settings, kernel))
    {
        responseBuilder.Append(message);
        Console.Write(message);
    }
}

This is a simple chat completion app.

Run the application with:

dotnet run

My prompt was:

Red or white wine with beef steak?

The response was:

AI:  Both red and white wines can pair well with beef steak, but a red wine is generally the more traditional choice. Red wines, such as Cabernet Sauvignon, Merlot, or Pinot Noir, have flavors that complement the rich and savory flavors of beef. However, if you prefer a lighter taste, a white wine such as Pinot Noir or Chardonnay can also work well with beef steak. Ultimately, it comes down to personal preference.

Conclusion

We have seen how to use SLMs hosted by VS Code through the AI Toolkit extension. We were able to communicate with the model from these two C# applications: (1) a app the uses OpenAI packages, and (2) an app that uses Sematic Kernel.