Updated the domain model with entities, repos and domain services. Looks good. I think I addressed all the user stories however if I fell short we'll revise as needed.

This commit was merged in pull request #6.
This commit is contained in:
2026-04-10 17:43:58 +05:00
parent 57040bc656
commit 8eb8cd4fbf
14 changed files with 621 additions and 6 deletions
+65
View File
@@ -0,0 +1,65 @@
namespace LibraryApp.Domain.Entities;
public class Book
{
public BookId Id { get; private set; }
public BookTitle Title { get; private set; }
public BookAuthor Author { get; private set; }
public Book(BookTitle title, BookAuthor author)
: this(new BookId(string.Empty), title, author)
{ }
public Book(BookId id, BookTitle title, BookAuthor author)
{
Id = id;
Title = title;
Author = author;
}
public Book UpdateTitle(string newTitle)
{
Title = new (newTitle);
return this;
}
public Book UpdateAuthor(string newAuthor)
{
Author = new (newAuthor);
return this;
}
}
public record BookId : DbId
{
public BookId(string id) : base(id) {}
}
public record BookTitle
{
public string Title { get; init; }
public BookTitle(string title)
{
if(string.IsNullOrWhiteSpace(title))
{
throw new ArgumentException("Title cannot be empty.");
}
Title = title;
}
}
public record BookAuthor
{
public string Author { get; init; }
public BookAuthor(string author)
{
if(string.IsNullOrWhiteSpace(author))
{
throw new ArgumentException("Author cannot be empty.");
}
Author = author;
}
}
@@ -0,0 +1,99 @@
namespace LibraryApp.Domain.Entities;
public enum BookInstanceStatus
{
OnShelf,
Lent,
PutAside,
Lost
}
public class BookInstance
{
public BookInstanceId Id { get; private set; }
public BookId BookId { get; private set; }
public ShelfId ShelfId { get; private set; }
public BookInstanceBarcode Barcode { get; private set; }
public BookInstanceStatus Status { get; private set; }
public BookInstance(BookId bookId, ShelfId shelfId, BookInstanceBarcode barcode)
: this(new BookInstanceId(string.Empty), bookId, shelfId, barcode, BookInstanceStatus.OnShelf)
{ }
public BookInstance(BookInstanceId id, BookId bookId, ShelfId shelfId, BookInstanceBarcode barcode, BookInstanceStatus status)
{
Id = id;
BookId = bookId;
ShelfId = shelfId;
Barcode = barcode;
Status = status;
}
public BookInstance MarkAsLent()
{
if (Status != BookInstanceStatus.OnShelf)
throw new InvalidOperationException($"Cannot lend a book that is not on the shelf. Current status: {Status}.");
Status = BookInstanceStatus.Lent;
return this;
}
public BookInstance ReturnToShelf()
{
if (Status != BookInstanceStatus.Lent)
throw new InvalidOperationException($"Cannot return a book that is not lent out. Current status: {Status}.");
Status = BookInstanceStatus.OnShelf;
return this;
}
public BookInstance MarkAsPutAside()
{
if (Status != BookInstanceStatus.OnShelf)
throw new InvalidOperationException($"Cannot put aside a book that is not on the shelf. Current status: {Status}.");
Status = BookInstanceStatus.PutAside;
return this;
}
public BookInstance PlaceOnShelf(ShelfId shelfId)
{
if (Status != BookInstanceStatus.PutAside)
throw new InvalidOperationException($"Cannot place on shelf a book that is not put aside. Current status: {Status}.");
ShelfId = shelfId;
Status = BookInstanceStatus.OnShelf;
return this;
}
public BookInstance MarkAsLost()
{
if (Status != BookInstanceStatus.OnShelf)
throw new InvalidOperationException($"Cannot mark as lost a book that is not on the shelf. Current status: {Status}.");
Status = BookInstanceStatus.Lost;
return this;
}
public BookInstance UnmarkAsLost()
{
if (Status != BookInstanceStatus.Lost)
throw new InvalidOperationException($"Cannot unmark as lost a book that is not marked as lost. Current status: {Status}.");
Status = BookInstanceStatus.OnShelf;
return this;
}
}
public record BookInstanceId : DbId
{
public BookInstanceId(string id) : base(id) {}
}
public record BookInstanceBarcode
{
public string Barcode { get; init; }
public BookInstanceBarcode(string barcode)
{
if(string.IsNullOrWhiteSpace(barcode))
{
throw new ArgumentException("Barcode cannot be empty.");
}
Barcode = barcode;
}
}
+72
View File
@@ -0,0 +1,72 @@
namespace LibraryApp.Domain.Entities;
public class LendRecord
{
public LendRecordId Id { get; private set; }
public BookInstanceId BookInstanceId { get; private set; }
public MemberId MemberId { get; private set; }
public LendCode Code { get; private set; }
public DateTime LendDate { get; private set; }
public DateTime? ReturnDate { get; private set; }
public LendRecord(BookInstanceId bookInstanceId, MemberId memberId, LendCode code)
: this(new LendRecordId(string.Empty), bookInstanceId, memberId, code, DateTime.UtcNow, null)
{ }
public LendRecord(LendRecordId id, BookInstanceId bookInstanceId, MemberId memberId, LendCode code, DateTime lendDate, DateTime? returnDate)
{
Id = id;
BookInstanceId = bookInstanceId;
MemberId = memberId;
Code = code;
LendDate = lendDate;
ReturnDate = returnDate;
}
public LendRecord MarkAsReturned()
{
if(ReturnDate != null)
throw new InvalidOperationException("This lend record is already marked as returned.");
ReturnDate = DateTime.UtcNow;
return this;
}
public LendRecord UpdateLendDate(DateTime newLendDate)
{
if(ReturnDate != null)
throw new InvalidOperationException("Cannot update lend date for a record that is already marked as returned.");
LendDate = newLendDate;
return this;
}
public LendRecord UnmarkAsReturned()
{
if(ReturnDate == null)
throw new InvalidOperationException("This lend record is not marked as returned.");
ReturnDate = null;
return this;
}
}
public record LendRecordId : DbId
{
public LendRecordId(string id) : base(id) {}
}
/// <summary>
/// A short, human-friendly code written on the physical lend slip alongside the member's name.
/// Must be unique per lend record and no longer than 8 characters.
/// </summary>
public record LendCode
{
public string Code { get; init; }
public LendCode(string code)
{
if (string.IsNullOrWhiteSpace(code))
throw new ArgumentException("Lend code cannot be empty.");
if (code.Length > 8)
throw new ArgumentException("Lend code cannot exceed 8 characters.");
Code = code.ToUpperInvariant();
}
}
+119
View File
@@ -0,0 +1,119 @@
namespace LibraryApp.Domain.Entities;
public class Member
{
public MemberId Id { get; private set; }
public MemberName Name { get; private set; }
public List<MemberEmail> Emails { get; private set; } = new();
public List<MemberPhone> Phones { get; private set; } = new();
public int PrimaryEmailIndex { get; private set; } = 0;
public int PrimaryPhoneIndex { get; private set; } = 0;
public Member(MemberName name, MemberEmail email, MemberPhone phone)
: this(new MemberId(string.Empty), name, email, phone)
{ }
public Member(MemberId id, MemberName name, MemberEmail email, MemberPhone phone)
{
Id = id;
Name = name;
Emails.Add(email);
Phones.Add(phone);
}
public Member UpdateName(string newName)
{
Name = new (newName);
return this;
}
public Member AddEmail(string email)
{
Emails.Add(new MemberEmail(email));
return this;
}
public Member AddPhone(string phone)
{
Phones.Add(new MemberPhone(phone));
return this;
}
public Member SetPrimaryEmail(MemberEmail email)
{
var index = Emails.FindIndex(e => e.Email == email.Email);
if(index == -1)
throw new ArgumentException("Email not found in member's email list.", nameof(email));
PrimaryEmailIndex = index;
return this;
}
public Member SetPrimaryPhone(MemberPhone phone)
{
var index = Phones.FindIndex(p => p.Phone == phone.Phone);
if(index == -1)
throw new ArgumentException("Phone not found in member's phone list.", nameof(phone));
PrimaryPhoneIndex = index;
return this;
}
}
public record MemberId : DbId
{
public MemberId(string id) : base(id) {}
}
public record MemberEmail
{
public string Email { get; init; }
public MemberEmail(string email)
{
if(string.IsNullOrWhiteSpace(email))
throw new ArgumentException("Email cannot be empty.");
if(!email.Contains("@"))
throw new ArgumentException("Email must contain '@'.");
if(!email.Contains("."))
throw new ArgumentException("Email must contain '.'.");
if(email.StartsWith("@") || email.EndsWith("@"))
throw new ArgumentException("Email cannot start or end with '@'.");
var atCount = email.Count(c => c == '@');
if(atCount > 1)
throw new ArgumentException("Email cannot contain more than one '@'.");
var dotCount = email.Count(c => c == '.');
if(dotCount > 1)
throw new ArgumentException("Email cannot contain more than one '.'.");
Email = email;
}
}
public record MemberName
{
public string Name { get; init; }
public MemberName(string name)
{
if(string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Member name cannot be empty.");
}
Name = name;
}
}
public record MemberPhone
{
private const string VALID_PHONE_PATTERN = @"^\+?[1-9]\d{1,14}$";
public string Phone { get; init; }
public MemberPhone(string phone)
{
if(string.IsNullOrWhiteSpace(phone))
throw new ArgumentException("Phone number cannot be empty.");
if(!System.Text.RegularExpressions.Regex.IsMatch(phone, VALID_PHONE_PATTERN))
throw new ArgumentException("Phone number is not in a valid format.");
Phone = phone;
}
}
+44
View File
@@ -0,0 +1,44 @@
namespace LibraryApp.Domain.Entities;
public class Shelf
{
public ShelfId Id { get; private set; }
public ShelfName Name { get; private set; }
public DateTime? LastCountedAt { get; private set; }
public Shelf(ShelfName name)
: this(new ShelfId(string.Empty), name, null)
{ }
public Shelf(ShelfId id, ShelfName name, DateTime? lastCountedAt)
{
Id = id;
Name = name;
LastCountedAt = lastCountedAt;
}
public Shelf RecordCount()
{
LastCountedAt = DateTime.UtcNow;
return this;
}
}
public record ShelfId : DbId
{
public ShelfId(string id) : base(id) {}
}
public record ShelfName
{
public string Name { get; init; }
public ShelfName(string name)
{
if(string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Shelf name cannot be empty.");
}
Name = name;
}
}