Today, I wanted to know if the content of a table in the database had changed or not. I ended up with creating a ComputeHash method that returns a different hash when the content of a table in the database has changed.
After I created this method, I asked myself, how can I expose this method to my other Repository classes? This is the solution that I came up with.
Keep in mind that, the code in this article is by no means production-ready, as I wanted to keep the example simple to understand. And the ComputeHash method has also room for improvement.
The source code of the completed application is available on GitHub.
The following tools are needed to run the solution.
- .NET 8.0 SDK
- Vscode, Visual Studio 2022, or another compatible IDE.
- ABP CLI Version 8.0.0
- Install or update the ABP CLI
dotnet tool install -g Volo.Abp.Cli || dotnet tool update -g Volo.Abp.Cli
- Use the following ABP CLI command to create a new Blazor ABP application:
abp new BookStore -u blazor -o BookStore
- Open the solution in Visual Studio (or your favorite IDE).
- Run the
BookStore.DbMigrator
application to apply the migrations and seed the initial data. - Run the
BookStore.HttpApi.Host
application to start the server-side.
To follow along, you will need to have the first part of the BookStore tutorial ready.
Create a folder BaseEfCoreRepo in the EntityFrameworkCore project and add a class BaseEfCoreRepository.cs. As you can see below, the class inherits from EfCoreRepository and is made abstract. The ComputeHash method calculates a Hash for a given list of string input.
You also need to implement the DeleteAsync, the DeleteManyAsync, the FindAsync and the GetAsync methods.
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BookStore.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
using static System.String;
using static System.Text.Encoding;
namespace BookStore.BaseEfCoreRepo
{
public abstract class BaseEfCoreRepository<TEntity, TKey>(IDbContextProvider<BookStoreDbContext> dbContextProvider)
: EfCoreRepository<BookStoreDbContext, TEntity>(dbContextProvider),
IBasicRepository<TEntity, TKey>
where TEntity : class, IEntity<TKey>
{
protected string ComputeHash(IEnumerable<string> strings)
{
var items = strings.ToList();
if (items.Count == 0)
{
return Empty;
}
using var sha1 = SHA1.Create();
var bytes = sha1.ComputeHash(UTF8.GetBytes(Concat(items)));
var builder = new StringBuilder(bytes.Length * 2);
foreach (var b in bytes) // can be "x2" if you want lowercase
{
builder.Append(b.ToString("X2"));
}
return builder.ToString();
}
public async Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken cancellationToken = default)
=> await base.DeleteAsync(x => x.Id.Equals(id), autoSave, cancellationToken);
public async Task DeleteManyAsync(IEnumerable<TKey> ids, bool autoSave = false,
CancellationToken cancellationToken = default)
{
var entities = await (await GetDbSetAsync()).Where(x => ids.Contains(x.Id)).ToListAsync(cancellationToken);
await base.DeleteManyAsync(entities, autoSave, cancellationToken);
}
public async Task<TEntity?> FindAsync(TKey id, bool includeDetails = true,
CancellationToken cancellationToken = default)
=> await base.FindAsync(x => x.Id.Equals(id), includeDetails, cancellationToken);
public async Task<TEntity> GetAsync(TKey id, bool includeDetails = true,
CancellationToken cancellationToken = default)
=> await base.GetAsync(x => x.Id.Equals(id), includeDetails, cancellationToken);
}
}
Create a folder Interfaces in the Domain project and add the interface IHaveGetHashAsyncRepository.cs
Every Repository that implements the IHaveGetHashAsyncRepository interface will need to implement the GetHashAsync method
using System;
using System.Threading.Tasks;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories;
namespace BookStore.Interfaces
{
namespace BookStore.Domain.Interfaces
{
public interface IHaveGetHashAsyncRepository<TEntity, TKey> : IRepository<TEntity, TKey>
where TEntity : class, IEntity<TKey>
{
Task<string> GetHashAsync(Guid? tenantId);
}
}
}
Create an IBookRepository interface in the Books folder of the Domain project that implements the IHaveGetHashAsyncRepository interface
using System;
using BookStore.Interfaces.BookStore.Domain.Interfaces;
namespace BookStore.Books
{
public interface IBookRepository : IHaveGetHashAsyncRepository<Book, Guid>;
}
Create a BookRepository class in the Books folder of the EntityFrameworkCore project. Let the BookRepository class inherit from the BaseEfCoreRepository class you created, and also implement the IBookRepository interface.
When you implement the IBookRepository interface you will need to have a GetHashAsync method in your BookRepository class. In method you can now call the ComputeHash method from the BaseEfCoreRepository class.
using System;
using System.Linq;
using System.Threading.Tasks;
using BookStore.BaseEfCoreRepo;
using BookStore.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
namespace BookStore.Books
{
public class BookRepository(IDbContextProvider<BookStoreDbContext> dbContextProvider)
: BaseEfCoreRepository<Book, Guid>(dbContextProvider), IBookRepository
{
public async Task<string> GetHashAsync(Guid? tenantId)
{
using var disposable = CurrentTenant.Change(tenantId);
return ComputeHash(
(await GetDbContextAsync()).Books.Select(x => $"{x.Id}{x.Name}{x.Type}{x.PublishDate}{x.Price}"));
}
}
}
Create a GetHasDto class in the Books folder of the Application.Contracts
using System;
namespace BookStore.Books
{
public class GetHashDto
{
public Guid? TenantId { get; set; }
}
}
In the IBookAppService interface add a GetHashAsync method definition to the
using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace BookStore.Books
{
public interface IBookAppService : ICrudAppService<BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto>
{
Task<string> GetHashAsync(GetHashDto input);
}
}
Update the content of BookAppService class in the Application project.
using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace BookStore.Books
{
public class BookAppService(IBookRepository repository)
: CrudAppService<Book, BookDto, Guid, PagedAndSortedResultRequestDto, CreateUpdateBookDto>(repository),
IBookAppService
{
public async Task<string> GetHashAsync(GetHashDto input)
=> await repository.GetHashAsync(input.TenantId);
}
}
I added the GetHashAsync method in the BookAppService only for testing purposes.
Start the BookStore.HttpApi.Host project to have the Swagger page launched. Navigate to the api/app/book/hash endpoint in the Swagger page.
Click first on the Try it out button and then on the Execute button.
The GetHashAsync method in the BookAppService will be hit and should return every time the same hash string when you have the same 2 books in the database.
If you delete one of them, the method will return another hash.
Get the source code on GitHub.
Enjoy and have fun!