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.

Monday, March 9, 2026

MCP server with Laravel

In this article, we will create a tool in our Laravel MCP server to add to-do items. We will then consume the MCP server from an MCP client like VS Code.

Source Code: https://github.com/medhatelmasry/mcp-todo-laravel

Getting Started

Create a standard Laravel app with the following command:

composer create-project laravel/laravel mcp-todo-laravel
cd mcp-todo-laravel

Install the official Laravel MCP package via Composer:

composer require laravel/mcp

Publish the MCP configuration and routing files:

php artisan vendor:publish --tag=ai-routes

This creates routes/ai.php, where we will register your MCP servers.

Open your application in VS Code. 

We need to register the ai.php in the application bootstrap file. Edit the bootstrap/app.php file. Make these changes:

1) Add this at the top:

use Illuminate\Support\Facades\Route;

2) Add this code just under “health: '/up',”:

then: function () {
  Route::prefix('mcp')
    ->middleware('api') // api middleware disables CSRF checks
    ->group(base_path('routes/ai.php'));
},


Create and Register the Server 

Generate an MCP server class to group our tools:

php artisan make:mcp-server TodoServer

This creates the following file:

<?php

namespace App\Mcp\Servers;

use Laravel\Mcp\Server;
use Laravel\Mcp\Server\Attributes\Instructions;
use Laravel\Mcp\Server\Attributes\Name;
use Laravel\Mcp\Server\Attributes\Version;

#[Name(Todo Server')]
#[Version('0.0.1')]
#[Instructions('This server is used to manage the to-do list. Tools are used to add to-do items.')]
class TodoServer extends Server {
    protected array $tools = [
        //
    ];

    protected array $resources = [
        //
    ];

    protected array $prompts = [
        //
    ];
}

Update the file with the text highlighted in yellow above.

Then, register it in routes/ai.php as either a local or web server (or both) by replacing the content with the following:


<?php
use App\Mcp\Servers\TodoServer;
use Laravel\Mcp\Facades\Mcp;

// Web server: accessible via HTTP POST at /mcp/todo
Mcp::web('/mcp/todo', TodoServer::class);

// Local server: runs as an Artisan command
Mcp::local('todo', TodoServer::class);


Create an MCP Tool 

Tools define the actions an AI can perform. Generate a new tool class with:

php artisan make:mcp-tool AddTodoTool

This adds the following tool:


<?php

namespace App\Mcp\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tool;

#[Description('A description of what this tool does.')]
class AddTodoTool extends Tool {
    public function handle(Request $request): Response {
        //
        return Response::text('The content generated by the tool.');
    }
    public function schema(JsonSchema $schema): array {
        return [
            //
        ];
    }
}

Replace the above file with the following code:

<?php

namespace App\Mcp\Tools;

use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tool;

#[Description('Add a to-do item to the database. It takes description, isDone (Boolean) and date as parameters.')]
class AddTodoTool extends Tool {
    public function handle(Request $request): Response {
        $validated = $request->validate( 
            [
                'description' => 'required|string',
                'isDone' => 'required|boolean',
                'created_at' => 'required|string',
            ]);

        logger($validated);

        return Response::text('The content generated by the tool.');
    }

    public function schema(JsonSchema $schema): array {
        return [
            'description' => $schema->string()
                ->description("The description of the to-do item")
                ->required(),
            'isDone' => $schema->boolean()
                ->description("True if done and False if not done")
                ->required(),
            'created_at' => $schema->string()
                ->description("The date when the to-do item was created")
                ->required(),
        ];
    }
}

Register the above tool in the $tools array in app/Mcp/Servers/TodoServer.php. Add the code highlighted in yellow below.

use App\Mcp\Tools\AddTodoTool;

. . . . . . . . . .

protected array $tools = [
    AddTodoTool::class,
];

Let’s now run the server with:

php artisan serve


Connecting to a Client

To test our server, we can use the MCP Inspector or add the endpoint to an AI IDE like VS Code. 

Let us first test with the MCP Inspector. In another terminal window from the same folder, start the MCP inspector with:

php artisan mcp:inspector mcp/todo

The inspector will open in your default browsesr. Add :8000 to the URL, then click on the black Connect button.

Click on Tools, followed by "List Tools".

Click on "Add Todo Tool", enter a description (like: Add to-do item: go to the gym), enter a date, then click on the "Run Tool" button - as shown below.

The following confirmation will be displayed:

We now know that the MCP server and to-do tool are working as expected. Let's consume the MCP service from VS Code. In Visual Studio Code, from the top menu, select View >> Pallette

Choose "MCP: Add Server ...".

Choose "HTTP (HTTP or Server-Sent Events).

Enter http://localhost:8000/mcp/todo for the URL, then hit ENTER.

Give the server ID: todo-mcp-server.

Choose: Workspace Available in the workspace, runs locally.

A file gets created under .vscode named mcp.json that looks like this:


{
	"servers": {
		"todo-mcp-server": {
			"url": "http://localhost:8000/mcp/todo",
			"type": "http"
		}
	},
	"inputs": []
}

Make sure the server is running.

In Visual Studio Code, open the chat panel by clicking on the below tool.

Choose Agent mode.


Next, click on the "Configure Tools ..." tool as shown below.

The "Configure Tools ..." panel will open at the top and you should see that our todo-mcp-server is running. 

Close the "Configure Tools ..." panel by clicking on the blue OK button.

Enter the following prompt into the chat windows:

using the todo mcp server, add this todo item: fix the dish washer dated 2026-03-07

VS Code will seek your permission to proceed. Click on the "Allow in this Session" button.

Sample Output:

Extending the Laravel MCP Tool

Let us get our Todo tool to add items into a SQLite database. We can get AI to do some of the coding for us. Enter this prompt into the chat window:
Create a model called Todo, Add a migration. The database table should have description (string) and isDone ( Boolean). In the model, add all columns as fillable.

This may request that you agree to the execution of this terminal window command:

It then asks you to run this command to apply the migration:

php artisan migrate

Go ahead and run the above command. 

In app/Mcp/Tools/AddTodoTool.php:

1) Add this at the top:

use App\Models\Todo;

2) Add this code to the handle() method just before the final return statement:

Todo::create($validated);

Test it out by entering this prompt:

I need to put gas in the car and buy bread.

Sample Result:

The todos table in the database/database.sqlite database file will contain the data that was just inserted into the database.

Conclusion

We have successfully added a Todo MCP tool in a Laravel application. There are so many opportunities to get AI to participate in a new way of concievinng applications that users can interface with using a AI chat interaction.

Wednesday, February 25, 2026

Explore A2A protocol with .NET and GitHub Models

Let's explore the Agent-to-Agent (A2A) protocol using .NET. The A2A protocol standardizes communication between agents. It allows agents built with different frameworks and technologies to seamlesssly communicate with one-another.

What's A2A?

A2A is a standardized protocol that supports:

  • Agent discovery through agent cards
  • Message-based communication between agents
  • Long-running agentic processes via tasks
  • Cross-platform interoperability between different agent frameworks

The A2A protocol was developed by Google and later donated to the Linux Foundation.For more information, visit A2A protocol specification.

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

Get Started

In the following example, we will learn how to expose an agent with A2A. The example uses an AI model hosted on GitHub. In addition, we will use Swagger to simplify testing.

In a working directory on your computer, create an ASP.NET Minimal API project named A2Aapi with the following terminal window command:

dotnet new webapi -o A2Aapi
cd A2Aapi
dotnet new gitignore

Install the following NuGet packages:

# Hosting.A2A.AspNetCore for A2A protocol integration
dotnet add package Microsoft.Agents.AI.Hosting.A2A.AspNetCore -v 1.0.0-preview.260219.1

# Libraries to connect to GitHub AI models
dotnet add package Azure.Identity
dotnet add package Microsoft.Extensions.AI.OpenAI

# Swagger to test app
dotnet add package Microsoft.AspNetCore.OpenApi
dotnet add package Swashbuckle.AspNetCore


Configure connection to GitHub AI Models

You will need to get a Personal Access Token from GitHub. If this is the first time, follow this tutorial.

Add the following JSON to appsettings.Development.json file:

"GitHub": {
    "Token": "put-your-github-personal-access-token-here",
    "ApiEndpoint": "https://models.github.ai/inference",
    "Model": "openai/gpt-4o-mini"
}

NOTE: Replace put-your-github-personal-access-token-here with your GitHub Personal Access Token.

Edit the .gitignore file in the A2Aapi folder and add to it appsettings.Development.json so that your secrets do not find their way into source control by mistake.

Replace contents of Program.cs with the following code:

using OpenAI;
using Microsoft.Agents.AI.Hosting;
using Microsoft.Extensions.AI;
using Azure;
using OpenAI.Chat;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();
builder.Services.AddSwaggerGen();

string githubToken = builder.Configuration["GitHub:Token"]
    ?? throw new InvalidOperationException("GitHub:Token is not set.");
string apiEndpoint = builder.Configuration["GitHub:ApiEndpoint"]
    ?? throw new InvalidOperationException("GitHub:ApiEndpoint is not set.");
string model = builder.Configuration["GitHub:Model"]
    ?? throw new InvalidOperationException("GitHub:Model is not set.");

// Register the chat client
IChatClient chatClient = new ChatClient(
    model,
    new AzureKeyCredential(githubToken),
    new OpenAIClientOptions
    {
        Endpoint = new Uri(apiEndpoint)
    }
)
.AsIChatClient();

builder.Services.AddSingleton(chatClient);

// Register agents
var pirateAgent = builder.AddAIAgent("pirate", instructions: "You are a pirate. Speak like a pirate.");

var app = builder.Build();

app.MapOpenApi();
app.UseSwagger();
app.UseSwaggerUI();

// Expose the agent via A2A protocol. You can also customize the agentCard
app.MapA2A(pirateAgent, path: "/a2a/pirate", agentCard: new()
{
    Name = "Pirate Agent",
    Description = "An agent that speaks like a pirate.",
    Version = "1.0"
});

app.Run();


Test Agent

Run the web app with:

dotnet run

We have two options to test our agent: we can either use Swallger by pointing our browser to the /swagger endpoint, or we can use the A2Aapi.http REST Client that is built into the ASP.NET Minimal API template.

Option 1 - using Swagger

Point your browser to the URL displayed the the terminal window with /swagger. In my case it would be http://localhost:5112/swagger. You will see an interface similar to this:

Cloose the POST /a2a/pirate/v1/message:stream endpoint.

Click on the "Try it out" 

Enter the following JSON request then click on the Execute button:
{
  "message": {
    "kind": "message",
    "role": "user",
    "parts": [
      {
        "kind": "text",
        "text": "Hey pirate! Tell me where have you been",
        "metadata": {}
      }
    ],
    "messageId": null,
    "contextId": "foo"
  }
}

The server response looks like this:

This is the prompt we sent to the agent:

Hey pirate! Tell me where have you been

This is the response from the agent:

Ahoy, matey! I've been sailin' the seven seas, searchin' fer treasure and chasin' down the fiercest storms!

From the shores of Tortuga to the depths of Davy Jones' locker, me heart be filled with tales of adventure. And where be ye anchorin" yer ship, eh?

The response includes the contextId (conversation identifier), messageId (message identifier), and the actual content from the pirate agent.

Option 2 - using .http REST Client

If you are using VS Code, install the following VS Code extension:


Edit the A2Aapi.http in your project and add this request:
###
# Send A2A request to the pirate agent
POST {{A2Aapi_HostAddress}}/a2a/pirate/v1/message:stream
Accept: application/json
Content-Type: application/json

{
  "message": {
    "kind": "message",
    "role": "user",
    "parts": [
      {
        "kind": "text",
        "text": "Hey pirate! Tell me where have you been",
        "metadata": {}
      }
    ],
    "messageId": null,
    "contextId": "foo"
  }
}

Click on the "Send Request" link as shown below:

The response will show in a separate panel like this:

AgentCard Configuration

The AgentCard provides metadata about your agent for discovery and integration:

app.MapA2A(agent, "/a2a/my-agent", agentCard: new() {
   Name = "My Agent",
   Description = "A helpful agent that assists with tasks.",
   Version = "1.0",
});

The agent card can be accessed by sending this request:

# Send A2A request to the pirate agent
GET {{baseAddress}}/a2a/pirate/v1/card


Properties of the Agent Card

NameDisplay name of the agent
DescriptionBrief description of the agent
VersionVersion string for the agent
UrlEndpoint URL (automatically assigned if not specified)
CapabilitiesOptional metadata about streaming, push notifications, and other features


Exposing More Agents

You can expose multiple agents in a single application, as long as their endpoints don't collide. Here's an example:

Add the following code to Program.cs right under the "// Register agents" comment line:

var mathAgent = builder.AddAIAgent("math", instructions: "You are a math expert.");
var scienceAgent = builder.AddAIAgent("science", instructions: "You are a science expert.");

Similarly, add these endpoint mappings to Program.cs right above the last "app.Run();" statement:

app.MapA2A(mathAgent, "/a2a/math");
app.MapA2A(scienceAgent, "/a2a/science");

You can test the math agent and science agents with these respective requests:

Test math agent

###
# Send A2A request to the math agent
POST {{A2Aapi_HostAddress}}/a2a/math/v1/message:stream
Accept: application/json
Content-Type: application/json

{
  "message": {
    "kind": "message",
    "role": "user",
    "parts": [
      {
        "kind": "text",
        "text": "add 2 and 7",
        "metadata": {}
      }
    ],
    "messageId": null,
    "contextId": null
  }
}


Test science agent

###
# Send A2A request to the science agent
POST {{A2Aapi_HostAddress}}/a2a/science/v1/message:stream
Accept: application/json
Content-Type: application/json

{
  "message": {
    "kind": "message",
    "role": "user",
    "parts": [
      {
        "kind": "text",
        "text": "how far is saturn from earth?",
        "metadata": {}
      }
    ],
    "messageId": null,
    "contextId": null
  }
}


Conclusion

Therea re many emerging protocols that are giving us an insight into the future landscapte of the Agentic AI world o the future. This is one amone others. I trust that is article gives you in insight into the significance of the A2A protocol.

References

A2A Integration

Agent2Agent (A2A) Protocol

Monday, February 16, 2026

Scaffolding Blazor pages with microsoft.dotnet-scaffold

In this tutorial, I will show you how to build a server-side Blazor application that connects directly to a SQLite database using Entity Framework Core. We will scaffold the CRUD pages with the microsoft.dotnet-scaffold tool. There is also a quick introduction into the QuickGrid component.

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

Companion Video: https://youtu.be/4hbE-GZ-WZA

Pre-requisites

  1. .NET Framework 10+
  2. VS Code (or any other .NET editor)
  3. dotnet-ef tool
  4. microsoft.dotnet-scaffold tool

Getting Started

In a terminal window, go to your working directory. Enter the following commands to create a Server-Side Blazor application inside a directory called BlazorStudents:

dotnet new blazor -int server --auth individual -o BlazorStudents
cd BlazorStudents

Run the application by entering the following command:

dotnet watch

The following page will load into your default browser:

The default page after creating and running a server-side blazor template.

Open the BlazorStudents folder in Visual Studio Code (or any other .NET editor).

We will work with a very simple student model. Therefore, add a Student class file in a folder named Models with the following content: 

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

    [Required(ErrorMessage = "You must enter first name.")]
    public string? FirstName { get; set; }

    [Required(ErrorMessage = "You must enter last name.")]
    public string? LastName { get; set; }

    [Required(ErrorMessage = "You must enter school.")]
    public string? School { get; set; }
    
    [Required(ErrorMessage = "You must enter gender.")]
    public string? Gender { get; set; }
    
    [Required(ErrorMessage = "You must enter date of birth.")]
    public DateTime? DateOfBirth { get; set; }
}

From within a terminal window at the root of your BlazorStudents project,  run the following command to add some required packages:

dotnet add package CsvHelper

The CsvHelper package will help us read data from a CSV.

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 CSV data at https://gist.github.com/medhatelmasry/e8d4edc2772a538419adda45e8f82685 and save it in a text file named students.csv in the wwwroot folder.

Edit Data/ApplicationDbContext.cs and add to the class the following property and methods:

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

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.csv" };
  var csvFilePath = Path.Combine(p);

  var config = new CsvConfiguration(CultureInfo.InvariantCulture) {
    PrepareHeaderForMatch = args => args.Header.ToLower(),
  };

  var data = new List<Student>().AsEnumerable();
  using (var reader = new StreamReader(csvFilePath)) {
    using (var csvReader = new CsvReader(reader, config)) {
      data = csvReader.GetRecords<Student>().ToList();
    }
  }

  return data;
}

Notice the above code is adding contents of the wwwroot/students.csv file as seed data into the database.

We are now ready to apply Entity Framework migrations, create the database and seed data. If you have not done so already, you will need to globally install the Entity Framework CLI tool. This tool is installed globally on your computer by running the following command in a terminal window:

dotnet tool install --global dotnet-ef

To have a clean start with Entity Framework migrations, delete the Data/Migrations folder and the Data/app.db files.

From within a terminal window inside the BlazorStudents root directory, run the following command to create migrations:

dotnet ef migrations add Stu -o Data/Migrations

This results in the creation of a migration file ending with the name ....Stu.cs in the Data/Migrations folder. 

The next step is to create the SQLite Data/app.db database file. This is done by adding the following code to Program.cs, right before app.Run():

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

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

Run the application. This will cause students data to be seeded in the database.

Scaffolding Blazor Components

If you have not done so already, install the dotnet scaffold tool with this terminal window command:

dotnet tool install -g microsoft.dotnet-scaffold

Let us scaffold the CRUD pages for students. Run the scaffold tool from the root directory of your application with this command:

dotnet scaffold

Follow these steps....

After running the "dotnet scaffold" tool, when asked to pick a scaffolding category, select "blazor".

When asked to pick a scaffolding command, choose "Razor Components with EntityFrameworkCore (dotnet-scaffold)".

When asked for a .NET prject file, choose the only one available: BlazorStudents.csproj.

When asked for model name, choose Student (Student).

When asked for database context class, enter ApplicationDbContext.

When asked for database provider, choose: sqlite-efcore (sqlite-efcore).

When asked for page type, choose: CRUD (CRUD).

When asked to include prerelease packages, answer no.

The scaffolding process adds the following pages to your application:

There is a new folder named StudentPages (under Components/Pages) with all the razor components for CRUD operations.

Edit Components/Layout/NavMenu.razor to add this menu item:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="students">
        <span class="bi bi-lock-nav-menu" aria-hidden="true"></span> Students
    </NavLink>
</div>

Find the following code in Program.cs and delete it because the scaffold tool registered ApplicatioDbContext using DbContextFactory:

builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlite(connectionString));

Run the application with:

dotnet watch

The app will launch in your default browser.

After the web app launches in your default browser, click on students in the left navigation. A list of students will display in the main panel

You should have the full CRUD experience once you click on Students in the left navigation.

QuickGrid

If you open StudentPages/Index.razor in your editor, you will notice that the QuickGrid Blazor component is being used to display students data. This is the code that uses QuickGrid:

<QuickGrid Class="table" Items="context.Students">
  <PropertyColumn Property="student => student.FirstName" />
  <PropertyColumn Property="student => student.LastName" />
  <PropertyColumn Property="student => student.School" />
  <PropertyColumn Property="student => student.DateOfBirth" />

  <TemplateColumn Context="student">
    <a href="@($"students/edit?studentid={student.StudentId}")">Edit</a> |
    <a href="@($"students/details?studentid={student.StudentId}")">Details</a> |
    <a href="@($"students/delete?studentid={student.StudentId}")">Delete</a>
  </TemplateColumn>
</QuickGrid>

Visit the QuickGrid for Blazor site for more information on this freely available component.

Pagination

Let's extend the QuickGrid component to add paging to StudentPages/Index.razor. Add this directive right under the first line (@page "/students"):

@rendermode InteractiveServer

The above directive tells Blazor that a component should be rendered on the server and made fully interactive using the Blazor Server model.

Add this instance variable to the @code { .... } block

private PaginationState pagination = new PaginationState { ItemsPerPage = 10 };

Next, Add the following attribute to the opening <QuickGrid .... > tag:

Pagination="@pagination"

Finally, place this Paginator component right after the closing </QuickGrid> tag:

<Paginator State="@pagination" />

Refreah the Students List page. You will see pagination in action.

Sorting

Making the columns sortable involves just adding an attribute to the <PropertyColumn...> opening tags for FirstName, LastName, School, and DateOfBirth as follows:

<PropertyColumn Property="student => student.FirstName" Sortable="true" />
<PropertyColumn Property="student => student.LastName" Sortable="true" />
<PropertyColumn Property="student => student.School" Sortable="true" />
<PropertyColumn Property="student => student.DateOfBirth" Sortable="true" />

You can now click on the column titles to sort the columns as needed:

Filtering

Add this filter input field right above the <Quickgrid ...> opening tag:

<p>There are @filtered.Count() students.</p>

<div class="search-box">
    <input class="form-control me-sm-2 " style="width: 95%" type="search" autofocus @bind="itemsFilter"
           @bind:event="oninput" placeholder="Search Filter ..." />
</div>

Add these C# properties inside the @code { . . .  } block:

string? itemsFilter;

private List<Student>? studentList {
  get {
    var data = context.Students!.ToList();
    if (!data.Any()) {
      return null;
    } else {

      return data;
    }
  }
}

private IQueryable<Student> filtered {
  get {
    if (studentList == null || !studentList.Any()) {
      return Enumerable.Empty<Student>().AsQueryable();
    }

    if (string.IsNullOrEmpty(itemsFilter)) {
      return studentList!.AsQueryable();
    } else {
      var filteredList = studentList!.AsQueryable()
      .Where(
      b => b.FirstName!.Contains(itemsFilter, StringComparison.CurrentCultureIgnoreCase)
      || b.LastName!.Contains(itemsFilter, StringComparison.CurrentCultureIgnoreCase)
      || b.School!.Contains(itemsFilter, StringComparison.CurrentCultureIgnoreCase)
      || b.Gender!.Contains(itemsFilter, StringComparison.CurrentCultureIgnoreCase)
      || b.DateOfBirth.ToString()!.Contains(itemsFilter, StringComparison.CurrentCultureIgnoreCase)
      );
      return filteredList;
    }
  }
}

In the opening <QuickGrid ... > tag, change the value of the Items attribure from "context.Students" to simply:

@filtered

You can now filter any text in any of the columns as shown below:

If you enter 1998 in the filter, all those born in 1998 will display in the main panel.

Component CSS

With Blazor components, it is easy create CSS that targets individual components. Simply create a file with the same name as the component and add to it .css. 

For example:

Create a file named StudentPages/Index.razor.css in the same folder as the component itself with this styling:

p.create-new {
    background-color: orange;
}

In StudentPages/Index.razor component, add the following styling to the <p> tag that contains the “Create New” link as follows:

<p class="create-new">
    <a href="students/create">Create New</a>
</p>

You will notice that styling is successfully applied to the StudentPages/Index.razor component.

Image showing that the styling in StudentPages/Index.razor.css has affected the Index.razor component by setting the background color of the "Create New" link to orange.

The .NET scaffold tool can be used for more than creating pages for Blazor app. Among other things, it can be used for scaffolding Aspire, API, MVC Controllerts, and Itentity.