Compare commits

..

2 Commits

Author SHA1 Message Date
Kylnic28
6fba038bfb feat: base domain 2024-10-08 20:54:40 +02:00
Kylnic28
17a7e651c8 feat: builders, specifications 2024-10-05 20:36:34 +02:00
24 changed files with 432 additions and 17 deletions

View File

@@ -3,7 +3,31 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.11.35303.130 VisualStudioVersion = 17.11.35303.130
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bahla.Tests", "src\Bahla.Tests\Bahla.Tests.csproj", "{F1392CEF-BD63-4208-8ADA-95DF407E0D28}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bahla.Domain", "src\Bahla.Domain\Bahla.Domain.csproj", "{543F323C-31BF-4BEA-9503-F9A26E963FC8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bahla.Persistence", "src\Bahla.Persistence\Bahla.Persistence.csproj", "{FBEE999C-7DFE-451B-AB01-7E7AE5C3ED95}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F1392CEF-BD63-4208-8ADA-95DF407E0D28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F1392CEF-BD63-4208-8ADA-95DF407E0D28}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F1392CEF-BD63-4208-8ADA-95DF407E0D28}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F1392CEF-BD63-4208-8ADA-95DF407E0D28}.Release|Any CPU.Build.0 = Release|Any CPU
{543F323C-31BF-4BEA-9503-F9A26E963FC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{543F323C-31BF-4BEA-9503-F9A26E963FC8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{543F323C-31BF-4BEA-9503-F9A26E963FC8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{543F323C-31BF-4BEA-9503-F9A26E963FC8}.Release|Any CPU.Build.0 = Release|Any CPU
{FBEE999C-7DFE-451B-AB01-7E7AE5C3ED95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FBEE999C-7DFE-451B-AB01-7E7AE5C3ED95}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FBEE999C-7DFE-451B-AB01-7E7AE5C3ED95}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FBEE999C-7DFE-451B-AB01-7E7AE5C3ED95}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,13 @@
using Bahla.Domain.Entities.Base;
namespace Bahla.Domain.Builders.Base
{
public interface IEntityBuilder<out T> where T : IEntity
{
/// <summary>
/// Build the entity.
/// </summary>
/// <returns>Entity</returns>
T Build();
}
}

View File

@@ -0,0 +1,37 @@
using Bahla.Domain.Entities;
using Bahla.Domain.Types;
namespace Bahla.Domain.Builders.Base
{
public interface IRoleBuilder : IEntityBuilder<Role>
{
/// <summary>
/// Defines the identifier for the role, if not provided, a new identifier will be generated.
/// </summary>
/// <param name="identifier">Identifier</param>
/// <returns>Builder</returns>
IRoleBuilder WithIdentifier(Identifier identifier);
/// <summary>
/// Defines the name for the role.
/// </summary>
/// <param name="roleName">Role name</param>
/// <returns>Builder</returns>
IRoleBuilder WithName(string roleName);
/// <summary>
/// Defines the value mask for the role. Maximum allowed is 32 bits (1 << 32).
/// </summary>
/// <param name="valueMask">Role bitmask</param>
/// <returns>Builder</returns>
IRoleBuilder WithValueMask(uint valueMask);
/// <summary>
/// Clones the role.
/// </summary>
/// <param name="role">Role</param>
/// <returns>Builder</returns>
IRoleBuilder Clone(Role role);
}
}

View File

@@ -0,0 +1,60 @@
using Bahla.Domain.Builders.Base;
using Bahla.Domain.Entities;
using Bahla.Domain.Exceptions;
using Bahla.Domain.Specifications.Base;
using Bahla.Domain.Types;
namespace Bahla.Domain.Builders
{
internal sealed class RoleNameIsNotEmptySpecification : CompositeSpecification<RoleBuilder>
{
public override bool IsSatisfiedBy(RoleBuilder entity)
=> !string.IsNullOrEmpty(entity._roleName) && !string.IsNullOrWhiteSpace(entity._roleName);
}
internal sealed class RoleValueMaskIsNotZeroSpecification : CompositeSpecification<RoleBuilder>
{
public override bool IsSatisfiedBy(RoleBuilder entity)
=> entity._valueMask != 0;
}
public sealed class RoleBuilder : IRoleBuilder
{
internal Identifier _identifier = Identifier.Generate;
internal string _roleName = string.Empty;
internal uint _valueMask;
public Role Build()
=> new RoleNameIsNotEmptySpecification()
.IsSatisfiedBy(this) ? new Role(this) : throw new EntityBuilderException("Role name cannot be null or empty");
public IRoleBuilder WithIdentifier(Identifier identifier)
{
_identifier = identifier;
return this;
}
public IRoleBuilder WithName(string roleName)
{
_roleName = roleName;
return this;
}
public IRoleBuilder WithValueMask(uint valueMask)
{
_valueMask = valueMask;
return this;
}
public IRoleBuilder Clone(Role role)
{
_identifier = role.UUID;
_roleName = role.RoleName;
_valueMask = role.ValueMask;
return this;
}
}
}

View File

@@ -0,0 +1,6 @@
using Bahla.Domain.Types;
namespace Bahla.Domain.Entities.Base
{
public abstract record Entity(Identifier UUID) : IEntity;
}

View File

@@ -0,0 +1,15 @@
using Bahla.Domain.Types;
namespace Bahla.Domain.Entities.Base
{
/// <summary>
/// Interface that describes an independent entity.
/// </summary>
public interface IEntity
{
/// <summary>
/// Unique identifier for the entity.
/// </summary>
Identifier UUID { get; init; }
}
}

View File

@@ -0,0 +1,26 @@
using Bahla.Domain.Builders;
using Bahla.Domain.Builders.Base;
using Bahla.Domain.Entities.Base;
using Bahla.Domain.Types;
namespace Bahla.Domain.Entities
{
public sealed record Role : Entity
{
public string RoleName { get; init; }
public uint ValueMask { get; init; }
internal Role(Identifier identifier, string roleName, uint valueMask) : base(identifier)
{
RoleName = roleName;
ValueMask = valueMask;
}
internal Role(RoleBuilder builder) : base(builder._identifier)
{
RoleName = builder._roleName;
ValueMask = builder._valueMask;
}
public static IRoleBuilder Builder => new RoleBuilder();
}
}

View File

@@ -0,0 +1,4 @@
namespace Bahla.Domain.Exceptions
{
public abstract class DomainException(string message) : Exception(string.Concat("Domain Exception: ", " ", message)) { }
}

View File

@@ -0,0 +1,7 @@
namespace Bahla.Domain.Exceptions
{
public sealed class EntityBuilderException : DomainException
{
internal EntityBuilderException(string message) : base(message) { }
}
}

View File

@@ -0,0 +1,14 @@
using Bahla.Domain.Entities.Base;
using Bahla.Domain.Specifications.Base;
namespace Bahla.Domain.Specifications
{
public sealed class AndNotSpecification<T>(IEntitySpecification<T> left, IEntitySpecification<T> right) : CompositeSpecification<T> where T : IEntity
{
private readonly IEntitySpecification<T> _left = left;
private readonly IEntitySpecification<T> _right = right;
public override bool IsSatisfiedBy(T entity)
=> _left.IsSatisfiedBy(entity) && !_right.IsSatisfiedBy(entity);
}
}

View File

@@ -0,0 +1,13 @@
using Bahla.Domain.Entities.Base;
using Bahla.Domain.Specifications.Base;
namespace Bahla.Domain.Specifications
{
public sealed class AndSpecification<T>(IEntitySpecification<T> left, IEntitySpecification<T> right) : CompositeSpecification<T> where T : IEntity
{
private readonly IEntitySpecification<T> _left = left;
private readonly IEntitySpecification<T> _right = right;
public override bool IsSatisfiedBy(T entity)
=> _left.IsSatisfiedBy(entity) && _right.IsSatisfiedBy(entity);
}
}

View File

@@ -0,0 +1,24 @@
using Bahla.Domain.Entities.Base;
using Bahla.Domain.Specifications.Base;
namespace Bahla.Domain.Specifications
{
public abstract class CompositeSpecification<T> : IEntitySpecification<T> where T : IEntity
{
public abstract bool IsSatisfiedBy(T entity);
public IEntitySpecification<T> And(IEntitySpecification<T> specification)
=> new AndSpecification(this, specification);
public IEntitySpecification<T> AndNot(IEntitySpecification<T> specification)
=> new AndNotSpecification(this, specification);
public IEntitySpecification<T> Not()
=> new NotSpecification(this);
public IEntitySpecification<T> Or(IEntitySpecification<T> specification)
=> new OrSpecification(this, specification);
IEntitySpecification<T> OrNot(IEntitySpecification<T> specification)
=> new OrNotSpecification(this, specification);
}
}

View File

@@ -0,0 +1,15 @@
using Bahla.Domain.Entities.Base;
namespace Bahla.Domain.Specifications.Base
{
public interface IEntitySpecification<T> where T : IEntity
{
bool IsSatisfiedBy(T entity);
IEntitySpecification<T> And(IEntitySpecification<T> other);
IEntitySpecification<T> AndNot(IEntitySpecification<T> other);
IEntitySpecification<T> Or(IEntitySpecification<T> other);
IEntitySpecification<T> OrNot(IEntitySpecification<T> other);
IEntitySpecification<T> Not();
}
}

View File

@@ -0,0 +1,12 @@
using Bahla.Domain.Entities.Base;
using Bahla.Domain.Specifications.Base;
namespace Bahla.Domain.Specifications
{
public sealed class NotSpecification<T>(IEntitySpecification<T> other) : CompositeSpecification<T> where T : IEntity
{
private readonly IEntitySpecification<T> _other = other;
public override bool IsSatisfiedBy(T entity)
=> !_other.IsSatisfiedBy(entity);
}
}

View File

@@ -0,0 +1,14 @@
using Bahla.Domain.Entities.Base;
using Bahla.Domain.Specifications.Base;
namespace Bahla.Domain.Specifications
{
public sealed class OrNotSpecification<T>(IEntitySpecification<T> left, IEntitySpecification<T> right) : CompositeSpecification<T> where T : IEntity
{
private readonly IEntitySpecification<T> _left = left;
private readonly IEntitySpecification<T> _right = right;
public override bool IsSatisfiedBy(T entity)
=> _left.IsSatisfiedBy(entity) || !_right.IsSatisfiedBy(entity);
}
}

View File

@@ -0,0 +1,14 @@
using Bahla.Domain.Entities.Base;
using Bahla.Domain.Specifications.Base;
namespace Bahla.Domain.Specifications
{
public sealed class OrSpecification<T>(IEntitySpecification<T> left, IEntitySpecification<T> right) : CompositeSpecification<T> where T : IEntity
{
private readonly IEntitySpecification<T> _left = left;
private readonly IEntitySpecification<T> _right = right;
public override bool IsSatisfiedBy(T entity)
=> _left.IsSatisfiedBy(entity) || _right.IsSatisfiedBy(entity);
}
}

View File

@@ -0,0 +1,12 @@
namespace Bahla.Domain.Types
{
public readonly record struct Identifier(Guid UUID) : IComparable<Identifier>
{
public static implicit operator Identifier(Guid uuid) => new(uuid);
public static implicit operator Identifier(string uuid) => new(Guid.Parse(uuid));
public static Identifier Generate => Guid.NewGuid();
public override string ToString() => UUID.ToString();
public int CompareTo(Identifier other)
=> UUID.CompareTo(other.UUID);
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Folder Include="Base\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,7 @@
namespace Bahla.Persistence
{
public class Class1
{
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
@@ -17,8 +17,17 @@
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Bahla.Domain\Bahla.Domain.csproj" />
<ProjectReference Include="..\Bahla.Persistence\Bahla.Persistence.csproj" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Using Include="NUnit.Framework" /> <Using Include="NUnit.Framework" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Entities\" />
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,44 @@
using Bahla.Domain.Entities;
using Bahla.Domain.Exceptions;
namespace Bahla.Tests.Builders
{
internal class BuilderTests
{
[Test, Description("Test to create a role through builder.")]
public void TestRoleBuilder()
{
var role = Role.Builder
.WithName("Administrator")
.WithValueMask(1 << 0)
.Build();
var clonedRole = Role.Builder
.Clone(role)
.Build();
Assert.Multiple(() =>
{
Assert.That(role.RoleName, Is.EqualTo(clonedRole.RoleName));
Assert.That(role.ValueMask, Is.EqualTo(clonedRole.ValueMask));
Assert.That(role.UUID, Is.EqualTo(clonedRole.UUID));
Assert.That(clonedRole, Is.Not.SameAs(role));
});
}
[Test, Description("Test to create a role with a empty or null name.")]
public void TestRoleBuilderBadName()
{
Assert.Throws<EntityBuilderException>(() =>
{
Role.Builder
.WithName("")
.WithValueMask(1 << 0)
.Build();
});
}
}
}

View File

@@ -0,0 +1,39 @@
using Bahla.Domain.Types;
namespace Bahla.Tests.Entities
{
internal class TypeIdentifierTests
{
const string StringIdentifier = "316f94a7-fb3b-44f0-abe1-88c2a3c99109";
const string StringIdentifier2 = "2c304fa1-19a4-432d-a729-73bdd45586ed";
[Test]
public void TestAssignmentGuid()
{
Identifier firstIdentifier = Guid.Parse(StringIdentifier);
Identifier secondIdentifier = Guid.Parse(StringIdentifier2);
Assert.Multiple(() =>
{
Assert.That(firstIdentifier, Is.Not.EqualTo(secondIdentifier));
Assert.That(firstIdentifier.ToString(), Is.EqualTo(StringIdentifier));
Assert.That(secondIdentifier.ToString(), Is.EqualTo(StringIdentifier2));
});
}
[Test]
public void TestAssignmentString()
{
Identifier firstIdentifier = StringIdentifier;
Identifier secondIdentifier = StringIdentifier2;
Assert.Multiple(() =>
{
Assert.That(firstIdentifier, Is.Not.EqualTo(secondIdentifier));
Assert.That(firstIdentifier.ToString(), Is.EqualTo(StringIdentifier));
Assert.That(secondIdentifier.ToString(), Is.EqualTo(StringIdentifier2));
});
}
}
}

View File

@@ -1,16 +0,0 @@
namespace Bahla.Tests
{
public class Tests
{
[SetUp]
public void Setup()
{
}
[Test]
public void Test1()
{
Assert.Pass();
}
}
}