Skip to content
28 changes: 10 additions & 18 deletions Lab1.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,7 @@ When we're done, we will have a solution file called `Nimble.Modulith.slnx` whic

Follow these steps to create the modular monolith solution from scratch. You can do this in a folder called `labs` (which is in the .gitignore file and so won't be added to git by default). If you want source control, create a new repo or use a subfolder called `mylabs` which should be tracked by git.

### 1. Create the solution file

```bash
dotnet new sln --name Nimble.Modulith --output .
```

This creates a `Nimble.Modulith.slnx` file in the repository root.

### 2. Create the AspireHost project
### 1. Create the AspireHost project

First, install the Aspire CLI if you haven't already:

Expand All @@ -44,7 +36,7 @@ aspire new aspire
```
When asked, give the name Nimble.Modulith. Keep the default output path. No tests.

### 3. Change to slnx file
### 2. Change to slnx file

```bash
cd Nimble.Modulith
Expand All @@ -61,7 +53,7 @@ and files:

Nimble.Modulith.slnx

### 4. Create the ASP.NET Core minimal Web API project
### 3. Create the ASP.NET Core minimal Web API project

Run the following commands from the same folder as your slnx file.

Expand All @@ -70,26 +62,26 @@ dotnet new webapi --name Nimble.Modulith.Web --output Nimble.Modulith.Web --use-
dotnet sln add Nimble.Modulith.Web/Nimble.Modulith.Web.csproj --solution-folder "_Host"
```

### 5. Create the Users class library
### 4. Create the Users class library

```bash
dotnet new classlib --name Nimble.Modulith.Users --output Nimble.Modulith.Users --framework net10.0
```

### 6. Create solution folder for Users Module
### 5. Create solution folder for Users Module

```bash
dotnet sln add Nimble.Modulith.Users/Nimble.Modulith.Users.csproj --solution-folder "Users Module"
```

### 7. Create the Users.Contracts class library
### 6. Create the Users.Contracts class library

```bash
dotnet new classlib --name Nimble.Modulith.Users.Contracts --output Nimble.Modulith.Users.Contracts --framework net10.0
dotnet sln add Nimble.Modulith.Users.Contracts/Nimble.Modulith.Users.Contracts.csproj --solution-folder "Users Module"
```

### 8. Verify all projects are in the solution file
### 7. Verify all projects are in the solution file

```bash
dotnet sln list
Expand All @@ -108,7 +100,7 @@ dotnet sln add Nimble.Modulith.ServiceDefaults/Nimble.Modulith.ServiceDefaults.c

This will change the default startup project. When you open in an IDE you'll need to set AppHost as the startup project manually.

### 9. Use Central Package Management
### 8. Use Central Package Management

We're going to have a lot of projects. We don't want to have different package versions all over the place. Run this from the folder with the slnx file. We're going to use this tool to help: [CentralizedPackageConverter](https://github.com/Webreaper/CentralisedPackageConverter)

Expand All @@ -117,7 +109,7 @@ We're going to have a lot of projects. We don't want to have different package v
central-pkg-converter .
```

### 10. Ensure all projects target .NET 10
### 9. Ensure all projects target .NET 10

The Aspire projects created by `aspire new` may target a different framework version. Use a central `Directory.Build.props` file to set the .NET version and remove the `<TargetFramework>net9.0</TargetFramework>` element from the two Aspire projects.

Expand Down Expand Up @@ -197,4 +189,4 @@ The whole thing should look like this:

## Summary

At this point we have the basics: 2 Aspire projects, a eb host project, and 2 projects that represent our Users module. In the next lab we will configure Aspire to work with the web host and module and get user registration working.
At this point we have the basics: 2 Aspire projects, a Web host project, and 2 projects that represent our Users module. In the next lab we will configure Aspire to work with the web host and module and get user registration working.
10 changes: 9 additions & 1 deletion Lab2.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ public class Register : Endpoint<RegisterRequest, RegisterResponse>
}
```

Now we should be able to run the application and test it by navigating to the /register endpoint (which youc an also find from /swagger).
Now we should be able to run the application and test it by navigating to the /register endpoint (which you can also find from /swagger).

### 5. Create the UsersDbContext

Expand All @@ -162,6 +162,14 @@ public class UsersDbContext : IdentityDbContext<IdentityUser>
: base(options)
{
}

protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);

// Apply Users module specific configurations
builder.HasDefaultSchema("Users");
}
}
```

Expand Down
86 changes: 39 additions & 47 deletions Lab4.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,9 +326,6 @@ Create `Nimble.Modulith.Customers/Domain/Interfaces/IRepository.cs`:
```csharp
using Ardalis.Specification;

```csharp
using Ardalis.Specification;

namespace Nimble.Modulith.Customers.Domain.Interfaces;

public interface IRepository<T> : IRepositoryBase<T> where T : class
Expand Down Expand Up @@ -1363,7 +1360,8 @@ public class DeleteOrderItemHandler(IRepository<Order> repository)
{
public async ValueTask<Result<OrderDto>> Handle(DeleteOrderItemCommand command, CancellationToken ct)
{
var order = await repository.GetByIdAsync(command.OrderId, ct);
var spec = new OrderByIdSpec(command.OrderId);
var order = await repository.FirstOrDefaultAsync(spec, ct);

if (order is null)
{
Expand Down Expand Up @@ -1910,25 +1908,31 @@ public class ListByDate(IMediator mediator) : EndpointWithoutRequest<List<OrderR
Create `Nimble.Modulith.Customers/CustomersModuleExtensions.cs`:

```csharp
```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Nimble.Modulith.Customers.Domain.Interfaces;
using Nimble.Modulith.Customers.Infrastructure.Data;
using Serilog;

namespace Nimble.Modulith.Customers;

public static class CustomersModuleExtensions
{
public static IHostApplicationBuilder AddCustomersModuleServices(this IHostApplicationBuilder builder, ILogger logger)
public static IHostApplicationBuilder AddCustomersModuleServices(
this IHostApplicationBuilder builder,
ILogger logger)
{
// Register the DbContext with Aspire
// Add SQL Server DbContext with Aspire
builder.AddSqlServerDbContext<CustomersDbContext>("customersdb");

// Register repositories
builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
builder.Services.AddScoped(typeof(IReadRepository<>), typeof(EfReadRepository<>));

logger.Information("{Module} module services registered", nameof(CustomersModuleExtensions).Replace("ModuleExtensions", ""));

return builder;
}

Expand All @@ -1937,6 +1941,7 @@ public static class CustomersModuleExtensions
using var scope = app.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<CustomersDbContext>();
await context.Database.MigrateAsync();

return app;
}
}
Expand All @@ -1946,14 +1951,19 @@ public static class CustomersModuleExtensions

### 1. Update Program.cs

Open `Nimble.Modulith.Web/Program.cs` and add the Customers module registration after the Users and Products modules:
Open `Nimble.Modulith.Web/Program.cs` and
- add the Mediator source generation after adding the service defaults
- add the Customers module registration after the Users and Products modules

```csharp
using Nimble.Modulith.Users;
using Nimble.Modulith.Products;
using FastEndpoints;
using FastEndpoints.Security;
using FastEndpoints.Swagger;
using Mediator;
using Nimble.Modulith.Customers;
using Nimble.Modulith.Products;
using Nimble.Modulith.Users;
using Serilog;
using Mediator;

var logger = Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
Expand All @@ -1963,7 +1973,6 @@ var logger = Log.Logger = new LoggerConfiguration()
logger.Information("Starting web host");

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((_, config) => config.ReadFrom.Configuration(builder.Configuration));

// Add service defaults (Aspire configuration)
builder.AddServiceDefaults();
Expand All @@ -1976,46 +1985,36 @@ builder.Services.AddMediator(options =>

// Add FastEndpoints with JWT Bearer Authentication and Authorization
builder.Services.AddFastEndpoints()
.AddAuthenticationJwtBearer(o => o.SigningKey = builder.Configuration["Auth:JwtSecret"]!)
.AddAuthorization();

// Add Swagger/OpenAPI
builder.Services.SwaggerDocument(o =>
{
o.DocumentSettings = s =>
.AddAuthenticationJwtBearer(s =>
{
s.Title = "Nimble Modulith API";
s.Version = "v1";
};
});
s.SigningKey = builder.Configuration["Auth:JwtSecret"];
})
.AddAuthorization()
.SwaggerDocument();

// Register modules using IHostApplicationBuilder pattern
// Add module services
builder.AddUsersModuleServices(logger);
builder.AddProductsModuleServices(logger);
builder.AddCustomersModuleServices(logger);

var app = builder.Build();

// Configure the HTTP request pipeline
app.UseDefaultExceptionHandler();
app.UseAuthentication();
app.UseAuthorization();

// Configure FastEndpoints
app.UseFastEndpoints(config =>
{
config.Endpoints.RoutePrefix = string.Empty;
});

// Only use Swagger in Development
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwaggerGen();
app.MapOpenApi();
}

app.MapDefaultEndpoints();
app.UseHttpsRedirection();

// Ensure databases are created and migrated
// Add authentication and authorization middleware
app.UseAuthentication();
app.UseAuthorization();

app.UseFastEndpoints()
.UseSwaggerGen();

// Ensure module databases are created
await app.EnsureUsersModuleDatabaseAsync();
await app.EnsureProductsModuleDatabaseAsync();
await app.EnsureCustomersModuleDatabaseAsync();
Expand Down Expand Up @@ -2117,13 +2116,6 @@ GET {{Nimble.Modulith.Web_HostAddress}}/orders/1

### List Orders by Date
GET {{Nimble.Modulith.Web_HostAddress}}/orders/by-date/2025-10-24

### Update Order Status
PATCH {{Nimble.Modulith.Web_HostAddress}}/orders/1/status
Content-Type: application/json
{
"status": "Processing"
}
```

## Key Points
Expand Down
1 change: 0 additions & 1 deletion Lab5.md
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,6 @@ This creates a migration that will insert the Admin and Customer roles when the
The Users module needs to send email commands, so add a reference to the Email.Contracts project:

```powershell
cd Nimble.Modulith.Users
dotnet add reference ../Nimble.Modulith.Email.Contracts/Nimble.Modulith.Email.Contracts.csproj
```

Expand Down
3 changes: 0 additions & 3 deletions Lab6.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,6 @@ public class GetProductDetailsQueryHandler(ProductsDbContext dbContext)

### 1.6: Update CreateOrderHandler to Fetch Product Details

### 1.6: Update CreateOrderHandler to Fetch Product Details

Modify the handler to query product details (name and price) from the Products module:

**`Nimble.Modulith.Customers/UseCases/Orders/Commands/CreateOrderHandler.cs`:**
Expand Down Expand Up @@ -1012,7 +1010,6 @@ The Team
- Only generate password and send welcome email with credentials if creating a new user
- Send different email message for existing users getting a customer profile
- This prevents duplicate user errors when creating customers for existing accounts
```

## Step 3: Implement Password Reset

Expand Down