BiblioTech already has a mature domain, a decoupled architecture, a complete API, and a test
suite — unit and integration — confirming that everything works as expected. One last step
remains, the one that turns all that work into something a real user can use: deploying it.
This lesson publishes the API with dotnet publish, packages it into a simple Docker container,
configures its behavior differently depending on the environment (development versus production),
and touches, without going into detail on any specific platform, on how all of this fits into a
continuous integration and deployment pipeline. It's the course's final lesson: it closes not just
Module 9, but the entire journey that began with a simple "Hello World" in Module 1's first
lesson.
Contents
dotnet publish: from source code to a deployable artifact- Basic containerization with Docker
- Per-environment configuration:
appsettings.jsonand environment variables - The production connection string
- Basic CI/CD considerations
- BiblioTech, deployed
dotnet publish: from source code to a deployable artifact
dotnet publish: from source code to a deployable artifactUntil now, every lesson in the course ran with dotnet run, which compiles and starts the
application in a single step meant for development. dotnet publish, by contrast, generates the
application's deployment-ready version: the exact files needed to run it on another machine,
with no need for the source code or the full .NET SDK, only the runtime:
-c Release: compiles in Release mode instead of Debug (the default used in development), with optimizations enabled and without the extra information that makes step-by-step debugging easier (Lesson 4) but isn't needed in production.-o ./published: the output folder with the result: the assemblies (.dll) forBiblioTech.Api,BiblioTech.Domain, andBiblioTech.Persistence, their dependencies, and an entry-point executable.
The contents of ./published are exactly what gets copied to a server, or packaged inside a
Docker container in the next section: the source code, BiblioTech.Tests, and any development
project are no longer needed to run the application from there.
- Basic containerization with Docker
Docker packages an application together with everything it needs to run (the .NET runtime, in
this case) into a self-contained unit called an image, which runs identically on any machine
with Docker installed, regardless of anything else that machine has installed. A simple
Dockerfile for BiblioTech.Api:
# Stage 1: build and publish the application with the full .NET SDK
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore BiblioTech.Api
RUN dotnet publish BiblioTech.Api -c Release -o /published
# Stage 2: the final image only needs the runtime, not the full SDK
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
COPY --from=build /published .
EXPOSE 8080
ENTRYPOINT ["dotnet", "BiblioTech.Api.dll"]This Dockerfile uses a technique called a multi-stage build: the first stage (build) uses
the full SDK image (heavier) only to compile and publish; the second stage (final), the one
actually distributed and run, starts from a much lighter image that only brings the ASP.NET Core
runtime, without the build tools no longer needed once /published has been generated.
docker run -p 8080:8080 exposes the container's port 8080 on the host machine's port 8080,
letting you access the API exactly as if it were running locally with dotnet run.
- Per-environment configuration:
appsettings.json and environment variables
appsettings.json and environment variablesASP.NET Core automatically distinguishes between environments (development, production...) and loads the corresponding configuration by combining several files and sources, in a specific priority order:
BiblioTech.Api/ ├── appsettings.json // base configuration, common to all environments ├── appsettings.Development.json // only applies in the "Development" environment (local dotnet run) └── appsettings.Production.json // only applies in the "Production" environment (deployed)
// appsettings.Development.json: more detailed logging locally
{
"ConnectionStrings": {
"BiblioTech": "Data Source=bibliotech-dev.db"
},
"Logging": {
"LogLevel": {
"Default": "Debug"
}
}
}// appsettings.Production.json: no connection string here (see section 4)
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
}
}| Configuration source | When it applies | Priority |
|---|---|---|
appsettings.json |
Always, as the base | Lowest |
appsettings.{Environment}.json |
Only if ASPNETCORE_ENVIRONMENT matches {Environment} |
Medium |
| Environment variables | Whenever they're defined | High |
| Command-line arguments | Only if passed at runtime | Highest |
This hierarchy lets you, for example, have convenient development defaults in
appsettings.Development.json (a local SQLite database), while in production an environment
variable overrides that same value without touching any deployed file — exactly what solves
Lesson 2's security non-functional requirement (not leaving sensitive data in the source code or in
version-controlled configuration files).
- The production connection string
The production database's connection string (Module 5, Entity Framework Core over SQLite,
Lesson 2's decision) is exactly the kind of data that must never be written directly into
appsettings.Production.json or into any file uploaded to version control. Instead, it's defined
as an environment variable on the server or inside the Docker container itself:
export ConnectionStrings__BiblioTech="Data Source=/data/bibliotech-production.db"
docker run -p 8080:8080 \
-e ConnectionStrings__BiblioTech="Data Source=/data/bibliotech-production.db" \
-e ASPNETCORE_ENVIRONMENT=Production \
bibliotech-apiThe double underscore (__) in ConnectionStrings__BiblioTech is how ASP.NET Core translates an
environment variable's hierarchy into the same nested structure it would have in JSON
("ConnectionStrings": { "BiblioTech": "..." }). The Program.cs code already written in 09-03
(builder.Configuration.GetConnectionString("BiblioTech")) needs no change at all: it always reads
the same way, regardless of whether the final value came from a development JSON file or a
production environment variable. This is the same underlying idea that motivated
ILibraryRepository in Module 8: the code that consumes a value shouldn't need to know exactly
where that value comes from.
- Basic CI/CD considerations
CI/CD (Continuous Integration/Continuous Deployment) is the practice of automating, through a pipeline, the steps from a code change to its deployment, instead of running them by hand every time. A typical pipeline for BiblioTech, without tying itself to any specific platform (GitHub Actions, GitLab CI, Azure DevOps, and others all offer variants of the same thing), would chain these steps:
flowchart LR
A["Code change<br/>(git push)"] --> B["Build<br/>dotnet build"]
B --> C["Test<br/>dotnet test<br/>(unit + integration, Lesson 4)"]
C --> D{"All passing?"}
D -->|"Yes"| E["Publish<br/>dotnet publish + docker build"]
D -->|"No"| F["Pipeline stops<br/>nothing broken gets deployed"]
E --> G["Deployment<br/>(specific platform, beyond this lesson's scope)"]
The core value of this pipeline isn't the automation itself, but the guarantee it provides: no change reaches production without having gone, automatically and with no manual step, through exactly the same tests run in Lesson 4. That lesson's quality checklist stops depending on someone remembering to run it by hand before every deployment.
- BiblioTech, deployed
With this, BiblioTech completes its journey: a publishable Docker image built with docker build,
configured differently in development and production with no code changes, with its production
connection string outside the source code, and with a clear path (though not implemented in detail
in this course) toward a CI/CD pipeline that automatically verifies every change before deploying
it. The system that started as a console exercise in Module 1 ends up being a real application,
with an HTTP API, database persistence, automated tests, and a reproducible deployment process.
Common Mistakes and Tips
- Writing the production connection string directly in
appsettings.Production.json: that file is usually version-controlled along with the rest of the source code; any sensitive production data must arrive through an environment variable or a secrets manager, never written there. - Publishing in
Debugmode instead ofRelease: this loses the compiler's optimizations and publishes extra information meant only for debugging in development (Lesson 4), not for running in production. - Using a single Docker stage with the full SDK for the final image: it works, but produces an image much heavier than necessary, with build tools no longer used once the application is published; the multi-stage build from section 2 solves this with no extra effort.
- Tip: before considering the deployment complete, explicitly confirm this lesson's three
points separately: that
dotnet publishgenerates the correct artifact, that the Docker image starts up and responds locally withdocker run, and that the production configuration contains no sensitive data written directly into a version-controlled file.
Exercises
-
Add an
ASPNETCORE_ENVIRONMENT=Productionenvironment variable to section 2'sDockerfileusing theENVinstruction, so that any container built from this image starts in production mode by default, unless told otherwise when runningdocker run. -
Describe, in your own words and with no need to configure any real platform, the four steps of section 5's CI/CD pipeline applied to a concrete change: adding the
GET /members/{id}endpoint you wrote in the previous lesson's exercise 1.
Solutions
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
WORKDIR /app
ENV ASPNETCORE_ENVIRONMENT=Production
COPY --from=build /published .
EXPOSE 8080
ENTRYPOINT ["dotnet", "BiblioTech.Api.dll"]
(1) Build: when git push runs with the new endpoint, the pipeline runs dotnet build over
the whole solution, confirming it compiles with no errors. (2) Test: it runs dotnet test,
including both Module 8's unit tests and Lesson 4's integration tests — if the new endpoint had
no integration test of its own, it would be good practice to add one before this step. (3) All
passing: if any test fails, the pipeline stops here and the change doesn't move forward. (4)
Publish and deploy: only if the previous step succeeded, dotnet publish and docker build
run to generate the new image, which is then deployed according to whichever platform was
chosen.
Conclusion
This is the end of the C# Programming course. The journey started in Module 1 with setting up the
development environment and a first program that printed "Hello World" to the console; it
continued through Module 2's control structures and exception handling; it built, in Module 3,
BiblioTech's first real object model — LibraryItem, Book, Magazine, Member, Loan — with
classes, inheritance, polymorphism, and encapsulation; it expanded that in Module 4 with
interfaces, generics, collections, LINQ, and asynchronous programming; it gave it persistent
memory in Module 5, with four different persistence mechanisms and a first connection to external
services; it explored, in Module 6, .NET's more internal capabilities — reflection, attributes,
memory management, and concurrency; it gave it a face, in Module 7, with five different interface
technologies, desktop, web, and mobile; and it matured its design in Module 8, with coding
standards, design patterns, dependency injection, unit tests, and disciplined refactoring.
This last module has done nothing more than bring that whole path together into a single, complete system: clear requirements, an architecture chosen with judgment, an API exposing the domain built over nine modules, a test suite backing every change, and a reproducible deployment process that reliably carries that code all the way to where someone can actually use it.
From "Hello World" to an application deployed in production: that's the complete journey of this course, and it's also, in essence, the journey of any real software project. What sustains that journey isn't having memorized C# syntax, but having practiced, over and over on the same domain, making design decisions, decoupling responsibilities, trusting tests over luck, and thinking about whoever will use the code as much as whoever writes it. Those are the tools that will keep being useful long after any specific syntax detail is obsolete. Congratulations on making it this far: BiblioTech is now finished, but what you've learned building it doesn't have to stop here. The next C# project is now entirely up to you.
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
