Page 14 - MSDN Magazine, October 2017
P. 14

Domain-Driven Design Fundamentals, which I created with Steve Smith (bit.ly/PS-DDD). There are other important facets to a value object and I use a ValueObject base class created by Jimmy Bogard (bit.ly/13SWd9h) to implement them.
PersonFullName is used to encapsulate common rules in my domain for using a person’s name in any other entity or type. There are a number of notable features of this class. Although it hasn’t changed from the earlier version, I didn’t provide the full listing in the previous article. Therefore, there are a few things to explain here, in particular the Empty factory method and the IsEmpty method. Because of the way Owned Entity is implemented in EF Core, it can’t be null in the owning class. In my domain, Person- FullName is used to store a samurai’s secret identity, but there’s no rule that it must be populated. This creates a conflict between my business rules and the EF Core rules. Again, I have a simple enough solution that I don’t feel the need to create and maintain a separate data model, and it doesn’t impact how Samurai is used. I don’t want anyone using my domain API to have to remember the EF Core rule, so I built two factory methods: You use Create if you have the values and Empty if you don’t. And the IsEmpty method can quickly determine the state of a PersonFullName. The entities that use PersonFullName as a property will need to lever- age this logic and then anyone using those entities won’t have to know anything about the EF Core rule.
Tying It All Together with the Aggregate Root
Finally, the Samurai class is listed in Figure 5. Samurai is the root of the aggregate. An aggregate root is a guardian for the entire aggregate, ensuring the validity of its internal objects and keeping them consistent. As the root of this aggregate, the Samurai type is responsible for how its Entrance, Quotes and SecretIdentity prop- erties are created and managed.
Like the other classes, Samurai has an overloaded constructor, which is the only way to instantiate a new Samurai. The only data expected when creating a new samurai is the samurai’s known name. The constructor sets the Name property and also generates
Figure 4 The PersonFullName Value Object
a value for the GuidId property. The SamuraiId property will get populated by the database. The GuidId property ensures that my domain isn’t dependent on the data layer to have a unique identity and that’s what’s used to connect the non-root entities (Entrance and Quote) to the Samurai, even if the Samurai hasn’t yet been persisted and honored with a value in the SamuraiId field. The constructor appends “: this()” to call the parameterless con- structor in the constructor chain. The parameterless constructor (reminder: it’s also used by EF Core when creating objects from query results) will ensure that the Quotes collection is instantiated and that SecretIdentity is created. This is where I use that Empty factory method. Even if someone writing code with the Samurai never provides values for the SecretIdentity property, EF Core will be satisfied because the property isn’t null.
The full encapsulation of Quotes in Samurai isn’t new. I’m taking advantage of the support for IEnumerable that I discussed in an earlier column on EF Core 1.1 (msdn.com/magazine/mt745093).
The fully encapsulated Entrance property has changed from the previous sample in only two minor ways. First, because I removed the factory method from Entrance, I’m now instantiating it direct- ly. Second, the Entrance constructor now takes additional values so I’m passing those in even though at this time the Samurai class isn’t currently doing anything with these extra values.
Figure 5 The Samurai Entity, Which Is the Root of the Aggregate
public class Samurai {
public Samurai (string name): this() {
Name = name; GuidId=Guid.NewGuid(); IsDirty=true;
}
private Samurai () {
_quotes = new List<Quote> ();
SecretIdentity = PersonFullName.Empty (); }
public int Id { get; private set; } public Guid GuidId{get;private set;} public string Name { get; private set; } public bool IsDirty { get; private set; }
private readonly List<Quote> _quotes = new List<Quote> (); public IEnumerable<Quote> Quotes => _quotes.ToList (); public void AddQuote (string quoteText) {
// TODO: Ensure this isn't a duplicate of an item already in Quotes collection _quotes.Add (Quote.Create(GuidId,quoteText));
IsDirty=true; }
private Entrance _entrance;
private Entrance Entrance { get { return _entrance; } }
public void CreateEntrance (int minute, string sceneName, string description) {
_entrance = Entrance.Create (GuidId, minute, sceneName, description); IsDirty=true;
}
public string EntranceScene => _entrance?.SceneName;
private PersonFullName SecretIdentity { get; set; } public string RevealSecretIdentity () {
if (SecretIdentity.IsEmpty ()) { return "It's a secret";
} else {
return SecretIdentity.FullName ();
} }
public void Identify (string first, string last) { SecretIdentity = PersonFullName.Create (first, last); IsDirty=true;
} }
public class PersonFullName : ValueObject<PersonFullName> {
public static PersonFullName Create (string first, string last) {
return new PersonFullName (first, last); }
public static PersonFullName Empty () { return new PersonFullName (null, null);
}
private PersonFullName () { }
public bool IsEmpty () {
if (string.IsNullOrEmpty (First) && string.IsNullOrEmpty (Last)) {
return true; } else {
return false; }
}
private PersonFullName (string first, string last) {
First = first;
Last = last; }
public string First { get; private set; } public string Last { get; private set; }
public string FullName () => First + " " + Last;
} }
10 msdn magazine
Data Points


































































































   12   13   14   15   16