· 13 min read
Part 1: Laying the Foundation - ASP.NET Core API Setup & Best Practices
Okay, this is going to be a comprehensive guide! We’ll build this up piece by piece, like constructing a detailed blog series for your Astrowind template.
Let’s imagine our “eLibrary” project will manage Books, Authors, Users (with Roles), and allow users to “borrow” or view PDFs of books.
Astrowind Blog Post: Building a Modern Web App with ASP.NET Core & React
Part 1: Laying the Foundation - ASP.NET Core API Setup & Best Practices
Welcome to our series on building a robust, modern web application! We’ll be using ASP.NET Core for our backend API and React for a dynamic frontend. In this first part, we’ll focus on setting up our ASP.NET Core project, establishing good conventions, and incorporating foundational best practices. Our example project will be an “eLibrary” system.
1.1. Project Initialization: The “eLibrary.API”
We’ll start by creating our ASP.NET Core Web API project.
Using Visual Studio GUI:
- Open Visual Studio.
- Click on “Create a new project”.
- Search for “ASP.NET Core Web API” and select it. Click Next. (Imagine a screenshot here: VS New Project dialog with “ASP.NET Core Web API” highlighted)
- Project Name:
eLibrary.API
- Location: Choose your preferred directory.
- Solution Name:
eLibrary
(This allows us to add other projects like a Class Library later). Click Next. - Framework: Select the latest stable .NET version (e.g., .NET 8.0).
- Authentication type: Keep it as “None” for now. We’ll add Identity manually for more control.
- Configure for HTTPS: Checked (Recommended).
- Enable Docker: Unchecked (Can be added later if needed).
- Use controllers (uncheck to use minimal APIs): Keep Checked. We’ll use traditional controllers.
- Enable OpenAPI support: Checked (This gives us Swagger/OpenAPI UI out-of-the-box, great for API testing). (Imagine a screenshot here: VS Additional Information dialog with these options set)
- Click Create.
Visual Studio will scaffold a basic API project.
Initial Project Structure (Key Files/Folders):
Controllers/
: Contains your API controllers (e.g.,WeatherForecastController.cs
- we’ll delete this).Properties/launchSettings.json
: Configures how the project runs locally (ports, environment variables).appsettings.json
&appsettings.Development.json
: Configuration files.Program.cs
: The main entry point of your application where services are configured and the HTTP request pipeline is built.
1.2. Essential NuGet Packages
We’ll need a few core packages.
Using Visual Studio GUI (Package Manager Console):
- Go to Tools > NuGet Package Manager > Package Manager Console.
- Install Entity Framework Core for data access:
Install-Package Microsoft.EntityFrameworkCore.SqlServer # Output: (NuGet installation messages) Install-Package Microsoft.EntityFrameworkCore.Tools # Output: (NuGet installation messages - for migrations) Install-Package Microsoft.EntityFrameworkCore.Design # Output: (NuGet installation messages - for design-time tooling like migrations)
- Install AutoMapper for DTO mapping (we’ll discuss DTOs soon):
Install-Package AutoMapper # Output: (NuGet installation messages) Install-Package AutoMapper.Extensions.Microsoft.DependencyInjection # Output: (NuGet installation messages - for DI integration)
Alternatively, using Manage NuGet Packages GUI:
- Right-click on the
eLibrary.API
project in Solution Explorer. - Select “Manage NuGet Packages…“.
- Go to the “Browse” tab.
- Search for and install:
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
Microsoft.EntityFrameworkCore.Design
AutoMapper
AutoMapper.Extensions.Microsoft.DependencyInjection
(Imagine a screenshot here: VS NuGet Package Manager GUI showing a search for a package)
1.3. Structuring Your Application: Core Folders
A good folder structure makes your project maintainable. Let’s create some standard folders within the eLibrary.API
project:
Models/
(orEntities/
): For your domain model classes (database entities).Data/
: For yourDbContext
and database-related configurations (migrations folder will also live here).DTOs/
: For Data Transfer Objects.Repositories/
: For repository pattern interfaces and implementations.Services/
: For business logic services.Mappings/
: For AutoMapper profiles.Helpers/
: For utility classes.
Using Visual Studio GUI (Solution Explorer):
- Right-click on the
eLibrary.API
project. - Select Add > New Folder.
- Name it
Models
. Repeat forData
,DTOs
,Repositories
,Services
,Mappings
,Helpers
.
(Imagine a screenshot of Solution Explorer showing these new folders)
1.4. Defining Domain Models (Entities)
Let’s define some basic entities for our eLibrary.
Create Models/Book.cs
:
// eLibrary.API/Models/Book.cs
using System.ComponentModel.DataAnnotations;
namespace eLibrary.API.Models
{
public class Book
{
public int Id { get; set; } // Primary Key (by convention)
[Required]
[StringLength(200)]
public string Title { get; set; } = string.Empty;
[StringLength(13, MinimumLength = 10)] // ISBN-10 or ISBN-13
public string? Isbn { get; set; }
public DateTime PublishedDate { get; set; }
public string? Publisher { get; set; }
public int AuthorId { get; set; } // Foreign Key
public Author? Author { get; set; } // Navigation property
public string? PdfFilePath { get; set; } // Path to the stored PDF
}
}
Create Models/Author.cs
:
// eLibrary.API/Models/Author.cs
using System.ComponentModel.DataAnnotations;
namespace eLibrary.API.Models
{
public class Author
{
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; } = string.Empty;
public string? Biography { get; set; }
public ICollection<Book> Books { get; set; } = new List<Book>(); // Navigation property
}
}
Later, we’ll add an ApplicationUser
model when we set up Identity.
1.5. Setting up Entity Framework Core: The DbContext
The DbContext
is your gateway to the database.
Create Data/ApplicationDbContext.cs
:
// eLibrary.API/Data/ApplicationDbContext.cs
using eLibrary.API.Models;
using Microsoft.EntityFrameworkCore;
namespace eLibrary.API.Data
{
public class ApplicationDbContext : DbContext // Will change to IdentityDbContext later
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Book> Books { get; set; }
public DbSet<Author> Authors { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Example ofFluent API configuration (optional for simple cases)
// modelBuilder.Entity<Book>()
// .HasOne(b => b.Author)
// .WithMany(a => a.Books)
// .HasForeignKey(b => b.AuthorId);
// Seed data (optional, good for development/testing)
modelBuilder.Entity<Author>().HasData(
new Author { Id = 1, Name = "J.R.R. Tolkien", Biography = "Author of The Lord of the Rings." },
new Author { Id = 2, Name = "George Orwell", Biography = "Author of 1984." }
);
modelBuilder.Entity<Book>().HasData(
new Book { Id = 1, Title = "The Hobbit", AuthorId = 1, PublishedDate = new DateTime(1937, 9, 21), Publisher = "Allen & Unwin" },
new Book { Id = 2, Title = "Nineteen Eighty-Four", AuthorId = 2, PublishedDate = new DateTime(1949, 6, 8), Publisher = "Secker & Warburg" }
);
}
}
}
Register DbContext
in Program.cs
:
Open Program.cs
. We need to:
- Add
using
statements foreLibrary.API.Data
andMicrosoft.EntityFrameworkCore
. - Configure the
DbContext
service.
// eLibrary.API/Program.cs
using eLibrary.API.Data; // Add this
using Microsoft.EntityFrameworkCore; // Add this
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// 1. Configure DbContext
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
// Output: connectionString will be null if not set in appsettings.json, or the actual string.
// Example if set: "Server=(localdb)\\mssqllocaldb;Database=eLibraryDB;Trusted_Connection=True;"
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
// Output: (No direct output, but registers ApplicationDbContext with DI using SQL Server)
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// We'll add Authentication and Authorization here later
// app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Add Connection String to appsettings.json
:
Open appsettings.json
and add a connection string.
// eLibrary.API/appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=eLibraryDB;Trusted_Connection=True;MultipleActiveResultSets=true"
// Output (when read by app): This string value is used by EF Core to connect to the database.
}
}
(localdb)\mssqllocaldb
is SQL Server Express LocalDB, usually installed with Visual Studio.Database=eLibraryDB
is the name of the database that will be created.
1.6. Database Migrations
Migrations allow you to evolve your database schema as your models change.
Using Visual Studio GUI (Package Manager Console):
Ensure
eLibrary.API
is the default project in Package Manager Console.Run the command to create the initial migration:
Add-Migration InitialCreate # Output: (Shows progress, then...) # To undo this action, use Remove-Migration. # Build started... # Build succeeded. # (A new folder `Migrations` is created with timestamped migration files)
This command inspects your
DbContext
and models, compares them to the (non-existent) database, and generates C# code to create the schema. You’ll find a newMigrations
folder with files like_InitialCreate.cs
.Apply the migration to the database:
Update-Database # Output: (Shows progress, then...) # Build started... # Build succeeded. # Applying migration 'XXXXXXXXXXXXXX_InitialCreate'. # Done.
This command executes the SQL generated by the migration, creating your database and tables (
Books
,Authors
, and EF Core’s internal__EFMigrationsHistory
table).
Verify Database (Optional):
You can use SQL Server Object Explorer in Visual Studio (View > SQL Server Object Explorer) to connect to (localdb)\mssqllocaldb
and see your new eLibraryDB
database with its tables and seed data.
(Imagine a screenshot of SQL Server Object Explorer showing the eLibraryDB tables and data)
1.7. The Repository Pattern
The Repository Pattern abstracts data access logic, making your application more testable and maintainable by decoupling your business logic from the concrete data access implementation (EF Core in this case).
1.7.1. Generic Repository Interface (IRepository.cs
)
Create Repositories/IRepository.cs
:
// eLibrary.API/Repositories/IRepository.cs
using System.Linq.Expressions;
namespace eLibrary.API.Repositories
{
public interface IRepository<T> where T : class // T must be a class (entity)
{
Task<T?> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
// Example of a more complex query method
Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);
Task AddAsync(T entity);
Task AddRangeAsync(IEnumerable<T> entities);
void Update(T entity); // EF Core tracks changes, so often synchronous
void Remove(T entity);
void RemoveRange(IEnumerable<T> entities);
Task<int> SaveChangesAsync(); // To commit transactions
}
}
1.7.2. Generic Repository Implementation (Repository.cs
)
Create Repositories/Repository.cs
:
// eLibrary.API/Repositories/Repository.cs
using eLibrary.API.Data;
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
namespace eLibrary.API.Repositories
{
public class Repository<T> : IRepository<T> where T : class
{
protected readonly ApplicationDbContext _context;
protected readonly DbSet<T> _dbSet;
public Repository(ApplicationDbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_dbSet = _context.Set<T>();
}
public async Task<T?> GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
// Output (if found, e.g., id=1): An instance of T with Id 1.
// Output (if not found): null
}
public async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
// Output: A list of all entities of type T.
}
public async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.Where(predicate).ToListAsync();
// Output (e.g. predicate = b => b.Title.Contains("Hobbit")): List of books with "Hobbit" in title.
}
public async Task AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
// Output: (No direct output, entity is now tracked by EF Core as 'Added')
}
public async Task AddRangeAsync(IEnumerable<T> entities)
{
await _dbSet.AddRangeAsync(entities);
// Output: (No direct output, entities are tracked as 'Added')
}
public void Update(T entity)
{
_dbSet.Attach(entity); // Attach if not tracked
_context.Entry(entity).State = EntityState.Modified;
// Output: (No direct output, entity is tracked as 'Modified')
}
public void Remove(T entity)
{
if (_context.Entry(entity).State == EntityState.Detached)
{
_dbSet.Attach(entity);
}
_dbSet.Remove(entity);
// Output: (No direct output, entity is tracked as 'Deleted')
}
public void RemoveRange(IEnumerable<T> entities)
{
_dbSet.RemoveRange(entities);
// Output: (No direct output, entities are tracked as 'Deleted')
}
public async Task<int> SaveChangesAsync()
{
return await _context.SaveChangesAsync();
// Output: Number of state entries written to the database.
}
}
}
1.7.3. Specific Repository Interfaces (Example: IBookRepository
)
For entities with custom query needs, you can create specific repository interfaces.
Create Repositories/IBookRepository.cs
:
// eLibrary.API/Repositories/IBookRepository.cs
using eLibrary.API.Models;
namespace eLibrary.API.Repositories
{
public interface IBookRepository : IRepository<Book>
{
Task<IEnumerable<Book>> GetBooksByAuthorAsync(int authorId);
Task<Book?> GetBookWithAuthorAsync(int bookId);
// Add other Book-specific methods here
}
}
1.7.4. Specific Repository Implementation (Example: BookRepository
)
Create Repositories/BookRepository.cs
:
// eLibrary.API/Repositories/BookRepository.cs
using eLibrary.API.Data;
using eLibrary.API.Models;
using Microsoft.EntityFrameworkCore;
namespace eLibrary.API.Repositories
{
public class BookRepository : Repository<Book>, IBookRepository
{
public BookRepository(ApplicationDbContext context) : base(context)
{
}
public async Task<IEnumerable<Book>> GetBooksByAuthorAsync(int authorId)
{
return await _dbSet
.Where(b => b.AuthorId == authorId)
.Include(b => b.Author) // Eager load Author
.ToListAsync();
// Output (if authorId=1): List of books by author with Id 1, including their Author details.
}
public async Task<Book?> GetBookWithAuthorAsync(int bookId)
{
return await _dbSet
.Include(b => b.Author) // Eager load Author
.FirstOrDefaultAsync(b => b.Id == bookId);
// Output (if bookId=1): The Book with Id 1, including its Author details.
}
}
}
We’ll do the same for IAuthorRepository
and AuthorRepository
if needed.
1.7.5. Register Repositories for Dependency Injection
In Program.cs
, register your repositories:
// eLibrary.API/Program.cs
// ... (other usings)
using eLibrary.API.Repositories; // Add this
var builder = WebApplication.CreateBuilder(args);
// ... (DbContext registration)
// 2. Register Repositories
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); // Generic
builder.Services.AddScoped<IBookRepository, BookRepository>();
// builder.Services.AddScoped<IAuthorRepository, AuthorRepository>(); // Add later if created
builder.Services.AddControllers();
// ... (rest of Program.cs)
AddScoped
means a new instance of the repository will be created for each HTTP request.
1.8. Data Transfer Objects (DTOs)
DTOs are simple objects used to transfer data between layers (e.g., service layer to controllers, or API to client). Why DTOs?
- Decoupling: Your API contract (what data you send/receive) is not tied directly to your database entity structure.
- Security: You only expose the data you intend to. Avoids over-posting or exposing sensitive fields.
- Payload Optimization: Send only necessary data, reducing payload size.
- Data Shaping/Validation: DTOs can have their own validation attributes specific to an API operation.
Example DTOs for Books:
Create DTOs/BookDto.cs
: (For reading book data)
// eLibrary.API/DTOs/BookDto.cs
namespace eLibrary.API.DTOs
{
public class BookDto
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string? Isbn { get; set; }
public DateTime PublishedDate { get; set; }
public string? Publisher { get; set; }
public int AuthorId { get; set; }
public string? AuthorName { get; set; } // Flattened data
public string? PdfFilePath { get; set; }
}
}
Create DTOs/CreateBookDto.cs
: (For creating a new book)
// eLibrary.API/DTOs/CreateBookDto.cs
using System.ComponentModel.DataAnnotations;
namespace eLibrary.API.DTOs
{
public class CreateBookDto
{
[Required]
[StringLength(200)]
public string Title { get; set; } = string.Empty;
[StringLength(13, MinimumLength = 10)]
public string? Isbn { get; set; }
[Required]
public DateTime? PublishedDate { get; set; } // Nullable to allow custom validation messages
public string? Publisher { get; set; }
[Required]
public int? AuthorId { get; set; } // Nullable for validation
// PDF handling will be added later - perhaps a file upload field name or base64 string.
// For now, PdfFilePath will be set by the backend.
}
}
Create DTOs/UpdateBookDto.cs
: (For updating an existing book)
// eLibrary.API/DTOs/UpdateBookDto.cs
using System.ComponentModel.DataAnnotations;
namespace eLibrary.API.DTOs
{
public class UpdateBookDto
{
[Required]
[StringLength(200)]
public string Title { get; set; } = string.Empty;
[StringLength(13, MinimumLength = 10)]
public string? Isbn { get; set; }
[Required]
public DateTime? PublishedDate { get; set; }
public string? Publisher { get; set; }
[Required]
public int? AuthorId { get; set; }
}
}
1.9. AutoMapper for DTO Mapping
AutoMapper simplifies the process of mapping between objects (e.g., Book
entity to BookDto
).
1.9.1. Create Mapping Profiles
Create Mappings/MappingProfile.cs
:
// eLibrary.API/Mappings/MappingProfile.cs
using AutoMapper;
using eLibrary.API.DTOs;
using eLibrary.API.Models;
namespace eLibrary.API.Mappings
{
public class MappingProfile : Profile
{
public MappingProfile()
{
// Book Mappings
CreateMap<Book, BookDto>()
.ForMember(dest => dest.AuthorName, opt => opt.MapFrom(src => src.Author != null ? src.Author.Name : null));
// Output of MapFrom: If src.Author exists, AuthorName gets src.Author.Name, else null.
CreateMap<CreateBookDto, Book>(); // For creating new books
// Output: A Book entity is created with properties from CreateBookDto.
CreateMap<UpdateBookDto, Book>(); // For updating existing books
// Output: An existing Book entity's properties are updated from UpdateBookDto.
// Author Mappings (Example)
CreateMap<Author, AuthorDto>(); // Assuming AuthorDto exists
CreateMap<CreateAuthorDto, Author>(); // Assuming CreateAuthorDto exists
// You can add more mappings here as needed
}
}
}
(You’d also create AuthorDto.cs
, CreateAuthorDto.cs
in the DTOs
folder similar to the Book DTOs.)
1.9.2. Register AutoMapper in Program.cs
// eLibrary.API/Program.cs
// ... (other usings)
using eLibrary.API.Mappings; // Add this
var builder = WebApplication.CreateBuilder(args);
// ... (DbContext and Repository registrations)
// 3. Register AutoMapper
builder.Services.AddAutoMapper(typeof(MappingProfile));
// Output: (No direct output, but AutoMapper services are registered and profiles scanned from the assembly containing MappingProfile)
builder.Services.AddControllers();
// ... (rest of Program.cs)
Phew! That’s a lot for Part 1, but it lays a very solid groundwork. We’ve set up the project, EF Core, the repository pattern, DTOs, and AutoMapper.
Next Up (Part 2 Preview):
- Implementing Service Layer for Business Logic.
- Creating API Controllers.
- Basic API Authentication with ASP.NET Core Identity and JWTs.
We’ll continue from here in the next message. Let me know when you’re ready!