The best way to truly master any technology is to build something with itāand nothing beats creating CRUD (Create, Read, Update, Delete) operations when it comes to learning the core concepts. These operations form the backbone of almost every application, making them the perfect starting point for hands-on learning.
In this comprehensive course, weāll walk you through every step of the process ā from setting up your project to implementing robust CRUD operations and managing database schema with Code First Migrations. Whether youāre a seasoned developer or just getting started with ASP.NET Core, this course will help you build scalable and maintainable APIs that follow industry best practices.
This is by-far the most detailed guide available today for the .NET Community and Developers.
By the end of this article, youāll be able to easily build .NET 9 Web APIs, follow best practices while dealing with Entity Framework Core, and stand out in your next backend development interview. Letās get started!
PS, this guide will be valid for future releases of .NET too, like .NET 10 or above.
What We will Build?
We will build a .NET 9 Web API for managing movie details with comprehensive CRUD functionality, including:
- Create a New Movie: Add a new movie by providing details such as title, director, release date, and genre.
- Get a Movie by ID: Retrieve the details of a specific movie using its unique identifier.
- Get All Movies: List all movies.
- Update a Movie: Update the information of an existing movie by specifying its ID and the new details.
- Delete a Movie: Remove a movie from the database using its ID.
Key Specifications:
- PostgreSQL Database on Docker: Easily set up and manage the database in a containerized environment.
- OpenAPI Support with Swagger UI: Generate and explore API documentation effortlessly.
- Domain Driven Design: We will carefully design our Domain Entity (Movie) with DDD Practices.
- Entity Framework Core: Leverage EF Core for seamless database interactions with code-first migrations.
- Entity Configurations: Maintain clean and scalable database mappings with dedicated configuration classes.
- Minimal APIs: Build lightweight and performant APIs with minimal boilerplate.
- Data Seeding & Migration Workflows: Simplify initial data population and schema management.
- In-Memory Database for Development (Optional): Speed up development with an in-memory database.
- Best Practices: Ensure clean, maintainable, and scalable code using industry-standard best practices.
This Guide will be an invaluable resource for developers of all experience levels, providing hands-on experience with modern .NET development techniques and practical backend API implementation.
Who is this Guide for?
This guide is for .NET developers of all experience levels looking to build scalable Web APIs with modern development practices. Full-stack and backend developers can gain practical insights into API development with PostgreSQL, Docker, and Minimal APIs.
This is a very beginner friendly guide. Note that the entire source code of this implementation is available for FREE in my GitHub repository. You can find the links at the bottom of this article.
If youāre eager to build a well-structured .NET 9 Web API, this guide is for you.
PreRequisites
Here are the prerequisites,
- Visual Studio IDE - Install from here.
- .NET 9 SDK Installed - Install from here.
- Docker Desktop installed on your machine - Install from here.
- Internet access to download PostgreSQL Docker Image.
Project Setup
Letās get started with the development. Open up Visual Studio IDE (I personally use Visual Studio 2022 Community Edition), and create a new ASP.NET Core WebAPI
Project.
We will select the .NET 9 Framework, and keep everything else as the default.
Make sure that you have enabled the Enable OpenAPI support
checkbox.
I will cleanup the solution, and remove the weather forecast sample code that ships with the default .NET template.
I have also added a simple GET
Endpoint to Program.cs
that returns a āHello Worldā message.
Configuring OpenAPI and Scalar UI
As we know that, starting from .NET 9 and above, the default .NET Templates do not include Swagger. Thatās why we have OpenAPI as part of the project configuration. If you arenāt already aware of this breaking change, I have written a article covering why Microsoft has taken this decision, and what are the alternatives to Swagger. You can read the article from below.
However, letās install the Scalar Package and wire it up with our shiny new .NET WebAPI.
Once installed, letās integrate it with our Web API. Open up Program.cs
and add in the following highlighted file.
Also, open the launchSettings.json file and add the below highlighted lines so that the ScalarU is launched as soon as the WebAPI runs.
With that done, letās build and run our application. This would open up the browser at scalar/v1
as below.
I will keep things simple for now. However, there are tons of customizations available at both the OpenAPI and Scalar levels. To know more about this, please read this article.
Adding the Base Class - Domain Driven Design
As mentioned earlier, we will be following a simple DDD, aka Domain Driven Design Approach to structure our .NET application. Domain-Driven Design (DDD) focuses on modeling the software to match the business domain, ensuring a scalable and maintainable architecture. By following this approach, we aim to build a solution that is both understandable and adaptable to changing requirements.
If you want to have an in-depth article on DDD, do let me know in the comments section.
In Domain-Driven Design (DDD) and clean architecture practices, having a base class for entities is a common design pattern. The base class EntityBase
allows us to centralize common properties and behaviors that all domain entities will share. This provides consistency and reduces code duplication across the project.
Letās create a new folder named Models
and add a new class named EntityBase
.
An EntityBase
class serves as a foundational blueprint for all entities in a domain-driven application. It encapsulates common properties like Id
, Created
, and LastModified
, which are essential for tracking an entityās identity and lifecycle events. By centralizing these properties in a base class, we reduce code duplication and maintain a consistent structure across all entity classes.
Making the class abstract ensures that it cannot be instantiated on its own, as it is meant to provide shared functionality rather than represent a standalone concept. Entities in the domain, such as Movie
, inherit from EntityBase
to gain these common attributes and methods while defining their unique properties and behaviors. The use of access modifiers like private init
for the Id
ensures immutability after creation, promoting integrity. Similarly, restricting setters for Created
and LastModified
enforces controlled updates, preserving reliable timestamp information. The UpdateLastModified
method allows entities to track modifications seamlessly, supporting audit trails and data integrity.
Adding the Movie Entity - Domain Driven Design
Next up, letās build the actual entity. Create a new class named Movie
inside the Models folder.
This entity will represent the core information about a movie in our system. An entity is typically defined by having an identity (Id) and encapsulating business rules that relate to its data.
The Movie
class represents a movie entity in the application and builds on the EntityBase
class. It includes properties like Title
, Genre
, ReleaseDate
, and Rating
to define the essential details of a movie. These properties have private setters to ensure that changes can only be made within the class, maintaining the integrity of the object.
To create a new movie instance, the class provides a Create()
method instead of directly exposing constructors. This approach allows input validation before the object is created, ensuring that the entity always starts in a valid state. The private
constructor supports ORM frameworks that require parameter-less constructors to instantiate objects during database operations.
The Update()
method lets you modify the properties of a movie while ensuring the new values are valid. It updates the LastModified
property to track when changes were made. Validation rules are enforced to prevent invalid data, such as empty titles or genres, future release dates, and ratings outside the 0-10 range.
This design focuses on maintaining the correctness of the movie entity and enforcing domain rules at the entity level. It helps ensure that objects are always in a valid state and promotes clean and maintainable code.
Adding EFCore & PostgreSQL Packages
With the required entity model in place, itās time to set up the necessary NuGet packages for integrating Entity Framework Core with a PostgreSQL database. Run the following commands in the NuGet Package Manager Console:
These packages provide core EF functionality, design-time tools for migrations, and PostgreSQL database provider support, respectively.
Adding DBContext
Create a MovieDbContext class to manage the database operations for the application. I have created a folder named āPersistenceā and added a new class named MovieDbContext
. Hereās a clean and well-structured implementation:
The MovieDbContext
class is a custom implementation of EF Coreās DbContext
, which serves as the primary way to interact with the database. By inheriting from DbContext
, this class provides access to core database operations such as querying, updating, and saving changes.
The constructor takes DbContextOptions<MovieDbContext>
as a parameter, which allows the configuration of the context, including the database provider and connection string. This design ensures that the context remains flexible and easily configurable for different environments.
The DbSet<Movie>
property exposes a strongly typed collection of Movie
entities. This collection maps directly to the Movies
table in the database and provides a clean way to perform CRUD operations using LINQ queries.
In the OnModelCreating
method, the HasDefaultSchema
call sets the schema to āapp,ā which is useful for organizing tables under a specific namespace in PostgreSQL. The call to ApplyConfigurationsFromAssembly
ensures that any entity configurations defined in the assembly are automatically applied, promoting cleaner and more maintainable code. Finally, the base.OnModelCreating(modelBuilder);
call ensures that the default configurations defined by the DbContext
are also applied.
IEntityTypeConfiguration: Why?
In Entity Framework Core, configuration for database entities can be done in multiple ways. One option is to use Data Annotations directly in the entity classes. However, this approach can clutter the model classes and tightly couple them to EF Core. A cleaner and more maintainable approach is to use the IEntityTypeConfiguration<T>
interface, which allows us to separate entity configurations into dedicated classes.
This interface provides a Configure
method where you can define various database mapping details, such as table names, keys, relationships, constraints, and indexes. This decoupled approach improves code organization and promotes adherence to the Single Responsibility Principle, making your codebase easier to manage and extend. Additionally, it enables better reusability and flexibility, especially in larger projects where multiple entities require complex configurations.
By using IEntityTypeConfiguration
, you can centralize all database-specific logic for an entity, ensuring that your model classes remain focused solely on business logic. This separation also aligns well with migration and schema evolution scenarios, as all configurations are defined in one place, making it easier to maintain and update the database mappings over time.
Under the Persistence
folder, create a new folder called Configurations
and add a new class MovieConfiguration
.
The MovieConfiguration
class implements IEntityTypeConfiguration<Movie>
, allowing us to define how the Movie
entity should be mapped to the database. This configuration is cleanly encapsulated in the Configure
method.
Inside the Configure
method, the first step is defining the table name using builder.ToTable("Movies")
. This explicitly maps the entity to the Movies
table, even if EF Core would infer the name by default.
Next, the primary key is defined using builder.HasKey(m => m.Id);
. This ensures that the Id
property is treated as the primary key when the database schema is created.
Property configurations are specified using the Property
method. For example, builder.Property(m => m.Title).IsRequired().HasMaxLength(200);
ensures that the Title
property is mandatory and has a maximum length of 200 characters. Similarly, other properties such as Genre
, ReleaseDate
, and Rating
are also marked as required, with some constraints like maximum length.
Timestamps are configured for the Created
and LastModified
properties. The line builder.Property(m => m.Created).IsRequired().ValueGeneratedOnAdd();
ensures that Created
is set when the entity is first added. On the other hand, builder.Property(m => m.LastModified).IsRequired().ValueGeneratedOnUpdate();
ensures that LastModified
is updated whenever the entity is modified.
Finally, builder.HasIndex(m => m.Title);
creates a database index on the Title
column. This can significantly improve query performance when searching or filtering by Title
.
This structured configuration keeps entity models clean and focuses purely on the domain logic, while all database-related settings are neatly managed in a separate class.
Running PostgreSQL on Docker Container
To quickly and efficiently set up a PostgreSQL instance, Docker is an ideal solution. It enables you to run a fully isolated database environment, without making changes to your local development setup.
Before proceeding, ensure that Docker Desktop is installed on your machine. Itās also beneficial to have a basic understanding of Docker. If youāre new to Docker, feel free to check out my article to get up to speed.
Create a new file named docker-compose.yml
at the root of the solution and add the following.
This docker-compose.yml
file sets up a PostgreSQL service in a Docker container. It uses the official postgres
image and assigns the container a name postgres
. The service is configured to restart automatically if the container stops or the system reboots.
The environment variables set up the default PostgreSQL user, password, and database (admin
, secret
, and dotnetHero
respectively). It also maps the PostgreSQL port 5432
on the container to the hostās port 5432
, allowing external access to the database.
Lastly, it uses a Docker volume (postgres_data
) to persist database data even if the container is removed or restarted, ensuring data is not lost.
I typically use pgAdmin to navigate through the objects in PostgreSQL databases.
Once done, Open a terminal, navigate to the directory containing the docker-compose.yml, and execute:
This will pull the PostgreSQL image, set up the container, and expose it on port 5432
.
Running PostgreSQL in a Docker container is a great way to simplify your development and testing environment. It offers a consistent, isolated setup that can easily be replicated across different machines. With minimal configuration, you can quickly integrate PostgreSQL into your application.
Other Options:
-
Using Local Installation: You can install PostgreSQL directly on your machine. While this provides full control over the environment, it can be less portable and may lead to versioning issues when working across different systems.
-
Managed PostgreSQL Services (Cloud): Services like Amazon RDS, Azure Database for PostgreSQL, and Google Cloud SQL offer fully managed PostgreSQL instances. These are ideal for production environments where scalability, availability, and maintenance are crucial.
-
Virtual Machines: Running PostgreSQL in a virtual machine (VM) is another option, especially when you need a more isolated environment or specific configurations. However, this is heavier than using Docker and requires more system resources.
Each option has its advantages depending on your projectās needs, from ease of use with Docker to the scalability and maintenance benefits of managed cloud services.
Connection String Configuration
Next, letās configure the connection string for your PostgreSQL database. Open the appsettings.json
file and add the following:
The key "DefaultConnection"
can be customized, but make sure to use the same name consistently in your code when registering the DbContext, as it will reference this key for database connection details.
Registering the DbContext
To register the MovieDbContext
in your application, add the following code in the Program.cs
file:
Explanation:
AddDbContext<MovieDbContext>
: This registers theMovieDbContext
with the dependency injection container, enabling it to be used throughout the application.- UseNpgsql: This method tells EF Core to use PostgreSQL as the database provider, with the connection string fetched from
appsettings.json
. - GetConnectionString(āDefaultConnectionā): Retrieves the connection string with the key
"DefaultConnection"
from yourappsettings.json
file.
With this configuration, the MovieDbContext
is now ready to interact with the PostgreSQL database using the specified connection string.
Adding the Code First Migrations
Now, letās add the necessary migrations to generate the database schema based on your entity model.
-
Add a migration: Run the following command in the terminal to create a migration that will generate the database schema based on your
MovieDbContext
:- This will generate a migration named
InitialCreate
. You can replace this with any other descriptive name if needed. - The migration contains instructions for EF Core on how to create the tables and define their structure.
- This will generate a migration named
-
Update the database: After adding the migration, you can apply it to the database with the following command:
- This will execute the migration and create the database and tables (if they donāt already exist) based on the defined
DbContext
and models. - It applies the changes to the PostgreSQL database specified in your connection string.
- This will execute the migration and create the database and tables (if they donāt already exist) based on the defined
With this setup, you can now manage your database schema through EF Core migrations and easily sync changes with your PostgreSQL database.
And here is what my Migrations file looks like.
Seeding Data
Now that we have our database tables in place, we need some initial data to the Movies table. Here is where Seeding comes.
Seeding data is a process of populating a database with initial data, which can be essential during development, testing, or even in production environments. Itās an important step to ensure that your application has the necessary data available to work with when it starts.
In our MovieDbContext
, add the following code.
EF 9 introduced the UseSeeding
and UseAsyncSeeding
methods, which streamline the process of seeding the database with initial data. These methods simplify the way developers can implement custom initialization logic, offering a central location for all data seeding operations. The advantage of using these methods is that they integrate seamlessly with EF Coreās migration locking mechanism, which ensures that data seeding happens safely and without concurrency issues.
One of the key benefits of these new methods is their automatic execution during operations like EnsureCreated
, Migrate
, and the dotnet ef database update
command. This happens even if there are no changes to the model or migrations, ensuring that the database is consistently seeded with the required data whenever the application is set up or updated.
By leveraging UseSeeding
and UseAsyncSeeding
, developers can manage and execute seeding tasks in a more organized, reliable, and concurrent-safe manner.
In summary, this code checks if a movie with a specific title exists in the database. If it doesnāt, it creates a new movie and saves it to the database. This is a common pattern used for seeding data or ensuring that certain records are present in the database.
Finally, to trigger the seeding process, letās add a EnsureCreatedAsync
call. In Program.cs file, add the following.
The provided code snippet is an example of how to ensure that the database is created asynchronously when the application starts. First, it creates a scoped service provider using app.Services.CreateAsyncScope()
. This is important because it ensures that services within the scope, such as the MovieDbContext
, are disposed of properly when no longer needed. Within this scope, the code retrieves the required MovieDbContext
from the service provider using GetRequiredService<MovieDbContext>()
. Then, it calls dbContext.Database.EnsureCreatedAsync()
, which asynchronously checks if the database exists and creates it if it doesnāt. This ensures that the database is in place before any data operations are performed, preventing errors that might occur if the database is missing. This pattern is typically used in applications where database creation is required at startup but where migrations are not being used (e.g., for small-scale or prototype applications).
This ensures the database is created and the seed data is added.
Adding the DTOs - ViewModels
DTOs (Data Transfer Objects) play an essential role in software development by serving as lightweight objects used to transfer data between different parts of an application, such as between the backend and the frontend. They are particularly important in web APIs and service-oriented architectures.
One primary reason for using DTOs is to decouple the internal domain models from the external representation sent over the network. Directly exposing domain entities can lead to security risks, data overexposure, and unwanted changes in the structure when evolving the application. DTOs allow developers to define exactly what data should be shared, providing better control over serialization and data formatting.
Another advantage is improved performance and bandwidth efficiency. DTOs allow you to shape the response payload by sending only the required fields rather than the entire domain object. This reduces the size of the transmitted data, which is critical in scenarios involving slow networks or mobile applications.
DTOs also play a vital role in data validation and mapping. They allow you to perform input validation at the boundary of your application, ensuring that only valid and well-structured data reaches your domain models. Additionally, they simplify mapping data between domain models and client-facing views, often using libraries like AutoMapper.
In summary, DTOs help achieve clean architecture by decoupling the internal and external representations of data, enhancing security, performance, and maintainability. They are a best practice for designing scalable and maintainable APIs.
In our case, if letās say a client requests for a Movie with ID #3, if the API returns the Movie class, it would lead to overexposure of fields like CreatedOn and other sensitive properties. So, instead of directly exposing the Movie
entity, we can return a well-defined DTO that includes only the relevant and safe properties, such as Title
, Genre
, ReleaseDate
, and Rating
. By doing so, we maintain better control over the data sent to the client and ensure that sensitive fields like CreatedOn
and LastModified
are hidden from public access.
Additionally, using DTOs provides flexibility in shaping the API responses. For example, we can create different DTOs based on the client requirements, such as a MovieDetailsDTO
with comprehensive information and a MovieSummaryDTO
with just the essential details for a list view. This approach also ensures that any future changes to the internal domain models do not directly impact the external API contract, making the system more maintainable and scalable.
In cases where transformations are required, such as converting DateTimeOffset
values to a user-friendly format, DTOs provide an ideal layer to handle such custom mappings without affecting the core domain logic. Overall, the use of DTOs fosters better API design, enhances security, and supports a cleaner separation of concerns.
Letās create a new folder named DTOs
and add the following records.
Creating DTOs for the Movie
entity helps maintain a clean separation between the domain model and API requests or responses. In this case, we define three records within a new DTOs
folder.
The CreateMovieDto
record encapsulates the data required when a client creates a new movie entry. This includes Title
, Genre
, ReleaseDate
, and Rating
. By using this DTO, we ensure that the API only accepts the relevant information needed to initialize a movie.
The UpdateMovieDto
record handles the data when updating an existing movie. Its properties mirror those in CreateMovieDto
, as updating typically involves modifying the same fields as creation.
Lastly, MovieDto
is designed for responses sent back to the client. It includes an Id
property to uniquely identify the movie along with the other fields. By returning this DTO instead of the full Movie
entity, we avoid exposing internal details and maintain control over the APIās data contract.
This setup using DTOs with records makes the API cleaner, more secure, and aligned with best practices for RESTful services.
The IMovieService Interface
Here is the core part of the implementation, services. Create a new folder named Services
and add the following IMovieService
interface.
The CreateMovieAsync
method handles the creation of a new movie by accepting a CreateMovieDto
command and returning the newly created movie as a MovieDto
.
The GetMovieByIdAsync
method retrieves a specific movie based on its unique identifier (Guid
).
GetAllMoviesAsync
fetches a collection of all movies, returning them as an IEnumerable<MovieDto>
.
UpdateMovieAsync
allows updating an existing movie. The method accepts the movieās identifier and an UpdateMovieDto
, which contains the updated data.
Lastly, DeleteMovieAsync
is responsible for deleting a movie based on its unique identifier.
By defining these methods in the IMovieService
interface, the application maintains a clear separation of concerns, enabling easier testing, better maintainability, and adherence to SOLID principles, especially the Dependency Inversion Principle.
CRUD Operations - MovieService Implementation
Now that we have defined the interface, Letās implement it. Create a new class named MovieService
.
Letās add in the following CRUD Operations, one by one.
Create
The CreateMovieAsync
method is responsible for handling the creation of a new movie in the database. It begins by invoking the Movie.Create
factory method, which encapsulates the entity creation logic. This ensures that any business rules or validations are enforced during the creation process.
Once the movie entity is created, it is added to the DbContext using AddAsync
. This operation marks the entity for tracking, signaling that it should be persisted to the database when changes are saved.
Next, SaveChangesAsync
is called to persist the new movie entry to the database. Using asynchronous methods ensures that the API remains responsive and can handle other requests efficiently.
Finally, the method returns a MovieDto
object, which contains only the essential fields to be exposed to the client. This approach prevents overexposure of sensitive data and maintains a clean separation between the entity and the APIās response model.
Read
The GetAllMoviesAsync
method retrieves all movies from the database without tracking changes for better performance. It maps each movie to a MovieDto
, ensuring only essential fields are returned. The use of ToListAsync
allows for efficient asynchronous database access.
The GetMovieByIdAsync
method fetches a specific movie by its Id
from the database without tracking changes. If no matching movie is found, it returns null
. If found, it maps the movie entity to a MovieDto
and returns it, ensuring only essential data is exposed.
Update
The UpdateMovieAsync
method handles updating an existing movie in the database. It first searches for the movie by its Id
. If no movie is found, it throws an ArgumentNullException
. If found, it updates the movieās properties using the provided UpdateMovieDto
and then saves the changes to the database. This approach ensures efficient entity tracking and persistence within EF Core.
Delete
The DeleteMovieAsync
method is pretty straightforward. It looks up the movie by its Id
. If a match is found, it removes the movie from the database and commits the change by calling SaveChangesAsync()
. If no movie is found, it quietly does nothingāno exceptions, no drama.
Some of you might argue that throwing an exception when a movie isnāt found would be more explicit and help catch potential issues. However, in this case, itās unnecessary overhead for a simple delete operation. If the movie doesnāt exist, thereās nothing to remove, and the outcome is already what we want. Keeping the logic clean and lightweight makes the code easier to maintain and avoids unnecessary exception handling.
Minimal API Endpoints
Now that weāve implemented the core logic for our movie service, itās time to expose these functionalities via API endpoints. For this demonstration, weāll use Minimal API Endpoints, which provide a simple and concise way to set up APIs without the overhead of controllers.
These are the conventional routes following REST Standards.
Conventional REST API Standards
- POST
/api/movies
: Add a new movie - GET
/api/movies
: Get all movies - GET
/api/movies/{id}
: Get a movie by ID - PUT
/api/movies/{id}
: Update a movieās details - DELETE
/api/movies/{id}
: Delete a movie by ID
Hereās how we can wire up the MovieService logic with the necessary API routes:
First, we register the IMovieService and its implementation in the Program.cs
. This allows us to inject the service into our endpoints.
The above mentioned 5 CRUD Endpoints can be added into the Program.cs
as well, but I would do something better.
To make the code cleaner and more maintainable, we can organize the API routes into a separate class, which helps keep the Program.cs file more focused and less cluttered. This structure also improves readability and reusability.
I created a folder named Endpoints
and added a class called MovieEndpoints
.
The MovieEndpoints
class defines a set of API routes for movie-related operations using Minimal API style. The MapMovieEndpoints
method is an extension for IEndpointRouteBuilder
and is responsible for configuring the various HTTP methods (POST, GET, PUT, DELETE) for the /api/movies
route.
- The method begins by defining a route group
/api/movies
and tagging it as āMoviesā for better organization in tools like Swagger. - POST
/api/movies/
: This route allows creating a new movie. It accepts aCreateMovieDto
object, calls theCreateMovieAsync
method from the service, and returns a201 Created
response along with the newly created movieās data. - GET
/api/movies/
: This route retrieves all movies from the database by calling theGetAllMoviesAsync
method and returns the list of movies as a200 OK
response. - GET
/api/movies/{id}
: This route fetches a movie by its ID. If the movie is found, it returns the movie details; if not, it responds with a404 Not Found
message. - PUT
/api/movies/{id}
: This route allows updating an existing movieās details. It calls theUpdateMovieAsync
method with the movieās ID and updated data from theUpdateMovieDto
. It returns a204 No Content
status upon successful update. - DELETE
/api/movies/{id}
: This route deletes a movie by its ID. It returns a204 No Content
.
The overall structure keeps the API organized and clean, enabling scalable and easy-to-manage routes for the movie-related CRUD operations.
Finally, add the following extension to your Program.cs
to register the above defined endpoints.
This will wire up all the movie-related routes, and your application is now ready to handle movie-related requests through these clean and structured API endpoints.
Testing the API with Scalar
Thatās everything you need to do from the code aspect. You can find the entire source code of this implementation on my GitHub repository, the link to which is available at the bottom of this article.
Letās test this .NET 9 CRUD Application now. Build and run the application, and open up the Scalar UI.
Here is what you will see, all the API Endpoints ready to go!
I first tried the Get All Endpoint.
Since we added seed data, we can see that there is already a movie record available for us. This entry would be added as soon as the API boots up.
Letās create a new entry.
You can see that we get back a 201 Created response.
Try out the Get All endpoint to verify that our entry has been created.
Similarly, feel free to test out the
- Get By ID
- Delete
- Update Endpoints as well!
Next Steps
That wraps up our implementation! But there are still a few things you can do to elevate this .NET 9 Web API even further. Here are some next steps to enhance its functionality:
-
Add Logging: Enhance the observability of your application by integrating structured logging using Serilog. Logging provides insights into whatās happening in your API, making it easier to troubleshoot issues. If youāre new to Serilog, check out my detailed guide on integrating it into ASP.NET Core here.
-
Add Input Validation: Although weāve validated our domain layer, itās a good practice to validate data at the DTO level as well. This ensures that only valid data reaches the business logic. You can easily achieve this with the FluentValidation library. If youāre unfamiliar with it, Iāve written a comprehensive tutorial here.
-
Introduce Paging, Sorting, and Filtering: For larger datasets, adding features like paging, sorting, and filtering will significantly improve the performance and usability of your API. This makes it easy to retrieve specific subsets of data, particularly for endpoints that return large lists.
-
Dockerize the Application: Dockerizing your application makes it more portable and scalable. It simplifies deployments across different environments. If youāre new to Docker, check out my Getting Started Guide for Docker to learn how to containerize your .NET applications.
-
Deploy to the Cloud: Finally, take your application to the cloud! Whether itās AWS, Azure, or Google Cloud, deploying your API to the cloud enhances scalability, security, and manageability. Each cloud provider has its own deployment strategies, but they all offer a variety of services to support your application.
By following these steps, you can transform your API into a production-ready, robust solution that scales efficiently while offering a seamless experience to users.
Summary
I hope this guide has helped you build a solid foundation for your .NET Web API. I have put in a lot of effort and time into this article to make it a truly valuable resource for the .NET Developers!
If you found this article useful, Iād love to hear your feedback! Did you enjoy it? What other topics would you like me to cover next? Drop a comment or reach out, and let me know what youād like to see in the future.
And if you think others might benefit from this article, feel free to share it with your network. Sharing is caring, and it helps the community grow!
Thanks for reading, and stay tuned for more helpful content!