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, September 3, 2026

Enhancing C# Development with GitHub Copilot CLI and VS Code

GitHub Copilot is transforming how developers work by turning AI from a simple code generator into a practical development partner. In this article, we explore how to use GitHub Copilot CLI and Visual Studio Code together with the Awesome Copilot ecosystem to build better .NET applications, customize AI behavior, and bring domain-specific guidance directly into the development workflow. From designing models and applying best practices to installing reusable agents, instructions, and skills, this walkthrough shows how to make Copilot feel deeply integrated with the way you code.

Using GitHub Copilot CLI

Create a sample .NET Console app

From within a termnal window in a working directory, type the following commands:

dotnet new console -o AwesomeAthlete
cd AwesomeAthlete

Install GitHub Copilot CLI

Follow instructions at Installing GitHub Copilot CLI to install the GitGub Copilot CLI.

Start the copilot app by typing the following command inside the same folder as the .NET app created earlier:

copilot

Explore online plugins repo

Visit: https://github.com/github/awesome-copilot and review the plugins/csharp-dotnet-development plugin.

Install the plugin by entering this command in GitHub Copilot CLI:

/plugin install csharp-dotnet-development@awesome-copilot

To view a list of plugins, enter this command:

/plugin list

Let's get the plugin to do something useful. For example, ask the plugin to help you design some domain models by entering this command:

@csharp-dotnet-development help me design a class model for Athlete

Thereafter, you can enter this command to add the required C# classes:

add the class models to my app and put code in Program.cs that calls those classes and prints sample data to the console

Among others, the plugin contains a command named /csharp-dotnet-development:dotnet-best-practices. We can use this command by entering this instruction:

/csharp-dotnet-development:dotnet-best-practices

Since we have a very simple C# application, the only suggestion made by AI is to add documentation to the domain classes.

Exit GitHub Copilot CLI by typing in the /exit command.

/exit

Using Visual Studio Code (VS Code)

If you do not already have VS Code installed on your computer, you can get it from https://code.visualstudio.com/. Make sure to install the GitHub Copilot Chat extension before proceeding.

Open the AwesomeAthlete folder in VS Code. You ca do that by simply typing in the following from a termninal window from inside the AwesomeAthlete folder:

code .

Customize or Extend the Plugin

You can modify plugin components to suit your needs.

In plugin folder: ~/.copilot/installed-plugins/awesome-copilot/<plugin-name>/

  • Edit com.github.copilot/agents/*.md to change agent behavior
  • Edit skills/*/SKILL.md to add new skills

Restart VS Code to reload changes

Installing Awesome Copilot artifacts

Point your browser to Awesome GitHub Copilot. Let's try installing the Caveman Mode instruction that optimizes token interaction with AI. Click on Instructions.

Find the Caveman Mode instruction. Click on VS Code Install.

Click Yes on this dialog.

Choose to install the instructions in the current workspace under folder .github, instead of globally.

Accept the default name caveman-mode for the instructions.

The caveman-mode-instructions.md file is saved in your workspace under .github.

Here is an example on how to use this instruction. Enter the following into the VS Code chat window:

caveman-mode how many years did it take to build the empire state building in new york

The prompt is distilled to the essential keywords: Empire State Building construction duration. The response stays concise and focused, avoiding unnecessary wording. This keeps token usage low and saves you money.

Awesome Copilot

The Awesome Copilot (By Tim Heuer) extension for VS Code allows you to browse and download instructions, prompts, chat modes, and agents from the Awesome Copilot community.

Install the above extension into your VS Code. You wiill find Awesome Copilot in your explorer.

You will find folders Instructions, Agents, and Skills.

For example, expand the Agents node, then select CSharpExpert.agent.md.

If you decide to download this agent, click on the download tool beside it.

You can change the name of the agent .md file. Simply hit Enter to accept the default name.

The agent gets installed in your local workspace under .github/agents.

You can also modify any of these AI artifacts after you install them locally.

💡 TIP: There are new AI artifacts being added regularly. Click on refresh to load the latest.

@agentPlugins

There are multiple ways of installing AI plugins into VS Code. In your VS Code Extensions tab enter the keywoord @agentPlugins.

There are many plugins that you can choose from. You can narrow down the list by adding a filter. For example, I entered the csharp filter and received a short list of the plugings that fit that keyword:

Click on the csharp-dotnet-development plugin. You will see more information about the plugin. In this example, the plugin consists of a number of slash (/) commands and one agent.

I you are in a C# application, you can use the /csharp-dotnet-development:csharp-xunit slash command to generate test cases with this prompt:

/csharp-dotnet-development:csharp-xunit generate test cases

Plugins consist of plugin.json file, skills, agents, hooks, and MCP. Below is an illustration of the plugin architecture:

Any agents that are installed in your environment can be invoked from the chat window. For example, in the below illustration, I installed the CSharpExpert.agent.md agent locally and it is appears in the chat window ready to be invoked:

You can, at any time, disable or uninstall any plugin by right-clicking on it in the extensions tab and choosing disable or uninstall.

Conclusion

GitHub Copilot plugins extend AI-assisted development beyond simple code generation. With GitHub Copilot CLI, VS Code, and the Awesome Copilot ecosystem, you can add specialized agents, instructions, skills, hooks, and commands to your C# workflow.

These tools help you design .NET applications, apply best practices, generate tests, and customize Copilot to match your development style. Explore the available artifacts, install the ones that fit your needs, and adapt them to make GitHub Copilot a more effective development partner.

References

Manage Agents, Instructions, Prompts, & Skills in Seconds with this VS Code Extension

Get Awesome-Copilot custom chat modes and prompt files - right from within GitHub Copilot Chat.

GitHub Copilot Agent Plugins: Package & Distribute Skills, MCP, Hooks & Custom Agents