Skip to content

Instantly share code, notes, and snippets.

@emadb
Last active March 4, 2023 22:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save emadb/d2a0337faa0cfc942317e5a32fc103f2 to your computer and use it in GitHub Desktop.
Save emadb/d2a0337faa0cfc942317e5a32fc103f2 to your computer and use it in GitHub Desktop.
Validation and domain
// Controller HTTP
public class Controller
{
public UserResponse Post(UserRequest req)
{
User user = createUserValidator.Validate(req);
// Fino a qui sono nel contesto HTTP
// Qui entro nel dominio. Forse sarebbe meglio usare un gateway per accedere al dominio
// ma dipende dal tipo di applicazione.
user.Create();
// Qui torno nel contesto HTTP
return CreateUserResponse(user);
}
}
// Valiratore dell'operazione di creazione di un utente (sono ancora nel contesto HTTP)
public class CreateUserValidator
{
private IUserRepository repo;
public CreateUserValidator(IUserRepository repo)
{
this.repo = repo;
}
public User Validate(UserRequest req)
{
if (String.IsNullOrEmpty(req.Email)
{
throw MissingEmailException();
}
// Potrei verificare anche se l'email è univoca
IList<Roles> roles = req.Roles.Select(r => repo.GetRole(r));
if (roles.Some(r => r == RoleNotFound.Value))
{
throw MissingRoleException();
}
return UserFactory.create(req.email, roles);
}
}
// DM: classe di dominio
public class User : AggregateRoot
{
public User(/*varie dipendenze: repo, serivizi, ecc...*/)
{}
public User Create(User user)
{
// Fai qualcosa con l'utente:
// - schedula l'invio di una mail tramite apposito servizio
// - fa qualcos'altro
return this.userRepository.save(user);
}
}
@emanuelefirmani
Copy link

using System;
using System.Collections.Generic;
using System.Linq;
using LanguageExt;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace EmApp
{
    enum Role { Reader, Contributor, Admin }
    record ApiUser(Guid Id, string Email, List<Role> Roles);

    [ApiController]
    [Route("user")]
    class Controller : ControllerBase
    {
        readonly UserFactory _userFactory;
        readonly UserRepository _userRepository;
        readonly UserRoleRepository _userRoleRepository;
        readonly EmAppDbContext _dbContext;

        public Controller() // Poor man DI
        {
            _dbContext = new EmAppDbContext();
            _userRepository = new UserRepository(_dbContext.Users);
            _userRoleRepository = new UserRoleRepository(_dbContext.UserRoles);
            _userFactory = new UserFactory(_userRepository, new RoleRepository(_dbContext.Roles));
        }

        public Controller(UserFactory userFactory, UserRepository userRepository, UserRoleRepository userRoleRepository, EmAppDbContext dbContext)
        {
            _userFactory = userFactory;
            _userRepository = userRepository;
            _userRoleRepository = userRoleRepository;
            _dbContext = dbContext;
        }

        [HttpPost]
        public IActionResult CreateUser(ApiUser user)
        {
            var generatedUser = _userFactory.Generate(user);

            return generatedUser.Match<IActionResult>(
                userWithRoles =>
                {
                    AddUser(userWithRoles);
                    return NoContent();
                },
                error => BadRequest(error)
            );
        }

        void AddUser(DbUserWithRoles userWithRoles)
        {
            var user = userWithRoles.User;
            _userRepository.Add(user);
            userWithRoles.Roles.ForEach(role => _userRoleRepository.Add(user.Id, role.Id));
            _dbContext.SaveChanges();
        }
    }

    class UserFactory
    {
        readonly UserRepository _userRepository;
        readonly RoleRepository _roleRepository;

        internal UserFactory(UserRepository userRepository, RoleRepository roleRepository)
        {
            _userRepository = userRepository;
            _roleRepository = roleRepository;
        }

        internal Either<string, DbUserWithRoles> Generate(ApiUser user)
        {
            if (_userRepository.AlreadyExists(user.Email))
                return $"Email {user.Email} already in use";
            if (_userRepository.AlreadyExists(user.Id))
                return $"Id {user.Id} already in use";

            var roles = user.Roles.ToDictionary(x => x, x => _roleRepository.Get(x));
            (List<DbRole> Roles, List<string> Errors) initialState = (new List<DbRole>(), new List<string>());
            var (dbRoles, errors) = roles.Fold(initialState, (state, item) =>
            {
                item.Value.Match(
                    some => state.Roles.Add(some),
                    () => state.Errors.Add($"Role {item.Key} not found")
                );
                return state;
            });

            if (errors.Any())
                return string.Join("|", errors);
            return new DbUserWithRoles(new DbUser(user.Id, user.Email), dbRoles);
        }
    }

    class UserRepository
    {
        readonly DbSet<DbUser> _users;

        public UserRepository(DbSet<DbUser> users)
        {
            _users = users;
        }

        internal bool AlreadyExists(Guid id) => _users.Any(x => x.Id == id);
        internal bool AlreadyExists(string email) => _users.Any(x => x.Email == email);
        internal void Add(DbUser user) => _users.Add(user);
    }

    class RoleRepository
    {
        readonly DbSet<DbRole> _roles;

        public RoleRepository(DbSet<DbRole> roles)
        {
            _roles = roles;
        }

        internal Option<DbRole> Get(Role name) => _roles.FirstOrDefault(x => x.Name == name.ToString()) ?? Option<DbRole>.None;
    }

    class UserRoleRepository
    {
        readonly DbSet<DbUserRole> _userRoles;

        public UserRoleRepository(DbSet<DbUserRole> userRoles)
        {
            _userRoles = userRoles;
        }

        internal void Add(Guid userId, Guid roleId) => _userRoles.Add(new DbUserRole(userId, roleId));
    }

    record DbUserWithRoles(DbUser User, List<DbRole> Roles);
    record DbUser(Guid Id, string Email);
    record DbRole(Guid Id, string Name);
    record DbUserRole(Guid UserId, Guid RoleId);

    class EmAppDbContext : DbContext
    {
        public DbSet<DbUser> Users { get; set; }
        public DbSet<DbRole> Roles { get; set; }
        public DbSet<DbUserRole> UserRoles { get; set; }
    }
}

For sake of simplicity, I didn't create all the needed interfaces.

@emanuelefirmani
Copy link

emanuelefirmani commented Feb 27, 2023

using System;
using System.Collections.Generic;
using System.Linq;
using LanguageExt;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace EmApp
{
    enum Role { Reader, Contributor, Admin }
    record ApiUser(Guid Id, string Email, List<Role> Roles);

    [ApiController]
    [Route("users")]
    class Controller : ControllerBase
    {
        readonly UserFactory _userFactory;
        readonly UserRepository _userRepository;
        readonly UserRoleRepository _userRoleRepository;
        readonly EmAppDbContext _dbContext;

        public Controller() // Poor man DI
        {
            _dbContext = new EmAppDbContext();
            _userRepository = new UserRepository(_dbContext.Users);
            _userRoleRepository = new UserRoleRepository(_dbContext.UserRoles);
            _userFactory = new UserFactory(_userRepository, new RoleRepository(_dbContext.Roles));
        }

        public Controller(UserFactory userFactory, UserRepository userRepository, UserRoleRepository userRoleRepository, EmAppDbContext dbContext)
        {
            _userFactory = userFactory;
            _userRepository = userRepository;
            _userRoleRepository = userRoleRepository;
            _dbContext = dbContext;
        }

        [HttpPost]
        public IActionResult CreateUser(ApiUser user)
        {
            var generatedUser = _userFactory.Generate(user);

            return generatedUser.Match<IActionResult>(
                userWithRoles =>
                {
                    AddUser(userWithRoles);
                    return NoContent();
                },
                error => BadRequest(error)
            );
        }

        void AddUser(DbUserWithRoles userWithRoles)
        {
            var user = userWithRoles.User;
            _userRepository.Add(user);
            userWithRoles.Roles.ForEach(role => _userRoleRepository.Add(user.Id, role.Id));
            _dbContext.SaveChanges();
        }
    }

    class UserFactory
    {
        readonly UserRepository _userRepository;
        readonly RoleRepository _roleRepository;

        internal UserFactory(UserRepository userRepository, RoleRepository roleRepository)
        {
            _userRepository = userRepository;
            _roleRepository = roleRepository;
        }

        internal Either<string, DbUserWithRoles> Generate(ApiUser user)
        {
            if (_userRepository.AlreadyExists(user.Email))
                return $"Email {user.Email} already in use";
            if (_userRepository.AlreadyExists(user.Id))
                return $"Id {user.Id} already in use";

            var roles = user.Roles.ToDictionary(x => x, x => _roleRepository.Get(x));
            (List<DbRole> Roles, List<string> Errors) initialState = (new List<DbRole>(), new List<string>());
            var (dbRoles, errors) = roles.Fold(initialState, (state, item) =>
            {
                item.Value.Match(
                    some => state.Roles.Add(some),
                    () => state.Errors.Add($"Role {item.Key} not found")
                );
                return state;
            });

            if (errors.Any())
                return string.Join("|", errors);
            return new DbUserWithRoles(new DbUser(user.Id, user.Email), dbRoles);
        }
    }

    class UserRepository
    {
        readonly DbSet<DbUser> _users;

        public UserRepository(DbSet<DbUser> users)
        {
            _users = users;
        }

        internal bool AlreadyExists(Guid id) => _users.Any(x => x.Id == id);
        internal bool AlreadyExists(string email) => _users.Any(x => x.Email == email);
        internal void Add(DbUser user) => _users.Add(user);
    }

    class RoleRepository
    {
        readonly DbSet<DbRole> _roles;

        public RoleRepository(DbSet<DbRole> roles)
        {
            _roles = roles;
        }

        internal Option<DbRole> Get(Role name) => _roles.FirstOrDefault(x => x.Name == name.ToString()) ?? Option<DbRole>.None;
    }

    class UserRoleRepository
    {
        readonly DbSet<DbUserRole> _userRoles;

        public UserRoleRepository(DbSet<DbUserRole> userRoles)
        {
            _userRoles = userRoles;
        }

        internal void Add(Guid userId, Guid roleId) => _userRoles.Add(new DbUserRole(userId, roleId));
    }

    record DbUserWithRoles(DbUser User, List<DbRole> Roles);
    record DbUser(Guid Id, string Email);
    record DbRole(Guid Id, string Name);
    record DbUserRole(Guid UserId, Guid RoleId);

    class EmAppDbContext : DbContext
    {
        public DbSet<DbUser> Users { get; set; }
        public DbSet<DbRole> Roles { get; set; }
        public DbSet<DbUserRole> UserRoles { get; set; }
    }
}

For sake of simplicity, I didn't introduce any interface

@emanuelefirmani
Copy link

Ad essere pignoli, sarebbe dovuto essere /users/id, in quanto nel mio esempio l'id e' noto a priori

@arialdomartini
Copy link

arialdomartini commented Mar 4, 2023

class EmaDB
{
    class Controller : ASP.ControllerBase
    {
        private Context _db;

        [ASP.HttpPost]
        public ASP.IActionResult CreateUser(ApiUser apiUser)
        {
            return DbUser
                .WithEmail(apiUser.Email)
                .ForAll(apiUser.Roles, LoadRole)
                .Match<ASP.IActionResult>(u =>
                    {
                        _db.Users.Add(u);
                        _db.SaveChanges();
                        return NoContent();
                    },
                    () => BadRequest("Roles not found"));
        }

        Option<Role> LoadRole(ApiRole apiRole) =>
            _db.Roles.SingleOrDefault(r => r.Name == apiRole.Name);
    }
}

static class UserExtensions
{
    internal static Option<DbUser> ForAll(this DbUser dbUser, IEnumerable<ApiRole> roles, Func<ApiRole, Option<Role>> loadRole) =>
        roles.Select(loadRole).Sequence().Select(roles => dbUser with { Roles = roles.ToArray() });
}

internal record ApiUser(String Email, IEnumerable<ApiRole> Roles);
internal record ApiRole(String Name);

internal record DbUser(String Email, IEnumerable<Role> Roles = null)
{
    public static DbUser WithEmail(string email) => new(Email: email);
}

internal record Role(Guid Id, String Name);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment