Skip to main content

Persistence Layer

This layer is responsible for all database communication. Here you'll learn how to properly set up this layer in detail. Let's start with the database context.

Database Context

To connect to a database you must create a database context. We provide support to the databases supported by Entity Framework Core. You can find the complete list here. We'll provide some examples here, for all other databases, please check their own documentation.

note

If you're using multi tenancy, make sure you pass true to GetConnectionString(true) and add the appropriate connection strings by tenant.

note

If you're using a connection string with a name different from "DefaultConnection", you can specify that in GetConnectionString(defaultConnectionName: "MyCustomConnectionString"). Also, all your connection strings must be inside the ConnectionStrings tag in your appsettings.json file.

SQL Server

YourProject.Persistence/DatabaseContext.cs
public class DatabaseContext : BaseContext
{
public DatabaseContext()
{
SourceAssemblies.Add(typeof(DatabaseContext).Assembly);
}

protected override void Setup(DbContextOptionsBuilder optionsBuilder)
{
if (optionsBuilder.IsConfigured)
{
return;
}

optionsBuilder.UseSqlServer(GetConnectionString());
}
}

SQLite

YourProject.Persistence/DatabaseContext.cs
public class DatabaseContext : BaseContext
{
public DatabaseContext()
{
SourceAssemblies.Add(typeof(DatabaseContext).Assembly);
SQLitePCL.Batteries_V2.Init();
Database.EnsureCreated();
}

protected override void Setup(DbContextOptionsBuilder optionsBuilder)
{
if (optionsBuilder.IsConfigured)
{
return;
}

var dbPath = Path.Combine(FileSystem.AppDataDirectory, "MySQLiteDB.db3");

optionsBuilder.UseSqlite($"Filename={dbPath}");
}
}

MySql/MariaDB

note

For MySql/MariaDB we recommend using the Pomelo package, not the Oracle one due to compatibility issues.

YourProject.Persistence/DatabaseContext.cs
public class DatabaseContext : BaseContext
{
public DatabaseContext()
{
SourceAssemblies.Add(typeof(DatabaseContext).Assembly);
}

protected override void Setup(DbContextOptionsBuilder optionsBuilder)
{
if (optionsBuilder.IsConfigured)
{
return;
}

// Replace with your server version and type.
// Use 'MariaDbServerVersion' for MariaDB.
// Alternatively, use 'ServerVersion.AutoDetect(connectionString)'.
var serverVersion = new MySqlServerVersion(new Version(8, 0, 29));

optionsBuilder
.UseMySql(GetConnectionString(), serverVersion)
// The following three options help with debugging, but should
// be changed or removed for production.
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging()
.EnableDetailedErrors()
}
}

Entities

In order to create entities we must inherit from one of the four provided base classes. They all use the same identifier name, "Id", but they have different types, they are:

Base classID's data type
BaseEntityGuid
BaseIntEntityint
BaseLongEntitylong
BaseStringEntitystring

Usage:

[Table("Categories")]
public class Category : BaseEntity
{
/// <summary>
/// Name.
/// </summary>
public string Name { get; set; }
}

This code will generate a table Categories with the properties Id as unique identifier and Name as nvarchar.

Custom Base Entity

Should you need to create a custom base entity with another type or a different identifier name, you must implement IEntity:

public abstract class BaseDoubleEntity : IEntity
{
[Key]
public double MyIdentifier { get; set; }

protected BaseDoubleEntity()
{
MyIdentifier = 0;
}
}

Many-to-many

In order to create a many-to-many relationship, we just need to add referring collections to each end.

note

Should you need to customize the relationship, take a look at the Mappers section.

[Table("Tags")]
public class Tag : BaseEntity
{
public string Name { get; set; }

public List<KnowledgeArticle> KnowledgeArticles { get; set; }

public Tag()
{
KnowledgeArticles = new List<KnowledgeArticle>();
}
}
[Table("KnowledgeBase")]
public class KnowledgeArticle : BaseEntity
{
public string Title { get; set; }

public string Content { get; set; }

public List<Tag> Tags { get; set; }

public KnowledgeArticle()
{
Tags = new List<Tag>();
}
}

One-to-many

In order to create a one-to-many relationship, we need to add a collection to one end and the referring foreign key to the other end.

note

Should you need to customize the relationship, take a look at the Mappers section.

[Table("Users")]
public class User : BaseEntity
{
public string Name { get; set; }

public List<Email> Emails { get; set; }

public User()
{
Emails = new List<Email>();
}
}
[Table("Emails")]
public class Email : BaseEntity
{
public string Address { get; set; }

public Guid UserId { get; set; }

public User User { get; set; }
}

One-to-one

In order to create a one-to-one relationship, we need to add the referring foreign key to only one end but the navigation properties should be added on both ends. This will ensure a true one-to-one relationship.

note

Should you need to customize the relationship, take a look at the Mappers section.

[Table("Users")]
public class User : BaseEntity
{
public string Name { get; set; }

public Guid EmailId { get; set; }

public Email Email { get; set; }
}
[Table("Emails")]
public class Email : BaseEntity
{
public string Address { get; set; }

public User User { get; set; }
}

Mappers

With mappers, you can customize the entities' mappings. Here you have access to EntityTypeBuilder<TEntity>.

Many-to-many With More Than One Navigation

When we have more than one navigation to the same entity, we can use a mapper to help us define which property belongs to which collection.

YourProject.Persistence/Entities/MemberProfile.cs
[Table("ProfilesOfMembers")]
public class MemberProfile : BaseEntity
{
public string Name { get; set; }

public List<ScheduledVisit> VisitsAsMain { get; set; }

public List<ScheduledVisit> VisitsAsHelper { get; set; }

public MemberProfile()
{
VisitsAsMain = new List<ScheduledVisit>();
VisitsAsHelper = new List<ScheduledVisit>();
}
}
YourProject.Persistence/Entities/ScheduledVisit.cs
[Table("ScheduledVisits")]
public class ScheduledVisit : RemovableEntity
{
public DateTime? ScheduledDate { get; set; }

public Guid? AssignedToId { get; set; }

public MemberProfile? AssignedTo { get; set; }

public Guid? HelperId { get; set; }

public MemberProfile? Helper { get; set; }
}
YourProject.Persistence/Mappers/MemberProfileMapper.cs
public class MemberProfileMapper : BaseEntityMapper<MemberProfile>
{
public override void Map(EntityTypeBuilder<MemberProfile> entityBuilder)
{
entityBuilder
.HasMany(m => m.VisitsAsMain)
.WithOne(v => v.AssignedTo)
.OnDelete(DeleteBehavior.Restrict);

entityBuilder
.HasMany(m => m.VisitsAsHelper)
.WithOne(v => v.Helper)
.OnDelete(DeleteBehavior.Restrict);
}
}
info

To learn more about EntityTypeBuilder and what's available, please check here.

Validators

This functionality is used to validate entities before they're saved. Inside your persistence project, create a folder named Validators, if it doesn't exist and add your validator. It must inherit from BaseEntityValidator. You may make use of the FluentValidation library in order to perform most validations. When you use ValidateRules the error context is already populated with the errors found, if any. Should you need to add extra errors, you can do so by invoking the ValidationErrorContext.AddError() method.

Using the RuleFor method you can create rules for specific fields and then use FluentValidator to define the constraints. For collections, you can use the RulesForEach method.

A very detailed example is found below:

YourProject.Persistence/Validators/InstitutionValidator.cs
public class InstitutionValidator : BaseEntityValidator<Institution>
{
private readonly ValidationErrorContext _validationContext;

public InstitutionValidator(ValidationErrorContext validationContext)
: base(validationContext)
{
_validationContext = validationContext;
}

public override async Task Validate(
IUnitOfWork unitOfWork,
Institution current,
Institution original,
EntityState state
)
{
switch (state)
{
case EntityState.Added:
RuleFor(p => p.Description).MinimumLength(10);
RuleFor(p => p.Name).NotEmpty();
RulesForEach(i => i.Doctors).Set(builder =>
{
builder.RuleFor(d => d.Name).NotEmpty()
.WithMessage("Doctor's name is required.");
builder.RuleFor(d => d.Age).GreaterThan(17)
.WithMessage("Doctor must be at least 18 years old.");
});
RulesForEach(i => i.Doctors).Must((entity, element) =>
{
var license = await unitOfWork.GetRepository<MedicalLicense>()
.GetFirstOrDefaultAsync(ml => ml.Code == element.LicenseCode);

if (license == null)
{
_validationContext.AddError(
Constants.Validator.Hidden,
"No license found for this doctor."
);
return false;
}

return true;
});

var isValid = ValidateRules(current);

if (!isValid)
{
return;
}

var instWithSameName = await unitOfWork.GetRepository<Institution>()
.GetFirstOrDefaultAsync(i => i.Name == current.Name);

if (instWithSameName != null)
{
_validationContext.AddError(
Constants.Validator.Hidden,
"There is already an institution with the provided name."
);
}

break;
case EntityState.Modified:
// ...
}
}
}

If you're using RulesForEach to validate children, you can make use of the following methods:

MethodDescription
SetDefines a set of rules for each item of the collection.
NotNullAsserts no item of the collection is null.
MustAsserts all items of the collection match the informed condition.
WhereFilters the collection according to the predicate.
caution

Once all validations have been created, the ValidateRules method should be invoked passing the current entity (current).

The available states are:

StateDescription
AddedBeing managed by the context. This data has been created.
ModifiedBeing managed by the context and exists in the database. One or more properties have been changed.
DeletedBeing managed by the context and exists in the database. It has been removed.
UnchangedBeing managed by the context and exists in the database. Its data has not been changed.
DetachedNot being managed by the context.

Most of the time you'll only be using the most common states, like Added, Modified and Deleted, but the others are provided, should you need them.

Events

Events allow you to perform actions before and/or after an entity is persisted. Inside your persistence project, create a folder named EventHandlers, if it doesn't exist and add your event. It must inherit from one of the following types:

  • To perform actions before saving: BeforeSaveEntityNotificationHandler<TEntity>
  • To perform actions after saving: AfterSaveEntityNotificationHandler<TEntity>

Then, implement the required Handle method and add your logic.

caution

Custom repositories inheriting from Repository instead of Repository<TEntity cannot have an event attached. They work only for specific entities. If you're using a custom repository with more than one entity (inheriting from Repository), you should implement your before/after save logic manually.

In the following example, every time the User entity is created, before it's persisted, a new validation token will be created and associated with that user:

YourProject.Persistence/EventHandlers/UserBeforeSave.cs
public class UserBeforeSave : BeforeSaveEntityNotificationHandler<User>
{
public override async Task Handle(
BeforeSaveEntityNotification<User> notification,
CancellationToken cancellationToken
)
{
if (notification.EntityState == EntityState.Added)
{
var validationToken = new ValidationToken
{
CreationDate = DateTime.UtcNow,
ExpirationDate = DateTime.UtcNow.AddDays(1),
Token = $"{RandomProvider.GetThreadRandom().Next(100000, 1000000)}"
};
notification.Entity.ValidationToken =
await notification.UnitOfWork.GetRepository<ValidationToken>().AddAsync(
validationToken,
cancellationToken
);
}
}
}

In the next one, an email is sent every time the User entity is created, after it's persisted. If the entity was modified instead and the change was the password, we also send an email:

info

In the notification parameter you can find the current entity (property Entity) and the one in the database (property OriginalEntity). OriginalEntity is null when the entity is being created.

YourProject.Persistence/EventHandlers/UserAfterSave.cs
public class UserAfterSave : AfterSaveEntityNotificationHandler<User>
{
public override async Task Handle(
AfterSaveEntityNotification<User> notification,
CancellationToken cancellationToken
)
{
switch (notification.EntityState)
{
case EntityState.Modified:
if (notification.Entity.Password != notification.OriginalEntity.Password)
{
await EmailSender.SendPasswordHasChanged(notification.Entity.Email);
}
break;
case EntityState.Added:
await EmailSender.SendValidationRequest(
notification.Entity.Email,
notification.Entity.ValidationToken!.Token
);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}

The available states are:

StateDescription
AddedBeing managed by the context. This data has been created.
ModifiedBeing managed by the context and exists in the database. One or more properties have been changed.
DeletedBeing managed by the context and exists in the database. It has been removed.
UnchangedBeing managed by the context and exists in the database. Its data has not been changed.
DetachedNot being managed by the context.

Most of the time you'll only be using the most common states, like Added, Modified and Deleted, but the others are provided, should you need them.

Repository

A repository allows communication with a database table or tables. It also provides CRUD operations.

IMPORTANT

The UpDevs SDK automatically creates repositories for all entities, so you don't have to do so manually. Once you create an entity, you can already request its repository. You're only required to create a repository class file when you need to add extra methods to that repository, or you need an aggregate root.

Methods Provided

note

When a method has the [Async] text it means it's available in both forms. Whenever async is available, usage of CancellationToken is also provided.

Method/SignatureDescription
GetAll

Signature:
IQueryable<TEntity> GetAll(
 Expression<Func<TEntity, bool>>? predicate = null,
 Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
 Func<IQueryable<TEntity>, IQueryable<TEntity>>? include = null,
 bool disableTracking = false
)
Gets the IQueryable<TEntity> based on a predicate and orderby delegate.
GetAll

Signature:
IQueryable<TEntity> GetAll(
 SearchRequest? request,
 Expression<Func<TEntity, bool>>? searchExpression = null,
 Expression<Func<TEntity, bool>>? extraQuery = null,
 string[]? excludedFilters = null,
 Func<IQueryable<TEntity>, IQueryable<TEntity>>? include = null,
 bool disableTracking = false
)
Gets the IQueryable<TEntity> based on the search request, search expression and extra query.
GetPagedList[Async]

Signature:
IPagedList<TEntity> GetPagedList(
 Expression<Func<TEntity, bool>>? predicate = null,
 Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
 Func<IQueryable<TEntity>, IQueryable<TEntity>>? include = null,
 int pageIndex = 0,
 int pageSize = 20,
 bool disableTracking = false
)
Gets the IPagedList<TEntity> based on a predicate, orderby delegate and page information.
GetPagedList[Async]

Signature:
IPagedList<TResult> GetPagedList<TResult>(
 Expression<Func<TEntity, TResult>> selector,
 Expression<Func<TEntity, bool>>? predicate = null,
 Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
 Func<IQueryable<TEntity>, IQueryable<TEntity>>? include = null,
 int pageIndex = 0,
 int pageSize = 20,
 bool disableTracking = false
)
Gets the IPagedList<TResult> based on a predicate, orderby delegate and page information.
GetPagedList[Async]

Signature:
IPagedList<TEntity> GetPagedList(
 SearchRequest? request,
 Expression<Func<TEntity, bool>>? searchExpression = null,
 Expression<Func<TEntity, bool>>? extraQuery = null,
 string[]? excludedFilters = null,
 Func<IQueryable<TEntity>, IQueryable<TEntity>>? include = null,
 bool disableTracking = false
)
Gets the IPagedList<TResult> based on the search request, search expression and extra query.
GetById[Async]

Signature:
TEntity? GetById<TId>(
 TId id, Func<IQueryable<TEntity>,
 IQueryable<TEntity>>? include = null
)
Retrieves a record with the informed ID and all its navigation properties loaded.
GetFirstOrDefault[Async]

Signature:
TEntity? GetFirstOrDefault(
 Expression<Func<TEntity, bool>>? predicate = null,
 Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
 Func<IQueryable<TEntity>, IQueryable<TEntity>>? include = null,
 bool disableTracking = false
)
Gets the first or default entity based on a predicate, orderby delegate and include delegate.
FromSql

Signature:
IQueryable<TEntity> FromSql(string sql, params object[] parameters)
Uses raw SQL queries to fetch the specified TEntity data.
Find[Async]

Signature:
TEntity? Find(params object[] keyValues)
Finds an entity with the given primary key values. If found, it is attached to the context and returned. If no entity is found, then null is returned.
Find[Async]

Signature:
TEntity? Find<TId>(params TId[] keyValues)
Finds an entity with the given primary key values. If found, it is attached to the context and returned. If no entity is found, then null is returned.
Filter[Async]

Signature:
IPagedList<TEntity> Filter(ColumnFilter[] filters, int pageIndex = 0)
Filters the entities according to the informed filters.
FilterAsQueryable[Async]

Signature:
IQueryable<TEntity> FilterAsQueryable(
 ColumnFilter[] filters,
 int pageIndex = 0
)
Filters the entities according to the informed filters.
FilterBySpec[Async]

Signature:
IPagedList<TEntity> FilterBySpec(
 ISpecification<TEntity> filter,
 int pageIndex = 0,
 SearchRequest? request = null
)
Filters the entities according to the informed specification.
FilterBySpecAsQueryable[Async]

Signature:
IQueryable<TEntity> FilterBySpecAsQueryable(
 ISpecification<TEntity> filter,
 int pageIndex = 0,
 SearchRequest? request = null
)
Filters the entities according to the informed specification.
Count[Async]

Signature:
int Count(Expression<Func<TEntity, bool>>? predicate = null)
Gets the count based on a predicate, if informed.
Count[Async]

Signature:
int Count(
 SearchRequest? request,
 Expression<Func<TEntity, bool>>? searchExpression = null,
 Expression<Func<TEntity, bool>>? extraQuery = null,
 string[]? excludedFilters = null,
 Func<IQueryable<TEntity>, IQueryable<TEntity>>? include = null,
 bool disableTracking = false
)
Gets the count based on the parameters, if informed.
Sum[Async]

Signature:
decimal Sum(Expression<Func<TEntity, decimal>> sumPredicate)
Gets the sum of all the values in the property informed. Has overloads for double, float, int and long.
Sum[Async]

Signature:
decimal Sum(
 SearchRequest? request,
 Expression<Func<TEntity, decimal>> sumPredicate,
 Expression<Func<TEntity, bool>>? searchExpression = null,
 Expression<Func<TEntity, bool>>? extraQuery = null,
 string[]? excludedFilters = null,
 Func<IQueryable<TEntity>, IQueryable<TEntity>>? include = null,
 bool disableTracking = false
)
Gets the sum of all the values in the property informed. Has overloads for double, float, int and long.
Add[Async]

Signature:
TEntity Add(TEntity entity)
Inserts a new entity.
Add[Async]

Signature:
void Add(params TEntity[] entities)
Inserts a range of entities.
Update

Signature:
TEntity Update(TEntity entity)
Updates the specified entity.
Update

Signature:
TEntity Update(
 object id,
 TEntity entity,
 IEnumerable<string> changedProperties,
 bool shouldClearCollections = false,
 PropertyCopyType[]? excludedTypes = null
)
Updates only the entity properties informed.
Update

Signature:
void Update(params TEntity[] entities)
Updates the specified entities.
Delete[Async]

Signature:
void Delete(params object[] keyValues)
Deletes the entity by the specified primary keys.
SoftDelete[Async]

Signature:
void SoftDelete(
 string propertyName = "IsDeleted",
 params object[] keyValues
)
Updates the entity to a removed status. It doesn't remove the entity from the database, instead it updates the flag that indicates whether that entity has been removed.
Delete

Signature:
void Delete(TEntity entity)
Deletes the specified entity.
SoftDelete

Signature:
void SoftDelete(TEntity entity, string propertyName = "IsDeleted")
Updates the entity to a removed status. It doesn't remove the entity from the database, instead it updates the flag that indicates whether that entity has been removed.
Delete

Signature:
void Delete(params TEntity[] entities)
Deletes the specified entities.
SoftDelete

Signature:
void SoftDelete(
 string propertyName = "IsDeleted",
 params TEntity[] entities
)
Updates the entities to a removed status. It doesn't remove the entities from the database, instead it updates the flag that indicates whether that entities have been removed.
DeleteWhere

Signature:
void DeleteWhere(Expression<Func<TEntity, bool>>? predicate = null)
Deletes one or more entities matching the predicate.
SoftDeleteWhere

Signature:
void SoftDeleteWhere(
 Expression<Func<TEntity, bool>>? predicate = null,
 string propertyName = "IsDeleted"
)
Updates the entities matching the predicate to a removed status.

Custom Repository

The UpDevs SDK automatically creates repositories for all entities, so you don't have to do so manually. Once you create an entity, you can already request its repository. You're only required to create a repository class file when you need to add extra methods to that repository, or you need an aggregate root. For those cases, here's how you do it:

Single Entity

public class DoctorRepository : Repository<Doctor>
{
public DoctorRepository(
DbContext dbContext,
DataEfExceptionMessagesResource dataEfExceptionMessagesResource
) : base(dbContext, dataEfExceptionMessagesResource) { }

public async Task UpdateCustomData()
{
// Here you can use DbSet or the default repository methods.
return;
}
}

Multiple Entities

public class DoctorInstitutionRepository : Repository
{
public DoctorInstitutionRepository(DbContext dbContext) : base(dbContext) { }

public async Task<Entity[]> GetAllDoctorsWithInstitutions()
{
// Here you can use DbSet or the default repository methods.
return;
}
}

UnitOfWork

UnitOfWork 1 is responsible for providing access to repositories, keeping track of changes made to the database, transactions as well and persisting everything to the database.

Methods Provided

note

Even though all these methods are provided, usually, you'll only be using GetRepository<TEntity> and SaveChanges[Async].

MethodSignatureDescription
GetRepositoryIRepository<TEntity> GetRepository<TEntity>()Gets the specified repository for the TEntity provided.
GetRepositoryTRepository GetRepository<TRepository, TEntity>()Gets the specific repository specified.
GetCustomRepositoryTRepository GetCustomRepository<TRepository>()Gets the specific repository specified. Utilized for repositories not related to a specific entity, AggregateRoots for example.
SaveChangesSaveResponse SaveChanges()Saves all changes made in this context to the database.
SaveChangesAsyncTask<SaveResponse> SaveChangesAsync()Saves all changes made in this context to the database in an asynchronous way.
SaveChangesAsyncTask<SaveResponse> SaveChangesAsync(params IUnitOfWork[] unitsOfWork)Saves all changes made in this context to the database with distributed transaction in an asynchronous way.
ExecuteSqlCommandint ExecuteSqlCommand(string sql, params object[] parameters)Executes the specified raw SQL command.
FromSqlIQueryable<TEntity> FromSql<TEntity>(string sql, params object[] parameters)Uses raw SQL queries to fetch the specified TEntity data.
TrackGraphvoid TrackGraph(object rootEntity, Action<EntityEntryGraphNode> callback)Begins tracking an entity and any entities that are reachable by traversing its navigation properties.
Disposevoid Dispose()Finalizes all tasks and free resources.

Usage

Like any other service, you just need to inject it into any constructor, but its usage is so common that we have helpers in certain places. Here you can find a list of those places. If you're trying to use it in a place not informed here, you must inject it into the constructor.

Domain Service

Inside all domain services you can access UnitOfWork through the protected property UnitOfWork. Here's an example on line 8:

public class MyDomainService : BaseDomainService, IMyDomainService
{
public MyDomainService(IServiceProvider serviceProvider, IUnitOfWork unitOfWork)
: base(serviceProvider, unitOfWork) { }

public async Task<DoctorResponse[]> GetAllDoctorsWhoMadeTransfersRecently()
{
var historyItems = await UnitOfWork.GetRepository<CaseHistoryItem>()
.FilterBySpecAsQueryableAsync(
new DoctorsWhoMadeTransfersInPastMonthsSpecification(4)
);
var doctors = historyItems.Select(ch => ch.Doctor).ToArray();

return ToResponse<DoctorResponse[]>(doctors);
}
}

Domain Event

Inside any domain event handler (BeforeSave/AfterSave), you can access the UnitOfWork from the notification parameter. Here's an example on line 17:

public class UserBeforeSave : BeforeSaveEntityNotificationHandler<User>
{
public override async Task Handle(
BeforeSaveEntityNotification<User> notification,
CancellationToken cancellationToken
)
{
if (notification.EntityState == EntityState.Added)
{
var validationToken = new ValidationToken
{
CreationDate = DateTime.UtcNow,
ExpirationDate = DateTime.UtcNow.AddDays(1),
Token = $"{RandomProvider.GetThreadRandom().Next(100000, 1000000)}"
};
notification.Entity.ValidationToken =
await notification.UnitOfWork.GetRepository<ValidationToken>().AddAsync(
validationToken,
cancellationToken
);
}
}
}

Validator

Inside any validator, you can access the UnitOfWork from the unitOfWork parameter. Here's an example on line 20:

info

Inside the validator, you can find the current entity (property current) and the one in the database (property original). original is null when the entity is being created.

public class InstitutionValidator : BaseEntityValidator<Institution>
{
public InstitutionValidator(ValidationErrorContext validationContext)
: base(validationContext) { }

public override async Task Validate(
IUnitOfWork unitOfWork,
Institution current,
Institution original,
EntityState state
)
{
switch (state)
{
case EntityState.Added:
RuleFor(p => p.Description).MinimumLength(10);
RuleFor(p => p.Name).NotEmpty();
ValidateRules(current);

var instWithSameName = await unitOfWork.GetRepository<Institution>()
.GetFirstOrDefaultAsync(i => i.Name == current.Name);

if (instWithSameName != null)
{
// ...
}

break;
case EntityState.Modified:
// ...
}
}
}

Specification

Inside all specifications you can access UnitOfWork through the protected property UnitOfWork. Here's an example on line 13:

public class ManagersWhoMadeTransfersInPastMonthsSpecification 
: BaseSpecification<HistoryItem>
{
private readonly int _pastMonths;
private readonly Hospital _hospital;

public ManagersWhoMadeTransfersInPastMonthsSpecification(
Guid hospitalId,
int pastMonths = 3
)
{
_pastMonths = pastMonths;
_hospital = UnitOfWork.GetRepository<Hospital>().GetById(hospitalId);
PageSize = 10;
Include = historyItem => historyItem.Include(hi => hi.HospitalManager);
OrderBy = historyItem => historyItem.OrderBy(hi => hi.CreationDate)
}

public override Expression<Func<CaseHistoryItem, bool>> IsSatisfiedBy()
{
var startDate = DateTime.UtcNow.AddMonths(-_pastMonths);
return historyItem => historyItem.IsTransferRelated
&& historyItem.CreationDate >= startDate
&& historyItem.HospitalManagerId != null
&& historyItem.HospitalManagerId == _hospital.HospitalManagerId;
}
}

Footnotes

  1. What is UnitOfWork

    https://martinfowler.com/eaaCatalog/unitOfWork.html

    https://www.c-sharpcorner.com/UploadFile/b1df45/unit-of-work-in-repository-pattern/