Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<Company>Soulv Software (Pty) Ltd</Company>
<Copyright>Copyright (c) Soulv Software 2025</Copyright>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<LangVersion>13</LangVersion>
<PackageProjectUrl>https://github.com/MarkDerman/OrdinaryInfrastructure</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/MarkDerman/OrdinaryInfrastructure</RepositoryUrl>
Expand Down
25 changes: 25 additions & 0 deletions Domain/Core/AbstractIdentityQuerySpecification.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Linq.Expressions;
using System.Numerics;

namespace Odin.Domain;

/// <inheritdoc />
public abstract class AbstractIdentityQuerySpecification<TAggregateRoot, TId>
: AbstractQuerySpecification<TAggregateRoot>
where TAggregateRoot : class, IIdentityAggregateRoot<TId>
where TId : struct, IEqualityOperators<TId, TId, bool>
{
/// <summary>
/// Default constructor for AbstractQuerySpecification
/// </summary>
/// <param name="criteria"></param>
protected AbstractIdentityQuerySpecification(Expression<Func<TAggregateRoot, bool>>? criteria) : base(criteria)
{
}
/// <summary>
///
/// </summary>
protected AbstractIdentityQuerySpecification()
{
}
}
93 changes: 93 additions & 0 deletions Domain/Core/AbstractQuerySpecification.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System.Linq.Expressions;

namespace Odin.Domain;

/// <inheritdoc />
public abstract class AbstractQuerySpecification<TAggregateRoot>
: IQuerySpecification<TAggregateRoot>
where TAggregateRoot : class, IAggregateRoot
{
/// <summary>
/// Default constructor for AbstractQuerySpecification
/// </summary>
/// <param name="criteria"></param>
protected AbstractQuerySpecification(Expression<Func<TAggregateRoot, bool>>? criteria)
{
Criteria = criteria;
}

/// <summary>
///
/// </summary>
protected AbstractQuerySpecification()
{
}

/// <inheritdoc />
public Expression<Func<TAggregateRoot, bool>>? Criteria { get; protected set; }

/// <inheritdoc />
public IReadOnlyList<Expression<Func<TAggregateRoot, object>>>? Includes => _includes;
private List<Expression<Func<TAggregateRoot, object>>>? _includes;

/// <inheritdoc />
public Expression<Func<TAggregateRoot, object>>? OrderBy { get; private set; }

/// <inheritdoc />
public Expression<Func<TAggregateRoot, object>>? OrderByDescending { get; private set; }

/// <inheritdoc />
public int Take { get; private set; }

/// <inheritdoc />
public int Skip { get; private set; }

/// <inheritdoc />
public bool IsPagingEnabled { get; private set; }

/// <summary>
/// Adds a query include
/// </summary>
/// <param name="includeExpression"></param>
protected void AddCriteria(Expression<Func<TAggregateRoot, object>> includeExpression)
{
if (_includes == null) _includes = new List<Expression<Func<TAggregateRoot, object>>>();
_includes.Add(includeExpression);
}

/// <summary>
/// Adds a query include
/// </summary>
/// <param name="includeExpression"></param>
protected void AddInclude(Expression<Func<TAggregateRoot, object>> includeExpression)
{
if (_includes == null) _includes = new List<Expression<Func<TAggregateRoot, object>>>();
_includes.Add(includeExpression);
}

/// <summary>
/// Adds ordering ascending
/// </summary>
/// <param name="orderByExpression"></param>
protected void ApplyOrderBy(Expression<Func<TAggregateRoot, object>> orderByExpression)
=> OrderBy = orderByExpression;

/// <summary>
/// Adds ordering descending
/// </summary>
/// <param name="orderByExpression"></param>
protected void ApplyOrderByDescending(Expression<Func<TAggregateRoot, object>> orderByExpression)
=> OrderByDescending = orderByExpression;

/// <summary>
/// Applies pagination options.
/// </summary>
/// <param name="skip"></param>
/// <param name="take"></param>
protected void ApplyPaging(int skip, int take)
{
Skip = skip;
Take = take;
IsPagingEnabled = true;
}
}
28 changes: 28 additions & 0 deletions Domain/Core/DomainEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

namespace Odin.Domain
{
/// <summary>
/// Base class for a domain event.
/// </summary>
public abstract class DomainEvent
{
/// <summary>
/// Default constructor
/// </summary>
/// <param name="now"></param>
protected DomainEvent(DateTimeOffset now)
{
OccurredAt = now;
}

/// <summary>
/// Whether the event has been published or not.
/// </summary>
public bool IsPublished { get; set; }

/// <summary>
/// Gets the timestamp when the event occurred.
/// </summary>
public DateTimeOffset OccurredAt { get; protected set; }
}
}
8 changes: 8 additions & 0 deletions Domain/Core/DomainException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Odin.Domain;

/// <summary>
/// Represents an exception originating from the core application domain.
/// </summary>
public class DomainException : ApplicationException
{
}
9 changes: 9 additions & 0 deletions Domain/Core/IAggregateRoot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Odin.Domain;

/// <summary>
/// Marker interface to identify an 'aggregate root' entity
/// (in domain driven design language).
/// </summary>
public interface IAggregateRoot
{
}
20 changes: 20 additions & 0 deletions Domain/Core/IIdentityAggregateRoot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Numerics;

namespace Odin.Domain;

/// <summary>
/// Marker interface to identify an 'aggregate root' entity
/// that has a primitive unique identifier property,
/// and typically a single column primary key.
/// (in domain driven design language).
/// </summary>
// ReSharper disable once TypeParameterCanBeVariant
public interface IIdentityAggregateRoot<TId> : IAggregateRoot
where TId : struct, IEqualityOperators<TId, TId, bool>
{
/// <summary>
/// Unique identifier of the entity.
/// Commonly int16\32\64, Guid or string.
/// </summary>
TId Id { get; }
}
47 changes: 47 additions & 0 deletions Domain/Core/IQuerySpecification.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Linq.Expressions;

namespace Odin.Domain;

/// <summary>
/// Represents a specification for filter criteria, preloading includes,
/// ordering and pagination for a repository query.
/// </summary>
/// <typeparam name="TAggregateRoot"></typeparam>
public interface IQuerySpecification<TAggregateRoot>
where TAggregateRoot : class, IAggregateRoot
{
/// <summary>
/// The filter criteria (Where clause)
/// </summary>
Expression<Func<TAggregateRoot, bool>>? Criteria { get; }

/// <summary>
/// Eager loading (Include clauses)
/// </summary>
IReadOnlyList<Expression<Func<TAggregateRoot, object>>>? Includes { get; }

/// <summary>
/// Ordering ascending
/// </summary>
Expression<Func<TAggregateRoot, object>>? OrderBy { get; }

/// <summary>
/// Ordering descending
/// </summary>
Expression<Func<TAggregateRoot, object>>? OrderByDescending { get; }

/// <summary>
/// Pagination - Take
/// </summary>
int Take { get; }

/// <summary>
/// Pagination - Skip
/// </summary>
int Skip { get; }

/// <summary>
/// Whether pagination is enabled
/// </summary>
bool IsPagingEnabled { get; }
}
97 changes: 97 additions & 0 deletions Domain/Core/IRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Linq.Expressions;
using System.Numerics;

namespace Odin.Domain
{
/// <summary>
/// Represents a repository for persisting entities to a data store.
/// </summary>
/// <typeparam name="TAggregateRoot">The type of the aggregate root.</typeparam>
public interface IRepository<TAggregateRoot> : IAsyncDisposable
where TAggregateRoot : class, IAggregateRoot
{
/// <summary>
/// Adds an entity to the context but does not persist it to the database.
/// </summary>
/// <param name="entity">The entity to add.</param>
void Add(TAggregateRoot entity);

/// <summary>
/// Adds a range of entities to the context but does not persist them to the database.
/// </summary>
/// <param name="entities">The entities to add.</param>
void AddRange(IEnumerable<TAggregateRoot> entities);

/// <summary>
/// Updates an entity in the database.
/// </summary>
/// <param name="entity">The entity to update.</param>
void Update(TAggregateRoot entity);

/// <summary>
/// Deletes an entity from the database.
/// </summary>
/// <param name="entity">The entity to delete.</param>
void Delete(TAggregateRoot entity);

/// <summary>
/// Attempts to fetch a single entity based on the query specified.
/// </summary>
/// <param name="querySpecification"></param>
/// <param name="ct"></param>
/// <returns></returns>
Task<TAggregateRoot?> FetchAsync(IQuerySpecification<TAggregateRoot> querySpecification,
CancellationToken ct = default);

/// <summary>
/// Fetches many entities based on the query specified.
/// </summary>
/// <param name="querySpecification"></param>
/// <param name="ct"></param>
/// <returns></returns>
Task<IReadOnlyList<TAggregateRoot>> FetchManyAsync(
IQuerySpecification<TAggregateRoot> querySpecification, CancellationToken ct = default);

/// <summary>
/// Gets the unit of work, used to commit changes to the database,
/// and possibly fire domain events.
/// </summary>
IUnitOfWork UnitOfWork { get; }
}

/// <summary>
/// Represents a repository for persisting entities with a single column
/// primary key to a data store.
/// </summary>
/// <typeparam name="TAggregateRoot"></typeparam>
/// <typeparam name="TId"></typeparam>
public interface IRepository<TAggregateRoot, TId> : IRepository<TAggregateRoot>
where TAggregateRoot : class, IIdentityAggregateRoot<TId>
where TId : struct, IEqualityOperators<TId, TId, bool>
{
/// <summary>
/// Fetches an entity by its identifier, returning null if not found.
/// </summary>
/// <param name="id"></param>
/// <param name="ct"></param>
/// <returns></returns>
Task<TAggregateRoot?> FetchByIdAsync(TId id, CancellationToken ct = default);

/// <summary>
/// Fetches an entity by its identifier, returning null if not found.
/// </summary>
/// <param name="id"></param>
/// <param name="includes"></param>
/// <param name="ct"></param>
/// <returns></returns>
Task<TAggregateRoot?> FetchByIdAsync(TId id, IReadOnlyList<Expression<Func<TAggregateRoot, object>>>? includes
, CancellationToken ct = default);

/// <summary>
/// Fetches entities from a list of their identifiers, returning an empty list if none found.
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
Task<IReadOnlyList<TAggregateRoot>> FetchManyByIdAsync(params ReadOnlySpan<TId> ids);
}
}
17 changes: 17 additions & 0 deletions Domain/Core/IUnitOfWork.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

namespace Odin.Domain
{
/// <summary>
/// Intended to wrap DbContext.SaveChangesAsync to support domain event publishing
/// at database persistence time.
/// </summary>
public interface IUnitOfWork
{
/// <summary>
/// Commit database changes and publish any unpublished domain events.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken));
}
}
17 changes: 17 additions & 0 deletions Domain/Core/Odin.Domain.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>true</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<Description>

</Description>
<WarningsAsErrors>1591;1573;</WarningsAsErrors> <!-- Not to be removed. Documentation is required. -->
</PropertyGroup>

<ItemGroup>
<None Include="..\README.md" Pack="true" PackagePath=""/>
</ItemGroup>
</Project>
Loading
Loading