A Brief Guide to Entity Framework EF 8 Implementation

KhalilKhalil
5 min read

Entity Framework Core (EF Core) is a powerful Object-Relational Mapper (ORM) that allows .NET developers to interact with databases using .NET objects. It simplifies data access by enabling you to work with data as strongly typed objects without writing raw SQL queries. This guide will help you get started with EF Core, covering the basics of setup, data modeling, and performing CRUD (Create, Read, Update, Delete) operations.

Step 1: Install EF Core EF8 packages

Before we start with the implementation, let's create BookStore.EFCore project and install the following EF 8 packages:

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer

As you can see we are using SQL Server as our data provider. You should be able to confirm this by looking at the packages:

We also need to add the packages in our startup project:

Step 2: Implementing Entity Framework EF8

Let's define our Application DbContext. Since we have only book entity, we will define Books DbSet for now.

using BookStore.Domain;
using Microsoft.EntityFrameworkCore;

namespace BookStore.EFCore;

public class ApplicationContext(DbContextOptions options) : DbContext(options)
{
    public required DbSet<Book> Books { get; init; }
}
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace BookStore.EFCore;

public static class EfCoreExtensions
{
    public static void AddApplicationContext(this IServiceCollection services,
        string? connectionString)
    {
        if (!string.IsNullOrEmpty(connectionString))
        {
            services.AddDbContext<ApplicationContext>(options => 
                options.UseSqlServer(connectionString));
        }
    }
}

In your startup project ie. BookStore.HttpAPI, add the ApplicationContext:

Program.cs

using BookStore.EFCore;
using BookStore.Services;
using FastEndpoints;
using FastEndpoints.Swagger;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddFastEndpoints()
    .SwaggerDocument();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

//Add this - grab connection string from appsettings.json
var connectionString = builder.Configuration.GetConnectionString("Default");
builder.Services.AddApplicationContext(connectionString);

var app = builder.Build();

app.UseFastEndpoints(c =>
{
    c.Endpoints.RoutePrefix = "api";
}).UseSwaggerGen();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.Run();

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "Default": "Server=localhost,1433;Database=BookStore;User Id=sa;password=pa55w00rd!;Trusted_Connection=False;MultipleActiveResultSets=true;TrustServerCertificate=true"
  }
}

In Terminal run the following commands:

  1. To create your initial migration: dotnet ef migrations add InitialMigration -s BookStore.HttpApi -p BookStore.EFCore

  2. To update and apply the migration: dotnet ef database update -s BookStore.HttpApi -p BookStore.EFCore

Since we are using SQL Server, let's check in Azure Data Studio if the database is created and the initial migration applied:

Step 3: Setting up a repository layer

Setting up a repository layer is crucial in software development as it encapsulates and abstracts data access logic, promoting clean architecture and separation of concerns. This approach enhances maintainability by allowing localized changes without affecting business logic and improves testability through the use of mock repositories. Additionally, it promotes code reusability, consistency, and standardization across the application, making the codebase more predictable and scalable. The repository layer also facilitates enhanced data management, enabling features like transactions and caching, ultimately resulting in a more robust and adaptable application.

Let's build the repository for our Book entity and we are going to name it "BookRepository":

using BookStore.Domain;
using Microsoft.EntityFrameworkCore;

namespace BookStore.EFCore.Repositories;

internal class BookRepository(ApplicationContext context) : IBookRepository
{
    public async Task<List<Book>> GetAll(CancellationToken ctx = default)
    {
        return await context.Books.ToListAsync(ctx);
    }

    public async Task<Book?> GetById(int id, CancellationToken ctx = default)
    {
        return await context.Books.FirstOrDefaultAsync(x => x.Id == id, ctx);
    }

    public async Task<Book?> AddBook(Book entity, CancellationToken ctx = default)
    {
        var entry = await context.Books.AddAsync(entity,ctx);
        await context.SaveChangesAsync(ctx);
        return entry.Entity;
    }
}
public interface IBookRepository
{
    Task<List<Book>> GetAll(CancellationToken ctx = default);
    Task<Book?> GetById(int id, CancellationToken ctx = default);
    Task<Book?> AddBook(Book entity, CancellationToken ctx = default);
}
using BookStore.EFCore.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace BookStore.EFCore;

public static class EfCoreExtensions
{
    public static void AddApplicationContext(this IServiceCollection services,
        string? connectionString)
    {
        if (!string.IsNullOrEmpty(connectionString))
        {
            services.AddDbContext<ApplicationContext>(options => 
                options.UseSqlServer(connectionString));
        }
        //Add this
        services.AddBookStoreRepositories();
    }
    //Add this
    private static void AddBookStoreRepositories(this IServiceCollection services)
    {
        services.AddTransient<IBookRepository, BookRepository>();
    }
}

Step 4: Setting up a service layer

Let's create a project to host our services: BookStore.Services. Here we should be able to call in the repository methods we created in the previous step.

using BookStore.Contracts.Books;
using BookStore.Domain;
using BookStore.EFCore.Repositories;

namespace BookStore.Services.Services;

public class BookService(IBookRepository bookRepository) : IBookService
{
    public async Task<List<BookDto>> GetAll(CancellationToken ctx = default)
    {
        var data = await bookRepository.GetAll(ctx);

        return data.Select(x => new BookDto()
        {
            Id = x.Id,
            Title = x.Title,
            Isbn = x.Isbn,
            PublishedDate = x.PublishedDate
        }).ToList();
    }

    public async Task<BookDto?> GetById(int bookId, CancellationToken ctx = default)
    {
        var book = await bookRepository.GetById(bookId, ctx);

        if (book is null)
        {
            return null;
        }

        return new BookDto()
        {
            Id = book.Id,
            Title = book.Title,
            Isbn = book.Isbn,
            PublishedDate = book.PublishedDate
        };
    }

    public async Task<BookDto?> CreateBook(CreateBookInputDto input, CancellationToken ctx = default)
    {
        var book = new Book()
        {
            Title = input.Title,
            Isbn = input.Isbn,
            PublishedDate = input.PublishedDate
        };

        await bookRepository.AddBook(book, ctx);

        return new BookDto()
        {
            Id = book.Id,
            Title = book.Title,
            Isbn = book.Isbn,
            PublishedDate = book.PublishedDate
        };
    }
}
using BookStore.Contracts.Books;

namespace BookStore.Services.Services;

public interface IBookService
{
    Task<List<BookDto>> GetAll(CancellationToken ctx = default);
    Task<BookDto?> GetById(int bookId, CancellationToken ctx = default);
    Task<BookDto?> CreateBook(CreateBookInputDto input, CancellationToken ctx = default);
}
using BookStore.Services.Services;
using Microsoft.Extensions.DependencyInjection;

namespace BookStore.Services;

public static class BookStoreServicesExtensions
{
    public static void AddBookStoreServices(this IServiceCollection services)
    {
        services.AddTransient<IBookService, BookService>();
    }
}

Let's update the program.cs and add the AppServices service to the pipeline.

using BookStore.EFCore;
using BookStore.Services;
using FastEndpoints;
using FastEndpoints.Swagger;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddFastEndpoints()
    .SwaggerDocument();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var connectionString = builder.Configuration.GetConnectionString("Default");
builder.Services.AddApplicationContext(connectionString);

//Add this
builder.Services.AddBookStoreServices();

var app = builder.Build();

app.UseFastEndpoints(c =>
{
    c.Endpoints.RoutePrefix = "api";
}).UseSwaggerGen();


if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.Run();

Perfect! Let's run the app and call create book endpoint.

Let's check the response :

Let's check if the new book has been created in the database:

Conclusion

Entity Framework Core simplifies the interaction with databases in .NET applications by allowing developers to work with data as C# objects. This guide covered the basics of setting up EF Core, defining a data model, creating a database context, and performing CRUD operations. As you become more comfortable with EF Core, you can explore advanced features like LINQ queries, relationships, and migrations to manage more complex scenarios. Happy coding!

0
Subscribe to my newsletter

Read articles from Khalil directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Khalil
Khalil