'Best practice to manage entities values and value changing events

Little introduction: we have a complex entity and overgrown business logic related to it. With various fields that we can change and fields that updates from external project management software (PMS) like MS Project and some others.

The problem is that it's hard to centralize business logic for changing every fields cause that changes can offend other fields some fields are calculated but should be calculated only in several business scenarios. And different synchronization processes uses different business logic that depends on external data of specific PMS.

At this moment we have such ways to change the fields in our solution:

  1. Constructor with parameters and private parameterless constructor

    public class SomeEntity
    {
        public string SomeField;
    
        private SomeEntity ()
        {
        }
    
        public SomeEntity (string someField)
        {
            SomeField = someField;
        }
    }
    
  2. Private set with public method to change field value

    public class SomeEntity
    {
        public string SomeField {get; private set;}
    
        public void SetSomeField(string newValue)
        {
            // there may be some checks
            if (string.IsNullOrEmpty(newValue))
            {
                throw new Exception();
            }
    
            SomeField = newValue;
        }
    }
    
  3. Event methods that perform operations and set some fields

    public class SomeEntity
    {
        public string SomeField { get; private set; }
        public string SomePublishedField { get; private set; }
    
        public void PublishEntity(string publishValue)
        {
            SomeField = publishValue;
            SomePublishedField = $"{publishValue} {DateTime.Now()}";
        }
    }
    
  4. Public setters

    public class SomeEntity
    {
        public string SomeField { get; set; }
    }
    
  5. Services that implements business logic:

    public class SomeService : ISomeService
    {
        private DbContext _dbContext;
        private ISomeApprovalsService _approvalsService;
    
        public async Task UpdateFromMspAsync (MspSomeEntity mspEntity,
                          CancellationToken cancellationToken = default)
        {
            var entity = await _dbContext.SomeEntities
                                         .Include (e => e.Process)
                                         .SingleAsync (e => e.MspId == mspEntity.Id, cancellationToken);
            switch mspEntity.Status:
                case MspStatusEnum.Cancelled:
                    entity.Process.State = ProcessStateEnum.Rejected;
                    entity.Status = EntityStatusEnum.Stopped;
                break;
                case MspStatusEnum.Accepted:
                    _approvalsService.SendApprovals (entity.Process);
                    entity.Status = EntityStatusEnum.Finished;
                break;
    
          await _dbContext.SaveChangesAsync (cancellationToken);
        }
    }
    
  6. State machine inside entity

    public class SomeEntity
    {
        private StateMachine<TriggerEnum, StateEnum> _stateMachine;
    
        public SomeEntity()
        {
            ConfigureStateMachine();
        }
    
        public string SomeField1 { get; set; }
        public string SomeField2 { get; set; }
        public string SomeField3 { get; set; }
    
        private void ConfigureStateMachine()
        {
            _statusStateMachine.Configure(StateEnum.Processing)
                    .OnEntry(s=>SomeField1 = null)
                    .Permit(TriggerEnum.Approve, StateEnum.Approved);
    
            _statusStateMachine.Configure(StateEnum.Approved)
                    .OnEntry(s=> SomeField1 = SomeField2 + SomeField3)
                    .Permit(TriggerEnum.Publish, StateEnum.Finished)
                    .Permit(TriggerEnum.Cancel, StateEnum.Canceled);
    
            // etc
        }
    
        public void Trigger (TriggerEnum trigger) => _statusStateMachine.Fire(trigger);
    
      }
    
  7. State machine as service to prevent buisness logic leaks inside of entity.

    var machine = _services.GetService<IStateMachine<SomeEntity, TriggerEnum>>();
    
    var entity = await _dbContext.SomeEntities.FirstAsync();
    
    IAttachedStateMachine<TriggerEnum> attachedMachine = machine.AttachToEntity(entity);
    attachedMachine.Trigger(TriggerEnum.Publish);
    

It's wrong by architecture to have so many ways of changing values and we want to refactor this but to change approach, best practice must be chosen.

Please share your experience of resolving similar situation.

Update: found approach for DDD that called "aggregation root". It's looks good but only on paper (in theory) and works good with simple examples like "User, customer, shopping cart, order". On practice on every private setter you will create setter method (like in #2 of my examples). Also different methods for every system you work with. Not even talking about business logic inside database entity that violates SOLID's "single responsibility principle".



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source