Question

Overview

Domain-driven design prescribes modelling a particular domain with classes that contain both state and behaviour. Microsoft attempt to support this methodology in modern C# applications through EF Core, which allows constituting graphs of entity objects from a database.

I'm building an application where I model individual configurable business rules as entities. These individual rules belong to a "commission ruleset", and the ruleset arranges these rules into a pipeline that sequentially perform updates on objects passed through that pipeline.

enter image description here

These rules depend on external services to lookup or calculate various things in order to do their job.

What is the best way to allow them to access these services, given they are DDD entities constituted from the database?

Does it make sense to pass through a context object alongside the object being updated which exposes all services that a business rule might require? This feels like it would be an anti-pattern but I can't think of a better alternative.

Details

I'm working on a redesign of the commissions system at my company. This particular domain has a high degree of business complexity and low technical complexity, so I figured DDD was a good fit here.

Background

At our company, we have a number of product consultants (salesmen) that earn commission on orders they sell within a period (a month-long time period). Product consultants are each assigned to a particular commission plan which determines the commission rules that apply to them. A commission rule is a (potentially configurable) business rule that in some way impacts the commission they earn within a period. Commission rules are combined together to form a commission ruleset.

Requirement

Product consultants should be able to view their statement, which is a snapshot of the commission they've earned so far in the period along with any relevant information that factors into this (orders sold, performance targets met, etc.).

Attempted Solution

Following Microsoft's recommendations for DDD-based architectural design, we created a number of classes to model the domain which:

  • Are mapped to and from the database using EF Core
  • Contain both state and relevant business logic (i.e. are not anaemic)

The diagram below shows a simplified view of how these entities look from a persistence point of view:

enter image description here

During statement generation, a set of records in the database are constituted to an object graph with this structure:

enter image description here

A basic statement is generated and then passed to CommissionRuleset, which sequentially passes the statement to each of the rules that compose it. As well as updating the calculated commission, these rules my potentially affect other parts of the statement that ultimately control how the statement is visually represented to the product consultant:

enter image description here

These rules need to be able access services that calculate their performance, lookup their previous payment records, etc. Right now, I handle this by making all those services available from the statement itself:

enter image description here

In this sense, each commission rule modifies the statement using data it accesses from the statement itself. This doesn't feel like the cleanest solution, and it's quickly turning Statement into a god class.

One potential alternative would be to somehow inject these services into the individual commission rules themselves, like this:

enter image description here

However, EF Core does not support injecting arbitrary services into entities during construction (yet), and even if it did, I'm not sure if it's good practice to inject domain services into entity constructors.

The only practical remaining alternative I can think of is to pass a "context object" alongside the statement being generated, which would allow the business rules to get the data they need. So far all the external calculations/lookups these business rules require relate to the product consultant the statement is being generated for (performance calculations or records, payment records, etc.), so I could create at least create a nominally cohesive aggregate service (or context object) along the lines of IProductConsultantDataService or something.

Is this the best approach or is there a better alternative to this?


Sidenote: there are some parts of my redesign that I feel iffy about, but the decision to model business rules as separate classes that can be arranged in a pipeline feels incredibly "right" for this particular system.

There are many business rules in this system, and new ones are likely to come along in the future. New rules can be encapsulated as their own individual classes, added to the pipeline independently, removed independently, tested in isolation; arbitrary sets of rules with arbitrary configurations can be arranged in arbitrary orders within a ruleset, and using EF Core this all cleanly persists and reconstitutes from the database.

I'm very happy with this particular aspect of the design.

No correct solution

OTHER TIPS

There are three ways that Dependency Injection can be done

  • Constructor injection: the dependencies are provided as extra arguments to the constructor
  • With setters: After the object has been created but before the dependencies are required, a setter is called that sets the dependencies (usually a separate setter call for each dependency)
  • As extra arguments: Rather that storing the dependencies in the object that needs them, the dependencies are provided as extra arguments to the method(s) where they are needed.

I am not familiar enough with EF Core to tell how feasible the first two options are. You already stated that the first option is not supported, but you can check if it is feasible to set the dependencies right after each Rule is created.

If neither is feasible, then passing the dependencies in the call that needs them is a very valid alternative. And bundling them in a parameter object, or a context object as you called it, is also a well known technique to combine highly related function arguments.

On the one hand, you could consider breaking with the DDD-non-anemic entities and move the logic into actual service classes that contain both the required dependency and the CommissionRule.

If you want to keep it within the CommissionRule, you need to inject the required dependency, and I believe using the method injection is fine. You already suggested passing a "context" object with all dependencies. However, if you only want the single required dependency passed, you could do something along the lines of a visitor pattern (pseudocode):

class ProductConsultantDataService 
    BusinessRuleDependency1 dependency1; // same for other dependencies
    
    call(BusinessRule1 rule1, Statement statement)
        rule1.calculate(statement, dependency1)


class BusinessRule1
    accept(BusinessRuleService service, Statement statement)
        service.call(this, statement);
        
    calculate(Statement statement, BusinessRuleDependency1 dependency) 
        ...

One of the benefits is that each rule only accesses the dependency it explicitly knows. This only works well if each rule has a very small number of dependencies, else your approach of passing the parameter object is probably better. Furthermore, this would require you to change this service once a new rule is added (even though it is a trivial change).

EDIT: this answer gives an idea that it is possible to also inject the dependency into the entity. (You decide if having a service inside an entity is actually such a good idea)

Licensed under: CC-BY-SA with attribution
scroll top