Skip to content

Commit

Permalink
Merge pull request iokode#39 from Angeling3/persistence
Browse files Browse the repository at this point in the history
Add NHibernate unit of work implementation
  • Loading branch information
montyclt authored Dec 16, 2024
2 parents 0df3079 + 8d26058 commit fbf3dbc
Show file tree
Hide file tree
Showing 39 changed files with 1,735 additions and 79 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../MSBuild/Base.props"/>
<Import Project="../MSBuild/Packable.props"/>

<ItemGroup Label="Package references">
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="NHibernate" Version="5.5.*"/>
</ItemGroup>

<ItemGroup Label="Project dependencies">
<ProjectReference Include="..\Bootstrapping\Bootstrapping.csproj" />
<ProjectReference Include="..\Foundation\Foundation.csproj"/>
</ItemGroup>
</Project>
152 changes: 152 additions & 0 deletions src/ContractImplementations.NHibernate/EntitySet.cs
Original file line number Diff line number Diff line change
@@ -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<T> : IEntitySet<T> where T : Entity
{
private readonly ISession _session;

public EntitySet(ISession session)
{
_session = session;
}

public async Task<T> GetByIdAsync(object id, CancellationToken cancellationToken = default)
{
try
{
return await _session.LoadAsync<T>(id, cancellationToken);
}
catch (ObjectNotFoundException ex)
{
throw new EntityNotFoundException(id, ex);
}
}

public async Task<T> GetByIdOrDefaultAsync(object id, CancellationToken cancellationToken = default)
{
return await _session.GetAsync<T>(id, cancellationToken);
}

public async Task<T> SingleAsync(Filter? filter, CancellationToken cancellationToken = default)
{
var criteria = _session.CreateCriteria<T>();
ApplyFilter(criteria, filter);

try
{
var result = await criteria.UniqueResultAsync<T>(cancellationToken);
if (result == null)
{
throw new EmptyResultException();
}

return result;
}
catch(NHNonUniqueResultException ex)
{
throw new NonUniqueResultException(ex);
}
}

public async Task<T?> SingleOrDefaultAsync(Filter? filter, CancellationToken cancellationToken = default)
{
var criteria = _session.CreateCriteria<T>();
ApplyFilter(criteria, filter);

try
{
return await criteria.UniqueResultAsync<T>(cancellationToken);
}
catch(NHNonUniqueResultException ex)
{
throw new NonUniqueResultException(ex);
}
}

public async Task<T> FirstAsync(Filter? filter, CancellationToken cancellationToken = default)
{
var criteria = _session.CreateCriteria<T>();
ApplyFilter(criteria, filter);
criteria.SetMaxResults(1);

var list = await criteria.ListAsync<T>(cancellationToken);
if (list.Count == 0)
{
throw new EmptyResultException();
}

return list[0];
}

public async Task<T?> FirstOrDefaultAsync(Filter? filter, CancellationToken cancellationToken = default)
{
var criteria = _session.CreateCriteria<T>();
ApplyFilter(criteria, filter);
criteria.SetMaxResults(1);

var list = await criteria.ListAsync<T>(cancellationToken);
return list.Count == 0 ? null : list[0];
}

public async Task<IReadOnlyCollection<T>> ManyAsync(Filter? filter, CancellationToken cancellationToken = default)
{
var criteria = _session.CreateCriteria<T>();
ApplyFilter(criteria, filter);

var list = await criteria.ListAsync<T>(cancellationToken);
return (IReadOnlyCollection<T>)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;
}
}
197 changes: 197 additions & 0 deletions src/ContractImplementations.NHibernate/UnitOfWork.cs
Original file line number Diff line number Diff line change
@@ -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<Type, Repository> 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>(T entity, CancellationToken cancellationToken = default) where T : Entity
{
ThrowsIfRolledBack();
await this.session.PersistAsync(entity, cancellationToken);
}

public Task<bool> IsTrackedAsync<T>(T entity, CancellationToken cancellationToken = default) where T : Entity
{
ThrowsIfRolledBack();
return Task.FromResult(session.Contains(entity));
}

public async Task StopTrackingAsync<T>(T entity, CancellationToken cancellationToken = default) where T : Entity
{
ThrowsIfRolledBack();
await this.session.EvictAsync(entity, cancellationToken);
}

public async Task<bool> HasChangesAsync(CancellationToken cancellationToken)
{
ThrowsIfRolledBack();
return await this.session.IsDirtyAsync(cancellationToken);
}

public IEntitySet<T> GetEntitySet<T>() where T : Entity
{
ThrowsIfRolledBack();
return new EntitySet<T>(this.session);
}

public async Task<ICollection<T>> RawProjection<T>(string query, IList<object>? parameters = null, CancellationToken cancellationToken = default)
{
ThrowsIfRolledBack();

if (parameters != null && !parameters.Any())
{
parameters = null!;
}

var transaction = GetTransaction();
return (await this.session.Connection.QueryAsync<T>(query, parameters, transaction)).ToArray();
}

public Repository GetRepository(Type repositoryType)
{
ThrowsIfRolledBack();
IUnitOfWork.EnsureTypeIsRepository(repositoryType);

if (this.repositories.TryGetValue(repositoryType, out var repo))
{
return repo;
}

// Create an instance of the repository. This assumes the repository has
// a parameterless constructor or a constructor we can access non-publicly.
repo = (Repository)Activator.CreateInstance(repositoryType, nonPublic: true)!;

// Use reflection to set the UnitOfWork property.
// The property is defined on the Repository base class, so we reflect on typeof(Repository).
// The compiler generates a backing field named `<UnitOfWork>k__BackingField` for auto-properties.
var field = typeof(Repository).GetField("unitOfWork", BindingFlags.Instance | BindingFlags.NonPublic);

if (field is null)
{
throw new UnreachableException("Could not find the 'unitOfWork' field.");
}

// Set the field value to the current IUnitOfWork instance
field.SetValue(repo, this);

this.repositories[repositoryType] = repo;
return repo;
}

public async Task SaveChangesAsync(CancellationToken cancellationToken)
{
ThrowsIfRolledBack();
bool isTransaction = IsTransactionActive;

if (!isTransaction)
{
await BeginTransactionAsync(cancellationToken);
}

await this.session.FlushAsync(cancellationToken);

if (!isTransaction)
{
await CommitTransactionAsync(cancellationToken);
}
}

public async ValueTask DisposeAsync()
{
if (this.IsTransactionActive)
{
await this.transaction!.RollbackAsync();
}
this.transaction?.Dispose();
this.session.Dispose();
}

private void ThrowsIfRolledBack()
{
if (IsRolledBack)
{
throw new UnitOfWorkRolledBackException();
}
}

private IDbTransaction GetTransaction()
{
using(var command = this.session.Connection.CreateCommand())
{
this.session.Transaction.Enlist(command);
return command.Transaction;
}
}
}
16 changes: 0 additions & 16 deletions src/Foundation/Persistence/AggregateRoot.cs

This file was deleted.

10 changes: 0 additions & 10 deletions src/Foundation/Persistence/IRepository.cs

This file was deleted.

Loading

0 comments on commit fbf3dbc

Please sign in to comment.