[Updated] Building a Generic Service for CRUD Operations in C# .NET Core

A. Waris
6 min readJun 22, 2023

--

In this article, we will explore how to create a generic service class in a C# .NET Core application to handle CRUD (Create, Read, Update, Delete) operations. This generic service will communicate with the database context and return results for the specified models and DTOs. We will go through the code implementation step by step and discuss how to use this generic service class effectively.

Prerequisites

To follow along with this tutorial, make sure you have the following:

  • Basic knowledge of C# and .NET Core
  • Working knowledge of Entity Framework Core and database operations

Overview of the Generic Service Class

The GenericCRUDService class is a generic implementation of the IGenericCRUDService interface. It provides methods to perform CRUD operations on entities, allowing flexibility for different models and DTOs. Let’s take a closer look at the interface and its methods:

public interface IGenericCRUDService<TModel, TDto>
{
Task<IEnumerable<TDto>> GetAll(Expression<Func<TModel, bool>>? where = null, params string[] includes);
Task<TDto?> GetById(Expression<Func<TModel, bool>> predicateToGetId, params string[] includes);
Task<TDto> Add(TDto dto, params Expression<Func<TModel, object>>[] references);
Task<TDto> Update(TDto dto, Expression<Func<TModel, bool>>? where = null, params Expression<Func<TModel, object>>[] references);
Task<bool> Delete(IGuid id);
}

The interface defines the following methods:

  • GetAll: Retrieves all entities based on an optional predicate and includes related entities.
  • GetById: Retrieves a single entity using a predicate to determine the identifier.
  • Add: Adds a new entity to the database context.
  • Update: Updates an existing entity based on a provided predicate and supplied data.
  • Delete: Deletes an entity using an IGuid identifier.

Now let’s dive into the implementation details of the GenericCRUDService class.

Implementation of the GenericService Class

The GenericCRUDService class is a concrete implementation of the IGenericCRUDService interface. It requires an instance of IMapper (from AutoMapper) and an DbContext object to perform the necessary operations.

public class GenericCRUDService<TModel, TDto> : IGenericCRUDService<TModel, TDto>
where TModel : class
where TDto : class
{
private readonly IMapper _mapper;
private readonly IContext _dbContext;

public GenericCRUDService(IMapper mapper, IContext dbContext)
{
_mapper = mapper;
_dbContext = dbContext;
}
// Rest of the methods will be implemented here
}

The constructor of the GenericCRUDService class accepts an IMapper instance and a IContext object. These dependencies are injected using constructor injection to ensure loose coupling and easier testing.

Implementing the GetAll Method

 public async Task<IEnumerable<TDto>> GetAll(Expression<Func<TModel, bool>>? where = null,
params string[] includes)
{
var query = ApplyIncludes(_dbContext.Set<TModel>(), includes);

if (where != null)
{
query = query.Where(where);
}

var entities = await query.ToListAsync();
return _mapper.Map<IEnumerable<TDto>>(entities);
}

The GetAll method constructs an IQueryable query based on the provided where clause and include parameters. It applies these conditions to the query and then maps the result to a list of DTOs.

Implementing the GetById Method

public async Task<TDto?> GetById(Expression<Func<TModel, bool>> predicateToGetId, params string[] includes)
{
var query = ApplyIncludes(_dbContext.Set<TModel>(), includes);

var entity = await query.FirstOrDefaultAsync(predicateToGetId);

return entity == null ? null : _mapper.Map<TDto>(entity);
}

The GetById method first creates a query with the provided includes. It then retrieves the first entity that satisfies the predicateToGetId. The entity, if found, is mapped to its corresponding DTO.

Implementing the Add Method

public async Task<TDto> Add(TDto dto, params Expression<Func<TModel, object>>[] references)
{
var entity = _mapper.Map<TModel>(dto);
LoadReferences(entity, references);

await _dbContext.Set<TModel>().AddAsync(entity);
await _dbContext.SaveChangesAsync();

return _mapper.Map<TDto>(entity);
}

The Add method first maps the provided DTO to the entity type. Then it loads the entity's navigation properties, adds the entity to the database context, and saves the changes.

Implementing the Update Method

public async Task<TDto> Update(TDto dto, Expression<Func<TModel, bool>>? where = null, params Expression<Func<TModel, object>>[] references)
{
var entity = await _dbContext.Set<TModel>().FirstOrDefaultAsync(where ?? throw new ArgumentNullException(nameof(where)));
_mapper.Map(dto, entity);
LoadReferences(entity, references);

_dbContext.Update(entity);
await _dbContext.SaveChangesAsync();

return _mapper.Map<TDto>(entity);
}

The Update method retrieves the entity to be updated based on a provided predicate, maps the DTO properties onto the entity, loads the navigation properties, and then updates the entity in the database context.

Implementing the Delete Method

The Delete method deletes an entity by its identifier.

public async Task<bool> Delete(IGuid id)
{
var entity = await _dbContext.Set<TModel>().FindAsync(id);
if (entity == null) return false;

_dbContext.Remove(entity);
await _dbContext.SaveChangesAsync();

return true;
}

The Delete method first retrieves the entity using the provided id. If it finds the entity, it removes it from the database context and saves the changes.

The ApplyIncludes & LoadReferences methods:

private IQueryable<TModel> ApplyIncludes(IQueryable<TModel> query, params string[] includes)
{
return includes.Aggregate(query, (current, include) => current.Include(include));
}

private void LoadReferences(TModel entity, params Expression<Func<TModel, object>>[] references)
{
foreach (var reference in references)
{
_dbContext.Entry(entity).Reference(reference).Load();
}
}

ApplyIncludes is a helper method that applies multiple includes to the query. The LoadReferences method loads navigation properties for a specific entity. This is necessary for cases where some properties might not be loaded by default due to lazy loading.

That covers the implementation details for the GenericCRUDService class.

Here’s the full code: Updated GenericCRUDService

Using the Generic Service Class

Now that we have implemented the GenericService class, let's see how we can use it to handle CRUD operations in our application.

// Example usage
IGenericCRUDService<Customer, CustomerDto> customerService = new GenericCRUDService<Customer, CustomerDto>(mapper, dbContext);

// Get all customers
var allCustomers = await customerService.GetAll();

// Get customers based on a predicate and include related entities
var filteredCustomers = await customerService.GetAll(c => c.Age > 18 && c.IsActive, "Orders");

// Get a customer using a predicate
var customer = await customerService.GetById(c => c.Id == 1);

// Add a new customer
var newCustomerDto = new CustomerDto { Name = "John Doe", Age = 30 };
var addedCustomer = await customerService.Add(newCustomerDto);

// Update an existing customer
var existingCustomerDto = new CustomerDto { Id = 1, Name = "Updated Name", Age = 35 };
var updatedCustomer = await customerService.Update(existingCustomerDto, c => c.Id == 1);

// Delete a customer
var isDeleted = await customerService.Delete(id);

By using the generic service class, we can handle CRUD operations for different models and DTOs in a consistent and efficient manner.

Unit Testing

To unit test this service, we will use the Xunit testing library along with the Moq mocking library which are popular tools in the .NET Core ecosystem.

Below is a basic example of how you can structure your unit tests for the GenericCRUDService. The example will cover the GetById method.

Let’s start by setting up the test class:

public class GenericCRUDServiceTests
{
private readonly Mock<IMapper> _mockMapper;
private readonly Mock<DbSet<MyEntity>> _mockDbSet;
private readonly Mock<IContext> _mockContext;
private readonly GenericCRUDService<MyEntity, MyDto> _service;
private readonly MyEntity _entity;
private readonly MyDto _dto;

public GenericCRUDServiceTests()
{
// Setup your test data
_entity = new MyEntity { Id = Guid.NewGuid(), Name = "TestName" };
_dto = new MyDto { Id = _entity.Id, Name = _entity.Name };

// Setup the Mocks
_mockMapper = new Mock<IMapper>();
_mockDbSet = new Mock<DbSet<MyEntity>>();
_mockContext = new Mock<IContext>();

// Setup the service with mocks
_service = new GenericCRUDService<MyEntity, MyDto>(_mockMapper.Object, _mockContext.Object);
}
// Tests go here
}

Here we’re initializing the required mock objects and setting up the GenericCRUDService with mock implementations of IMapper and IContext.

Let’s write a test for the GetById method:

[Fact]
public async Task GetById_ShouldReturnDto_WhenEntityExists()
{
// Arrange
var data = new List<MyEntity> { _entity }.AsQueryable();

_mockDbSet.As<IQueryable<MyEntity>>().Setup(m => m.Provider).Returns(data.Provider);
_mockDbSet.As<IQueryable<MyEntity>>().Setup(m => m.Expression).Returns(data.Expression);
_mockDbSet.As<IQueryable<MyEntity>>().Setup(m => m.ElementType).Returns(data.ElementType);
_mockDbSet.As<IQueryable<MyEntity>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());

_mockContext.Setup(c => c.Set<MyEntity>()).Returns(_mockDbSet.Object);

_mockMapper.Setup(m => m.Map<MyDto>(_entity)).Returns(_dto);

// Act
var result = await _service.GetById(e => e.Id == _entity.Id);

// Assert
Assert.NotNull(result);
Assert.Equal(_dto, result);
}

In this test, we’re checking if GetById returns the correct DTO when an entity with the requested ID exists. We first setup the mock DbSet to return our test data, then setup the Set<TModel> method of IContext to return the mock DbSet. We also setup the IMapper to return our DTO when the correct entity is passed in.

The Act section invokes the GetById method with a predicate to find our test entity.

The Assert section checks whether the result is not null and whether the returned DTO matches the expected DTO.

Similarly, you can write tests for all other methods (GetAll, Add, Update, Delete) provided in the GenericCRUDService. The structure of the test will remain more or less the same - you arrange by setting up your mocks, act by invoking the method under test, and assert to verify the expected behavior.

Conclusion

In this article, we explored how to create a generic service class in a C# .NET Core application to handle CRUD operations. We discussed the implementation details of the GenericCRUDService class and learned how to use it effectively to perform database operations.

Using a generic service class can help reduce code duplication and provide a consistent way to handle CRUD operations across different models and DTOs in your application. It promotes reusability and simplifies maintenance, making your code more robust and maintainable.

I hope this article has been helpful in understanding how to build and use a generic service class for CRUD operations in C# .NET Core. Happy coding!

Here’s the GitHub Repo:

https://github.com/a-waris/dotnet-generic-crud-service
Feel free to clone it, contribute, and share it.

--

--

A. Waris

Full Stack Engineer | GoLang | .NET Core | Java Spring boot | AWS | Azure | Node.js | Python | Angular | React 🚀✨