In this part of our series, we’ll introduce data persistence in the Persistence Layer using Entity Framework Core (EF Core). By the end of this article, you’ll have a fully functional persistence layer that will interact with your application’s core logic, making use of repositories to query and store data in a relational database.
1. Introduction: Setting Up Data Persistence
The Persistence Layer is responsible for interacting with the database to store and retrieve data. In this article, we'll add Entity Framework Core to manage data persistence for our domain models, following the Clean Architecture principles to ensure the persistence logic remains separated from business logic.
We need to follow a few steps, but they’re not any different from what we normally need to do to enable the use of EF Core:
- Step 1: We’ll bring in some NuGet packages first.
-
Step 2: Then we’ll create a DbContext. In the context, we will define
DbSets
for all the entities we want EF Core to manage. - Step 3: Next, we’ll create a migration to reflect the database schema.
- Step 4: Finally, we’ll register EF Core in the Services Collection to use it across the application.
This step-by-step process will allow us to persist data efficiently while keeping the business logic clean and independent of infrastructure concerns.
2. Adding the Persistence Project
The first step is to create a new class library project named Persistence. This project will contain the data access logic using EF Core.
Steps:
- Right-click on your solution and add a new Class Library project named
GloboTicket.TicketManagement.Persistence
.
- In the Persistence project, add references to the Application project to access repository contracts and domain entities. Use the following
<ProjectReference>
tag in the.csproj
file: ```xml
3. Add the following **NuGet Packages** to the **Persistence** project:
- `Microsoft.EntityFrameworkCore.SqlServer`
- `Microsoft.Extensions.Options.ConfigurationExtensions`
Use the following `<PackageReference>` tags:
```xml
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
</ItemGroup>
Once the packages and project references are added, you can proceed to set up the persistence logic.
3. Setting Up the DbContext
The DbContext is central to EF Core, as it provides the necessary APIs to interact with the database. In the Persistence project, create a class named GloboTicketDbContext
that will manage the database interaction for our domain entities.
Example:
using GloboTicket.TicketManagement.Domain.Common;
using GloboTicket.TicketManagement.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace GloboTicket.TicketManagement.Persistence
{
public class GloboTicketDbContext : DbContext
{
public GloboTicketDbContext(DbContextOptions<GloboTicketDbContext> options)
: base(options)
{
}
public DbSet<Event> Events { get; set; }
public DbSet<Category> Categories { get; set; }
public DbSet<Order> Orders { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(GloboTicketDbContext).Assembly);
//seed data, added through migrations
var concertGuid = Guid.Parse("{B0788D2F-8003-43C1-92A4-EDC76A7C5DDE}");
var musicalGuid = Guid.Parse("{6313179F-7837-473A-A4D5-A5571B43E6A6}");
var playGuid = Guid.Parse("{BF3F3002-7E53-441E-8B76-F6280BE284AA}");
var conferenceGuid = Guid.Parse("{FE98F549-E790-4E9F-AA16-18C2292A2EE9}");
modelBuilder.Entity<Category>().HasData(new Category
{
CategoryId = concertGuid,
Name = "Concerts"
});
modelBuilder.Entity<Category>().HasData(new Category
{
CategoryId = musicalGuid,
Name = "Musicals"
});
modelBuilder.Entity<Category>().HasData(new Category
{
CategoryId = playGuid,
Name = "Plays"
});
modelBuilder.Entity<Category>().HasData(new Category
{
CategoryId = conferenceGuid,
Name = "Conferences"
});
modelBuilder.Entity<Event>().HasData(new Event
{
EventId = Guid.Parse("{EE272F8B-6096-4CB6-8625-BB4BB2D89E8B}"),
Name = "John Egbert Live",
Price = 65,
Artist = "John Egbert",
Date = DateTime.Now.AddMonths(6),
Description = "Join John for his farwell tour across 15 continents. John really needs no introduction since he has already mesmerized the world with his banjo.",
ImageUrl = "https://gillcleerenpluralsight.blob.core.windows.net/files/GloboTicket/banjo.jpg",
CategoryId = concertGuid
});
modelBuilder.Entity<Event>().HasData(new Event
{
EventId = Guid.Parse("{3448D5A4-0F72-4DD7-BF15-C14A46B26C00}"),
Name = "The State of Affairs: Michael Live!",
Price = 85,
Artist = "Michael Johnson",
Date = DateTime.Now.AddMonths(9),
Description = "Michael Johnson doesn't need an introduction. His 25 concert across the globe last year were seen by thousands. Can we add you to the list?",
ImageUrl = "https://gillcleerenpluralsight.blob.core.windows.net/files/GloboTicket/michael.jpg",
CategoryId = concertGuid
});
modelBuilder.Entity<Event>().HasData(new Event
{
EventId = Guid.Parse("{B419A7CA-3321-4F38-BE8E-4D7B6A529319}"),
Name = "Clash of the DJs",
Price = 85,
Artist = "DJ 'The Mike'",
Date = DateTime.Now.AddMonths(4),
Description = "DJs from all over the world will compete in this epic battle for eternal fame.",
ImageUrl = "https://gillcleerenpluralsight.blob.core.windows.net/files/GloboTicket/dj.jpg",
CategoryId = concertGuid
});
modelBuilder.Entity<Event>().HasData(new Event
{
EventId = Guid.Parse("{62787623-4C52-43FE-B0C9-B7044FB5929B}"),
Name = "Spanish guitar hits with Manuel",
Price = 25,
Artist = "Manuel Santinonisi",
Date = DateTime.Now.AddMonths(4),
Description = "Get on the hype of Spanish Guitar concerts with Manuel.",
ImageUrl = "https://gillcleerenpluralsight.blob.core.windows.net/files/GloboTicket/guitar.jpg",
CategoryId = concertGuid
});
modelBuilder.Entity<Event>().HasData(new Event
{
EventId = Guid.Parse("{1BABD057-E980-4CB3-9CD2-7FDD9E525668}"),
Name = "Techorama Belgium",
Price = 400,
Artist = "Many",
Date = DateTime.Now.AddMonths(10),
Description = "The best tech conference in the world",
ImageUrl = "https://gillcleerenpluralsight.blob.core.windows.net/files/GloboTicket/conf.jpg",
CategoryId = conferenceGuid
});
modelBuilder.Entity<Event>().HasData(new Event
{
EventId = Guid.Parse("{ADC42C09-08C1-4D2C-9F96-2D15BB1AF299}"),
Name = "To the Moon and Back",
Price = 135,
Artist = "Nick Sailor",
Date = DateTime.Now.AddMonths(8),
Description = "The critics are over the moon and so will you after you've watched this sing and dance extravaganza written by Nick Sailor, the man from 'My dad and sister'.",
ImageUrl = "https://gillcleerenpluralsight.blob.core.windows.net/files/GloboTicket/musical.jpg",
CategoryId = musicalGuid
});
modelBuilder.Entity<Order>().HasData(new Order
{
Id = Guid.Parse("{7E94BC5B-71A5-4C8C-BC3B-71BB7976237E}"),
OrderTotal = 400,
OrderPaid = true,
OrderPlaced = DateTime.Now,
UserId = Guid.Parse("{A441EB40-9636-4EE6-BE49-A66C5EC1330B}")
});
modelBuilder.Entity<Order>().HasData(new Order
{
Id = Guid.Parse("{86D3A045-B42D-4854-8150-D6A374948B6E}"),
OrderTotal = 135,
OrderPaid = true,
OrderPlaced = DateTime.Now,
UserId = Guid.Parse("{AC3CFAF5-34FD-4E4D-BC04-AD1083DDC340}")
});
modelBuilder.Entity<Order>().HasData(new Order
{
Id = Guid.Parse("{771CCA4B-066C-4AC7-B3DF-4D12837FE7E0}"),
OrderTotal = 85,
OrderPaid = true,
OrderPlaced = DateTime.Now,
UserId = Guid.Parse("{D97A15FC-0D32-41C6-9DDF-62F0735C4C1C}")
});
modelBuilder.Entity<Order>().HasData(new Order
{
Id = Guid.Parse("{3DCB3EA0-80B1-4781-B5C0-4D85C41E55A6}"),
OrderTotal = 245,
OrderPaid = true,
OrderPlaced = DateTime.Now,
UserId = Guid.Parse("{4AD901BE-F447-46DD-BCF7-DBE401AFA203}")
});
modelBuilder.Entity<Order>().HasData(new Order
{
Id = Guid.Parse("{E6A2679C-79A3-4EF1-A478-6F4C91B405B6}"),
OrderTotal = 142,
OrderPaid = true,
OrderPlaced = DateTime.Now,
UserId = Guid.Parse("{7AEB2C01-FE8E-4B84-A5BA-330BDF950F5C}")
});
modelBuilder.Entity<Order>().HasData(new Order
{
Id = Guid.Parse("{F5A6A3A0-4227-4973-ABB5-A63FBE725923}"),
OrderTotal = 40,
OrderPaid = true,
OrderPlaced = DateTime.Now,
UserId = Guid.Parse("{F5A6A3A0-4227-4973-ABB5-A63FBE725923}")
});
modelBuilder.Entity<Order>().HasData(new Order
{
Id = Guid.Parse("{BA0EB0EF-B69B-46FD-B8E2-41B4178AE7CB}"),
OrderTotal = 116,
OrderPaid = true,
OrderPlaced = DateTime.Now,
UserId = Guid.Parse("{7AEB2C01-FE8E-4B84-A5BA-330BDF950F5C}")
});
}
}
}
4. Configuration for Entities
In Clean Architecture, domain entities should remain free from infrastructure-specific concerns like database mappings. We achieve this by using configuration classes to define how each entity maps to the database schema. These configurations are applied during model creation.
Example: EventConfiguration
for the Event
entity
Create a folder named Configurations
inside the Persistence project and add the following configuration for the Event
entity:
public class EventConfiguration : IEntityTypeConfiguration<Event>
{
public void Configure(EntityTypeBuilder<Event> builder)
{
builder.Property(e => e.Name)
.IsRequired()
.HasMaxLength(50);
builder.Property(e => e.Date)
.IsRequired();
}
}
The ApplyConfigurationsFromAssembly
method in OnModelCreating
automatically picks up all configurations in the project.
5. ** SaveChangesAsync Override**
Since we want to keep track of entity creation and modification dates, we use an AuditableEntity
base class with common properties. We override SaveChangesAsync
in our DbContext
to automatically update these audit fields.
Example:
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
{
foreach (var entry in ChangeTracker.Entries<AuditableEntity>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedDate = DateTime.Now;
break;
case EntityState.Modified:
entry.Entity.LastModifiedDate = DateTime.Now;
break;
}
}
return base.SaveChangesAsync(cancellationToken);
}
This ensures that any entity changes are tracked properly for auditing purposes.
6. Implementing the Repository Pattern
As we’ve already defined the IAsyncRepository
interface, it’s time to implement the repository logic. The base repository will handle common CRUD operations, while specific repositories will deal with entity-specific operations.
Base Repository Example:
public class BaseRepository<T> : IAsyncRepository<T> where T : class
{
protected readonly GloboTicketDbContext _dbContext;
public BaseRepository(GloboTicketDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<T> GetByIdAsync(Guid id)
{
return await _dbContext.Set<T>().FindAsync(id);
}
public async Task<IReadOnlyList<T>> ListAllAsync()
{
return await _dbContext.Set<T>().ToListAsync();
}
public async Task<T> AddAsync(T entity)
{
await _dbContext.Set<T>().AddAsync(entity);
await _dbContext.SaveChangesAsync();
return entity;
}
public async Task UpdateAsync(T entity)
{
_dbContext.Entry(entity).State = EntityState.Modified;
await _dbContext.SaveChangesAsync();
}
public async Task DeleteAsync(T entity)
{
_dbContext.Set<T>().Remove(entity);
await _dbContext.SaveChangesAsync();
}
}
7. Specific Repositories for Entities
In the Persistence Layer, we define specific repositories to handle custom logic for our entities. These repositories extend the base repository and implement their respective interfaces to provide additional methods tailored for each entity.
CategoryRepository:
The CategoryRepository
will add specific logic for the Category
entity. It extends the BaseRepository
and implements the ICategoryRepository
contract.
using GloboTicket.TicketManagement.Application.Contracts.Persistence;
using GloboTicket.TicketManagement.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace GloboTicket.TicketManagement.Persistence.Repositories
{
public class CategoryRepository : BaseRepository<Category>, ICategoryRepository
{
public CategoryRepository(GloboTicketDbContext dbContext) : base(dbContext)
{
}
public async Task<List<Category>> GetCategoriesWithEvents(bool includePassedEvents)
{
var allCategories = await _dbContext.Categories.Include(x => x.Events).ToListAsync();
if (!includePassedEvents)
{
allCategories.ForEach(p => p.Events.ToList().RemoveAll(c => c.Date < DateTime.Today));
}
return allCategories;
}
}
}
EventRepository:
The EventRepository
handles logic specific to the Event
entity, including checking if an event's name and date combination is unique. It extends the BaseRepository
and implements the IEventRepository
contract.
using GloboTicket.TicketManagement.Application.Contracts.Persistence;
using GloboTicket.TicketManagement.Domain.Entities;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace GloboTicket.TicketManagement.Persistence.Repositories
{
public class EventRepository : BaseRepository<Event>, IEventRepository
{
public EventRepository(GloboTicketDbContext dbContext) : base(dbContext)
{
}
public Task<bool> IsEventNameAndDateUnique(string name, DateTime eventDate)
{
var matches = _dbContext.Events.Any(e => e.Name.Equals(name) && e.Date.Date.Equals(eventDate.Date));
return Task.FromResult(!matches); // Return true if no match is found (i.e., the name and date are unique)
}
}
}
Explanation:
-
CategoryRepository: The method
GetCategoriesWithEvents
retrieves all categories, including their associated events. IfincludePassedEvents
isfalse
, it removes past events (i.e., events with a date earlier than today). -
EventRepository: The method
IsEventNameAndDateUnique
checks if an event with the same name and date already exists in the database.
8. Registering EF Core in the Services Collection
Now that we’ve created the persistence logic, we need to register the DbContext and repositories in the application’s Services Collection. This is done in the Startup.cs
or Program.cs
file.
Example:
public static class PersistenceServiceRegistration
{
public static IServiceCollection AddPersistenceServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<GloboTicketDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("GloboTicketTicketManagementConnectionString")));
services.AddScoped(typeof(IAsyncRepository<>), typeof(BaseRepository<>));
services.AddScoped<ICategoryRepository, CategoryRepository>();
services.AddScoped<IEventRepository, EventRepository>();
services.AddScoped<IOrderRepository, OrderRepository>();
return services;
}
}
By doing this, EF Core is set up in our project and ready to be used for data persistence.
9. Conclusion and Next Steps
We’ve successfully integrated Entity Framework Core into our Persistence Layer. We now have:
- A
DbContext
that manages the database and provides DbSets for our entities. - A base repository for common CRUD operations.
- Specific repositories for custom logic.
- Registration of EF Core in the application’s services.
For the complete source code, you can visit the GitHub repository: https://github.com/mohamedtayel1980/clean-architecture
Top comments (0)