diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0bb609c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,32 @@
+# .NET
+dotnet/**/bin/
+dotnet/**/obj/
+dotnet/**/*.user
+dotnet/**/*.vs/
+dotnet/**/.vs/
+
+# React/Node
+react/node_modules/
+react/dist/
+react/.env
+react/.env.local
+react/.env.*.local
+
+# IDE
+.idea/
+.vscode/
+*.suo
+*.ntvs*
+*.njsproj
+*.sln.docstates
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
+npm-debug.log*
+
+# Claude Code plans
+.claude/
diff --git a/dotnet/.gitignore b/dotnet/.gitignore
new file mode 100644
index 0000000..d8ac706
--- /dev/null
+++ b/dotnet/.gitignore
@@ -0,0 +1,77 @@
+# Environment variables with secrets
+.env
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio cache/options directory
+.vs/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# NuGet Packages
+*.nupkg
+*.snupkg
+**/packages/*
+!**/packages/build/
+*.nuget.props
+*.nuget.targets
+
+# Docker
+**/.dockerignore
diff --git a/dotnet/AipsCore/AipsCore.csproj b/dotnet/AipsCore/AipsCore.csproj
new file mode 100644
index 0000000..0e42146
--- /dev/null
+++ b/dotnet/AipsCore/AipsCore.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/dotnet/AipsCore/Application/Abstract/Command/ICommand.cs b/dotnet/AipsCore/Application/Abstract/Command/ICommand.cs
new file mode 100644
index 0000000..91f1225
--- /dev/null
+++ b/dotnet/AipsCore/Application/Abstract/Command/ICommand.cs
@@ -0,0 +1,5 @@
+namespace AipsCore.Application.Abstract.Command;
+
+public interface ICommand {}
+
+public interface ICommand {}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Application/Abstract/Command/ICommandHandler.cs b/dotnet/AipsCore/Application/Abstract/Command/ICommandHandler.cs
new file mode 100644
index 0000000..06e69dc
--- /dev/null
+++ b/dotnet/AipsCore/Application/Abstract/Command/ICommandHandler.cs
@@ -0,0 +1,13 @@
+namespace AipsCore.Application.Abstract.Command;
+
+public interface ICommandHandler
+ where TCommand : ICommand
+{
+ Task Handle(TCommand command, CancellationToken cancellationToken = default);
+}
+
+public interface ICommandHandler
+ where TCommand : ICommand
+{
+ Task Handle(TCommand command, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Application/Abstract/Query/IQuery.cs b/dotnet/AipsCore/Application/Abstract/Query/IQuery.cs
new file mode 100644
index 0000000..bbb3de3
--- /dev/null
+++ b/dotnet/AipsCore/Application/Abstract/Query/IQuery.cs
@@ -0,0 +1,3 @@
+namespace AipsCore.Application.Abstract.Query;
+
+public interface IQuery {}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Application/Abstract/Query/IQueryHandler.cs b/dotnet/AipsCore/Application/Abstract/Query/IQueryHandler.cs
new file mode 100644
index 0000000..d3e8632
--- /dev/null
+++ b/dotnet/AipsCore/Application/Abstract/Query/IQueryHandler.cs
@@ -0,0 +1,7 @@
+namespace AipsCore.Application.Abstract.Query;
+
+public interface IQueryHandler
+ where TQuery : IQuery
+{
+ Task HandleAsync(TQuery query, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Abstract/Rule/AbstractRule.cs b/dotnet/AipsCore/Domain/Abstract/Rule/AbstractRule.cs
new file mode 100644
index 0000000..d1f2530
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Abstract/Rule/AbstractRule.cs
@@ -0,0 +1,18 @@
+using AipsCore.Domain.Common.Validation;
+
+namespace AipsCore.Domain.Abstract.Rule;
+
+public abstract class AbstractRule : IRule
+{
+ protected abstract string ErrorCode { get; }
+ protected abstract string ErrorMessage { get; }
+
+ public string ValueObjectName { protected get; set; } = "Unknown";
+
+ public ValidationError GetError()
+ {
+ return new ValidationError(ErrorCode, ErrorMessage);
+ }
+
+ public abstract bool Validate();
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Abstract/Rule/IRule.cs b/dotnet/AipsCore/Domain/Abstract/Rule/IRule.cs
new file mode 100644
index 0000000..c77ba62
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Abstract/Rule/IRule.cs
@@ -0,0 +1,10 @@
+using AipsCore.Domain.Common.Validation;
+
+namespace AipsCore.Domain.Abstract.Rule;
+
+public interface IRule
+{
+ ValidationError GetError();
+ bool Validate();
+ string ValueObjectName { set; }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Abstract/ValueObject/AbstractValueObject.cs b/dotnet/AipsCore/Domain/Abstract/ValueObject/AbstractValueObject.cs
new file mode 100644
index 0000000..910fa2f
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Abstract/ValueObject/AbstractValueObject.cs
@@ -0,0 +1,24 @@
+using AipsCore.Domain.Abstract.Rule;
+using AipsCore.Domain.Common.Validation;
+
+namespace AipsCore.Domain.Abstract.ValueObject;
+
+public abstract record AbstractValueObject
+{
+ protected abstract ICollection GetValidationRules();
+
+ protected void Validate()
+ {
+ var rules = GetValidationRules();
+ var validator = new Validator(rules, ValueObjectName);
+
+ validator.Validate();
+
+ if (!validator.Success)
+ {
+ throw validator.GetValidationException();
+ }
+ }
+
+ private string ValueObjectName => this.GetType().Name;
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Common/Validation/Rules/CharsetRule.cs b/dotnet/AipsCore/Domain/Common/Validation/Rules/CharsetRule.cs
new file mode 100644
index 0000000..ef2cf6a
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Common/Validation/Rules/CharsetRule.cs
@@ -0,0 +1,31 @@
+using AipsCore.Domain.Abstract;
+using AipsCore.Domain.Abstract.Rule;
+
+namespace AipsCore.Domain.Common.Validation.Rules;
+
+public class CharsetRule : AbstractRule
+{
+ private readonly string _stringValue;
+ private readonly char[] _charset;
+
+ protected CharsetRule(string stringValue, char[] charset)
+ {
+ _stringValue = stringValue;
+ _charset = charset;
+ }
+
+ protected override string ErrorCode => "charset";
+ protected override string ErrorMessage => $"Forbidden characters in '{ValueObjectName}'";
+
+ public override bool Validate()
+ {
+ foreach (char character in _stringValue)
+ {
+ if (!_charset.Contains(character))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Common/Validation/Rules/EmailRule.cs b/dotnet/AipsCore/Domain/Common/Validation/Rules/EmailRule.cs
new file mode 100644
index 0000000..a7b34c5
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Common/Validation/Rules/EmailRule.cs
@@ -0,0 +1,32 @@
+using System.Net.Mail;
+using AipsCore.Domain.Abstract;
+using AipsCore.Domain.Abstract.Rule;
+
+namespace AipsCore.Domain.Common.Validation.Rules;
+
+public class EmailRule : AbstractRule
+{
+ protected override string ErrorCode => "email_invalid";
+ protected override string ErrorMessage => "Email is not in the valid format";
+
+ private readonly string _emailValue;
+
+ public EmailRule(string emailValue)
+ {
+ _emailValue = emailValue;
+ }
+
+ public override bool Validate()
+ {
+ try
+ {
+ _ = new MailAddress(_emailValue);
+ }
+ catch
+ {
+ return false;
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Common/Validation/Rules/ExactLength.cs b/dotnet/AipsCore/Domain/Common/Validation/Rules/ExactLength.cs
new file mode 100644
index 0000000..4e884df
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Common/Validation/Rules/ExactLength.cs
@@ -0,0 +1,24 @@
+using AipsCore.Domain.Abstract;
+using AipsCore.Domain.Abstract.Rule;
+
+namespace AipsCore.Domain.Common.Validation.Rules;
+
+public class ExactLength : AbstractRule
+{
+ private readonly string _stringValue;
+ private readonly int _exactLentgh;
+ protected override string ErrorCode => "exact_length";
+ protected override string ErrorMessage
+ => $"Length of '{ValueObjectName}' must be {_exactLentgh} characters";
+
+ public ExactLength(string stringValue, int exactLentgh)
+ {
+ _stringValue = stringValue;
+ _exactLentgh = exactLentgh;
+ }
+
+ public override bool Validate()
+ {
+ return _stringValue.Length == _exactLentgh;
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Common/Validation/Rules/MaxLengthRule.cs b/dotnet/AipsCore/Domain/Common/Validation/Rules/MaxLengthRule.cs
new file mode 100644
index 0000000..798e177
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Common/Validation/Rules/MaxLengthRule.cs
@@ -0,0 +1,24 @@
+using AipsCore.Domain.Abstract;
+using AipsCore.Domain.Abstract.Rule;
+
+namespace AipsCore.Domain.Common.Validation.Rules;
+
+public class MaxLengthRule : AbstractRule
+{
+ private readonly string _stringValue;
+ private readonly int _maximumLentgh;
+ protected override string ErrorCode => "minimum_length";
+ protected override string ErrorMessage
+ => $"Length of '{ValueObjectName}' must be at most {_maximumLentgh} characters";
+
+ public MaxLengthRule(string stringValue, int maximumLentgh)
+ {
+ _stringValue = stringValue;
+ _maximumLentgh = maximumLentgh;
+ }
+
+ public override bool Validate()
+ {
+ return _stringValue.Length <= _maximumLentgh;
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Common/Validation/Rules/MinLengthRule.cs b/dotnet/AipsCore/Domain/Common/Validation/Rules/MinLengthRule.cs
new file mode 100644
index 0000000..2f9fd1d
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Common/Validation/Rules/MinLengthRule.cs
@@ -0,0 +1,24 @@
+using AipsCore.Domain.Abstract;
+using AipsCore.Domain.Abstract.Rule;
+
+namespace AipsCore.Domain.Common.Validation.Rules;
+
+public class MinLengthRule : AbstractRule
+{
+ private readonly string _stringValue;
+ private readonly int _minimumLentgh;
+ protected override string ErrorCode => "minimum_length";
+ protected override string ErrorMessage
+ => $"Length of '{ValueObjectName}' must be at least {_minimumLentgh} characters";
+
+ public MinLengthRule(string stringValue, int minimumLentgh)
+ {
+ _stringValue = stringValue;
+ _minimumLentgh = minimumLentgh;
+ }
+
+ public override bool Validate()
+ {
+ return _stringValue.Length >= _minimumLentgh;
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Common/Validation/ValidationError.cs b/dotnet/AipsCore/Domain/Common/Validation/ValidationError.cs
new file mode 100644
index 0000000..bab4969
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Common/Validation/ValidationError.cs
@@ -0,0 +1,3 @@
+namespace AipsCore.Domain.Common.Validation;
+
+public record ValidationError(string Code, string Message);
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Common/Validation/ValidationException.cs b/dotnet/AipsCore/Domain/Common/Validation/ValidationException.cs
new file mode 100644
index 0000000..6378716
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Common/Validation/ValidationException.cs
@@ -0,0 +1,13 @@
+using System.Collections.Immutable;
+
+namespace AipsCore.Domain.Common.Validation;
+
+public class ValidationException : Exception
+{
+ public ICollection ValidationErrors { get; private init; }
+
+ public ValidationException(ICollection validationErrors)
+ {
+ ValidationErrors = validationErrors;
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Common/Validation/Validator.cs b/dotnet/AipsCore/Domain/Common/Validation/Validator.cs
new file mode 100644
index 0000000..34f0072
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Common/Validation/Validator.cs
@@ -0,0 +1,55 @@
+using AipsCore.Domain.Abstract;
+using AipsCore.Domain.Abstract.Rule;
+
+namespace AipsCore.Domain.Common.Validation;
+
+public class Validator
+{
+ private readonly ICollection _validationRules;
+ private readonly string _valueObjectName;
+
+ public Validator(ICollection validationRules, string valueObjectName)
+ {
+ _validationRules = validationRules;
+ _valueObjectName = valueObjectName;
+ }
+
+ public bool Success { get; private set; } = false;
+
+ private ValidationException? _validationException = null;
+
+ public ValidationException GetValidationException()
+ {
+ if (_validationException is null)
+ {
+ return new ValidationException([]);
+ }
+ return _validationException;
+ }
+
+ public void Validate()
+ {
+ List errors = [];
+
+ foreach (var validationRule in _validationRules)
+ {
+ validationRule.ValueObjectName = _valueObjectName;
+
+ if (!validationRule.Validate())
+ {
+ errors.Add(validationRule.GetError());
+ }
+ }
+
+ if (errors.Any())
+ {
+ Success = false;
+ _validationException = new ValidationException(errors.ToArray());
+ }
+ else
+ {
+ Success = true;
+ _validationException = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Common/ValueObjects/DomainId.cs b/dotnet/AipsCore/Domain/Common/ValueObjects/DomainId.cs
new file mode 100644
index 0000000..6f76d85
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Common/ValueObjects/DomainId.cs
@@ -0,0 +1,22 @@
+using AipsCore.Domain.Abstract;
+using AipsCore.Domain.Abstract.Rule;
+using AipsCore.Domain.Abstract.ValueObject;
+using AipsCore.Domain.Common.Validation;
+
+namespace AipsCore.Domain.Common.ValueObjects;
+
+public record DomainId : AbstractValueObject
+{
+ public string IdValue { get; init; }
+
+ public DomainId(string IdValue)
+ {
+ this.IdValue = IdValue;
+ Validate();
+ }
+
+ protected override ICollection GetValidationRules()
+ {
+ return [];
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Common/ValueObjects/Email.cs b/dotnet/AipsCore/Domain/Common/ValueObjects/Email.cs
new file mode 100644
index 0000000..3da6daf
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Common/ValueObjects/Email.cs
@@ -0,0 +1,26 @@
+using AipsCore.Domain.Abstract;
+using AipsCore.Domain.Abstract.Rule;
+using AipsCore.Domain.Abstract.ValueObject;
+using AipsCore.Domain.Common.Validation;
+using AipsCore.Domain.Common.Validation.Rules;
+
+namespace AipsCore.Domain.Common.ValueObjects;
+
+public record Email : AbstractValueObject
+{
+ public string EmailValue { get; init; }
+
+ public Email(string EmailValue)
+ {
+ this.EmailValue = EmailValue;
+ Validate();
+ }
+
+ protected override ICollection GetValidationRules()
+ {
+ return
+ [
+ new EmailRule(EmailValue)
+ ];
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Models/User/User.cs b/dotnet/AipsCore/Domain/Models/User/User.cs
new file mode 100644
index 0000000..1ca404c
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Models/User/User.cs
@@ -0,0 +1,18 @@
+using AipsCore.Domain.Common.ValueObjects;
+using AipsCore.Domain.Models.User.ValueObjects;
+
+namespace AipsCore.Domain.Models.User;
+
+public class User
+{
+ public UserId Id { get; private set; }
+ public Email Email { get; private set; }
+ public Username Username { get; private set; }
+
+ public User(UserId id, Email email, Username username)
+ {
+ Id = id;
+ Email = email;
+ Username = username;
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Models/User/Validation/UsernameCharsetRule.cs b/dotnet/AipsCore/Domain/Models/User/Validation/UsernameCharsetRule.cs
new file mode 100644
index 0000000..59fa851
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Models/User/Validation/UsernameCharsetRule.cs
@@ -0,0 +1,26 @@
+using AipsCore.Domain.Common.Validation.Rules;
+using AipsCore.Utility.Text;
+
+namespace AipsCore.Domain.Models.User.Validation;
+
+public class UsernameCharsetRule : CharsetRule
+{
+ public UsernameCharsetRule(string stringValue)
+ : base(stringValue, GetUsernameCharset())
+ {
+ }
+
+ private static char[] GetUsernameCharset()
+ {
+ var alphanumericCharset = Charset.GetAlphanumericCharset();
+
+ char[] usernameCharset = [..alphanumericCharset, '_'];
+
+ return usernameCharset;
+ }
+
+ protected override string ErrorCode => "username_charset";
+
+ protected override string ErrorMessage =>
+ "Username contains invalid characters, only alphanumeric characters and '_' are allowed";
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Models/User/ValueObjects/UserId.cs b/dotnet/AipsCore/Domain/Models/User/ValueObjects/UserId.cs
new file mode 100644
index 0000000..022011e
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Models/User/ValueObjects/UserId.cs
@@ -0,0 +1,5 @@
+using AipsCore.Domain.Common.ValueObjects;
+
+namespace AipsCore.Domain.Models.User.ValueObjects;
+
+public record UserId(string IdValue) : DomainId(IdValue);
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Models/User/ValueObjects/Username.cs b/dotnet/AipsCore/Domain/Models/User/ValueObjects/Username.cs
new file mode 100644
index 0000000..8b2f6f1
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Models/User/ValueObjects/Username.cs
@@ -0,0 +1,33 @@
+using AipsCore.Domain.Abstract;
+using AipsCore.Domain.Abstract.Rule;
+using AipsCore.Domain.Abstract.ValueObject;
+using AipsCore.Domain.Common.Validation;
+using AipsCore.Domain.Common.Validation.Rules;
+using AipsCore.Domain.Common.ValueObjects;
+using AipsCore.Domain.Models.User.Validation;
+
+namespace AipsCore.Domain.Models.User.ValueObjects;
+
+public record Username : AbstractValueObject
+{
+ public string UsernameValue { get; init; }
+
+ public Username(string UsernameValue)
+ {
+ this.UsernameValue = UsernameValue;
+ Validate();
+ }
+
+ private const int MinimumLength = 8;
+ private const int MaximumLength = 20;
+
+ protected override ICollection GetValidationRules()
+ {
+ return
+ [
+ new MinLengthRule(UsernameValue, MinimumLength),
+ new MaxLengthRule(UsernameValue, MaximumLength),
+ new UsernameCharsetRule(UsernameValue)
+ ];
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Models/Whiteboard/Validation/WhitebordCodeCharsetRule.cs b/dotnet/AipsCore/Domain/Models/Whiteboard/Validation/WhitebordCodeCharsetRule.cs
new file mode 100644
index 0000000..2234acc
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Models/Whiteboard/Validation/WhitebordCodeCharsetRule.cs
@@ -0,0 +1,20 @@
+using AipsCore.Domain.Common.Validation.Rules;
+using AipsCore.Utility.Text;
+
+namespace AipsCore.Domain.Models.Whiteboard.Validation;
+
+public class WhitebordCodeCharsetRule : CharsetRule
+{
+ public WhitebordCodeCharsetRule(string stringValue)
+ : base(stringValue, GetWhiteboardCodeCharset())
+ {
+ }
+
+ private static char[] GetWhiteboardCodeCharset()
+ {
+ return Charset.GetNumericCharset();
+ }
+
+ protected override string ErrorCode => "whiteboard_code_charset";
+ protected override string ErrorMessage => "Whiteboard code must contain only numbers";
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Models/Whiteboard/ValueObjects/WhiteboardCode.cs b/dotnet/AipsCore/Domain/Models/Whiteboard/ValueObjects/WhiteboardCode.cs
new file mode 100644
index 0000000..3e7b30a
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Models/Whiteboard/ValueObjects/WhiteboardCode.cs
@@ -0,0 +1,31 @@
+using AipsCore.Domain.Abstract;
+using AipsCore.Domain.Abstract.Rule;
+using AipsCore.Domain.Abstract.ValueObject;
+using AipsCore.Domain.Common.Validation;
+using AipsCore.Domain.Common.Validation.Rules;
+using AipsCore.Domain.Common.ValueObjects;
+using AipsCore.Domain.Models.Whiteboard.Validation;
+
+namespace AipsCore.Domain.Models.Whiteboard.ValueObjects;
+
+public record WhiteboardCode : AbstractValueObject
+{
+ public string CodeValue { get; init; }
+
+ public WhiteboardCode(string CodeValue)
+ {
+ this.CodeValue = CodeValue;
+ Validate();
+ }
+
+ private const int CodeLength = 8;
+
+ protected override ICollection GetValidationRules()
+ {
+ return
+ [
+ new ExactLength(CodeValue, CodeLength),
+ new WhitebordCodeCharsetRule(CodeValue)
+ ];
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Models/Whiteboard/ValueObjects/WhiteboardId.cs b/dotnet/AipsCore/Domain/Models/Whiteboard/ValueObjects/WhiteboardId.cs
new file mode 100644
index 0000000..a9a46aa
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Models/Whiteboard/ValueObjects/WhiteboardId.cs
@@ -0,0 +1,5 @@
+using AipsCore.Domain.Common.ValueObjects;
+
+namespace AipsCore.Domain.Models.Whiteboard.ValueObjects;
+
+public record WhiteboardId(string IdValue) : DomainId(IdValue);
\ No newline at end of file
diff --git a/dotnet/AipsCore/Domain/Models/Whiteboard/Whiteboard.cs b/dotnet/AipsCore/Domain/Models/Whiteboard/Whiteboard.cs
new file mode 100644
index 0000000..1fbb366
--- /dev/null
+++ b/dotnet/AipsCore/Domain/Models/Whiteboard/Whiteboard.cs
@@ -0,0 +1,18 @@
+using AipsCore.Domain.Models.User.ValueObjects;
+using AipsCore.Domain.Models.Whiteboard.ValueObjects;
+
+namespace AipsCore.Domain.Models.Whiteboard;
+
+public class Whiteboard
+{
+ public WhiteboardId Id { get; private set; }
+ public UserId WhiteboardOwnerId { get; private set; }
+ public WhiteboardCode Code { get; private set; }
+
+ public Whiteboard(WhiteboardId id, User.User whiteboardOwner, WhiteboardCode code)
+ {
+ Id = id;
+ WhiteboardOwnerId = whiteboardOwner.Id;
+ Code = code;
+ }
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Infrastructure/Db/AipsDbContext.cs b/dotnet/AipsCore/Infrastructure/Db/AipsDbContext.cs
new file mode 100644
index 0000000..ab85fb3
--- /dev/null
+++ b/dotnet/AipsCore/Infrastructure/Db/AipsDbContext.cs
@@ -0,0 +1,6 @@
+namespace AipsCore.Infrastructure.Db;
+
+public class AipsDbContext
+{
+
+}
\ No newline at end of file
diff --git a/dotnet/AipsCore/Utility/Text/Charset.cs b/dotnet/AipsCore/Utility/Text/Charset.cs
new file mode 100644
index 0000000..ab91cb3
--- /dev/null
+++ b/dotnet/AipsCore/Utility/Text/Charset.cs
@@ -0,0 +1,41 @@
+namespace AipsCore.Utility.Text;
+
+public static class Charset
+{
+ public static char[] GetNumericCharset()
+ {
+ var charset = new List();
+
+ // 0–9
+ for (char c = '0'; c <= '9'; c++)
+ charset.Add(c);
+
+ return charset.ToArray();
+ }
+
+ public static char[] GetLettersCharset()
+ {
+
+ var charset = new List();
+
+ // a–z
+ for (char c = 'a'; c <= 'z'; c++)
+ charset.Add(c);
+
+ // A–Z
+ for (char c = 'A'; c <= 'Z'; c++)
+ charset.Add(c);
+
+ return charset.ToArray();
+ }
+
+ public static char[] GetAlphanumericCharset()
+ {
+ var lettersCharset = GetLettersCharset();
+ var numericCharset = GetNumericCharset();
+
+ char[] alphanumericCharset = [..numericCharset, ..lettersCharset];
+
+ return alphanumericCharset;
+ }
+}
\ No newline at end of file
diff --git a/dotnet/dotnet.sln b/dotnet/dotnet.sln
new file mode 100644
index 0000000..6c32bfc
--- /dev/null
+++ b/dotnet/dotnet.sln
@@ -0,0 +1,18 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E0259F3A-496C-4A5E-8D6D-D134F2A9E3D4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AipsCore", "AipsCore\AipsCore.csproj", "{C2EAA516-A045-4525-9A47-4AA8198FD104}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {C2EAA516-A045-4525-9A47-4AA8198FD104}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C2EAA516-A045-4525-9A47-4AA8198FD104}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C2EAA516-A045-4525-9A47-4AA8198FD104}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C2EAA516-A045-4525-9A47-4AA8198FD104}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal