· 18 min read

Part 2: Crafting API Logic - Services, Controllers, and First Endpoints

Okay, let’s dive into Part 2 of our Astrowind blog series!


Astrowind Blog Post: Building a Modern Web App with ASP.NET Core & React

Part 2: Crafting API Logic - Services, Controllers, and First Endpoints

In Part 1, we laid the foundation for our “eLibrary.API” project by setting up the project structure, Entity Framework Core, the Repository pattern, DTOs, and AutoMapper. Now, it’s time to build the core logic of our API: services to encapsulate business rules and controllers to expose our API endpoints.

2.1. The Service Layer: Encapsulating Business Logic

The Service Layer sits between your controllers (API layer) and your repositories (data access layer). Its primary responsibilities are:

  • Coordinating Repositories: A single service method might interact with multiple repositories.
  • Implementing Business Rules: Validation that goes beyond simple data annotations, complex calculations, or orchestrating workflows.
  • Mapping between DTOs and Entities: While controllers can do this, services are often a cleaner place, especially if business logic influences the mapping.
  • Unit of Work Management (Implicitly): By calling _repository.SaveChangesAsync() at the end of a service operation, you ensure atomicity for that operation.

2.1.1. Create Service Interfaces

Just like with repositories, we’ll define interfaces for our services.

Create Services/IBookService.cs:

// eLibrary.API/Services/IBookService.cs
using eLibrary.API.DTOs;
using eLibrary.API.Helpers; // We'll create this for paged results later

namespace eLibrary.API.Services
{
    public interface IBookService
    {
        Task<IEnumerable<BookDto>> GetAllBooksAsync();
        Task<BookDto?> GetBookByIdAsync(int id);
        Task<BookDto?> CreateBookAsync(CreateBookDto createBookDto);
        Task<bool> UpdateBookAsync(int id, UpdateBookDto updateBookDto);
        Task<bool> DeleteBookAsync(int id);
        // We can add methods for more complex scenarios, e.g., with pagination
        // Task<PagedList<BookDto>> GetBooksAsync(BookParams bookParams);
    }
}

(You’d similarly create IAuthorService.cs and other service interfaces as needed.)

2.1.2. Implement Service Classes

Now, let’s implement BookService.

Create Services/BookService.cs:

// eLibrary.API/Services/BookService.cs
using AutoMapper;
using eLibrary.API.DTOs;
using eLibrary.API.Models;
using eLibrary.API.Repositories; // For IBookRepository and IRepository<Author>
using Microsoft.Extensions.Logging; // For logging

namespace eLibrary.API.Services
{
    public class BookService : IBookService
    {
        private readonly IBookRepository _bookRepository;
        private readonly IRepository<Author> _authorRepository; // For author validation
        private readonly IMapper _mapper;
        private readonly ILogger<BookService> _logger;

        public BookService(
            IBookRepository bookRepository,
            IRepository<Author> authorRepository, // Inject author repository
            IMapper mapper,
            ILogger<BookService> logger)
        {
            _bookRepository = bookRepository;
            _authorRepository = authorRepository;
            _mapper = mapper;
            _logger = logger;
        }

        public async Task<IEnumerable<BookDto>> GetAllBooksAsync()
        {
            _logger.LogInformation("Fetching all books from service.");
            var books = await _bookRepository.GetAllAsync(); // Could use GetBookWithAuthorAsync if needed everywhere
            // If AuthorName is not populated by default by GetAllAsync, we might need a custom repo method or map differently
            // For simplicity now, assuming Author is eager loaded or we map it carefully
            return _mapper.Map<IEnumerable<BookDto>>(books);
            // Output: A collection of BookDto objects.
        }

        public async Task<BookDto?> GetBookByIdAsync(int id)
        {
            _logger.LogInformation("Fetching book with ID: {BookId} from service.", id);
            // var book = await _bookRepository.GetByIdAsync(id); // Standard GetById
            var book = await _bookRepository.GetBookWithAuthorAsync(id); // Use specific method to include Author
            if (book == null)
            {
                _logger.LogWarning("Book with ID: {BookId} not found.", id);
                return null;
            }
            return _mapper.Map<BookDto>(book);
            // Output (if found): A BookDto object for the requested book.
            // Output (if not found): null.
        }

        public async Task<BookDto?> CreateBookAsync(CreateBookDto createBookDto)
        {
            _logger.LogInformation("Attempting to create a new book with title: {BookTitle}", createBookDto.Title);

            // Business Rule: Check if AuthorId exists
            if (createBookDto.AuthorId.HasValue)
            {
                var authorExists = await _authorRepository.GetByIdAsync(createBookDto.AuthorId.Value);
                if (authorExists == null)
                {
                    _logger.LogWarning("Author with ID: {AuthorId} not found. Cannot create book.", createBookDto.AuthorId);
                    // You might throw a custom exception or return a specific result indicating failure
                    return null; // Or throw new ArgumentException("Author not found.");
                }
            }
            else
            {
                 _logger.LogWarning("AuthorId is required to create a book.");
                 return null; // Or throw new ArgumentException("AuthorId is required.");
            }


            var book = _mapper.Map<Book>(createBookDto);
            // book.PdfFilePath = "path/to/default.pdf"; // Handle PDF path later

            await _bookRepository.AddAsync(book);
            var success = await _bookRepository.SaveChangesAsync() > 0;

            if (success)
            {
                _logger.LogInformation("Book created successfully with ID: {BookId}", book.Id);
                // We need to fetch the book again to include author details for the DTO
                var createdBookWithDetails = await _bookRepository.GetBookWithAuthorAsync(book.Id);
                return _mapper.Map<BookDto>(createdBookWithDetails);
                // Output: A BookDto for the newly created book, including its generated ID.
            }

            _logger.LogError("Failed to save new book to the database.");
            return null;
        }

        public async Task<bool> UpdateBookAsync(int id, UpdateBookDto updateBookDto)
        {
            _logger.LogInformation("Attempting to update book with ID: {BookId}", id);
            var bookToUpdate = await _bookRepository.GetByIdAsync(id);

            if (bookToUpdate == null)
            {
                _logger.LogWarning("Book with ID: {BookId} not found for update.", id);
                return false;
            }

            // Business Rule: Check if new AuthorId exists (if changed)
            if (updateBookDto.AuthorId.HasValue && updateBookDto.AuthorId != bookToUpdate.AuthorId)
            {
                var authorExists = await _authorRepository.GetByIdAsync(updateBookDto.AuthorId.Value);
                if (authorExists == null)
                {
                    _logger.LogWarning("New Author with ID: {AuthorId} not found. Cannot update book.", updateBookDto.AuthorId);
                    return false; // Or throw
                }
            }

            _mapper.Map(updateBookDto, bookToUpdate); // Apply changes from DTO to entity
            _bookRepository.Update(bookToUpdate);
            var success = await _bookRepository.SaveChangesAsync() > 0;

            if (success)
            {
                _logger.LogInformation("Book with ID: {BookId} updated successfully.", id);
            }
            else
            {
                _logger.LogError("Failed to save updates for book with ID: {BookId} to the database.", id);
            }
            return success;
            // Output: true if update was successful and saved, false otherwise.
        }

        public async Task<bool> DeleteBookAsync(int id)
        {
            _logger.LogInformation("Attempting to delete book with ID: {BookId}", id);
            var bookToDelete = await _bookRepository.GetByIdAsync(id);

            if (bookToDelete == null)
            {
                _logger.LogWarning("Book with ID: {BookId} not found for deletion.", id);
                return false;
            }

            _bookRepository.Remove(bookToDelete);
            var success = await _bookRepository.SaveChangesAsync() > 0;

            if (success)
            {
                _logger.LogInformation("Book with ID: {BookId} deleted successfully.", id);
            }
            else
            {
                _logger.LogError("Failed to delete book with ID: {BookId} from the database.", id);
            }
            return success;
            // Output: true if deletion was successful and saved, false otherwise.
        }
    }
}

Important Notes for BookService:

  • We inject IBookRepository (specific) and IRepository<Author> (generic, for checking author existence).
  • We inject IMapper for DTO transformations.
  • We inject ILogger for logging, which is crucial for diagnostics.
  • Error Handling: The current error handling is basic (returning null or false). In a real application, you’d implement more robust error handling, possibly using custom exceptions or a more structured result pattern (e.g., a Result<T> object that contains data or error information).
  • SaveChangesAsync(): Called after modifications to commit changes to the database.

2.1.3. Register Services for Dependency Injection

In Program.cs, register your new services:

// eLibrary.API/Program.cs
// ... (other usings)
using eLibrary.API.Services; // Add this

var builder = WebApplication.CreateBuilder(args);

// ... (DbContext, Repository, AutoMapper registrations)

// 4. Register Services
builder.Services.AddScoped<IBookService, BookService>();
// builder.Services.AddScoped<IAuthorService, AuthorService>(); // Add later
builder.Services.AddScoped<IRepository<Author>, Repository<Author>>(); // Ensure generic Author repo is registered

builder.Services.AddControllers();
// ... (rest of Program.cs)

2.2. API Controllers: Exposing Endpoints

Controllers are the entry points to your API. They receive HTTP requests, use services to perform actions, and return HTTP responses.

2.2.1. Create BooksController.cs

Visual Studio can help scaffold this:

  1. In Solution Explorer, right-click on the Controllers folder.
  2. Select Add > Controller….
  3. Choose “API Controller with actions, using Entity Framework” (even though we’re using repositories/services, this gives a good template to modify). OR, choose “API Controller - Empty” for more manual control. Let’s choose Empty for this example to build from scratch. (Imagine a screenshot of the Add New Scaffolded Item dialog with “API Controller - Empty” selected)
  4. Name it BooksController.cs. Click Add.

Modify Controllers/BooksController.cs:

// eLibrary.API/Controllers/BooksController.cs
using eLibrary.API.DTOs;
using eLibrary.API.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging; // For logging

namespace eLibrary.API.Controllers
{
    [Route("api/[controller]")] //  api/books
    [ApiController]
    public class BooksController : ControllerBase
    {
        private readonly IBookService _bookService;
        private readonly ILogger<BooksController> _logger;

        public BooksController(IBookService bookService, ILogger<BooksController> logger)
        {
            _bookService = bookService;
            _logger = logger;
        }

        // GET: api/books
        [HttpGet]
        public async Task<ActionResult<IEnumerable<BookDto>>> GetBooks()
        {
            _logger.LogInformation("API endpoint called: GET api/books");
            var books = await _bookService.GetAllBooksAsync();
            return Ok(books);
            // Output (if successful, HTTP 200 OK):
            // [
            //   { "id": 1, "title": "The Hobbit", "isbn": null, "publishedDate": "1937-09-21T00:00:00", "publisher": "Allen & Unwin", "authorId": 1, "authorName": "J.R.R. Tolkien", "pdfFilePath": null },
            //   { "id": 2, "title": "Nineteen Eighty-Four", "isbn": null, "publishedDate": "1949-06-08T00:00:00", "publisher": "Secker & Warburg", "authorId": 2, "authorName": "George Orwell", "pdfFilePath": null }
            // ]
        }

        // GET: api/books/5
        [HttpGet("{id}")]
        public async Task<ActionResult<BookDto>> GetBook(int id)
        {
            _logger.LogInformation("API endpoint called: GET api/books/{BookId}", id);
            var book = await _bookService.GetBookByIdAsync(id);

            if (book == null)
            {
                _logger.LogWarning("Book with ID: {BookId} not found by controller.", id);
                return NotFound(); // HTTP 404 Not Found
                // Output (HTTP 404 Not Found):
                // {
                //   "type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
                //   "title": "Not Found",
                //   "status": 404,
                //   "traceId": "..."
                // }
            }
            return Ok(book);
            // Output (if found, e.g., id=1, HTTP 200 OK):
            // { "id": 1, "title": "The Hobbit", "isbn": null, "publishedDate": "1937-09-21T00:00:00", "publisher": "Allen & Unwin", "authorId": 1, "authorName": "J.R.R. Tolkien", "pdfFilePath": null }
        }

        // POST: api/books
        [HttpPost]
        public async Task<ActionResult<BookDto>> PostBook(CreateBookDto createBookDto)
        {
            _logger.LogInformation("API endpoint called: POST api/books with title: {BookTitle}", createBookDto.Title);
            if (!ModelState.IsValid)
            {
                _logger.LogWarning("Invalid model state for PostBook: {@ModelState}", ModelState);
                return BadRequest(ModelState); // HTTP 400 Bad Request
                // Output (if Title is missing, HTTP 400 Bad Request):
                // {
                //   "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
                //   "title": "One or more validation errors occurred.",
                //   "status": 400,
                //   "errors": { "Title": ["The Title field is required."] },
                //   "traceId": "..."
                // }
            }

            var createdBook = await _bookService.CreateBookAsync(createBookDto);

            if (createdBook == null)
            {
                // This could be because the author didn't exist, or save failed.
                // Service layer logged the specific reason.
                // Consider a more specific error DTO for the client.
                _logger.LogError("Book creation failed for title: {BookTitle}. See service logs for details.", createBookDto.Title);
                return BadRequest("Book creation failed. Check that the author exists or review server logs."); // HTTP 400
                // Output (if author doesn't exist, HTTP 400 Bad Request):
                // "Book creation failed. Check that the author exists or review server logs."
            }

            // HTTP 201 Created response with a Location header and the created resource.
            return CreatedAtAction(nameof(GetBook), new { id = createdBook.Id }, createdBook);
            // Output (if successful, HTTP 201 Created):
            // Header: Location: https://localhost:XXXX/api/books/3  (or whatever the new ID is)
            // Body:
            // { "id": 3, "title": "New Book Title", ..., "authorName": "J.R.R. Tolkien" }
        }

        // PUT: api/books/5
        [HttpPut("{id}")]
        public async Task<IActionResult> PutBook(int id, UpdateBookDto updateBookDto)
        {
            _logger.LogInformation("API endpoint called: PUT api/books/{BookId}", id);
             if (!ModelState.IsValid) // For [Required] attributes on UpdateBookDto
            {
                _logger.LogWarning("Invalid model state for PutBook: {@ModelState}", ModelState);
                return BadRequest(ModelState);
            }

            var success = await _bookService.UpdateBookAsync(id, updateBookDto);

            if (!success)
            {
                // Could be NotFound or some other update failure (e.g., new author doesn't exist)
                // To be more precise, the service could return a more detailed result or throw specific exceptions.
                // For now, if it wasn't found (GetByIdAsync in service returned null), it will return false.
                _logger.LogWarning("Update failed for book with ID: {BookId}. It might not exist or another issue occurred.", id);
                return NotFound($"Book with ID {id} not found or update failed."); // Or BadRequest depending on why.
                // Output (if book not found or update failed, HTTP 404 Not Found):
                // "Book with ID X not found or update failed."
            }

            return NoContent(); // HTTP 204 No Content (standard for successful PUT if no content returned)
            // Output (HTTP 204 No Content): (No body content, successful update)
        }

        // DELETE: api/books/5
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteBook(int id)
        {
            _logger.LogInformation("API endpoint called: DELETE api/books/{BookId}", id);
            var success = await _bookService.DeleteBookAsync(id);

            if (!success)
            {
                _logger.LogWarning("Deletion failed for book with ID: {BookId}. It might not exist.", id);
                return NotFound(); // HTTP 404 Not Found
                // Output (HTTP 404 Not Found): (Same as GetBook not found)
            }

            return NoContent(); // HTTP 204 No Content (standard for successful DELETE)
            // Output (HTTP 204 No Content): (No body content, successful deletion)
        }
    }
}

Key Controller Concepts:

  • [Route("api/[controller]")]: Defines the base route for this controller. [controller] is a token replaced by the controller name (minus “Controller”), so BooksController becomes /api/books.
  • [ApiController]: Enables API-specific features like automatic model validation responses (HTTP 400).
  • Dependency Injection: IBookService and ILogger are injected via the constructor.
  • ActionResult<T> / IActionResult: Standard return types for API actions.
    • Ok(data): HTTP 200 OK with data.
    • NotFound(): HTTP 404 Not Found.
    • BadRequest(ModelState) or BadRequest("message"): HTTP 400 Bad Request.
    • CreatedAtAction(...): HTTP 201 Created, with a Location header pointing to the new resource.
    • NoContent(): HTTP 204 No Content, for successful updates/deletes where no body is returned.
  • HTTP Verbs: [HttpGet], [HttpPost], [HttpPut], [HttpDelete] map actions to HTTP methods.
  • Route Templates: [HttpGet("{id}")] defines a route parameter.

2.2.2. Testing with Swagger UI

If you run your API project (usually by pressing F5 or the green play button in Visual Studio):

  1. A browser window should open, typically navigating to /swagger. (Imagine a screenshot of the Swagger UI page showing the eLibrary.API and the Books controller expanded)
  2. You’ll see your Books controller and its actions.
  3. You can:
    • Expand an action (e.g., GET /api/books/{id}).
    • Click “Try it out”.
    • Enter parameters (e.g., id = 1).
    • Click “Execute”. (Imagine a screenshot of Swagger UI with the GET by ID section, “Try it out” clicked, an ID entered, and the “Execute” button)
  4. Swagger will show you:
    • The curl command used.
    • The Request URL.
    • The Server Response (Code, Body, Headers). (Imagine a screenshot of Swagger UI showing a successful 200 response with JSON data for a book)

Try out the POST, PUT, and DELETE endpoints as well. For POST and PUT, Swagger provides a UI to enter the JSON body based on your CreateBookDto or UpdateBookDto.

Example: POSTing a new book using Swagger UI:

  1. Expand POST /api/books.
  2. Click “Try it out”.
  3. Modify the example JSON request body:
    {
      "title": "The Silmarillion",
      "isbn": "978-0618391110",
      "publishedDate": "1977-09-15T00:00:00Z", // Use ISO 8601 format
      "publisher": "George Allen & Unwin",
      "authorId": 1 // Assuming J.R.R. Tolkien has ID 1 from our seed data
    }
  4. Click “Execute”. You should get a 201 Created response with the new book’s details.

2.3. Setting up ASP.NET Core Identity

Now, let’s add user authentication and authorization to our API. ASP.NET Core Identity provides a complete system for managing users, passwords, roles, claims, tokens, etc.

2.3.1. Install Identity NuGet Package

If not already installed during initial project setup (though we chose “None” for authentication type initially to do it manually):

Using Visual Studio GUI (Package Manager Console):

Install-Package Microsoft.AspNetCore.Identity.EntityFrameworkCore
# Output: (NuGet installation messages)

2.3.2. Create ApplicationUser Model

We need a custom user class that inherits from IdentityUser. This allows us to add custom properties to our users if needed later (e.g., FullName, DateOfBirth).

Create Models/ApplicationUser.cs:

// eLibrary.API/Models/ApplicationUser.cs
using Microsoft.AspNetCore.Identity;

namespace eLibrary.API.Models
{
    public class ApplicationUser : IdentityUser // Inherits properties like Id, UserName, Email, PasswordHash, etc.
    {
        // You can add custom properties here if needed
        // public string? FirstName { get; set; }
        // public string? LastName { get; set; }
    }
}

2.3.3. Update ApplicationDbContext

Our DbContext needs to be aware of the Identity entities. Modify ApplicationDbContext to inherit from IdentityDbContext<ApplicationUser>.

Change Data/ApplicationDbContext.cs:

// eLibrary.API/Data/ApplicationDbContext.cs
using eLibrary.API.Models;
using Microsoft.AspNetCore.Identity; // Add this
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; // Add this
using Microsoft.EntityFrameworkCore;

namespace eLibrary.API.Data
{
    // Change from DbContext to IdentityDbContext<ApplicationUser>
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser, IdentityRole, string>
    // We specify IdentityRole for roles and string for the key type (default)
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        // IdentityDbContext will automatically include DbSets for:
        // Users (AspNetUsers), Roles (AspNetRoles), UserRoles (AspNetUserRoles),
        // UserClaims (AspNetUserClaims), RoleClaims (AspNetRoleClaims),
        // UserLogins (AspNetUserLogins), UserTokens (AspNetUserTokens)

        // Our existing DbSets remain:
        public DbSet<Book> Books { get; set; }
        public DbSet<Author> Authors { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder); // IMPORTANT: Call base.OnModelCreating for Identity

            // Configure your own entities as before
            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" }
            );

            // You can customize Identity table names or schemas here if needed, e.g.:
            // modelBuilder.Entity<ApplicationUser>().ToTable("Users");
            // modelBuilder.Entity<IdentityRole>().ToTable("Roles");
            // ... and so on for other Identity tables.
        }
    }
}

2.3.4. Configure Identity Services in Program.cs

Add Identity services to the dependency injection container and configure it to use EF Core.

Modify Program.cs:

// eLibrary.API/Program.cs
// ... (other usings)
using eLibrary.API.Models; // For ApplicationUser
using Microsoft.AspNetCore.Identity; // For Identity

var builder = WebApplication.CreateBuilder(args);

// ... (DbContext, Repository, AutoMapper, Service registrations)

// 5. Configure ASP.NET Core Identity
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
    {
        // Password settings (customize as needed)
        options.Password.RequireDigit = true;
        options.Password.RequireLowercase = true;
        options.Password.RequireUppercase = true;
        options.Password.RequireNonAlphanumeric = false; // Example: false for simplicity
        options.Password.RequiredLength = 6;
        options.Password.RequiredUniqueChars = 1;

        // Lockout settings
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
        options.Lockout.MaxFailedAccessAttempts = 5;
        options.Lockout.AllowedForNewUsers = true;

        // User settings
        options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
        options.User.RequireUniqueEmail = true; // Make email unique
    })
    .AddEntityFrameworkStores<ApplicationDbContext>() // Use EF Core for storage
    .AddDefaultTokenProviders(); // For generating tokens for password reset, email confirmation, etc.
// Output: (No direct output, but Identity services are registered and configured)

builder.Services.AddControllers();
// ...

var app = builder.Build();

// ... (Swagger, HttpsRedirection)

// IMPORTANT: Add Authentication and Authorization middleware
// Must be AFTER UseRouting and BEFORE UseEndpoints (or app.MapControllers)
app.UseAuthentication(); // This middleware enables authentication capabilities.
// Output (runtime): When a request comes in, this checks for auth headers/cookies.
app.UseAuthorization();  // This middleware enables authorization capabilities (e.g., [Authorize] attribute).
// Output (runtime): Checks if the authenticated user has permission for the resource.

app.MapControllers();

app.Run();

2.3.5. Add New Migration for Identity Tables

Since we’ve changed our DbContext to include Identity tables, we need a new migration.

Using Visual Studio GUI (Package Manager Console):

  1. Ensure eLibrary.API is the default project.
  2. Run:
    Add-Migration AddIdentitySchema
    # Output: (Build process, then migration files are generated for AspNetUsers, AspNetRoles, etc.)
    # Build started...
    # Build succeeded.
    # A new migration 'XXXXXXXXXXXXXX_AddIdentitySchema.cs' is created.
  3. Apply the migration:
    Update-Database
    # Output: (Build process, then applies changes to the database)
    # Build started...
    # Build succeeded.
    # Applying migration 'XXXXXXXXXXXXXX_AddIdentitySchema'.
    # Done.

If you check your database now (e.g., using SQL Server Object Explorer), you’ll see new tables like AspNetUsers, AspNetRoles, AspNetUserRoles, etc.

2.3.6. Seed Roles and Admin User (Programmatic Seeding)

It’s good practice to seed essential data like roles and an initial admin user. We can create a static seeder class.

Create Data/SeedData.cs:

// eLibrary.API/Data/SeedData.cs
using eLibrary.API.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; // For EnsureCreated
using System; // For ArgumentNullException
using System.Threading.Tasks; // For Task

namespace eLibrary.API.Data
{
    public static class SeedData
    {
        public static async Task Initialize(IServiceProvider serviceProvider, IConfiguration configuration)
        {
            var context = serviceProvider.GetRequiredService<ApplicationDbContext>();
            var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
            var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();

            // For development, you might want to ensure the DB is created.
            // In production, migrations are usually handled separately.
            // context.Database.EnsureCreated(); // Or use context.Database.Migrate();

            // Seed Roles
            string[] roleNames = { "Admin", "User" };
            foreach (var roleName in roleNames)
            {
                var roleExist = await roleManager.RoleExistsAsync(roleName);
                if (!roleExist)
                {
                    await roleManager.CreateAsync(new IdentityRole(roleName));
                    // Output (console if logging enabled): Role 'Admin' created. Role 'User' created.
                }
            }

            // Seed Admin User (get details from configuration for security)
            var adminEmail = configuration["AppSettings:AdminEmail"];
            var adminPassword = configuration["AppSettings:AdminPassword"];

            if (string.IsNullOrEmpty(adminEmail) || string.IsNullOrEmpty(adminPassword))
            {
                Console.WriteLine("Admin credentials not found in configuration. Skipping admin user seed.");
                // Output (console): Admin credentials not found in configuration. Skipping admin user seed.
                return; // Exit if config is missing
            }

            var adminUser = await userManager.FindByEmailAsync(adminEmail);
            if (adminUser == null)
            {
                adminUser = new ApplicationUser
                {
                    UserName = adminEmail, // Often UserName is same as Email
                    Email = adminEmail,
                    EmailConfirmed = true // Confirm email for seeded admin
                };
                var result = await userManager.CreateAsync(adminUser, adminPassword);
                // Output (console if logging enabled for Identity): User '[email protected]' created successfully.

                if (result.Succeeded)
                {
                    // Assign Admin role to the new admin user
                    await userManager.AddToRoleAsync(adminUser, "Admin");
                    // Output (console if logging enabled): User '[email protected]' added to role 'Admin'.
                }
                else
                {
                    // Log errors if user creation failed
                    Console.WriteLine($"Error creating admin user: {string.Join(", ", result.Errors.Select(e => e.Description))}");
                    // Output (console, e.g.): Error creating admin user: Passwords must have at least one uppercase ('A'-'Z').
                }
            }
        }
    }
}

Add admin credentials to appsettings.Development.json (DON’T put real production passwords in source control! Use User Secrets or Azure Key Vault for production).

// eLibrary.API/appsettings.Development.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "eLibrary.API.Services": "Information", // Example: More verbose for our services
      "eLibrary.API.Controllers": "Information"
    }
  },
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=eLibraryDB;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "AppSettings": { // Add this section
    "AdminEmail": "[email protected]",
    "AdminPassword": "Password123!" // Choose a strong password meeting your Identity criteria
    // Output (when read by app): These values are used by SeedData.
  }
}

Important: Adjust AdminPassword to meet the password requirements you set in Program.cs (e.g., Password123! has uppercase, lowercase, digit).

Call Seeder from Program.cs: Modify Program.cs to run the seeder when the application starts.

// eLibrary.API/Program.cs
// ... (other usings)
using eLibrary.API.Data; // For SeedData

var builder = WebApplication.CreateBuilder(args);
// ... (all service registrations)

var app = builder.Build();

// Seed Data (should be done carefully, especially in production)
using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    try
    {
        var configuration = services.GetRequiredService<IConfiguration>();
        await SeedData.Initialize(services, configuration);
        // Output (console): Logs from SeedData.Initialize based on what it does.
    }
    catch (Exception ex)
    {
        var logger = services.GetRequiredService<ILogger<Program>>();
        logger.LogError(ex, "An error occurred during database seeding.");
        // Output (console/logs): An error occurred during database seeding. [Exception details]
    }
}

// Configure the HTTP request pipeline.
// ... (rest of Program.cs)

Now, when you run the application, it should attempt to create the roles “Admin” and “User”, and an admin user with the credentials from appsettings.Development.json. You can verify this by checking the AspNetRoles and AspNetUsers tables in your database.


We’ve covered a lot in this part: building out the service layer, creating API controllers with CRUD operations, and setting up ASP.NET Core Identity for user management. This provides the core authenticated backend structure.

Next Up (Part 3 Preview):

  • Implementing JWT (JSON Web Token) Authentication.
  • Creating AuthController for user registration and login.
  • Securing API endpoints with [Authorize] attributes.
  • Role-based authorization.
  • Handling PDF file uploads and downloads.

Ready to continue to JWTs and securing our endpoints?

Back to Blog