diff --git a/src/ContractImplementations.NHibernate/ContractImplementations.NHibernate.csproj b/src/ContractImplementations.NHibernate/ContractImplementations.NHibernate.csproj
new file mode 100644
index 0000000..77c8624
--- /dev/null
+++ b/src/ContractImplementations.NHibernate/ContractImplementations.NHibernate.csproj
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ContractImplementations.NHibernate/EntitySet.cs b/src/ContractImplementations.NHibernate/EntitySet.cs
new file mode 100644
index 0000000..c6212d6
--- /dev/null
+++ b/src/ContractImplementations.NHibernate/EntitySet.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using IOKode.OpinionatedFramework.Persistence.QueryBuilder;
+using IOKode.OpinionatedFramework.Persistence.QueryBuilder.Exceptions;
+using IOKode.OpinionatedFramework.Persistence.QueryBuilder.Filters;
+using IOKode.OpinionatedFramework.Persistence.UnitOfWork;
+using NHibernate;
+using NHibernate.Criterion;
+using NHNonUniqueResultException = NHibernate.NonUniqueResultException;
+using NonUniqueResultException = IOKode.OpinionatedFramework.Persistence.QueryBuilder.Exceptions.NonUniqueResultException;
+
+namespace IOKode.OpinionatedFramework.ContractImplementations.NHibernate;
+
+public class EntitySet : IEntitySet where T : Entity
+{
+ private readonly ISession _session;
+
+ public EntitySet(ISession session)
+ {
+ _session = session;
+ }
+
+ public async Task GetByIdAsync(object id, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ return await _session.LoadAsync(id, cancellationToken);
+ }
+ catch (ObjectNotFoundException ex)
+ {
+ throw new EntityNotFoundException(id, ex);
+ }
+ }
+
+ public async Task GetByIdOrDefaultAsync(object id, CancellationToken cancellationToken = default)
+ {
+ return await _session.GetAsync(id, cancellationToken);
+ }
+
+ public async Task SingleAsync(Filter? filter, CancellationToken cancellationToken = default)
+ {
+ var criteria = _session.CreateCriteria();
+ ApplyFilter(criteria, filter);
+
+ try
+ {
+ var result = await criteria.UniqueResultAsync(cancellationToken);
+ if (result == null)
+ {
+ throw new EmptyResultException();
+ }
+
+ return result;
+ }
+ catch(NHNonUniqueResultException ex)
+ {
+ throw new NonUniqueResultException(ex);
+ }
+ }
+
+ public async Task SingleOrDefaultAsync(Filter? filter, CancellationToken cancellationToken = default)
+ {
+ var criteria = _session.CreateCriteria();
+ ApplyFilter(criteria, filter);
+
+ try
+ {
+ return await criteria.UniqueResultAsync(cancellationToken);
+ }
+ catch(NHNonUniqueResultException ex)
+ {
+ throw new NonUniqueResultException(ex);
+ }
+ }
+
+ public async Task FirstAsync(Filter? filter, CancellationToken cancellationToken = default)
+ {
+ var criteria = _session.CreateCriteria();
+ ApplyFilter(criteria, filter);
+ criteria.SetMaxResults(1);
+
+ var list = await criteria.ListAsync(cancellationToken);
+ if (list.Count == 0)
+ {
+ throw new EmptyResultException();
+ }
+
+ return list[0];
+ }
+
+ public async Task FirstOrDefaultAsync(Filter? filter, CancellationToken cancellationToken = default)
+ {
+ var criteria = _session.CreateCriteria();
+ ApplyFilter(criteria, filter);
+ criteria.SetMaxResults(1);
+
+ var list = await criteria.ListAsync(cancellationToken);
+ return list.Count == 0 ? null : list[0];
+ }
+
+ public async Task> ManyAsync(Filter? filter, CancellationToken cancellationToken = default)
+ {
+ var criteria = _session.CreateCriteria();
+ ApplyFilter(criteria, filter);
+
+ var list = await criteria.ListAsync(cancellationToken);
+ return (IReadOnlyCollection)list;
+ }
+
+ private void ApplyFilter(ICriteria criteria, Filter? filter)
+ {
+ if (filter == null)
+ {
+ return;
+ }
+
+ var criterion = BuildCriterion(filter);
+ criteria.Add(criterion);
+ }
+
+ private ICriterion BuildCriterion(Filter filter)
+ {
+ return filter switch
+ {
+ EqualsFilter eq => Restrictions.Eq(eq.FieldName, eq.Value),
+ LikeFilter like => Restrictions.Like(like.FieldName, like.Pattern, MatchMode.Anywhere),
+ InFilter inFilter => Restrictions.In(inFilter.FieldName, inFilter.Values),
+ BetweenFilter betweenFilter => Restrictions.Between(betweenFilter.FieldName, betweenFilter.Low, betweenFilter.High),
+ GreaterThanFilter greaterThanFilter => Restrictions.Gt(greaterThanFilter.FieldName, greaterThanFilter.Value),
+ LessThanFilter lessThanFilter => Restrictions.Lt(lessThanFilter.FieldName, lessThanFilter.Value),
+ AndFilter andFilter => BuildJunction(andFilter.Filters, isAnd: true),
+ OrFilter orFilter => BuildJunction(orFilter.Filters, isAnd: false),
+ NotFilter notFilter => Restrictions.Not(BuildCriterion(notFilter.Filter)),
+ NotEqualsFilter notEqualsFilter => Restrictions.Not(Restrictions.Eq(notEqualsFilter.FieldName, notEqualsFilter.Value)),
+ _ => throw new NotSupportedException($"Filter type '{filter.GetType().Name}' is not supported.")
+ };
+ }
+
+ private Junction BuildJunction(Filter[] conditions, bool isAnd)
+ {
+ Junction junction = isAnd ? Restrictions.Conjunction() : Restrictions.Disjunction();
+ foreach (var cond in conditions)
+ {
+ junction.Add(BuildCriterion(cond));
+ }
+
+ return junction;
+ }
+}
\ No newline at end of file
diff --git a/src/ContractImplementations.NHibernate/UnitOfWork.cs b/src/ContractImplementations.NHibernate/UnitOfWork.cs
new file mode 100644
index 0000000..2ca29c9
--- /dev/null
+++ b/src/ContractImplementations.NHibernate/UnitOfWork.cs
@@ -0,0 +1,197 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Dapper;
+using IOKode.OpinionatedFramework.Persistence.QueryBuilder;
+using IOKode.OpinionatedFramework.Persistence.UnitOfWork;
+using IOKode.OpinionatedFramework.Persistence.UnitOfWork.Exceptions;
+using NHibernate;
+
+namespace IOKode.OpinionatedFramework.ContractImplementations.NHibernate;
+
+public class UnitOfWork : IUnitOfWork, IAsyncDisposable
+{
+ private readonly Dictionary repositories = new();
+ private readonly ISession session;
+ private ITransaction? transaction;
+ private bool isRollbacked;
+
+ public UnitOfWork(ISessionFactory sessionFactory)
+ {
+ this.session = sessionFactory.OpenSession();
+ }
+
+ public bool IsRolledBack => this.isRollbacked;
+
+ public Task BeginTransactionAsync(CancellationToken cancellationToken = default)
+ {
+ ThrowsIfRolledBack();
+
+ this.transaction = this.session.BeginTransaction();
+ return Task.CompletedTask;
+ }
+
+ public async Task CommitTransactionAsync(CancellationToken cancellationToken = default)
+ {
+ ThrowsIfRolledBack();
+
+ if (this.transaction is null)
+ {
+ throw new InvalidOperationException("No transaction is active.");
+ }
+
+ await this.transaction.CommitAsync(cancellationToken);
+ this.transaction.Dispose();
+ this.transaction = null;
+ }
+
+ public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default)
+ {
+ ThrowsIfRolledBack();
+
+ if (this.transaction is null)
+ {
+ throw new InvalidOperationException("No transaction is active.");
+ }
+
+ await this.transaction.RollbackAsync(cancellationToken);
+ this.transaction.Dispose();
+ this.transaction = null;
+ this.session.Clear();
+ await DisposeAsync();
+ this.isRollbacked = true;
+ }
+
+ public bool IsTransactionActive
+ {
+ get
+ {
+ ThrowsIfRolledBack();
+ return this.transaction is {IsActive: true};
+ }
+ }
+
+ public async Task AddAsync(T entity, CancellationToken cancellationToken = default) where T : Entity
+ {
+ ThrowsIfRolledBack();
+ await this.session.PersistAsync(entity, cancellationToken);
+ }
+
+ public Task IsTrackedAsync(T entity, CancellationToken cancellationToken = default) where T : Entity
+ {
+ ThrowsIfRolledBack();
+ return Task.FromResult(session.Contains(entity));
+ }
+
+ public async Task StopTrackingAsync(T entity, CancellationToken cancellationToken = default) where T : Entity
+ {
+ ThrowsIfRolledBack();
+ await this.session.EvictAsync(entity, cancellationToken);
+ }
+
+ public async Task HasChangesAsync(CancellationToken cancellationToken)
+ {
+ ThrowsIfRolledBack();
+ return await this.session.IsDirtyAsync(cancellationToken);
+ }
+
+ public IEntitySet GetEntitySet() where T : Entity
+ {
+ ThrowsIfRolledBack();
+ return new EntitySet(this.session);
+ }
+
+ public async Task> RawProjection(string query, IList