Skip to content

Commit

Permalink
Add Menu Aggregate
Browse files Browse the repository at this point in the history
  • Loading branch information
xavierjohn committed Nov 17, 2022
1 parent 98abe7f commit 01b38b8
Show file tree
Hide file tree
Showing 15 changed files with 249 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

public interface IUserRepository
{
Task<Maybe<User>> GetUserByEmail(string email);
Task<Maybe<User>> GetUserByEmail(string email, CancellationToken cancellationToken);

void Add(User user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public RegisterCommandHandler(IJwtTokenGenerator jwtTokenGenerator, IUserReposit

public ValueTask<Result<AuthenticationResult, ErrorList>> Handle(RegisterCommand request, CancellationToken cancellationToken)
{
return ValidateUserDoesNotExist(request.Email)
return ValidateUserDoesNotExist(request.Email, cancellationToken)
.Bind(email => CreateUser(request))
.Bind(user =>
{
Expand All @@ -39,9 +39,9 @@ private Result<User, ErrorList> CreateUser(RegisterCommand command) =>
User.Create(command.FirstName, command.LastName, command.Email, command.Password)
.Tap(user => _userRepository.Add(user));

private async ValueTask<Result<string, ErrorList>> ValidateUserDoesNotExist(string email)
private async ValueTask<Result<string, ErrorList>> ValidateUserDoesNotExist(string email, CancellationToken cancellationToken)
{
var maybeUser = await _userRepository.GetUserByEmail(email);
var maybeUser = await _userRepository.GetUserByEmail(email, cancellationToken);
if (maybeUser.HasValue)
return Result.Failure<string, ErrorList>(new ErrorList { Errors.User.AlreadyExists(email) });
return Result.Success<string, ErrorList>(email);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public LoginQueryHandler(IJwtTokenGenerator jwtTokenGenerator, IUserRepository u
}

public async ValueTask<Result<AuthenticationResult, ErrorList>> Handle(LoginQuery request, CancellationToken cancellationToken) =>
await _userRepository.GetUserByEmail(request.Email)
await _userRepository.GetUserByEmail(request.Email, cancellationToken)
.ToResult(new ErrorList { Errors.User.DoesNotExist(request.Email) })
.Ensure(user => user.Password == request.Password, new ErrorList { Errors.Authentication.InvalidCredentials })
.Bind(user =>
Expand Down
12 changes: 12 additions & 0 deletions BuberDinner.Domain/src/AggregateRoot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace BuberDinner.Domain
{
using CSharpFunctionalExtensions;

public abstract class AggregateRoot<TId> : Entity<TId>
where TId : notnull
{
protected AggregateRoot(TId id) : base(id)
{
}
}
}
2 changes: 1 addition & 1 deletion BuberDinner.Domain/src/Entities/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class User : Entity<Guid>
public static Result<User, ErrorList> Create(string firstName, string lastName, string email, string password)
{
var user = new User(firstName, lastName, email, password);
return s_validator.Validate(user).ToResult(user);
return s_validator.ValidateToResult(user);
}


Expand Down
30 changes: 30 additions & 0 deletions BuberDinner.Domain/src/Menu/Entities/MenuItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace BuberDinner.Domain.Menu.Entities;
using BuberDinner.Domain.Menu.ValueObject;
using CSharpFunctionalExtensions;
using CSharpFunctionalExtensions.Errors;
using FluentValidation;

public class MenuItem : Entity<MenuItemId>
{
public string Name { get; }
public string Description { get; }

public static Result<MenuItem, ErrorList> Create(string name, string description)
{
MenuItem menuItem = new(MenuItemId.CreateUnique(), name, description);
return s_validator.ValidateToResult(menuItem);
}

private MenuItem(MenuItemId menuItemId, string name, string description)
{
Id = menuItemId;
Name = name;
Description = description;
}

static readonly InlineValidator<MenuItem> s_validator = new()
{
v => v.RuleFor(x => x.Name).NotEmpty(),
v => v.RuleFor(x => x.Description).NotEmpty(),
};
}
34 changes: 34 additions & 0 deletions BuberDinner.Domain/src/Menu/Entities/MenuSection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace BuberDinner.Domain.Menu.Entities;
using BuberDinner.Domain.Menu.ValueObject;
using CSharpFunctionalExtensions;
using CSharpFunctionalExtensions.Errors;
using FluentValidation;

public class MenuSection : Entity<MenuSectionId>
{
public string Name { get; }
public string Description { get; }

public IReadOnlyList<MenuItem> Items => _menuItems.AsReadOnly();

private readonly List<MenuItem> _menuItems = new();

public static Result<MenuSection, ErrorList> Create(string name, string description)
{
MenuSection menuItem = new(MenuSectionId.CreateUnique(), name, description);
return s_validator.ValidateToResult(menuItem);
}

private MenuSection(MenuSectionId menuItemId, string name, string description)
{
Id = menuItemId;
Name = name;
Description = description;
}

static readonly InlineValidator<MenuSection> s_validator = new()
{
v => v.RuleFor(x => x.Name).NotEmpty(),
v => v.RuleFor(x => x.Description).NotEmpty(),
};
}
41 changes: 41 additions & 0 deletions BuberDinner.Domain/src/Menu/Menu.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace BuberDinner.Domain.Menu
{
using System.Collections.Generic;
using BuberDinner.Domain.Menu.Entities;
using BuberDinner.Domain.Menu.ValueObject;
using CSharpFunctionalExtensions.Errors;
using CSharpFunctionalExtensions;
using FluentValidation;

public class Menu : AggregateRoot<MenuId>
{
public string Name { get; }
public string Description { get; }
public decimal AverageRating { get; }
public IReadOnlyList<MenuSection> Section => _menuSections.AsReadOnly();
public HostId HostId { get; }
public IReadOnlyList<DinnerId> DinnerIds => _dinnerIds.AsReadOnly();

private List<MenuSection> _menuSections = new();
private List<DinnerId> _dinnerIds = new();

public static Result<Menu, ErrorList> Create(string name, string description, HostId host)
{
Menu menu = new(MenuId.CreateUnique(), name, description, host);
return s_validator.ValidateToResult(menu);
}

private Menu(MenuId menuId, string name, string description, HostId hostId) : base(menuId)
{
Name = name;
Description = description;
HostId = hostId;
}

static readonly InlineValidator<Menu> s_validator = new()
{
v => v.RuleFor(x => x.Name).NotEmpty(),
v => v.RuleFor(x => x.Description).NotEmpty(),
};
}
}
21 changes: 21 additions & 0 deletions BuberDinner.Domain/src/Menu/ValueObject/DinnerId.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace BuberDinner.Domain.Menu.ValueObject
{
using System;
using CSharpFunctionalExtensions;
using CSharpFunctionalExtensions.Errors;

public class DinnerId : SimpleValueObject<Guid>
{
private DinnerId(Guid value) : base(value)
{
}

public static Result<DinnerId, ErrorList> Create(Guid id)
{
if (id == Guid.Empty)
return Result.Failure<DinnerId, ErrorList>(Error.Validation(nameof(id), "Id cannot be empty"));

return new DinnerId(id);
}
}
}
21 changes: 21 additions & 0 deletions BuberDinner.Domain/src/Menu/ValueObject/HostId.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace BuberDinner.Domain.Menu.ValueObject
{
using System;
using CSharpFunctionalExtensions;
using CSharpFunctionalExtensions.Errors;

public class HostId : SimpleValueObject<Guid>
{
private HostId(Guid value) : base(value)
{
}

public static Result<HostId, ErrorList> Create(Guid id)
{
if (id == Guid.Empty)
return Result.Failure<HostId, ErrorList>(Error.Validation(nameof(id), "Id cannot be empty"));

return new HostId(id);
}
}
}
24 changes: 24 additions & 0 deletions BuberDinner.Domain/src/Menu/ValueObject/MenuId.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace BuberDinner.Domain.Menu.ValueObject
{
using System;
using CSharpFunctionalExtensions;
using CSharpFunctionalExtensions.Errors;

public class MenuId : SimpleValueObject<Guid>
{
private MenuId(Guid value) : base(value)
{
}

public static Result<MenuId, ErrorList> Create(Guid id)
{
if (id == Guid.Empty)
return Result.Failure<MenuId, ErrorList>(Error.Validation(nameof(id), "Id cannot be empty"));

return new MenuId(id);
}

internal static MenuId CreateUnique() =>
new MenuId(Guid.NewGuid());
}
}
23 changes: 23 additions & 0 deletions BuberDinner.Domain/src/Menu/ValueObject/MenuItemId.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace BuberDinner.Domain.Menu.ValueObject
{
using System;
using CSharpFunctionalExtensions;
using CSharpFunctionalExtensions.Errors;

public class MenuItemId : SimpleValueObject<Guid>
{
private MenuItemId(Guid value) : base(value)
{
}

public static Result<MenuItemId, ErrorList> Create(Guid id)
{
if (id == Guid.Empty)
return Result.Failure<MenuItemId, ErrorList>(Error.Validation(nameof(id), "Id cannot be empty"));

return new MenuItemId(id);
}

public static MenuItemId CreateUnique() => new(Guid.NewGuid());
}
}
24 changes: 24 additions & 0 deletions BuberDinner.Domain/src/Menu/ValueObject/MenuSectionId.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace BuberDinner.Domain.Menu.ValueObject
{
using System;
using CSharpFunctionalExtensions;
using CSharpFunctionalExtensions.Errors;

public class MenuSectionId : SimpleValueObject<Guid>
{
private MenuSectionId(Guid value) : base(value)
{
}

public static Result<MenuSectionId, ErrorList> Create(Guid id)
{
if (id == Guid.Empty)
return Result.Failure<MenuSectionId, ErrorList>(Error.Validation(nameof(id), "Id cannot be empty"));

return new MenuSectionId(id);
}

internal static MenuSectionId CreateUnique() =>
new MenuSectionId(Guid.NewGuid());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ internal class UserRepository : IUserRepository

public void Add(User user) => s_users.Add(user);

public Task<Maybe<User>> GetUserByEmail(string email) =>
public Task<Maybe<User>> GetUserByEmail(string email, CancellationToken cancellationToken) =>
Task.FromResult(s_users.SingleOrDefault(u => u.Email == email) ?? Maybe<User>.None);
}
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
# Buber Dinner

## The idea

Allows you to turn your home into a restaurant where...
Just like people turning their homes into hotels via AirBNB.

## Concepts & Tech used

* .NET 7, EF Core
* Clean Architecture & Domain-Driven Design principles
* Common patterns such as CQRS, unit of work, repository, mediator
* Open source libraries such as MediatR, FluentValidation, ErroOr, Throw, Mapster
* Open source libraries such as Mediator, FluentValidation, CSharpFunctionalExtensions, Mapster
* Authentication: JWT tokens

### Clean Architecture
![](readme-assets/clean-architecture-diagram.png)
![](readme-assets/clean-architecture-diagram-2.png)
![](readme-assets/clean-architecture-detailed.png)

![Onion Layers](readme-assets/clean-architecture-diagram.png)
![High level blocks](readme-assets/clean-architecture-diagram-2.png)
![Lower level blocks](readme-assets/clean-architecture-detailed.png)

* The **Domain** and **Application** layers are the focus and therefore the core of the system.
* The Domain layer contains **enterprise logic** and **types**. The application layer contains **business logic** and **types**.
* Infrastructure and Presentation depend on Core, but not on one another.
* The **Domain** layer contains **business logic**, **AggregateRoot**, **Entities** and **ValueObjects**.
* The **Application** layer contains **business logic** and glue to combine Infrastructure & Domain.
* The **Infrastructure** layer contains glue code to connect the application to the outside world. It contains implementations of interfaces defined in the Application layer.
* The **Presentation** layer is the entry point to the system. It is responsible for **translating HTTP requests** into commands and queries for the application layer to handle.

0 comments on commit 01b38b8

Please sign in to comment.