feat: upgraded to .NET 10 + add API app

This commit is contained in:
Helnesis
2026-05-16 12:16:12 +02:00
parent 6fba038bfb
commit 4115c782f4
33 changed files with 495 additions and 165 deletions

13
.idea/.idea.Bahla.Backend/.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/modules.xml
/projectSettingsUpdater.xml
/.idea.Bahla.Backend.iml
/contentModel.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

1
.idea/.idea.Bahla.Backend/.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
Bahla.Backend

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View File

@@ -0,0 +1,12 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="GrazieInspection" enabled="false" level="GRAMMAR_ERROR" enabled_by_default="false" />
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

6
.idea/.idea.Bahla.Backend/.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

5
Bahla.Backend.slnx Normal file
View File

@@ -0,0 +1,5 @@
<Solution>
<Project Path="src/Bahla.Domain/Bahla.Domain.csproj" />
<Project Path="src/Bahla.Persistence/Bahla.Persistence.csproj" />
<Project Path="src/Bahla.Tests/Bahla.Tests.csproj" />
</Solution>

View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

9
src/Bahla.API/Program.cs Normal file
View File

@@ -0,0 +1,9 @@
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
builder.Services.AddOpenApi();
var app = builder.Build();
app.MapGet("/helloWorld", () => "Hello, World!");

View File

@@ -0,0 +1,12 @@
{
"profiles": {
"Bahla.API": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:63146;http://localhost:63147"
}
}
}

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
using Bahla.Domain.Entities;
using Bahla.Domain.Types;
namespace Bahla.Domain.Builders.Base
{
namespace Bahla.Domain.Builders.Base;
public interface IRoleBuilder : IEntityBuilder<Role>
{
/// <summary>
@@ -34,4 +34,3 @@ namespace Bahla.Domain.Builders.Base
/// <returns>Builder</returns>
IRoleBuilder Clone(Role role);
}
}

View File

@@ -0,0 +1,25 @@
using Bahla.Domain.Entities;
using Bahla.Domain.Enums;
namespace Bahla.Domain.Builders.Base;
public interface IUserBuilder : IEntityBuilder<User>
{
IUserBuilder WithEmail(string email);
IUserBuilder WithFirstname(string firstname);
IUserBuilder WithLastname(string lastname);
IUserBuilder WithRole(Role role);
IUserBuilder WithGender(Gender gender);
IUserBuilder WithBirthday(DateTime birthday);
IUserBuilder WithJoinedOn(DateTime joinedOn);
IUserBuilder WithSalt(IReadOnlyCollection<byte> salt);
IUserBuilder WithVerifier(IReadOnlyCollection<byte> verifier);
}

View File

@@ -0,0 +1,70 @@
using Bahla.Domain.Builders.Base;
using Bahla.Domain.Entities;
using Bahla.Domain.Enums;
using Bahla.Domain.Types;
namespace Bahla.Domain.Builders;
public sealed class UserBuilder : IUserBuilder
{
internal Identifier _identifier;
internal string _email;
internal string _firstname;
internal string _lastname;
internal Role _role;
internal Gender _gender;
internal DateTime _birthday;
internal DateTime _joinedDate;
internal IReadOnlyCollection<byte> _salt;
internal IReadOnlyCollection<byte> _verifier;
public User Build()
{
throw new NotImplementedException();
}
public IUserBuilder WithEmail(string email)
{
throw new NotImplementedException();
}
public IUserBuilder WithFirstname(string firstname)
{
throw new NotImplementedException();
}
public IUserBuilder WithLastname(string lastname)
{
throw new NotImplementedException();
}
public IUserBuilder WithRole(Role role)
{
throw new NotImplementedException();
}
public IUserBuilder WithGender(Gender gender)
{
throw new NotImplementedException();
}
public IUserBuilder WithBirthday(DateTime birthday)
{
throw new NotImplementedException();
}
public IUserBuilder WithJoinedOn(DateTime joinedOn)
{
throw new NotImplementedException();
}
public IUserBuilder WithSalt(IReadOnlyCollection<byte> salt)
{
throw new NotImplementedException();
}
public IUserBuilder WithVerifier(IReadOnlyCollection<byte> verifier)
{
throw new NotImplementedException();
}
}

View File

@@ -9,7 +9,7 @@ namespace Bahla.Domain.Entities
{
public string RoleName { get; init; }
public uint ValueMask { get; init; }
internal Role(Identifier identifier, string roleName, uint valueMask) : base(identifier)
private Role(Identifier identifier, string roleName, uint valueMask) : base(identifier)
{
RoleName = roleName;
ValueMask = valueMask;

View File

@@ -0,0 +1,52 @@
using Bahla.Domain.Builders;
using Bahla.Domain.Builders.Base;
using Bahla.Domain.Enums;
using Bahla.Domain.Types;
using Bahla.Domain.Entities.Base;
namespace Bahla.Domain.Entities;
public sealed record User : Entity
{
public string Email { get; init; }
public string Firstname { get; init; }
public string Lastname { get; init; }
public Role Role { get; init; }
public Gender Gender { get; init; }
public DateTime Birthday { get; init; }
public DateTime JoinedAt { get; init; }
public IReadOnlyCollection<byte> Salt { get; init; }
public IReadOnlyCollection<byte> Verifier { get; init; }
private User(Identifier identifier, string email, string firstname, string lastname, Role role, Gender gender, DateTime birthday, DateTime joinedAt, IReadOnlyCollection<byte> salt, IReadOnlyCollection<byte> verifier) : base(identifier)
{
Email = email;
Firstname = firstname;
Lastname = lastname;
Role = role;
Gender = gender;
Birthday = birthday;
JoinedAt = joinedAt;
Salt = salt;
Verifier = verifier;
}
internal User(UserBuilder builder) : base(builder._identifier)
{
Email = builder._email;
Firstname = builder._firstname;
Lastname = builder._lastname;
Role = builder._role;
Gender = builder._gender;
Birthday = builder._birthday;
JoinedAt = builder._joinedDate;
Salt = builder._salt;
Verifier = builder._verifier;
}
/// <summary>
/// Returns the builder
/// </summary>
public static IUserBuilder Builder => new UserBuilder();
}

View File

@@ -0,0 +1,2 @@
namespace Bahla.Domain.Enums;
public enum Gender { Male, Female, }

View File

@@ -3,10 +3,10 @@ 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
public sealed class AndNotSpecification<T>(ISpecification<T> left, ISpecification<T> right) : CompositeSpecification<T> where T : class
{
private readonly IEntitySpecification<T> _left = left;
private readonly IEntitySpecification<T> _right = right;
private readonly ISpecification<T> _left = left;
private readonly ISpecification<T> _right = right;
public override bool IsSatisfiedBy(T entity)
=> _left.IsSatisfiedBy(entity) && !_right.IsSatisfiedBy(entity);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,44 +0,0 @@
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,72 @@
using Bahla.Domain.Entities;
using Bahla.Domain.Exceptions;
namespace Bahla.Tests.Builders
{
internal class RoleBuilderTest
{
private const string R1Name = "Organisateur";
private const int R1ValueMask = 1 << 0;
private const string R2Name = "Animateur";
private const int R2ValueMask = 1 << 1;
private const string R3Name = "Bahla Bot";
private const int R3ValueMask = 1 << 2;
private const string R4Name = "Utilisateur";
private const int R4ValueMask = 1 << 3;
[Test, Description("Create 4 roles through builder and check if they're well created")]
public void TestRoleBuilder()
{
var r1 = Role.Builder
.WithName(R1Name)
.WithValueMask(R1ValueMask)
.Build();
var r2 = Role.Builder
.WithName(R2Name)
.WithValueMask(R2ValueMask)
.Build();
var r3 = Role.Builder
.WithName(R3Name)
.WithValueMask(R3ValueMask)
.Build();
var r4 = Role.Builder
.WithName(R4Name)
.WithValueMask(R4ValueMask)
.Build();
Assert.Multiple(() =>
{
Assert.That(r1.RoleName, Is.EqualTo(R1Name));
Assert.That(r1.ValueMask, Is.EqualTo(R1ValueMask));
Assert.That(r2.RoleName, Is.EqualTo(R2Name));
Assert.That(r2.ValueMask, Is.EqualTo(R2ValueMask));
Assert.That(r3.RoleName, Is.EqualTo(R3Name));
Assert.That(r3.ValueMask, Is.EqualTo(R3ValueMask));
Assert.That(r4.RoleName, Is.EqualTo(R4Name));
Assert.That(r4.ValueMask, Is.EqualTo(R4ValueMask));
});
}
[Test, Description("Create a role with an empty name and check if the builder throws a specification exception")]
public void TestRoleBuilderSpecification()
{
Assert.Throws<EntityBuilderException>(() =>
{
var r1 = Role.Builder
.Build();
});
}
}
}

View File

@@ -0,0 +1,94 @@
using System.Runtime.InteropServices.JavaScript;
using Bahla.Domain.Builders;
using Bahla.Domain.Entities;
using Bahla.Domain.Enums;
using Bahla.Domain.Types;
namespace Bahla.Tests;
public class TestDataProvider
{
public static readonly Identifier U1 = "d627dfe8-7896-483e-a78d-d5b5832a5543";
public static readonly Identifier U2 = "0d665df1-056d-4038-9d57-5999cb2bf72f";
public static readonly Identifier U3 = "066f7e1a-42ca-4139-9b2e-a6faeb880738";
public static readonly Identifier U4 = "081b9fe4-6c84-449b-9bdd-08c23b4b5e78";
public static Dictionary<Identifier, User> Users = new()
{
{
U1,
User.Builder
.WithEmail("john.doe@mail.com")
.WithFirstname("John")
.WithLastname("Doe")
.WithRole(Role.Builder
.WithName("Animateur/Animatrice")
.WithValueMask(1 << 1)
.Build())
.WithGender(Gender.Male)
.WithBirthday(new DateTime(1990, 1, 1))
.WithJoinedOn(DateTime.Now
.AddDays(5))
.WithSalt([0x1, 0x2, 0x3, 0x4])
.WithVerifier([0x5, 0x6, 0x7, 0x8])
.Build()
},
{
U2,
User.Builder
.WithEmail("jane.doe@mail.com")
.WithFirstname("Jane")
.WithLastname("Doe")
.WithRole(Role.Builder
.WithName("Organisateur/Organisatrice")
.WithValueMask(1 << 2)
.Build())
.WithGender(Gender.Female)
.WithBirthday(new DateTime(1994, 3, 28))
.WithJoinedOn(DateTime.Now
.AddDays(10))
.WithSalt([0x10, 0x30, 0x40, 0x50])
.WithVerifier([0xA, 0xB, 0xC, 0xD])
.Build()
},
{
U3,
User.Builder
.WithEmail("alice.bob@mail.com")
.WithFirstname("Alice")
.WithLastname("Bob")
.WithRole(Role.Builder
.WithName("Usager/Usagère")
.WithValueMask(1 << 0)
.Build())
.WithGender(Gender.Female)
.WithBirthday(new DateTime(1998, 14, 04))
.WithJoinedOn(DateTime.Now
.AddMonths(2))
.WithSalt([0xB, 0xE, 0xE, 0xF])
.WithVerifier([0xC, 0xA, 0xF, 0xE, 0xE])
.Build()
},
{
U4,
User.Builder
.WithEmail("max.dupont@mail.com")
.WithFirstname("Max")
.WithLastname("Dupont")
.WithGender(Gender.Male)
.WithRole(Role.Builder
.WithName("Usager/Usagère")
.WithValueMask(1 << 0)
.Build())
.WithBirthday(new DateTime(1999, 12, 31))
.WithJoinedOn(DateTime.Now
.AddYears(2))
.WithSalt([0xF, 0xF])
.WithVerifier([0xE, 0xA, 0xF, 0xD])
.Build()
}
};
}

View File

@@ -1,6 +1,6 @@
using Bahla.Domain.Types;
namespace Bahla.Tests.Entities
namespace Bahla.Tests.Types
{
internal class TypeIdentifierTests
{