2

I did a couple of simple projects following Anemic Domain Model . Now I am trying to do the similar type of projects in Rich Domain Model. But I am confused in implementations. Previously , I used to have a domain model, entity and all of its logic used to reside in Services and persistence was made through repository, which contained all functions related to database persistence.

This is what my User model deals with now:

  public class User : BaseEntity
    {
        private readonly UserRepository _userRepo;
        private readonly PasswordHash _passwordHash;
        private readonly USER_DETAIL _userDetail;

        protected User() { }

        public User(UserRepository userRepo, PasswordHash passwordHash, USER_DETAIL userDetail) : base()
        {
            _userRepo = userRepo;
            _passwordHash = passwordHash;
            _userDetail = userDetail;
        }

        public User(UserDto user_dto)
        {
            this.is_owner = user_dto.is_owner;
            this.id = user_dto.user_id;
            this.email = user_dto.email;
            this.normalized_email = this.email.ToUpper();
            this.is_active = user_dto.is_active;
        }

        private string _password, _registrationToken;

        [DataType(DataType.EmailAddress)]
        [MaxLength(100)]
        [Required]
        public string email { get; set; }

        [MaxLength(100)]
        [DataType(DataType.EmailAddress)]
        [Required]
        public string normalized_email { get; set; }

        [MaxLength(250)]
        [Required]
        public string password
        {
            get => _password;
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    throw new NonEmptyValueException("Password cannot be empty.");
                }
                _password = value;
            }
        }

        [MaxLength(1000)]
        [Required]
        public string registration_token
        {
            get => _registrationToken;
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    throw new NonEmptyValueException($"Registration token is required.");
                }
                _registrationToken = value;
            }
        }

        public bool is_active { get; set; } = true;

        public bool is_email_confirmed { get; set; } = false;

        public bool is_owner { get; set; } = false;

        public void disable()
        {
            is_active = false;
        }

        public void enable()
        {
            is_active = true;
        }

        public void markEmailAsConfirmed()
        {
            is_email_confirmed = true;
        }

        public User checkAuthenticity(string username, string password)
        {
            var user = _userRepo.getByUsername(username);
            if (user == null || !user.is_active || !user.is_email_confirmed)
            {
                return null;
            }

            if (!_passwordHash.ValidatePassword(password, user.password))
            {
                return null;
            }

            return user;
        }

        public void enable(ModificationDto dto)
        {
            try
            {
                var user = _userRepo.getById(dto.id) ?? throw new ItemNotFoundException($"User with the id {dto.id} doesnot exist.");

                user.enable();
                user.modified_date = DateTime.Now;
                user.modified_by = dto.modified_by;

                _userRepo.update(user);
            }
            catch (Exception)
            {
                throw;
            }
        }
        public void markEmailRegistered(string token)
        {
            try
            {
                var encryptedToken = _cryptography.Encrypt(token);

                var user = _userRepo.getByToken(encryptedToken) ?? throw new ItemNotFoundException($"Token didnot match.");

                if (user.is_email_confirmed)
                {
                    return;
                }

                user.markEmailAsConfirmed();

                _userRepo.update(user);
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        public void save()
        {
            try
            {
                bool isUsernameValid = checkNameValidity();

                if (!isUsernameValid)
                {
                    throw new DuplicateItemException("User with same name already exists.");
                }

                user.created_date = DateTime.Now;
                user.created_by = user_dto.created_by;

                user.password = _passwordHash.CreateHash(this.password);

                _userRepo.insert(user);
            }
            catch (Exception)
            {
                throw;
            }
        }

        public void update(UserDto user_dto)
        {
            try
            {

                User user = _userRepo.getById(user_dto.user_id) ?? throw new ItemNotFoundException($"User with the id {user_dto.user_id} doesnot exist.");

                bool isUsernameValid = checkNameValidity(user_dto);

                if (!isUsernameValid)
                {
                    throw new DuplicateItemException("User with same name already exists.");
                }

                _userAssembler.copy(user, user_dto);
                user.modified_by = user_dto.modified_by;
                user.modified_date = DateTime.Now;
                _userRepo.update(user);

                USER_DETAIL user_detail = new USER_DETAIL(user_dto);
                user_detail.saveOrUpdate();
            }
            catch (Exception)
            {
                throw;
            }
        }

        private bool checkNameValidity()
        {
            var userWithSameEmail = _userRepo.getByUsername(this.email);
            return userWithSameEmail == null || userWithSameEmail.id == this.id;
        }
    }

public class USER_DETAIL : BaseEntity
    {
        private readonly UserDetailRepository _userDetailRepo;
        private readonly FileHelper _fileHelper;

        private string _permanentAddress, _firstName, _lastName;
        private long _userId;

        protected USER_DETAIL() { }


        public USER_DETAIL(UserDetailRepository userDetailRepo, FileHelper fileHelper)
        {
            _userDetailRepo = userDetailRepo;
            _fileHelper = fileHelper;
        }

        public USER_DETAIL(UserDetailDto user_detail_dto)
        {
            this.first_name = user_detail_dto.first_name;
            this.last_name = user_detail_dto.last_name;
            this.user_id = user_detail_dto.user_id;
            this.permanent_address = user_detail_dto.permanent_address;
            this.temporary_address = user_detail_dto.temporary_address;
            this.primary_contact = user_detail_dto.primary_contact;
            this.secondary_contact = user_detail_dto.secondary_contact;
            this.image_path = user_detail_dto.image_path;
        }

        public USER_DETAIL(UserDto user_dto)
        {
            this.user_id = user_dto.user_id;
            this.first_name = user_dto.user_detail_dto.first_name;
            this.permanent_address = user_dto.user_detail_dto.permanent_address;
            this.temporary_address = user_dto.user_detail_dto.temporary_address;
            this.primary_contact = user_dto.user_detail_dto.primary_contact;
            this.secondary_contact = user_dto.user_detail_dto.secondary_contact;
            this.modified_by = user_dto.modified_by;
            this.modified_date = DateTime.Now;
        }

        public long user_id
        {
            get => _userId;
            set
            {
                if (value <= 0)
                {
                    throw new InvalidValueException("Invalid user id.");
                }
                _userId = value;
            }
        }

        [Required]
        [MaxLength(50)]
        public string first_name
        {
            get => _firstName;
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    throw new NonEmptyValueException("First name must be specified.");
                }
                _firstName = value;
            }
        }

        [Required]
        [MaxLength(50)]
        public string last_name
        {
            get => _lastName;
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    throw new NonEmptyValueException("Last name must be specified.");
                }
                _lastName = value;
            }
        }

        [Required]
        [MaxLength(100)]
        public string permanent_address
        {
            get => _permanentAddress;
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    throw new NonEmptyValueException("Address must be specified.");
                }
                _permanentAddress = value;
            }
        }

        [MaxLength(100)]
        public string temporary_address { get; set; }

        [Required]
        [MaxLength(15)]
        public string primary_contact { get; set; }

        [MaxLength(15)]
        public string secondary_contact { get; set; }

        [MaxLength(100)]
        public string image_path { get; set; }

        [ForeignKey(nameof(user_id))]
        public virtual User user { get; set; }

        public string getFullName()
        {
            return $"{first_name} {last_name}";
        }

        public void saveOrUpdate(UserDetailDto user_detail_dto, IFormFile file = null)
        {
            try
            {
                var userDetail = _userDetailRepo.getByUserId(user_detail_dto.user_id);

                if (userDetail == null)
                {
                    save(user_detail_dto, file);
                }
                else
                {
                    update(user_detail_dto, userDetail, file);
                }
            }
            catch (Exception)
            {
                throw;
            }
        }

        private void update(UserDetailDto user_detail_dto, USER_DETAIL user_detail, IFormFile file)
        {
            if (file != null)
            {
                if (user_detail.image_path != null)
                {
                    deleteImage(user_detail.image_path);
                }
                user_detail_dto.image_path = _fileHelper.saveFileAndGetFileName(file);
            }

            // no idea how data is copied from user_detail_dto to user_detail entity as passing dto to this class in constructor creates a new instance of entity and all the set properties , which are not passed from view (client side) are lost

            // Or is it that I need to copy all the values from dto to entity here again?

            user_detail.modified_by = user_detail_dto.modified_by;
            user_detail.modified_date = DateTime.Now;
            _userDetailRepo.update(user_detail);
        }

        private void save(UserDetailDto user_detail_dto, IFormFile file)
        {

            var userDetail = new USER_DETAIL(user_detail_dto);

            if (file != null)
            {
                userDetail.image_path = _fileHelper.saveFileAndGetFileName(file);
            }

            userDetail.created_by = user_detail_dto.created_by;
            userDetail.created_date = DateTime.Now;
            _userDetailRepo.insert(userDetail);
        }

        private void deleteImage(string iMAGE_PATH)
        {
            _fileHelper.deleteImage(iMAGE_PATH, _fileHelper.getPathToImageFolder());
        }
    }

User_Detail entity injected in User class is also an entity. I don't think this is good by any means and not sure even if it works. Multiple constructors are present in User entity. How does dependency injection resolve this ? I am having trouble even to solve compile time problem . I didn't find any sample/example of code that is dependent or uses dependency injection principle in domain model. Can anyone help me on this?

3 Answers 3

1

To summarize

A fat domain model doesn't mean putting it all in the same class. You've somewhat gone overboard on this.

(Fat) domain model methods should focus on operating on the current object, and you've added some methods that effectively ignore the current object's state, which doesn't make sense.


Reviewing the content

public User(UserRepository userRepo, PasswordHash passwordHash, USER_DETAIL userDetail) : base()
{
    _userRepo = userRepo;
    _passwordHash = passwordHash;
    _userDetail = userDetail;
}

public User(UserDto user_dto)
{
    this.is_owner = user_dto.is_owner;
    this.id = user_dto.user_id;
    this.email = user_dto.email;
    this.normalized_email = this.email.ToUpper();
    this.is_active = user_dto.is_active;
}

What I find weird here is that by separating the two constructors, you either have a filled out user or an empty user with a backing repository. Those are two wildly different states for an object to be in, and wildly different roles for it to play in your application.

I do understand that you want to inject dependencies, and that's a good thing, but if you're taking that route, I would avoid the second constructor and instead move that to a static method on your class.

[MaxLength(250)]
[Required]
public string password
{
    get => _password;
    set
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new NonEmptyValueException("Password cannot be empty.");
        }
        _password = value;
    }
}

All properties and their validation (within reason) do indeed belong in the model.

Note that if the validation is more than a simple value check, you should evaluate whether to put the validation in a different class and simply have User pass the buck to e.g. an injected EmailAddressValidator dependency.

public void disable()
{
    is_active = false;
}

Operations on the current object also belong in the model. I want you to take note here that this is the core essence of fat vs anemic. In a fat model, you describe the operations that you can perform on an object on the object type (class) itself.

Which leads me to:

public User checkAuthenticity(string username, string password)
{
    var user = _userRepo.getByUsername(username);
    if (user == null || !user.is_active || !user.is_email_confirmed)
    {
        return null;
    }

    if (!_passwordHash.ValidatePassword(password, user.password))
    {
        return null;
    }

    return user;
}

This method does not rely on the instanced object in any way (other than using the injected dependencies). I could make this a static method somewhere else and it would keep working (provided the same dependencies were injected).
The (instanced) class methods only make sense in cases where the consumer already has an instance (of User) and want to do operations on it. But in this case, the consumer only has credentials and wants to get a User object from it. That doesn't make sense to put as a class method on User.

This method does not belong in User, at least not in its current form. If you had relied on the object's user credentials instead of having to pass them to the method, that would make more sense. I can't definitively state whether this is the correct approach since the intention of the code is somewhat vague: are you trying to confirm credentials or are you trying to fetch a user from the db?

Slightly off-topic, I would severely change the logic here, it doesn't pass the smell test. Why are you passing a newly fetched user object from a pre-existing user object? Why would you return either an object or null, instead of just passing a boolean to confirm the login?

Either this method belongs somewhere outside of the User object (along the lines of User GetUserByCredentials(string username, string password), or you need to change the class method to something like bool IsAuthenticated() which uses the already filled in object state to match it to the database (this could also be a property then, but do mind to cache your results in case it gets called multiple times).

It seems like you're trying to use your User class as the sole gateway into the UserRepository, and that is not the purpose of a fat domain model. Even if it's fat, a domain model should not become a monolith.

public void update(UserDto user_dto)
{
    try
    {

        User user = _userRepo.getById(user_dto.user_id) ?? throw new ItemNotFoundException($"User with the id {user_dto.user_id} doesnot exist.");

        bool isUsernameValid = checkNameValidity(user_dto);

        if (!isUsernameValid)
        {
            throw new DuplicateItemException("User with same name already exists.");
        }

        _userAssembler.copy(user, user_dto);
        user.modified_by = user_dto.modified_by;
        user.modified_date = DateTime.Now;
        _userRepo.update(user);

        USER_DETAIL user_detail = new USER_DETAIL(user_dto);
        user_detail.saveOrUpdate();
    }
    catch (Exception)
    {
        throw;
    }
}

This method suffer from the same problem, it doesn't actually rely on the current User object in any meaningful way, it only uses it for its dependencies.

The weird thing is that your create method (save()) already does it the right way. I'd expect the create/update logic to use the same approach but they are vastly different here. One uses an input object, the other uses the actual object on which this method resides.


The feedback for your USER_DETAIL class is analogous.

1

There are lots of good points in the other comments, I would like to make an additional one however.

A "rich" domain model (i.e. object-orientation) is not (or should not be) technical. This is the thing that is most confusing to some people. In the procedural world (i.e. anemic model) you define all the data that you need, and sort out the functionality later somewhere else. You don't need to know it, because you have access to everything all the time.

In a "rich" domain model you have to start with the behavior. That is, what business-relevant functionality you need from this object. So you have to think about something you are not used to think about at this point. These objects can't be "generic", you can only use it in ways that is defined by the object, because there should be no access to any data whatsoever.

There can not be any saveOrUpdate() or similar public methods, because those are technical in nature. Also, there can not be an external Repository that has access to all internal data, because, well, that would defeat the purpose of having an object in the first place. These technical things belong, as other details of the implementation, hidden behind a business-relevant interface.

HTH.

0

It looks like you don't really have any logic here. Its all validation and persistence.

Basically its a pretty simple conversion though.

ADM

class Service
{
    void SendPackage(User user)
}

class User
{
    string Id;
    string Name;
}

Fat Model

class User
{
    string Id;
    string Name;
    void SendPackage() {...}
}

I would keep the repo seperate and not inject it.

class Repository
{
    void Save(User user);
}

I think its been pretty much proven that injecting the repos does nothing to help your code

5
  • "I think its been pretty much proven that injecting the repos does nothing to help your code" - for me this is conflating concepts - the "fat domain model" should be concerned with logical operations on a logical model, repositories are persistence which is a different beast altogether and belong at an impure orchestration/service level.
    – Ant P
    Sep 3, 2019 at 7:59
  • @AntP I agree, but the OP has injected the repos. I think its a pretty common thing to do.
    – Ewan
    Sep 3, 2019 at 8:01
  • In ADM, I take Dto as parameter in Service. So , I am having problem implementing it in Fat Model Sep 3, 2019 at 8:03
  • so do i in my example. when you move to a fat model your dto becomes the model
    – Ewan
    Sep 3, 2019 at 8:05
  • 1
    I think this answer is overcorrecting the issue. OP did go overboard by (at times) having the User class act as an empty shell around its repository dependency, but injecting the repository shouldn't be outright dismissed either. There are valid use cases, e.g. to assist with username validation (alerting if the username is already in use by someone else)
    – Flater
    Oct 3, 2019 at 9:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.