A database has already been defined to store the product catalog for eShop, along with an Entity Framework Core model, and a web app that ensures the database is created and updated to the latest schema by running migrations on startup.
- Open the
eShop.lab1.slnin Visual Studio or VS Code. - An Entity Framework Core model is already defined for this database in the
Catalog.Dataproject. Open theCatalogDbContext.csfile in this project and look at the code to see that the the various tables are defined via properties and classes implementingIEntityTypeConfiguration<TEntity>. - The
Catalog.Dataproject only defines theDbContextand entity types. The EF Core migrations are defined and managed in theCatalog.Data.Managerproject. This is a web project that includes some custom code to facilitate creating and seeding the database when the application starts. - Open the
Program.csfile in theCatalog.Data.Managerproject and take a few minutes to read the code, navigating to the definition/implementation of theAddMigration<TContext>method to get a sense of what it's doing to coordinate the creation of the database, application of migrations, and execution of the customIDbSeeder<TContext>class. Also note the custom health check that indicates the status of the database initialization.
In order to successfully run the the Catalog.Data.Manager application and see the database be created, a PostgreSQL database server is required. The project is pre-configured to connect to a local server using trust authentication in its appsettings.Development.json file. Docker is a simple way to run instances of databases for development.
We're going to run 2 docker container:, the PostgreSQL container, and pgAdmin. This will make it easy to inspect the database using a visual tool:
postgres
docker run --name postgres-dev -e POSTGRES_HOST_AUTH_METHOD=trust -p 5432:5432 -d postgrespgAdmin
docker run --name pgadmin-dev -e PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False -e PGADMIN_CONFIG_SERVER_MODE=False -e PGADMIN_DEFAULT_EMAIL=admin@domain.com -e PGADMIN_DEFAULT_PASSWORD=admin -p 0:80 -d dpage/pgadmin4Podman users: If using Podman CLI, omit the
-p 0:80argument when running thedocker runcommand above, as Podman has slightly different default behavior with regards to port binding.
-
Using Docker, create a container to host the PostgreSQL server with the following details:
- Name:
postgres-dev - Image:
postgres:latest - Ports:
5432 - Environment variables:
POSTGRES_HOST_AUTH_METHOD:trust
- Name:
-
Create another container to host an instance of pgAdmin:
- Name:
pgadmin-dev - Image:
dpage/pgadmin4:latest - Ports:
0(random) - Environment variables:
PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED:FalsePGADMIN_CONFIG_SERVER_MODE:FalsePGADMIN_DEFAULT_EMAIL:admin@domain.comPGADMIN_DEFAULT_PASSWORD:admin
- Name:
-
Launch the
Catalog.Data.Managerproject and navigate to its/healthendpoint in the browser. -
The application's logs will be shown in its console instance and will show the detail of it initializing the database. Once initialization has completed, the
/healthendpoint should return a Healthy response. -
Stop the application and try launching it again and seeing the output of the
/healthendpoint returnDegradedwhile the database initialization is still in progress. -
Find the port assigned to the pgAdmin container.
docker ps###Docker Desktop
-
Open the pgAdmin UI in another browser tab and add the local PostgreSQL server instance by right-clicking on the Servers node in the tree-view and selecting Register > Server (use
host.docker.internalin the Connection tab). -
Expand the tree-view nodes under the server node you registered to see the that the tables representing the Entity Framework Core model have been created.
Containers are extremely useful for hosting service dependencies, but rather than creating and connecting to them manually, we can use the features of .NET Aspire to drive this from C#.
-
In Visual Studio, right-mouse click on the
Catalog.Data.Managerproject and select Add > .NET Aspire Orchestrator Support.... Change the Project name prefix to "eShop" and click OK in the displayed dialog: -
Two new projects were added to the solution:
eShop.AppHostandeShop.ServiceDefaults. The AppHost project should also be set as the launch project for the solution. -
Open the
Program.csfile in the AppHost project and change the name assigned to theProjects.Catalog_Data_Managerproject to"catalog-db-mgr":var builder = DistributedApplication.CreateBuilder(args); builder.AddProject<Projects.Catalog_Data_Manager>("catalog-db-mgr"); builder.Build().Run();
-
Run the following commands in the
srcfolder to create theeShop.AppHostandeShop.ServiceDefaultsprojects.dotnet new aspire-apphost -n eShop.AppHost dotnet new aspire-servicedefaults -n eShop.ServiceDefaults dotnet sln add eShop.AppHost dotnet sln add eShop.ServiceDefaults
-
Now add a reference to the
eShop.AppHost:cd eShop.AppHost dotnet add reference ..\Catalog.Data.Manager
-
Open the
Program.csfile in the AppHost project and add the following code:var builder = DistributedApplication.CreateBuilder(args); builder.AddProject<Projects.Catalog_Data_Manager>("catalog-db-mgr"); builder.Build().Run();
-
Install the
Aspire.Hosting.PostgreSQLpackage in theeShop.AppHostproject:dotnet add package Aspire.Hosting.PostgreSQL
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="9.0.0" />
-
Use the methods on the
buildervariable to create a PostgreSQL instance calledpostgreswith pgAdmin enabled, and a database calledCatalogDB. Ensure that thecatalog-db-mgrproject resource is configured with a reference to thecatalogDb:var builder = DistributedApplication.CreateBuilder(args); var postgres = builder.AddPostgres("postgres") .WithPgAdmin(); var catalogDb = postgres.AddDatabase("CatalogDB"); builder.AddProject<Projects.Catalog_Data_Manager>("catalog-db-mgr") .WithReference(catalogDb); builder.Build().Run();
-
In the
Program.csfile of theCatalog.Data.Managerproject, remove the line that maps the health-checks endpoint. This is no longer required as it's done by default by theapp.MapDefaultEndpoints();line that was added when Aspire orchestration was added. -
Launch the AppHost project and see that the Aspire dashboard is opened in the browser, with the various resources that make up our new distributed application listed, including the
Catalog.Data.Managerproject and the Docker containers. -
Use the dashboard to inspect the environment variables of the
catalog-db-mgrresource and notice that it includes one that sets the connection string required to connect to theCatalogDBdatabase:This environment variable will automatically override the value specified in the project's appsettings.Development.json file as configuration values provided from environment variables have higher precedence than those from appsettings.json files by default.
-
Locate the pgAdmin resource in the dashboard and click on the hyperlink displayed for it in the Endpoints column. Note that the PostgreSQL server defined via the AppHost project was automatically registered with pgAdmin in the Aspire instances node:
-
Back on the dashboard, click on the Traces item from the left-side menu, then find the trace entry with the name catalog-db-mgr: Migration operation CatalogDbContext and click on the View link in the Details column to open the trace view. Note all the spans displayed that represent calls from the application to the database during the database initialization. You might even see the first span errored as the database container was not yet available as the startup order of the different resources are not coordinated:
Now that we've setup the solution to use Aspire for composing our distributed application, let's add an HTTP API that provides the catalog details stored in the database.
-
Add a new project to the solution using the ASP.NET Core Web API project template and call it
Catalog.API, and ensure the following options are configured:- Framework: .NET 8.0 (Long Term Support)
- Authentication Type: None
- Configure for HTTPS: disabled
- Enable container support: disabled
- Enable OpenAPI support: enabled
- Do not use top-level statements: disabled
- Use controllers: disabled
- Enlist in .NET Aspire orchestration: enabled
-
In the newly created project, update the package reference to
Swashbuckle.AspNetCoreto version6.5.0 -
Open the
Program.csfile of theeShop.AppHostproject, and update it so the API project you just added is named"catalog-api"and has a reference to theCatalogDB:var builder = DistributedApplication.CreateBuilder(args); var postgres = builder.AddPostgres("postgres") .WithPgAdmin(); var catalogDb = postgres.AddDatabase("CatalogDB"); builder.AddProject<Projects.Catalog_Data_Manager>("catalog-db-mgr") .WithReference(catalogDb); builder.AddProject<Projects.Catalog_API>("catalog-api") .WithReference(catalogDb); builder.Build().Run();
-
Add a project reference from the
Catalog.APIproject to theCatalog.Dataproject so that it can use Entity Framework Core to access the database. -
Open the
Program.csfile of theCatalog.APIproject and delete the sample code that defines the weather forecasts API. This is all the code beginning withvar summaries = new[]until the end of the file, with the exception of theapp.Run()line. -
Immediately after the line that calls
builder.AddServiceDefaults(), add a line to configure theCatalogDbContextin the application's DI container using the Npgsql Entity Framework Core Provider for PostgreSQL. Ensure that the name passed to the method matches the name defined for the database in the AppHost project'sProgram.csfile ("CatalogDB"). TheAddNpgsqlDbContextmethod comes from theAspire.Npgsql.EntityFrameworkCore.PostgreSQLAspire component:builder.AddServiceDefaults(); builder.AddNpgsqlDbContext<CatalogDbContext>("CatalogDB");
-
Create a new file called
CatalogApi.csand define a static class inside of it calledCatalogApiin theMicrosoft.AspNetCore.Buildernamespace:namespace Microsoft.AspNetCore.Builder; public static class CatalogApi { }
-
In this class, add an extension method named
MapCatalogApion theIEndpointRouteBuildertype, returning that same type:public static IEndpointRouteBuilder MapCatalogApi(this IEndpointRouteBuilder app) { return app; }
This method will define the endpoint routes for the Catalog API.
-
In the method, add a call to
app.MapGetto define an endpoint that responds to GET requests to the/itemspath, and is handled by an async lambda that accepts two parameters: aPaginationRequesttype that will represent the shape of the request, and theCatalogDbContextinstance that will come from the DI container:app.MapGet("/items", async ([AsParameters] PaginationRequest paginationRequest, CatalogDbContext db) => { });
-
Create a new file called
PaginationRequest.csin a new directory in the project calledModeland in it define a struct typePaginationRequestwith two integer properties,PageSizeandPageIndex. This type will represent the optional request parameters that can be passed to the/itemsendpoint:namespace eShop.Catalog.API.Model; public readonly struct PaginationRequest(int pageSize = 10, int pageIndex = 0) { public readonly int PageSize { get; } = pageSize; public readonly int PageIndex { get; } = pageIndex; }
-
Create another file called
PaginatedItems.csin theModeldirectory and in it define a typePaginatedItems<TItem>with propertiesPageIndex,PageSize,Count, andData. This type will represent the shape of the response that will be returned from the/itemsendpoint:namespace eShop.Catalog.API.Model; public class PaginatedItems<TItem>(int pageIndex, int pageSize, long count, IEnumerable<TItem> data) where TItem : class { public int PageIndex { get; } = pageIndex; public int PageSize { get; } = pageSize; public long Count { get; } = count; public IEnumerable<TItem> Data { get;} = data; }
-
Back in the
CatalogApi.csfile, implement the lambda body for the/itemsendpoint so that the appropriate page of catalog items is retrieved from the database and returned in the response. Objects returned from the lambda will be automatically JSON serialized. TheCatalogApi.csfile should now look something like this:using eShop.Catalog.API.Model; using eShop.Catalog.Data; using Microsoft.EntityFrameworkCore; namespace Microsoft.AspNetCore.Builder; public static class CatalogApi { public static IEndpointRouteBuilder MapCatalogApi(this IEndpointRouteBuilder app) { app.MapGet("/items", async ([AsParameters] PaginationRequest paginationRequest, CatalogDbContext db) => { var pageSize = paginationRequest.PageSize; var pageIndex = paginationRequest.PageIndex; var totalItems = await db.CatalogItems .LongCountAsync(); var itemsOnPage = await db.CatalogItems .OrderBy(c => c.Name) .Skip(pageSize * pageIndex) .Take(pageSize) .AsNoTracking() .ToListAsync(); return new PaginatedItems<CatalogItem>(pageIndex, pageSize, totalItems, itemsOnPage); }); return app; } }
-
Go back to the
Program.csfile of theCatalog.APIproject and add a call to the completedMapCatalogApimethod, just before the call toapp.Run():app.MapCatalogApi(); app.Run();
-
Ensure the solution builds successfully before continuing.
-
Launch the AppHost project and click the hyperlink in the dashboard for the
catalog-apiresource's endpoint. This will open a new browser tab displaying the Swagger UI for the Catalog API. -
Expand the API definition for the
/itemsendpoint and click the Try it out button, followed by the Execute button. You should see the catalog items returned as JSON in the Response body section: -
As an alternative to the Swagger UI, open the
Catalog.API.httpfile in theCatalog.APIproject. This file was added by default when the project was created. -
Edit the line defining a GET request to the
/weatherforecastsendpoint so that it instead hits the/itemsendpoint:@Catalog.API_HostAddress = http://localhost:5180 GET {{Catalog.API_HostAddress}}/items/ Accept: application/json ###
-
Click the Send request link displayed above it to send the request and have the response displayed:














