Search code examples
domain-driven-designrepository-patternclean-architecturefactories

How To Update Properties In Domain Aggregate Root Object


In a clean architecture project the domain layer contains: DTO interfaces, Events, Factories, Models, Exceptions, etc...

Every domain object contains a constructor with arguments through which data is passed.

I am using factories which accept a DTO interface from which domain objects are created.

The data models in the infrastructure layer implement the DTO interfaces in the domain layer.

DTO:

namespace Acme.Core.Domain.Identity.DTO
{
    public interface IBuyerDto : IPersonDto
    {
        IAddressDto BillingAddress { get; set; }
        IAddressDto ShippingAddress { get; set; }
    }
}

Domain Models:

namespace Acme.Core.Domain.Identity.Models.BuyerAggregate
{
    public sealed class Buyer : Aggregate<Buyer, BuyerId>, IPerson
    {
        public Buyer(BuyerId id, PersonName name, DateOfBirth dateOfBirth, Gender gender, string pictureUrl, Address billingAddress, Address shippingAddress, Account account) : base(id)
        {
            Name = name;
            DateOfBirth = dateOfBirth;
            Gender = gender;
            BillingAddress = billingAddress;
            ShippingAddress = shippingAddress;
            Account = Guard.Against.Null(account, nameof(account));
            PictureUrl = pictureUrl;
        }

        public Account Account { get; private set; }
        public PersonName Name { get; private set; }
        public DateOfBirth DateOfBirth { get; private set; }
        public string PictureUrl { get; private set; }
        public Gender Gender { get; private set; }
        public Address BillingAddress { get; private set; }
        public Address ShippingAddress { get; private set; }

        public void UpdateName(PersonName personName)
        {
            Name = personName;
        }

        public void UpdateBillingAddress(Address billingAddress)
        {
            BillingAddress = billingAddress;
        }

        public void UpdateShippingAddress(Address shippingAddress)
        {
            ShippingAddress = shippingAddress;
        }
    }
}

namespace Acme.Core.Domain.Identity.Models
{
    public class Account : Entity<Account, AccountId>
    {
        public Account(AccountId id, string userName, string normalizedUserName, string passwordHash, string concurrencyStamp, string securityStamp, string email, string normalizedEmail, bool emailConfirmed, string phoneNumber, bool phoneNumberConfirmed, bool twoFactorEnabled, DateTimeOffset? lockoutEnd, bool lockoutEnabled, int accessFailedCount, AccountStatus status, List<RoleId> roles, List<AccountClaim> accountClaims, List<AccountLogin> accountLogins, List<AccountToken> accountTokens) : base(id)
        {
            UserName = Guard.Against.NullOrWhiteSpace(userName, nameof(userName));
            NormalizedUserName = Guard.Against.NullOrWhiteSpace(normalizedUserName, nameof(normalizedUserName));
            PasswordHash = Guard.Against.NullOrWhiteSpace(passwordHash, nameof(passwordHash));
            ConcurrencyStamp = concurrencyStamp;
            SecurityStamp = securityStamp;
            Email = Guard.Against.NullOrWhiteSpace(email, nameof(email));
            NormalizedEmail = Guard.Against.NullOrWhiteSpace(normalizedEmail, nameof(normalizedEmail));
            EmailConfirmed = emailConfirmed;
            PhoneNumber = phoneNumber;
            PhoneNumberConfirmed = phoneNumberConfirmed;
            TwoFactorEnabled = twoFactorEnabled;
            LockoutEnd = lockoutEnd;
            LockoutEnabled = lockoutEnabled;
            AccessFailedCount = accessFailedCount;
            Status = Guard.Against.Null(status, nameof(status));
            _roles = Guard.Against.Null(roles, nameof(roles));
            _accountClaims = accountClaims;
            _accountLogins = accountLogins;
            _accountTokens = accountTokens;
        }

        public string UserName { get; private set; }
        public string NormalizedUserName { get; private set; }
        public string PasswordHash { get; private set; }
        public string ConcurrencyStamp { get; private set; }
        public string SecurityStamp { get; private set; }
        public string Email { get; private set; }
        public string NormalizedEmail { get; private set; }
        public bool EmailConfirmed { get; private set; }
        public string PhoneNumber { get; private set; }
        public bool PhoneNumberConfirmed { get; private set; }
        public bool TwoFactorEnabled { get; private set; }
        public DateTimeOffset? LockoutEnd { get; private set; }
        public bool LockoutEnabled { get; private set; }
        public int AccessFailedCount { get; private set; }
        public AccountStatus Status { get; private set; }
        private List<RoleId> _roles;
        public IReadOnlyCollection<RoleId> Roles
        {
            get
            {
                return _roles;
            }
        }
        private List<AccountClaim> _accountClaims;
        public IReadOnlyCollection<AccountClaim> AccountClaims
        {
            get
            {
                return _accountClaims;
            }
        }
        private List<AccountLogin> _accountLogins;
        public IReadOnlyCollection<AccountLogin> AccountLogins
        {
            get
            {
                return _accountLogins;
            }
        }
        private List<AccountToken> _accountTokens;
        public IReadOnlyCollection<AccountToken> AccountTokens
        {
            get
            {
                return _accountTokens;
            }
        }

        public void AddRole(long roleId)
        {
            var role = _roles.Where(x => x.GetValue().Equals(roleId)).FirstOrDefault();

            if (role == null)
            {
                _roles.Add(new RoleId(roleId));
            }
        }

        public void RemoveRole(long roleId)
        {
            var role = _roles.Where(x => x.GetValue().Equals(roleId)).FirstOrDefault();

            if (role == null)
            {
                _roles.Remove(role);
            }
        }

        public void ActivateAccount()
        {
            Status = AccountStatus.Active;
        }

        public void BanAccount()
        {
            Status = AccountStatus.Banned;
        }
        public void CloseAccount()
        {
            Status = AccountStatus.Closed;
        }
        public void LockAccount()
        {
            Status = AccountStatus.Locked;
        }
        public void NewAccount()
        {
            Status = AccountStatus.New;
        }
    }
}

Factories:

namespace Acme.Core.Domain.Identity.Factories
{
    public class BuyerAggregateFatory : IBuyerAggregateFactory
    {
        private readonly IPersonNameFactory _personNameFactory;
        private readonly IDateOfBirthFactory _dateOfBirthFactory;
        private readonly IGenderFactory _genderFactory;
        private readonly IAccountFactory _accountFactory;
        private readonly IAddressFactory _addressFactory;

        public BuyerAggregateFatory(IPersonNameFactory personNameFactory,
         IDateOfBirthFactory dateOfBirthFactory,
         IGenderFactory genderFactory,
         IAccountFactory accountFactory,
         IAddressFactory addressFactory)
        {
            _personNameFactory = Guard.Against.Null(personNameFactory);
            _dateOfBirthFactory = Guard.Against.Null(dateOfBirthFactory);
            _genderFactory = Guard.Against.Null(genderFactory);
            _accountFactory = Guard.Against.Null(accountFactory);
            _addressFactory = Guard.Against.Null(addressFactory);
        }

        public Buyer Create(IBuyerDto dto)
        {
            BuyerId aggregateId = new BuyerId(dto.Id);

            PersonName name = _personNameFactory.Create(dto.Name);

            DateOfBirth dob = _dateOfBirthFactory.Create(dto.DateOfBirth);

            Gender gender = _genderFactory.Create(dto.GenderId);

            Address billingAddress = _addressFactory.Create(dto.BillingAddress);

            Address shippingAddress = _addressFactory.Create(dto.ShippingAddress);

            Account account = _accountFactory.Create(dto.Account);

            return new Buyer(aggregateId, name, dob, gender, dto.PictureUrl, billingAddress, shippingAddress, account);
        }
    }
}

From the application layer a service class does the orchestration for the use case, using the repository interface and factory interface.

Use case 1: During an update operation I fetch existing data of the aggregate, from the database using a repository. I need to update one or two properties of a domain aggregate root object. Example: I need to update billing address or shipping address.

Use case 2: During an update operation, I fetch existing data of the aggregate, from the database using a repository. I need to update the account status. I am calling the status update method from domain aggregate root object. Example: buyerAggregate.Account.ActivateAccount()

Am i updating the domain aggregate root object and its properties in right way?


Solution

  • In use case 2, your aggregate would be the Account, not the Buyer. There's no need for the Buyer to be involved in the transaction.

    So, for this case, you would retrieve Account from the repository and then call ActivateAccount() directly.

    Any aggregate that you have designed for a use case should provide the full interface for making changes to the aggregate. In other words, your application layer will only work with properties and methods on the aggregate root. If a child entity needs changing that method should be implemented on your aggregate root. You should not directly interact with child properties of an aggregate. It is the aggregate's responsibility to avoid any invariants within its scope. If you change a child object directly, you may put the whole aggregate in an invalid state because the aggregate was not able to enforce controls.