Showing posts with label Tutorial. Show all posts
Showing posts with label Tutorial. 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.

Tuesday, September 29, 2026

docker-compose with MySQL and ASP.NET

This article discussed one approach to having your ASP.NET development environment work with MySQL running in a docker container.

Source code: https://github.com/medhatelmasry/AspMySQL

It is assumed that the following installed on your computer:

  1. .NET 10.0 
  2. Docker Desktop 
  3. ‘dotnet-ef’ tool 

Setting up MySQL docker container

To download a suitable MySQL image from Docker Hub and run it on your local computer, type the following command from within a terminal window:

docker run -d --name mysqldb -p 3333:3306 -e MYSQL_ROOT_PASSWORD=secret mysql:8.4

This starts a container named mysqldb that listens on port 3333 on your local computer. The root password is secret.

To ensure that the MySQL container is running, type the following from within a terminal window:

docker ps

You will see a message like the following:

CONTAINER ID   IMAGE       COMMAND                  CREATED       STATUS          PORTS                                         NAMES
4baa0ba99088   mysql:8.4   "docker-entrypoint.s…"   2 hours ago   Up 48 minutes   0.0.0.0:3333->3306/tcp, [::]:3333->3306/tcp   mysqldb

Creating our ASP.NET MVC App

Create an ASP.NET MVC app named AspMySQL with SQLite support by running the following terminal window commands:

dotnet new mvc --auth individual -o AspMySQL
cd AspMySQL

To run the web application and see what it looks like, enter the following command:

dotnet watch

The app starts in your default browser and looks like this:

We need a suitable MySQL Entity Framework driver. One such driver is the official MySQL driver from Oracle. Run this pair of commands to replace the SQLite driver with the Oracle MySQL driver:

dotnet remove package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package MySql.EntityFrameworkCore

Let us configure our web application so that the connection string can be constructed from environment variables. Open the Program.cs file in your favourite editor and comment out (or delete) the following statements around lines 8-10:

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(connectionString));

Replace the above code with the following:

var host = builder.Configuration["DBHOST"] ?? "localhost";
var port = builder.Configuration["DBPORT"] ?? "3333";
var password = builder.Configuration["DBPASSWORD"] ?? "secret";
var db = builder.Configuration["DBNAME"] ?? "aspnetDB";
var user = builder.Configuration["DBUSER"] ?? "root";

string connectionString = $"Server={host};Port={port};Database={db};User={user};Password={password};";

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

Five environment variables are used in the database connection string. These are: DBHOST, DBPORT , DBPASSWORD, DBNAME and DBUSER. If these environment variables are not found then they will take on default values: localhost, 3333, secret, aspnetDB and root respectively.

Go ahead and delete the connection string from appsettings.json as it is not needed anymore:

"ConnectionStrings": {
  "DefaultConnection": "DataSource=app.db;Cache=Shared"
},

Entity Framework Migrations

We can instruct our application to automatically process any outstanding Entity Framework migrations. This is done by adding the following statement 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();
}

Since SQLite is different from MySQL, we must delete all migrations and re-create them. Therefore, follow these steps:

  1. delete the Data/Migrations folder.
  2. delete file: app.db
  3. create new migrations with this terminal window command: 

dotnet ef migrations add M1 -o Data/Migrations

Test app

Now, let's test our web app and see whether it can talk to the containerized MySQL database server. Run the web application with the following terminal command:

dotnet watch

Click on the Register link on the top right side.

The ASP.NET MVC user register page.

I entered an Email, Password and Confirm password, then clicked on the Register button. The website then displays the following page that requires that you confirm the email address:

Click on the “Click here to confirm your account” link. This leads you to a confirmation page:

Login with the email address and password that you registered with.

The message on the top right side confirms that the user was saved and that communication between the ASP.NET app and MySQL is working as expected.

Dockeri-zing app

We will generate the release version of the application by executing the following command from a terminal window in the root directory of the web app:

dotnet publish -o distrib

The above command instructs dotnet to produce the release version of the application in the distrib directory. When you inspect the distrib directory, you will see files like the following:

The highlighted file in the above image is the main DLL file that is the entry point into the web application. Let us run the DLL. To do this, change to the distrib directory, then run your main DLL file with:

cd distrib
dotnet AspMySQL.dll

This displays the familiar messages from the web server that the app is ready to be accessed from a browser. 

Hit CTRL C to stop the web server.

We now have a good idea about the ASP.NET artifacts that need to be copied into a container. 

In a terminal window, stop and remove the MySQL container with:

docker rm -f mysqldb

Return to the root directory of your project by typing the following in a terminal window:

cd ..

Docker image for web app

We need to create a docker image that will contain the .NET runtime. At the time of writing this article, the current version of .NET is 10.0.

We can exclude files from being copied into the container image. Add a file named .dockerignore in the root of the web application with this content:

**/.git
**/.gitignore
**/node_modules
**/npm-debug.log
**/.DS_Store
**/bin
**/obj
**/distrib **/.vs **/.vscode **/.env **/*.user **/*.suo **/.idea **/coverage **/.nyc_output **/docker-compose*.yml **/Dockerfile* **/.github **/README.md **/LICENSE

Create a text file named Dockerfile and add to it the following content:

# Build stage
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build

WORKDIR /src

# Copy project file and restore dependencies
COPY *.csproj .
RUN dotnet restore

# Copy source code
COPY . .

# Publish application
RUN dotnet publish -c Release -o /app/publish

# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime

WORKDIR /app

COPY --from=build /app/publish .

ENV ASPNETCORE_URLS=http://+:80

EXPOSE 80
EXPOSE 443

# Run the application
ENTRYPOINT ["dotnet", "AspMySQL.dll"]

docker-compose.yml

We will next create a docker .yml file that orchestrates the entire system involving two containers: a MySQL database server and our web app. In the root folder of your application, create a text file named docker-compose.yml and add to it the following content:

volumes:
  mysqldata:

services:
  db:
    image: mysql:8.4
    volumes:
      - mysqldata:/var/lib/mysql
    #restart: always
    ports:
      - "3333:3306"
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_TCP_PORT: 3306
    healthcheck:
      test: [ "CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-psecret" ]
      interval: 5s
      timeout: 5s
      retries: 20
      start_period: 30s

  webapp:
    build:
      context: .
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "7777:80"
    #restart: always
    environment:
      - DBHOST=db
      - DBPORT=3306
      - DBPASSWORD=secret
      - DBNAME=snoopyDB
      - DBUSER=root

Running the yml file

To find out if this all works, go to a terminal window at the root directory of the application and run the following command:

docker-compose up -d --build

Point your browser to http://localhost:7777/ and you should see the main web page. Register a user, confirm the email, and login. It should all work as expected.

Cleanup

Run the following command to shutdown docker-compose and cleanup:

docker-compose down -v

Conclusion

We have seen how straight forward and easy it is to containerize an application and its database with docker-compose.

Thursday, August 27, 2026

AI Instructions, Agent Skills and Prompt Files in VS Code

In this tutorial we will use a very simple C# console application to reinforce some of the concepts pertaining to coding with AI in VS Code.

Pre-requisites

You will need .NET and VS Code in order to proceed with this walkthrough.

Getting Started

Create a new console app and open it in VS Code with the following terminal window commands:

dotnet new console -o Toons.Net
cd Toons.Net
code .

Replace contents of Program.cs with:

Toon[] toons = {
    new() {
        ID = 1,
        First = "Barney",
        Last = "Rubble",
        Gender = Gender.Male,
        Occupation = "Mining Assistant"
    },
    new() {
        ID = 2,
        First = "Betty",
        Last = "Rubble",
        Gender = Gender.Female,
        Occupation = "Nurse" },
    new() {
        ID = 3,
        First = "Fred",
        Last = "Flintstone",
        Gender = Gender.Male,
        Occupation = "Mining Manager" },
    new() {
        ID = 4,
        First = "Wilma",
        Last = "Flintstone",
        Gender = Gender.Female,
        Occupation = "Teacher" },
    new() {
        ID = 5,
        First = "Pebbles",
        Last = "Flintstone",
        Gender = Gender.Female,
        Occupation = "Toddler" },
};

foreach (var item in toons) {
    Console.Write($"ID: {item.ID}, ");
    Console.Write($"First: {item.First}, ");
    Console.Write($"Last: {item.Last}, ");
    Console.Write($"Gender: {item.Gender}, ");
    Console.WriteLine($"Occupation: {item.Occupation}");
}

public class Toon {
    public int ID { get; set; }
    public string? First { get; set; }
    public string? Last { get; set; }
    public Gender Gender { get; set; }
    public string? Occupation { get; set; }
}

public enum Gender {
    Male,
    Female
}

To see what it does, run the application by entering the following command in a terminal window inside the Toons.Net folder:

dotnet run

Custom Instructions

Custom instructions enable you to define common guidelines and rules that automatically influence how AI generates code and handles other development tasks. Instead of manually including context in every chat prompt, specify custom instructions in a Markdown file to ensure consistent AI responses that align with your coding practices and project requirements.

In a ./.github folder, add a file named copilot-instructions.md with this text that provides some coding principles and the manner by which AI will refer to you as Sensei:

# Please call me Sensei and speak with the calm discipline of a samurai.

## Naming Conventions
- Use PascalCase for component names, interfaces, and type aliases
- Use camelCase for variables, functions, and methods
- Prefix private class members with underscore (_)
- Use ALL_CAPS for constants

# Project-specific guidelines
- Use async/await for asynchronous operations
- When creating sample Toon data, ensure names are diverse and culturally inclusive
- When creating sample Toon data, use Occupations that represent a wide range of disciplines and regions

You should put your team coding standards in the copilot-instructions.md file. You may also wish to put this file at a workspace level, rather than a project level.

Note this interaction when you prompt the AI chat with "Hello":

Skills

Agent skills are folders of instructions, scripts, and resources that GitHub Copilot can load when relevant to perform specialized tasks.

You can think of agents skills as the micro-services of AI.

Agent skills are an open standard that work across multiple AI agents, including GitHub Copilot and VS Code, Copilot CLI, and Copilot Cloud Agent.

In folder ./.github/skills/hello-world, add a file named SKILL.md with this text:

---
name: hello-world
description: "Use when: you want a simple Hello World response in ASCII text."
---
# Hello World

When invoked, output exactly this line:

 _   _      _ _                             _     _ _
| | | | ___| | | ___    __      _____  _ __| | __| | |
| |_| |/ _ \ | |/ _ \   \ \ /\ / / _ \| '__| |/ _` | |
|  _  |  __/ | | (_) |   \ V  V / (_) | |  | | (_| |_|
|_| |_|\___|_|_|\___( )   \_/\_/ \___/|_|  |_|\__,_(_)
⚠️ The name of the skill must exactly match the folder name.
⚠️  It is mandatory to provide name and description.

Enter this prompt in the chat window:

add a simple Hello World response in ASCII text to Program.cs

It will add this code to Program.cs:

Console.WriteLine("""
 _   _      _ _                             _     _ _
| | | | ___| | | ___    __      _____  _ __| | __| | |
| |_| |/ _ \ | |/ _ \   \ \ /\ / / _ \| '__| |/ _` | |
|  _  |  __/ | | (_) |   \ V  V / (_) | |  | | (_| |_|
|_| |_|\___|_|_|\___( )   \_/\_/ \___/|_|  |_\__,_(_)
                    |/
""");

A good site to visit to get skills, instructions, plugins, and agents for VS Code is https://github.com/github/awesome-copilot. Point your browser to that site then navigate to /skills/dotnet-best-practices. Copy the content of the SKILL.md file from the code tab:

Visiting https://github.com/github/awesome-copilot is a good starting point for creating these .md files which will make you very efficient in your journey developing software with AI. 

Create a folder ./.github/skills/dotnet-best-practices and add to it a file named SKILL.md with the content that you copied. You can edit it as you see fit.

Add this prompt to the chat window:

Apply /dotnet-best-practices to this project

This results in best practices getting applied to your project. I noticed extensive documentation being added to Program.cs:

Console.WriteLine("""
 _   _      _ _                             _     _ _
| | | | ___| | | ___    __      _____  _ __| | __| | |
| |_| |/ _ \ | |/ _ \   \ \ /\ / / _ \| '__| |/ _` | |
|  _  |  __/ | | (_) |   \ V  V / (_) | |  | | (_| |_|
|_| |_|\___|_|_|\___( )   \_/\_/ \___/|_|  |_|\__,_(_)
""");

Toon[] toons = {
    new() {
        ID = 1,
        First = "Barney",
        Last = "Rubble",
        Gender = Gender.Male,
        Occupation = "Mining Assistant"
    },
    new() {
        ID = 2,
        First = "Betty",
        Last = "Rubble",
        Gender = Gender.Female,
        Occupation = "Nurse" },
    new() {
        ID = 3,
        First = "Fred",
        Last = "Flintstone",
        Gender = Gender.Male,
        Occupation = "Mining Manager" },
    new() {
        ID = 4,
        First = "Wilma",
        Last = "Flintstone",
        Gender = Gender.Female,
        Occupation = "Teacher" },
    new() {
        ID = 5,
        First = "Pebbles",
        Last = "Flintstone",
        Gender = Gender.Female,
        Occupation = "Toddler" },
};

foreach (var item in toons)
{
    Console.Write($"ID: {item.ID}, ");
    Console.Write($"First: {item.First}, ");
    Console.Write($"Last: {item.Last}, ");
    Console.Write($"Gender: {item.Gender}, ");
    Console.WriteLine($"Occupation: {item.Occupation}");
}

/// <summary>
/// Represents a character in the sample toon collection.
/// </summary>
public class Toon
{
    /// <summary>
    /// Gets the unique identifier for the toon.
    /// </summary>
    public int ID { get; init; }

    /// <summary>
    /// Gets the toon&apos;s first name.
    /// </summary>
    public required string First { get; init; }

    /// <summary>
    /// Gets the toon&apos;s last name.
    /// </summary>
    public required string Last { get; init; }

    /// <summary>
    /// Gets the toon&apos;s gender classification.
    /// </summary>
    public required Gender Gender { get; init; }

    /// <summary>
    /// Gets the toon&apos;s occupation.
    /// </summary>
    public required string Occupation { get; init; }
}

/// <summary>
/// Defines the gender classifications used by the sample data.
/// </summary>
public enum Gender
{
    /// <summary>
    /// Identifies a male toon.
    /// </summary>
    Male,

    /// <summary>
    /// Identifies a female toon.
    /// </summary>
    Female
}

Built-in skills and agents in VS Code

Let's ask copilot chat to add a README.md file to our project with this prompt:

Add a README.md file with relevant information about the current project.

View the built-in skills in VS Code by clicking the gear icon in the chat window:

Find the create-skill under Built-in.

Click on create-skill to view details of the agent skill. This opens the relevant SKILL.md file.

Let’s use create-skill in our software project. In the chat window, enter this prompt:

/create-skill that will update the README.md file whenever a feature is added to the project.

A new SKILL.md file is added to your project under ./.github/skills folder:

⚠️ The added feature can be given a different name than "update-readme-on-feature".

Let us add a feature to test it out. Add this prompt in the chat window:

Add a new feature that allows the list of toons to be sorted by id, first, last, gender, or occupation.

After the feature is added, you will notice that the README.md file gets updated accordingly:

Prompt files

Prompt files, also known as slash commands, let you simplify prompting for common tasks by encoding them as standalone Markdown files that you can invoke directly in chat. Each prompt file includes task-specific context and guidelines about how the task should be performed.

In folder ./.github/prompts, add a file named code-review-analyzer.md with this text:

---
name: Researcher
description: Research codebase patterns and gather context
tools: ['read', 'search']
model: Claude Sonnet 4.5 (copilot)
user-invocable: true
---
Research the existing codebase for relevant files, functions, and patterns.
Return a concise summary of your findings, including links to relevant code sections.
Report on any insights that may help in implementing new features.

If you like, you can get AI to write these instructions for you.

Invoke the analyser instructions by entering the /Researcher prompt in the chat window.

Conclusion

In this tutorial, we have learned the significance of AI Instructions, Agent Skills and Prompt Files in VS Code. The sky is the limit as to how far you can go with these concepts to make your coding experience much more efficient.

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 Swagger 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