ASP.NET Core – True Ultimate Guide
Section 18: Entity Framework Core
Entity Framework Core
EF Core is a modern, lightweight, and extensible Object-Relational Mapper (ORM) framework for .NET. It simplifies database interactions by allowing you to work with data as .NET objects (entities) rather than raw SQL queries.
How EF Core Works
- Entities and DbContext: You define classes that represent your database tables (entities) and a
DbContextclass that acts as a bridge between your entities and the database. - Mapping: EF Core handles the mapping between your entities and the database tables, including column names, data types, relationships, and constraints.
- Querying and Saving: You use LINQ (Language Integrated Query) to query your data and interact with your entities. EF Core translates your LINQ queries into efficient SQL statements and executes them against the database.
Pros of EF Core
- Developer Productivity: Reduced boilerplate code for database interactions.
- Object-Oriented Approach: Work with data using familiar object-oriented concepts.
- Strongly Typed Queries: LINQ provides compile-time type safety for your queries.
- Cross-Platform: Supports various database providers (SQL Server, SQLite, PostgreSQL, MySQL, etc.).
- Automatic Change Tracking: EF Core keeps track of changes made to entities.
- Migrations: The migrations feature simplifies database schema evolution.
Cons of EF Core
- Abstraction Overhead: The abstraction layer can sometimes lead to less optimized SQL queries compared to hand-written SQL.
- Learning Curve: There is still a learning curve to understand its concepts and best practices.
NuGet Packages for EF Core
Microsoft.EntityFrameworkCore: The core package.Microsoft.EntityFrameworkCore.SqlServer: Database provider for SQL Server.Microsoft.EntityFrameworkCore.Sqlite: Database provider for SQLite.Microsoft.EntityFrameworkCore.InMemory: An in-memory database provider, primarily used for testing.Microsoft.EntityFrameworkCore.Design: Tools for working with migrations and scaffolding.Microsoft.EntityFrameworkCore.Tools: Thedotnet efcommand-line tools.
Choosing a Database Provider
- SQL Server: A popular choice for enterprise applications.
- SQLite: A lightweight, file-based database suitable for small to medium-sized applications.
- PostgreSQL: A powerful open-source relational database.
- MySQL: Another open-source relational database popular for web applications.
- InMemory: Ideal for testing and scenarios where you don’t need data persistence.
Notes
- ORM: EF Core is an Object-Relational Mapper that simplifies database interaction.
- Core Concepts: Entities (
DbContext), mapping, querying with LINQ, change tracking, migrations. - Pros: Productivity, object-oriented, type safety, cross-platform, migrations.
- Cons: Potential for abstraction overhead, learning curve.
- Packages: The core package, database providers, design-time tools.
- Choose the Right Database: Consider factors like scalability, features, licensing, and your team’s expertise.
EF Core Architecture: A Three-Layer Approach
-
Conceptual Model (Entity Model):
- This is your C# code representation of the database schema. You define entity classes that represent your database tables, along with their properties (columns) and relationships between entities.
- The entity classes form the heart of your domain model, reflecting the real-world concepts your application deals with.
-
Mapping:
- EF Core handles the mapping between your entity classes and the underlying database schema. This includes mapping property names to column names, data types, relationships (foreign keys), and constraints.
- You can customize this mapping using fluent APIs or data annotations in your entity classes.
-
Storage Model (Database Schema):
- This is the actual structure of your database (tables, columns, relationships). EF Core can either generate the database schema based on your entity model or work with an existing database.
How EF Core Works: A Simplified View
-
DbContext:
- You create a
DbContextclass that acts as a session with the database. This class is responsible for tracking entity changes, managing transactions, and translating LINQ queries into SQL commands.
- You create a
-
Querying:
- You write LINQ queries against your
DbContextto fetch data from the database. - EF Core translates these LINQ queries into optimized SQL queries and executes them against the database.
- It then materializes the results into your entity objects.
- You write LINQ queries against your
-
Saving Changes:
- When you modify entities in your code, EF Core tracks those changes.
- When you call
SaveChanges()on yourDbContext, EF Core generates SQL commands to update the database based on the tracked changes.
EF Core Approaches: Which One to Choose?
-
Code First:
- You start by defining your entity classes, and EF Core creates the database schema based on those classes.
- Use this approach when you are starting from scratch or have full control over your database schema.
-
Database First:
- You start with an existing database, and EF Core generates entity classes based on the schema.
- Use this approach when you have a legacy database that you need to integrate with.
-
Model First (Not in EF Core):
- This approach involves designing a visual model (EDMX) of your database schema, and EF generates the code from the model. Not supported in EF Core.
Notes
- ORM: EF Core is an Object-Relational Mapper, bridging the gap between your code and the database.
- Key Components:
DbContext, entity classes, LINQ queries,SaveChanges(). - Approaches: Choose between Code First and Database First based on your project’s starting point.
DbContext
In EF Core, the DbContext class serves as a central hub for your database interaction. Think of it as a session with your database.
Responsibilities
- Connecting to the Database: The
DbContextestablishes the connection to your database using the connection string you provide. - Managing Entities: The
DbContexttracks changes made to entity instances, manages their lifecycle (adding, deleting, updating), and coordinates the persistence of those changes back to the database. - Querying Data: You use LINQ to formulate queries against your entities through the
DbContext. - Change Tracking: EF Core automatically tracks changes you make to your entities in memory. When you call
SaveChanges(), EF Core detects these changes and generates the appropriate SQL commands.
DbSet: Your Entity Collection
DbSet<TEntity> represents a collection of a specific entity type within your DbContext. It exposes methods for querying, adding, updating, and deleting entities of that type.
- Mapping: Each
DbSetproperty in yourDbContextis mapped to a corresponding table in your database. - Usage: You interact with the database through your
DbSetproperties. For example,context.Persons.Add(newPerson);would add a newPersonentity to the database.
Code Example: PersonsDbContext
// PersonsDbContext.cs
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Entities;
namespace Entities
{
public class PersonsDbContext : DbContext
{
public DbSet<Country> Countries { get; set; }
public DbSet<Person> Persons { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Country>().ToTable("Countries");
modelBuilder.Entity<Person>().ToTable("Persons");
}
}
}In this example:
PersonsDbContextderives fromDbContext.CountriesandPersonsareDbSetproperties representing theCountryandPersonentities respectively.OnModelCreatingis overridden to customize the mapping between your entities and database tables.
Notes
- DbContext:
- Represents a session with your database.
- Handles entity management, change tracking, querying, and saving changes.
- Often injected as a scoped service in your ASP.NET Core application.
- DbSet:
- Represents a collection of a specific entity type.
- Provides methods for querying, adding, updating, and deleting entities.
- Each
DbSetis mapped to a corresponding table in your database.
Connection Strings
A connection string is essentially the address your application uses to locate and connect to your database server. It contains vital information like:
- Data Source (Server): The name or IP address of the database server.
- Initial Catalog (Database Name): The specific database on the server.
- Credentials (Optional): Username and password if authentication is required.
- Additional Settings: Options like connection timeout, encryption settings, and more.
SQL Server Connection String Format (Example)
Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=PersonsDatabase;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False
- Data Source: Specifies the server name. Using local SQL Server Express LocalDB instance.
- Initial Catalog: The database to connect to is named “PersonsDatabase”.
- Integrated Security=True: Uses Windows authentication.
- Connect Timeout=30: Maximum time (in seconds) to wait for a connection.
Storing Connection Strings in ASP.NET Core
appsettings.json(Recommended):
{
"ConnectionStrings": {
"DefaultConnection": "..." // Your connection string here
}
}- Environment Variables:
set ASPNETCORE_ConnectionStrings__DefaultConnection="..." # In Command Prompt
$env:ASPNETCORE_ConnectionStrings__DefaultConnection = "..." # In PowerShell- User Secrets (Development Only): Best for keeping sensitive information out of your source code during development.
Injecting and Using the Connection String in EF Core
// Program.cs
builder.Services.AddDbContext<PersonsDbContext>(options => {
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});AddDbContext<PersonsDbContext>(): Registers yourDbContextwith the DI container.options.UseSqlServer(...): Specifies that you’re using SQL Server and provides the connection string.builder.Configuration.GetConnectionString("DefaultConnection"): Retrieves the connection string from the configuration.
Best Practices
- Separate Environments: Store different connection strings for development, staging, and production.
- Environment Variables in Production: Use environment variables to store your production connection string for security.
- User Secrets in Development: Use user secrets during development to keep sensitive data out of source control.
- Secure Storage: Consider using Azure Key Vault or other secret management solutions for production environments.
- Connection Resiliency: For production, implement connection resiliency strategies to handle transient database errors.
Seed Data in EF Core
Seed data refers to the initial data that you populate your database with when it’s created or when you apply a new migration.
Purpose
- Initial Data: Sets up your database with meaningful data for development, testing, or demonstration purposes.
- Reference Data: Populates tables with lookup data (e.g., countries, states, categories).
- Default Values: Establishes default values for specific columns.
How Seed Data Works in EF Core
OnModelCreatingMethod: You define your seed data within theOnModelCreatingmethod of yourDbContextclass.HasDataMethod: EF Core provides theHasDatamethod on theEntityTypeBuilderclass, which allows you to associate seed data with specific entities.- Migration Generation (Optional): If you’re using migrations, EF Core will automatically detect your seed data and generate the necessary SQL statements.
Code Example
// PersonsDbContext.cs
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Country>().ToTable("Countries");
modelBuilder.Entity<Person>().ToTable("Persons");
// Seed Data for Countries
string countriesJson = System.IO.File.ReadAllText("countries.json");
List<Country> countries = System.Text.Json.JsonSerializer.Deserialize<List<Country>>(countriesJson);
foreach (Country country in countries)
{
modelBuilder.Entity<Country>().HasData(country);
}
// Seed Data for Persons
string personsJson = System.IO.File.ReadAllText("persons.json");
List<Person> persons = System.Text.Json.JsonSerializer.Deserialize<List<Person>>(personsJson);
foreach (Person person in persons)
{
modelBuilder.Entity<Person>().HasData(person);
}
}Best Practices
- Separate Seed Data: Store seed data in separate files (e.g., JSON, CSV) to keep your
DbContextclean. - Idempotent Seeds: Design your seed data so that it can be applied multiple times without causing errors.
- Conditional Seeding: Consider using environment checks to apply different seed data based on the environment.
- Order of Seeding: If you have relationships between entities, ensure the correct seeding order to avoid foreign key constraint violations.
Code First Migrations
Code First Migrations in EF Core provide a structured and automated way to manage changes to your database schema as your application evolves.
Purpose
- Track Changes: Migrations track the changes you make to your entity classes and generate migration scripts.
- Version Control: Each migration has a unique version and can be tracked in source control.
- Apply Changes: You can use the
dotnet efcommand to apply migrations to your database. - Rollbacks: Migrations can be rolled back if needed.
When to Use Code First Migrations
- Code First Approach: Use migrations if you are following the Code First approach in EF Core.
- Evolving Schema: Whenever you make changes to your entity classes.
- Team Environments: Migrations are essential for team collaboration.
Using Code First Migrations with the Package Manager Console
-
Enable Migrations:
Enable-Migrations -
Add a Migration:
Add-Migration <MigrationName> -
View Migration Details:
Get-Migrations -
Update the Database:
Update-Database -
Revert a Migration:
Update-Database –TargetMigration <MigrationName>
Best Practices
- Small, Focused Migrations: Create small, incremental migrations that focus on a single feature.
- Descriptive Names: Use clear and descriptive names for your migrations.
- Data Preservation: When possible, design your migrations in a way that preserves existing data.
- Backup Your Database: Always make a backup before applying migrations, especially in production.
Fluent API
The Fluent API is an alternative to data annotations for configuring your EF Core model. It allows you to specify mapping and configuration details in code rather than using attributes on your entity classes.
Key Fluent API Methods
-
Entity Configuration:
ToTable(string tableName): Maps the entity to a specific database table.HasKey(e => e.Property): Specifies the primary key for the entity.HasAlternateKey(e => e.Property): Defines an alternate key (unique constraint).
-
Property Configuration:
HasColumnName(string name): Sets the column name in the database for the property.HasColumnType(string typeName): Sets the data type of the column (e.g.,nvarchar,int,datetime2).HasMaxLength(int maxLength): Sets the maximum length for a string property.IsRequired(): Makes the property required in the database (not nullable).HasDefaultValue(object value): Sets the default value for the property.HasDefaultValueSql(string sql): Sets the default value using a SQL expression.
-
Relationship Configuration:
HasOne(e => e.NavigationProperty): Configures a one-to-one relationship.WithMany(e => e.NavigationProperty): Configures a one-to-many relationship.HasForeignKey(e => e.Property): Specifies the foreign key property for the relationship.HasPrincipalKey(e => e.Property): Specifies the principal key property for the relationship.
-
Index and Constraint Configuration:
HasIndex(e => e.Property): Creates an index on a property or properties.HasCheckConstraint(string name, string sql): Adds a check constraint to the table.
Code Example
// PersonsDbContext.cs
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// ... table mappings and seed data ...
// Fluent API Configuration
modelBuilder.Entity<Person>()
.Property(temp => temp.TIN) // Configure the TIN property
.HasColumnName("TaxIdentificationNumber")
.HasColumnType("varchar(8)")
.HasDefaultValue("ABC12345");
// ... other Fluent API configurations ...
}In this code:
modelBuilder.Entity<Person>(): Gets the entity type builder for thePersonentity..Property(temp => temp.TIN): Gets the property builder for theTINproperty..HasColumnName("TaxIdentificationNumber"): Sets the column name in the database..HasColumnType("varchar(8)"): Sets the column’s data type tovarchar(8)..HasDefaultValue("ABC12345"): Sets the default value for theTINcolumn.
Referential Integrity, Primary Keys, and Foreign Keys
- Referential Integrity: A database concept that ensures the consistency and validity of relationships between tables.
- Primary Key: A unique identifier for each record in a table. In EF Core, you can mark a property as a primary key using the
[Key]attribute or the Fluent API’sHasKey()method. - Foreign Key: A column (or set of columns) in a table that refers to the primary key of another table. In EF Core, you define foreign keys using the
[ForeignKey]attribute, the Fluent API’sHasForeignKey()method, or by convention.
Managing Relationships in EF Core Models
- Navigation Properties:
- Properties in your entity classes that hold references to related entities.
- They allow you to navigate from one entity to its related entities without writing explicit joins.
public virtual ICollection<Person>? Persons { get; set; } // In the Country class- Fluent API:
// PersonsDbContext.cs
modelBuilder.Entity<Person>(entity =>
{
entity.HasOne(p => p.Country)
.WithMany(c => c.Persons)
.HasForeignKey(p => p.CountryID);
});This configuration establishes a one-to-many relationship between Country and Person. A country can have many persons, and a person belongs to one country.
LINQ Queries with Table Relations
You can use LINQ to query across relationships using navigation properties.
// Find all persons from the USA
var peopleFromUSA = _dbContext.Persons
.Where(p => p.Country.CountryName == "USA")
.ToList();Include() Method in LINQ Queries
The Include() method allows you to eagerly load related entities in a single query. This avoids the N+1 query problem.
// Find all persons from the USA, including their country details
var peopleFromUSA = _dbContext.Persons
.Include(p => p.Country) // Eagerly load the Country entity
.Where(p => p.Country.CountryName == "USA")
.ToList();Best Practices
- Choose the Right Relationship: Carefully consider the nature of your data to select the correct relationship type.
- Cascading Behavior: Decide how you want EF Core to handle cascading actions (e.g., when deleting a country, should it delete the associated persons?).
- Performance Considerations: Avoid overusing
Include()if you don’t need the related data. - Explicit Loading: If you need to load related data only in certain cases, use explicit loading (
Load()method) instead ofInclude().
CRUD Operations with Entity Framework Core
Controller Actions
Index (Read):
- Purpose: Display a list of entities.
- Data Source: Retrieves
Personentities using_personsService.GetAllPersons(), applying filters and sorting. - ViewBag: Populates with search fields, current search and sort criteria.
- View: Returns the “Index” view with the retrieved data.
Create (Create):
- Purpose: (GET) Displays a form for creating a new person. (POST) Processes the submitted form data.
- Data Source (GET): Retrieves countries from
_countriesService.GetAllCountries(). - Model Binding (POST): Binds the incoming form data to a
PersonAddRequestobject. - Validation: Checks if
ModelState.IsValid. - Logic: (POST-Valid) Calls
_personsService.AddPersonand redirects to Index. (POST-Invalid) Repopulates ViewBag and re-renders the Create view.
Edit (Update):
- Purpose: (GET) Displays a form for editing an existing person. (POST) Processes the submitted form data.
- Data Source (GET): Fetches the person to edit and retrieves countries for the dropdown.
- Logic: Same as Create action but updates an existing entity.
Delete (Delete):
- Purpose: (GET) Displays a confirmation page. (POST) Deletes the person.
- Logic (POST): Calls
_personsService.DeletePersonand redirects to Index.
EF Core CRUD Operations (in the PersonsService)
- AddPerson: Adds a new
Personentity to thePersonsDbSet and calls_db.SaveChanges(). - GetAllPersons: Retrieves all
Personentities using_db.Persons.Include("Country").ToListAsync(). - GetPersonByPersonID: Retrieves a specific person using
_db.Persons.FirstOrDefaultAsync(temp => temp.PersonID == personID). - UpdatePerson: Updates the properties of an existing person and calls
_db.SaveChanges(). - DeletePerson: Removes a person from the database and calls
_db.SaveChanges().
Best Practices
- Asynchronous Operations: Use
asyncandawaitfor database operations. - Dependency Injection: The services are injected into the controller, following the Dependency Inversion Principle.
- Data Transfer Objects (DTOs): Use DTOs to keep your domain model separate from your presentation layer.
- Validation: Both client-side and server-side validation should be used.
- Error Handling: Catch exceptions and provide appropriate error messages or redirections.
Generating PDFs in ASP.NET Core MVC
Creating PDF files within your ASP.NET Core MVC applications is valuable for generating reports, invoices, tickets, or any other documents. Rotativa is one popular library for this.
Rotativa: Leveraging Wkhtmltopdf for PDF Generation
Rotativa is a .NET library that wraps the wkhtmltopdf tool, a command-line utility that converts HTML content into PDF documents.
Notes About Rotativa
- Installation: Install the
Rotativa.AspNetCoreNuGet package. - ViewAsPdf: Rotativa provides a
ViewAsPdfaction result that you can return from your controller actions. - Customization: You can customize various aspects of the PDF, including margins, page orientation, header/footer content.
- Dependency on Wkhtmltopdf: Rotativa depends on the
wkhtmltopdfexecutable.
Code Example
// Controller Action (PersonsController.cs)
[Route("PersonsPDF")]
public async Task<IActionResult> PersonsPDF()
{
// Get list of persons
List<PersonResponse> persons = await _personsService.GetAllPersons();
// Return view as pdf
return new ViewAsPdf("PersonsPDF", persons, ViewData)
{
PageMargins = { Top = 20, Right = 20, Bottom = 20, Left = 20 },
PageOrientation = Orientation.Landscape
};
}<!-- View (PersonsPDF.cshtml) -->
@model IEnumerable<PersonResponse>
@{
Layout = null; // Disable the layout for PDF rendering
}
<link href="@("http://" + Context.Request.Host.ToString() + "/Stylesheet.css")" rel="stylesheet" />
<h1>Persons</h1>
<table class="table w-100 mt">
<thead>
<tr>
<th>Person Name</th>
<th>Email</th>
<th>Date of Birth</th>
<th>Age</th>
<th>Gender</th>
<th>Country</th>
<th>Address</th>
<th>Receive News Letters</th>
</tr>
</thead>
<tbody>
@foreach (PersonResponse person in Model)
{
<tr>
<td style="width:15%">@person.PersonName</td>
<td style="width:20%">@person.Email</td>
<td style="width:13%">@person.DateOfBirth?.ToString("dd MMM yyyy")</td>
<td style="width:9%">@person.Age</td>
<td style="width:9%">@person.Gender</td>
<td style="width:10%">@person.Country</td>
<td style="width:15%">@person.Address</td>
<td style="width:20%">@person.ReceiveNewsLetters</td>
</tr>
}
</tbody>
</table>Alternative Libraries
- iTextSharp/iText7: A powerful library for creating and manipulating PDF documents programmatically.
- QuestPDF: A modern, fluent library for generating PDFs from C# code.
- IronPDF: Allows you to convert HTML to PDF with ease.
Generating CSV Files in ASP.NET Core MVC
CSV files are a simple and widely supported format for exporting tabular data. CsvHelper is a popular library for reading and writing CSV files.
Notes About CsvHelper
- Installation: Install the
CsvHelperNuGet package. - CsvWriter and CsvReader: Core classes for writing and reading CSV data.
- Customization: You can customize how your CSV data is read or written using configuration options.
Code Example
// Controller Action (PersonsController.cs)
[Route("PersonsCSV")]
public async Task<IActionResult> PersonsCSV()
{
MemoryStream memoryStream = await _personsService.GetPersonsCSV();
return File(memoryStream, "application/octet-stream", "persons.csv");
}// Service Method (PersonsService.GetPersonsCSV())
public async Task<MemoryStream> GetPersonsCSV()
{
MemoryStream memoryStream = new MemoryStream();
StreamWriter streamWriter = new StreamWriter(memoryStream);
CsvWriter csvWriter = new CsvWriter(streamWriter, CultureInfo.InvariantCulture, leaveOpen: true);
csvWriter.WriteHeader<PersonResponse>(); // Write CSV headers
csvWriter.NextRecord();
List<PersonResponse> persons = _db.Persons
.Include("Country")
.Select(temp => temp.ToPersonResponse()).ToList();
await csvWriter.WriteRecordsAsync(persons); // Write person data as CSV rows
memoryStream.Position = 0; // Reset the stream position
return memoryStream;
}Best Practices
- Choose the Right Library: CsvHelper is a great choice for most scenarios.
- Streaming: For large datasets, consider streaming the CSV generation process.
- Customization: Customize the CSV format (delimiter, headers, etc.) as needed.
- Error Handling: Handle potential errors during CSV generation.
- Security: If you are including sensitive information, consider encryption.
Key Points to Remember
Entity Framework Core (EF Core)
- Purpose: Object-Relational Mapper (ORM) that simplifies database interaction in .NET.
- Core Concepts:
DbContext: A session with the database, tracks changes, handles queries.DbSet<T>: Represents a collection of a specific entity type.- Entities: C# classes that model your database tables.
- Mapping: Defines how entities and database tables/columns correspond.
- LINQ Queries: Query the database using C#.
- Change Tracking: EF Core tracks changes to entities for efficient updates.
- Migrations: Manages changes to your database schema over time.
EF Core Approaches
- Code First: Define your model with C# classes, EF Core creates the database.
- Database First: Start with an existing database, EF Core generates entity classes.
Fluent API
- Purpose: Alternative to data annotations for configuring the model in code.
- Key Methods:
ToTable(),HasKey(),HasAlternateKey(),Property(),HasOne(),WithMany(),HasIndex(),HasCheckConstraint().
Relationships
- Types: One-to-one, one-to-many, many-to-many.
- Navigation Properties: Properties that reference related entities.
- Foreign Keys: Define using
[ForeignKey], fluent API, or convention. - Include(): Eagerly load related entities in a single query.
Code First Migrations
- Purpose: Manage database schema changes as your model evolves.
- Commands:
Add-Migration,Update-Database,Remove-Migration.
Generating Files
- PDFs (e.g., Rotativa): Use
ViewAsPdfto render a view as a PDF. - CSVs (e.g., CsvHelper): Use
CsvWriterto write data to a CSV file or stream.
Best Practices
- Repository Pattern: Consider using it to abstract data access logic.
- Unit of Work Pattern: Group multiple database operations into a single transaction.
- Connection Resiliency: Implement strategies to handle transient errors.
- Caching: Cache query results where appropriate to improve performance.
Interview Tips
- Concepts: Explain the core concepts of EF Core and the benefits of using an ORM.
- Relationships: Demonstrate how to define and work with relationships between entities.
- Migrations: Discuss the importance of migrations for managing schema changes.
- Best Practices: Showcase your knowledge of best practices like using the repository pattern, asynchronous operations, and handling errors.