2

In our application we have a scenario where we need to validate an property update based on business rules and the context of the current user. I am trying to determine the best way to do the validation because I think that the domain model should not know about the current user. Our normal authorization is separate from the domain and is different than this scenario.

Where should this validation occur and is there a better way to handle it? Should the domain model know about the user? Any help or input is appreciated.

Simple Example: We have an order with an approved quantity. Only specific user types can update the quantity in only specific directions. Is this the correct way to validate in a domain aggregate?

public enum UserType
{
    ViewUserType,
    RequesterUserType,
    SupplierUserType
}

public class Order
{
    public int OrderId {get; private set}
    public int RequestedQuantity {get; private set}
    public int ApprovedQuantity {get; private set}

    public void RequestQuantity(int quantity, UserType userType)
    {
        if (userType == UserType.RequesterUserType)
        {
            this.RequestedQuantity = quantity;
        }
    }

    // Question: The direction that the approved quantity can change is a business rule
    // but directly deals with the context of the user.  Should the model know about the user
    // or should this validation be pulled out to either the application service, a model extension,
    // or maybe a specification?
    public void ApproveQuantity(int quantity, UserType userType)
    {
        if (userType == UserType.RequesterUserType)
        {
            if (quantity <= this.ApprovedQuantity)
            {
                // Requester type user can only update if lowering the approved quantity
                this.ApprovedQuantity = quantity;
            }
        }
        else if(userType == UserType.SupplierUserType)
        {
            if (quantity >= this.ApprovedQuantity)
            {
                // Supplier type user can only update if increasing the approved quantity
                this.ApprovedQuantity = quantity;
            }
        }
    }
}
Patrick D'Souza
  • 3,491
  • 2
  • 22
  • 39

2 Answers2

3

Instead of having this enum-like type (UserType), why not evolve these ROLES into fully fledged objects? It's the role a user plays you are concerned with, not the particular user. This pushes the authentication and verification that the user is indeed a SUPPLIER or REQUESTER to the layer above (well, actually, the calling code, in this case probably some sort of application service). Below a very rough, first iteration of what that might look like:

public class Order {
  public void RequestQuantity(int quantity, UserType userType)
  {
    this.RequestedQuantity = quantity;
  }

  public void ApproveToLowerOrEqualQuantity(int quantity) {
    if (quantity <= this.ApprovedQuantity)
    {
      // Requester type user can only update if lowering the approved quantity
      this.ApprovedQuantity = quantity;
    }
  }

  public void ApproveToHigherOrEqualtQuantity(int quantity) {
    if (quantity >= this.ApprovedQuantity)
    {
      // Supplier type user can only update if increasing the approved quantity
      this.ApprovedQuantity = quantity;
    }
  }
}

//Calling code
public class ApplicationServiceOfSomeSort {
   public void RequestQuantity(UserId userId, OrderId orderId, int quantity) {
     var requester = requesterRepository.FromUser(userId);
     requester.MustBeAbleToRequestQuantity();

     var order = orderRepository.GetById(orderId);
     order.RequestQuantity(quantity);
   }

   public void ApproveQuantityAsRequester(UserId userId, OrderId orderId, int quantity) {
     var requester = requesterRepository.FromUser(userId);
     requester.MustBeAbleToApproveQuantity();

     var order = orderRepository.GetById(orderId);
     order.ApproveToLowerOrEqualQuantity(quantity);
   }

   public void ApproveQuantityAsSupplier(UserId userId, OrderId orderId, int quantity) {
     var supplier = supplierRepository.FromUser(userId);
     supplier.MustBeAbleToApproveQuantity();

     var order = orderRepository.GetById(orderId);
     order.ApproveToHigherOrEqualQuantity(quantity);
   }
}

Granted, there's still a lot of "bad smell" surrounding this API, but it's a start.

Yves Reynhout
  • 2,982
  • 17
  • 23
  • Thanks for the feedback Yves. Is the intention of the 'requester.MustBeAbleToApproveQuantity()' method to check if the action is allowed and throw an exception or return a bool result if the action is not allowed? Right now I have the order service in the application layer. The server uses IOrderAuthorizer to determining what role the user is in. Then I do the update for the approvedquantity. – user2354863 May 07 '13 at 02:03
  • To add alittle more complexity, what if there were multiple statuses: Draft, Submitted, and Final status. In Submitted status, the requester can update the the approved quantity either way but in Final status they can only lower it (like the simple example). Would this change how you approach the problem? – user2354863 May 07 '13 at 02:04
  • The MustXXX methods throw in my example. – Yves Reynhout May 07 '13 at 14:34
  • These states, do you mean those are something an Order can be in? Are they tied to Supplier/Requester or do they always apply? Anyway, I get the feeling the language is off. It seems we're talking about increasing and decreasing the quantity, but call it approve quantity. Why is that? Why not call it explicitly increase and decrease and pass in the quantity to increase/decrease by? From an auth POV the authentication would become 'MustBeAbleToIncrease/DecreaseQuantity'. The state would be something internal to the Order (i.e. an invariant it would check). – Yves Reynhout May 07 '13 at 14:40
  • Good question. I was not very clear on explaining any background knowledge to the question. A requester can create an order and a quantity (requestedQuantity) from the supplier. The supplier can go in and enter the approved quantity (approvalquantity) of how much they can supply of the original requestedQuantity. After its been submitted/approved, then both the requester and supplier can edit the approved quantity in specific directions. – user2354863 May 07 '13 at 16:19
  • It probably sounds weird having requested and approved quantity on a order model. Try not to get caught up in why I would be setting these types of quantities on the order. I have a much more complex version that I am trying to code and just wanted to keep the model names simple for my question. I appreciate all the help. – user2354863 May 07 '13 at 16:21
  • For even more information on the problem I am facing, I also will have a ordershippedquantity, receivedquantity, returnedquantity, etc. – user2354863 May 07 '13 at 16:23
  • Again, going out on a limb here, but aren't those part of different bounded contexts? E.g. Shipping BC contains a ShippedOrder, Returned Goods BC contains a ReturnedOrder (or PartiallyReturnedOrder). – Yves Reynhout May 07 '13 at 18:32
  • You are correct in that they should be different bounded contexts. I'm working on a client project where I am not able to split them apart. If I was on a brand new project they would be different bounded contexts. – user2354863 May 07 '13 at 19:42
  • The real life example that I am trying to figure out involves a user renting inventory and returning it. A Rental (Order in my example) has a list of Parts. A requester needs the parts and creates a Rental request for the parts that are needed. A supplier approves different quantities for the parts on the request. The supplier then ships the Rental to the requester. Once the requester is done they have to ship back and return the rental. In this case, would there be separate bounded contexts? – user2354863 May 07 '13 at 19:50
  • That's hard to say. The language in this usecase sounds consistent, but that's about all I can say about it. – Yves Reynhout May 07 '13 at 20:15
  • When you say 'the language in this usecase sounds consistent' do you mean that it sounds like the same aggregate or different aggregates? – user2354863 May 07 '13 at 20:42
  • I meant that I don't see any splinters that would suggest different bounded contexts. But again, you are not gonna get a good answer because it requires lots more discussion and the real scenarios. Also, if you're not willing to split up into BCs (could be as simple as a namespace ;-)) then the model you end up with won't be as useful as you might want it to be - but that's highly speculative on my behalf. – Yves Reynhout May 09 '13 at 10:22
2

This is slightly inspired by Yves answer and your replies to it.

My personal mantra is to make implicit things explicit, as I like the way the code turns out after applying this principle:

public interface IProvideCurrentIdentityRoles
{
   bool CanRequestQuantity()
   bool CanApproveQuantity();
   bool CanOverruleQuantityOnSubmittedOrder();
   bool CanIncreaseQuantityOnFinalOrder();
   bool CanDecreaseQuantityOnFinalOrder();
}

public class Order
{
    public int OrderId {get; private set}
    public int RequestedQuantity {get; private set}
    public int ApprovedQuantity {get; private set}

    public void RequestQuantity(int quantity, IProvideCurrentIdentityRoles requester)
    {
        Guard.That(requester.CanRequestQuantity());
        this.RequestedQuantity = quantity;
    }

    public void ApproveQuantity(int quantity, IProvideCurrentIdentityRoles  approver)
    {
        if (quantity == this.RequestedQuantity)
        {
           Guard.That(approver.CanApproveQuantity());
        }
        else 
        {
           if (orderType == OrderType.Submitted)
           {
              Guard.That(approver.CanOverruleQuantityOnSubmittedOrder());
           }
           else if (orderType == OrderType.Final)
           {
              if (quantity > this.ApprovedQuantity)
              {
                 Guard.That(approver.CanIncreaseQuantityOnFinalOrder());
              }
              else 
              {
                 Guard.That(approver.CanDecreaseQuantityOnFinalOrder());
              }
           }
        }
        this.ApprovedQuantity = quantity;
     }
}
Tom
  • 511
  • 3
  • 12