With the requirements and planning from the previous lesson now closed, this lesson assembles the
complete Iteration 2: BiblioTech's API. No new domain logic is written — all the behavior already
exists in Library, in ILibraryRepository, and in its implementations; the work consists of
organizing that code into a multi-project solution structure, correctly registering dependencies
in ASP.NET Core's DI container (picking back up from 07-03 and 08-03), and exposing one endpoint
for each of the seven functional requirements defined in Lesson 2.
Contents
- Solution structure: several .NET projects
BiblioTech.Domain: the model already built, unchangedBiblioTech.Persistence:ILibraryRepositoryand its four implementationsBiblioTech.Api: registering services in the DI container- Endpoints: catalog, members, loans, returns, metadata
- Complete
Program.cs - Applying patterns and standards already covered
- Solution structure: several .NET projects
Until now, every lesson in the course worked with a single console project, or a single project
for whatever technology was in play. A real project of this size is better organized into
several separate .NET projects, grouped into a solution (.sln), each with its own
responsibility — the same single-responsibility principle from 08-01, now applied at the project
level instead of the class level:
dotnet new sln -n BiblioTech
dotnet new classlib -n BiblioTech.Domain
dotnet new classlib -n BiblioTech.Persistence
dotnet new webapi -n BiblioTech.Api
dotnet new xunit -n BiblioTech.Tests
dotnet sln add BiblioTech.Domain BiblioTech.Persistence BiblioTech.Api BiblioTech.Tests
dotnet add BiblioTech.Persistence reference BiblioTech.Domain
dotnet add BiblioTech.Api reference BiblioTech.Domain BiblioTech.Persistence
dotnet add BiblioTech.Tests reference BiblioTech.Domain BiblioTech.Persistenceflowchart TD
Api["BiblioTech.Api<br/>(ASP.NET Core Minimal API)"] --> Persistence["BiblioTech.Persistence<br/>(ILibraryRepository + 4 implementations)"]
Api --> Domain["BiblioTech.Domain<br/>(Library, LibraryItem, Member, Loan...)"]
Persistence --> Domain
Tests["BiblioTech.Tests<br/>(xUnit + Moq, Module 8)"] --> Domain
Tests --> Persistence
The arrows in the diagram show dependency direction (dotnet add reference): Domain doesn't
depend on any other project — it doesn't even know that persistence or an API exist — while
Persistence and Api depend on it. This direction isn't a coincidence: it's exactly the same
principle that motivated extracting ILibraryRepository in 08-03, now applied to folder and
project organization, not just to classes within a single project. An optional BiblioTech.Web
project (a Blazor client) would be treated exactly the same way: it would depend only on what's
strictly necessary to consume the API over HTTP, without referencing BiblioTech.Domain or
BiblioTech.Persistence directly (the client doesn't know those classes: it only knows the DTOs
the API exposes as JSON).
BiblioTech.Domain: the model already built, unchanged
BiblioTech.Domain: the model already built, unchangedThis project receives, without changing a single line of behavior, the classes built in Modules 2 through 4 and the penalty policies from Module 8:
BiblioTech.Domain/ ├── LibraryItem.cs // abstract class, Module 3 ├── Book.cs // Module 3 ├── Magazine.cs // Module 3 ├── Member.cs // Module 3, with OutstandingBalance and ApplyPenalty (08-05, exercise 2) ├── Loan.cs // Module 3, with CalculatePenalty(IPenaltyPolicy) (08-02) ├── ILendable.cs // Module 4 ├── ISearchable.cs // Module 4 ├── IPenaltyPolicy.cs // Module 8, with FixedPenalty and ProgressivePenalty └── Library.cs // Module 4, with Catalog/Members/Loans and LendBookAsync
Moving these classes into their own project is, in itself, an example of "extract class" at a larger scale (08-05, section 4): the domain becomes physically separated from any persistence or web-infrastructure detail, something previously achieved only through discipline within a single project and now enforced by the solution structure itself.
BiblioTech.Persistence: ILibraryRepository and its four implementations
BiblioTech.Persistence: ILibraryRepository and its four implementationsThis project receives, likewise with no change in behavior, the interface and the four classes built in 08-03:
BiblioTech.Persistence/ ├── ILibraryRepository.cs ├── TextRepository.cs ├── JsonRepository.cs ├── SqliteRepository.cs ├── EntityFrameworkRepository.cs └── LibraryDbContext.cs // Module 5, Entity Framework Core
Lesson 2 chose Entity Framework Core as the production persistence mechanism; the other three
implementations aren't deleted, they're simply not registered in BiblioTech.Api's DI container
(section 4). They remain available, for example, for quick manual testing with no database, exactly
as already seen in 08-03, section 7.
BiblioTech.Api: registering services in the DI container
BiblioTech.Api: registering services in the DI containerWith the project structure already in place, the service registration in Program.cs is
practically the same as the one built in 08-03, section 7, now with the rest of section 5's
endpoints added:
var builder = WebApplication.CreateBuilder(args);
// Persistence: Entity Framework Core over SQLite, Lesson 2's decision
builder.Services.AddDbContext<LibraryDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("BiblioTech")));
// ILibraryRepository resolved as EntityFrameworkRepository (08-03)
builder.Services.AddScoped<ILibraryRepository, EntityFrameworkRepository>();
// Library depends on ILibraryRepository, same lifetime as LibraryDbContext (08-03)
builder.Services.AddScoped<Library>();
// Penalty policy: Strategy from Module 8, holds no state of its own, valid as a Singleton
builder.Services.AddSingleton<IPenaltyPolicy, FixedPenalty>();Notice the deliberate difference from 07-03's builder.Configuration.GetConnectionString(...),
where the connection string was written directly in the code: here it's read from configuration
(appsettings.json), a preview of the per-environment configuration completed in Lesson 5, and a
direct application of Lesson 2's basic-security non-functional requirement (not leaving production
connection strings written in the source code).
- Endpoints: catalog, members, loans, returns, metadata
Each endpoint maps directly onto one of Lesson 2's seven functional requirements (FR1-FR7):
| Endpoint | Requirement | What it does |
|---|---|---|
GET /books |
FR3 (partial: listing) | Returns library.Catalog |
GET /books/search?q=... |
FR3 | Filters Catalog with LINQ (Module 4) |
POST /books |
FR1 | Adds an item with AddItem |
DELETE /books/{isbn} |
FR2 | Removes an item from the catalog |
POST /members |
FR4 | Adds a member with AddMember |
POST /loans |
FR5 | Registers a loan with LendBookAsync |
POST /loans/{id}/return |
FR6 | Calls RegisterReturn() and CalculatePenalty(policy) |
GET /books/{isbn}/metadata |
FR7 | Calls GetMetadataByIsbnAsync (Module 5) |
- Complete
Program.cs
Program.csPutting all the previous pieces together, here's BiblioTech.Api's complete Program.cs:
using BiblioTech.Domain;
using BiblioTech.Persistence;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// --- Service registration (DI container, 08-03) ---
builder.Services.AddDbContext<LibraryDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("BiblioTech")));
builder.Services.AddScoped<ILibraryRepository, EntityFrameworkRepository>();
builder.Services.AddScoped<Library>();
builder.Services.AddSingleton<IPenaltyPolicy, FixedPenalty>();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); // interactive testing UI, 07-03 section 8
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// --- FR3: list and search the catalog ---
app.MapGet("/books", (Library library) =>
{
library.LoadCatalog();
return Results.Ok(library.Catalog);
});
app.MapGet("/books/search", (string q, Library library) =>
{
library.LoadCatalog();
List<LibraryItem> results = library.Catalog
.Where(i => i.Title.Contains(q, StringComparison.OrdinalIgnoreCase)
|| i.Author.Contains(q, StringComparison.OrdinalIgnoreCase))
.ToList(); // LINQ, Module 4
return Results.Ok(results);
});
// --- FR1: add an item ---
app.MapPost("/books", (Book book, Library library) =>
{
library.AddItem(book);
library.SaveCatalog();
return Results.Created($"/books/{book.Isbn}", book);
});
// --- FR2: remove an item ---
app.MapDelete("/books/{isbn}", (string isbn, Library library) =>
{
library.LoadCatalog();
LibraryItem? item = library.Catalog
.FirstOrDefault(i => i is Book book && book.Isbn == isbn);
if (item is null)
{
return Results.NotFound($"No book exists with ISBN {isbn}.");
}
library.Catalog.Remove(item);
library.SaveCatalog();
return Results.NoContent();
});
// --- FR4: add a member ---
app.MapPost("/members", (Member member, Library library) =>
{
library.AddMember(member);
return Results.Created($"/members/{member.Id}", member);
});
// --- FR5: register a loan ---
app.MapPost("/loans", async (LoanRequest request, Library library) =>
{
Book? book = library.Catalog.OfType<Book>()
.FirstOrDefault(b => b.Isbn == request.Isbn);
Member? member = library.FindMemberById(request.MemberId);
if (book is null || member is null)
{
return Results.BadRequest("ISBN or member not found.");
}
try
{
await library.LendBookAsync(book, member); // validates availability and persists (08-05)
}
catch (InvalidOperationException ex)
{
return Results.Conflict(ex.Message);
}
return Results.Ok(library.Loans.Last());
});
// --- FR6: register a return, with penalty calculation ---
app.MapPost("/loans/{id}/return", (int id, Library library, IPenaltyPolicy policy) =>
{
Loan? loan = library.Loans.ElementAtOrDefault(id);
if (loan is null)
{
return Results.NotFound($"Loan #{id} doesn't exist.");
}
loan.RegisterReturn();
decimal penalty = loan.CalculatePenalty(policy);
if (penalty > 0)
{
loan.Member.ApplyPenalty(penalty); // 08-05, exercise 2
}
library.SaveCatalog();
return Results.Ok(new { loan.ReturnDate, Penalty = penalty });
});
// --- FR7: external metadata lookup ---
app.MapGet("/books/{isbn}/metadata", async (string isbn, Library library) =>
{
ExternalBookMetadata? metadata = await library.GetMetadataByIsbnAsync(isbn);
return metadata is not null
? Results.Ok(metadata)
: Results.NotFound($"No external metadata is available for ISBN {isbn}.");
});
app.Run();
// DTOs (Module 3, record): the expected body of a POST /loans
record LoanRequest(string Isbn, int MemberId);No endpoint contains its own business logic: each one just extracts data from the HTTP request
(the same binding mechanism from 07-03), calls an existing method on Library or its
dependencies, and translates the result into an HTTP response with
Results.Ok/Results.NotFound/Results.Conflict. That's precisely the sign that Module 8's
decoupled architecture worked as expected: the API is a thin layer over a domain that already knew
how to do all of this long before.
- Applying patterns and standards already covered
This Program.cs, although new as a file, doesn't introduce any practice not already seen in the
course:
- Constructor/parameter injection (08-03): each endpoint receives
Library,IPenaltyPolicy, orILibraryRepositoryalready resolved by the container, never creating them withnew. - Strategy (08-02):
IPenaltyPolicylets you switch fromFixedPenaltytoProgressivePenaltywith a single line in the service registration, without touching the return endpoint. - Exception handling as expected flow control (Module 2, 08-05): the
try/catcharoundInvalidOperationExceptioninPOST /loanstranslates an expected domain error (book unavailable) into a concrete HTTP code (409 Conflict), instead of letting it propagate as a generic 500 error. - Consistent naming (08-01):
LoanRequest,AddItem,SaveCatalogfollow exactly the same conventions already established in Module 8's Lesson 1.
Common Mistakes and Tips
- Referencing
BiblioTech.ApifromBiblioTech.Domain: this would invert the dependency direction from section 1's diagram; the domain must never know the web layer that consumes it exists. - Registering
IPenaltyPolicyasAddScopedinstead ofAddSingleton:FixedPenaltyholds no state of its own between calls (08-03, section 4), so it can be safely shared for the whole application's lifetime; using a shorter lifetime than necessary isn't incorrect, but wastesAddSingleton's reason for existing. - Writing business logic directly inside an endpoint (for example, calculating the penalty by
hand instead of calling
loan.CalculatePenalty(policy)): this would duplicate logic that already exists and is already tested (Module 8, Lesson 4), breaking the main advantage of reusing the existing domain. - Tip: if an endpoint starts exceeding 4-5 lines of its own logic (not counting parameter
extraction or the translation to
Results.*), that's a "long method" signal (08-05) at the endpoint level: a fragment probably needs extracting into aLibrarymethod.
Exercises
-
Add a
GET /members/{id}endpoint that returns a specific member's data usinglibrary.FindMemberById(id), returning404 Not Foundif it doesn't exist. -
The
POST /loans/{id}/returnendpoint from section 6 useslibrary.Loans.ElementAtOrDefault(id)to locate the loan by its position in the list. Explain why this is fragile in a real system, and what you would change in theLoanmodel to solve it more robustly.
Solutions
app.MapGet("/members/{id}", (int id, Library library) =>
{
Member? member = library.FindMemberById(id);
return member is not null
? Results.Ok(member)
: Results.NotFound($"No member exists with Id {id}.");
});
Using the position within library.Loans as if it were an identifier is fragile because that
position changes if any loan is removed from the list, or if the insertion order varies (for
example, when reloading the catalog from the repository); two different requests could end up
unintentionally referring to different loans. The robust solution is to add its own Id
property to Loan (following the same pattern Member.Id already has, Module 3), assigned
uniquely when each loan is created, and to search by that Id instead of by position.
Conclusion
This lesson has assembled the complete Iteration 2 of the project: a multi-project .NET solution
structure with separated responsibilities, the registration of ILibraryRepository and Library
in ASP.NET Core's DI container picking back up from 07-03 and 08-03, and a complete Program.cs
with one endpoint per each of Lesson 2's seven functional requirements, with no new business logic
that didn't already exist in BiblioTech's domain. The API works, but it hasn't yet been verified
with tests beyond Module 8's unit tests, nor reviewed against any formal quality criteria. The next
lesson, Testing and Debugging, completes that work: it expands the test suite with integration
tests over these same endpoints, and introduces the quality checklist that must be passed before
moving on to Lesson 5, Deployment.
C# Programming Course
Module 1: Introduction to C#
- Introduction to C#
- Setting Up the Development Environment
- Hello World Program
- Basic Syntax and Structure
- Variables and Data Types
- Arrays and Strings
Module 2: Control Structures
Module 3: Object-Oriented Programming
- Classes and Objects
- Methods
- Constructors and Destructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- Structs and Records: Value Types and Reference Types
Module 4: Advanced C# Concepts
- Interfaces
- Delegates and Events
- Pattern Matching and Modern C# Features
- Generics
- Collections
- LINQ (Language Integrated Query)
- Asynchronous Programming
Module 5: Working with Data
- File I/O
- Serialization
- Database Connectivity
- Entity Framework
- Working with JSON and Consuming REST APIs
Module 6: Advanced Topics
- Reflection
- Attributes
- Dynamic Programming
- Memory Management and Garbage Collection
- Multithreading and Parallel Programming
Module 7: Building Applications
Module 8: Best Practices and Design Patterns
- Coding Standards and Best Practices
- Design Patterns
- Dependency Injection and Inversion of Control
- Unit Testing
- Code Review and Refactoring
