Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Attribute-flavored Domain Driven Design

4.00/5 (2 votes)
6 Apr 2008CPOL8 min read 1   231  
A centralized business domain with a common base
Domain Driven Design

Introduction

This article focuses on Domain Driven Design, concentrating on using some principles from this area in combination with an 'attribute-flavored' approach. The contents are partially based on an article of mine that was published in the December 2007 issue of the Dutch Microsoft .NET Magazine.

Background

Domain Driven Design, often abbreviated as DDD, is a topic that can be encountered frequently these days. Some would undoubtedly refer to it as another hype, and as with any new kid on the block it is accompanied by debates as to its merits, as well as flocks of puristic believers. It is not my intention to venture down either road, also due to the fact that my interpretation of Domain Driven Design may be very different from the original intentions as written down by, most notably, Eric Evans and Jimmy Nilsson. I will list some references for further reading at the end of this article.

Some Principles

In DDD, the domain is centralized as the planet around which services revolve as if they were satellites or moons. In other words, the services are not themselves part of the business domain, but they use the domain.

The domain is explicitly intended as the business domain, or knowledge domain if you will, containing domain classes and business rules. Also, the domain is exposed by one or more repositories for retrieval purposes, and by factories for creational (lifetime) purposes.

A domain model should be replaceable by another domain model, ideally leaving the rest of your application intact. This is a principle that is not core to DDD, but one that I would like to adhere to.

The principles listed above are actually all examples of the more generic principle of 'separation of concerns'. That is not something new, but in Domain Driven Design, you could call this the mother of all principles.

The Ubiquitous Language

Given the principles above, you could imply that DDD is more of a 'duh ... that's common sense' approach. And in fact, it is. DDD does not actually add a new technology, nor does it add new language constructs, and as a methodology it is not actually new either. It combines a lot of stuff that is best practice in many minds and places (but, truthfully, not always at the front of our minds, and not always in the right place). It hammers down on the domain-centric mantra: "Business knowledge, my business, your business". That's not probably a mantra you'll find anywhere, as I just made it up, but the essence is keeping business objects, rules, vocabulary and knowledge centralized in one place and disclosable to services and applications that need them. And in doing so, making sure that every actor involved speaks this language. It must be unambiguous, as concise as possible, and fluent on every tongue. This is what Eric Evans refers to as the ubiquitous language.

Common Base, Common Ground

To enable compliance with the principles summed up above, a viable option is to have every domain model class inherit from a common base class. In the solution that accompanies this article, this is not just a choice between base class and interface, but a fundamental architectural provision that also makes it possible to replace a given model by another (the third principle above). Therefore, more than a base class, we have a complete base assembly that the domain model derives from. This 'basement assembly' contains provisions for properties and property collections, relations, and - as will be evident later on - for the custom attributes and validators that are a second foundation for a domain model such as proposed here.

Basement classes

The Code Base

The solution contains three projects:

  • Codebasement.DomainDrivenDesign.Basement: The basement assembly
  • Codebasement.DomainDrivenDesign.Model: A sample domain model
  • Codebasement.DomainDrivenDesign.App: A simple test form

The basement assembly is a mini framework for domain models. It cannot have references to the model (since the model references the basement assembly), therefore it relies on reflection if necessary to obtain information from the model, and for the most part contains generic constructs that can be used in domain model classes. Now, since code speaks more than words often can, let's look at an example of a domain class that is a derivative of BasementObject.

C#
using System;
using System.ComponentModel;
using System.Drawing.Design;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
using System.Xml.Serialization;
using Codebasement.DomainDrivenDesign.Basement.Attributes;
using Codebasement.DomainDrivenDesign.Basement.Basement;
using Codebasement.DomainDrivenDesign.Basement.Converters;
using Codebasement.DomainDrivenDesign.Basement.Enums;
using Codebasement.DomainDrivenDesign.Basement.Utilities;
using Codebasement.DomainDrivenDesign.Model.Editors;
using Codebasement.DomainDrivenDesign.Model.Enums;
using Codebasement.DomainDrivenDesign.Model.ModelClasses;
using Codebasement.DomainDrivenDesign.Model.Validators;

namespace Codebasement.DomainDrivenDesign.Model.ModelClasses
{
    /// <summary>
    /// This class contains the definition of a server.
    /// </summary>
    [Serializable, DisplayName("Server"), DefaultProperty("HostName")]
    [XmlInclude(typeof (Reservation))]
    [Relatable(BasementObjectType = typeof (Reservation), Cardinality = 0,
        CardinalityType = CardinalityType.CardinalityOrMore)]
    [Linkable]
    public class Server : BasementObject
    {
        private const ServerType _defaultServerType = ServerType.WEBSVR;

        /// <summary>
        /// Initializes a new instance of the server class.
        /// </summary>
        public Server()
        {
            ParameterCollection.Add(new Parameter(this, "ServerType"));
            ParameterCollection.Add(new Parameter(this, "HostName"));
            ParameterCollection.Add(new Parameter(this, "IPAddress"));
        }

        /// <summary>
        /// Initializes a new instance of the server class.
        /// </summary>
        protected Server(SerializationInfo info, StreamingContext context)
            : this()
        {
            ConstructParameters(info);
        }

        /// <summary>
        /// Gets or sets the name of the host.
        /// </summary>
        [Category("Identification")]
        [Mandatory]
        [Description("The hostname of the server.")]
        [RegularExpression(RegexExpressions.HostNameExclusionFormat,
            RegexMatchType.MatchExclusions, RegexOptions.Compiled,
            RegexExpressions.HostNameExclusionFormatHelp)]
        [RefreshProperties(RefreshProperties.All)]
        [StringLength(Min = 4, Max = 40)]
        public string HostName
        {
            get { return ParameterCollection["HostName"].Value; }
            set
            {
                if (PropertyValidators.ValidateStringLength(value, 4, 40))
                    ParameterCollection["HostName"].Value = value;
            }
        }

        /// <summary>
        /// Gets or sets the IP address.
        /// </summary>
        [Category("Identification")]
        [Description("Internet Protocol Address")]
        [RegularExpression(RegexExpressions.IPAddressFormat,
            RegexMatchType.MatchFormat, RegexOptions.Compiled,
            RegexExpressions.IPAddressFormatHelp)]
        public string IPAddress
        {
            get { return ParameterCollection["IPAddress"].Value; }
            set
            {
                if (PropertyValidators.ValidateIPAddress(value))
                    ParameterCollection["IPAddress"].Value = value;
            }
        }

        /// <summary>
        /// Gets or sets the type of the server.
        /// </summary>
        /// <value>The type of the server.</value>
        [Category("Server Type"), DisplayName("Main Server Usage")]
        [DefaultValue(_defaultServerType)]
        [TypeConverter(typeof (EnumDescriptionConverter))]
        [Editor(typeof (ServerTypeEditor), typeof (UITypeEditor))]
        public ServerType ServerType
        {
            get
            {
                string val = ParameterCollection["ServerType"].Value;
                return (ServerType)PropertyValidators.ValidateEnum(typeof(ServerType), 
		val, _defaultServerType);
            }
            set { ParameterCollection["ServerType"].Value = value.ToString(); }
        }

        /// <summary>
        /// Returns a string that represents the current object.
        /// </summary>
        public override string ToString()
        {
            return (!String.IsNullOrEmpty(HostName)
                        ? HostName
                        :
                            base.ToString());
        }
    }
}

Parameters and Properties

To have a generic way of persisting properties and to enable a uniform manner of validation, the properties of domain model instances are represented as parameters, bundled on the ParameterCollection of the base class. The internal value type of a parameter is a string, to allow for anything, but the literals are hidden within the getters and setters of properties and never see daylight other than as meaningful return types only.

Attributes

As you can see, a lot of attributes are used on the domain classes here; standard attributes such as Serializable, DefaultProperty, DisplayName, Description, DefaultValue and Category. Also, type converters and type editors are very useful instruments to extend the expressability of your domain model. I will not go into these standards (i.e. supplied in the .NET Framework) attributes in-depth, there are many articles around that explain their usage and implementation.

Besides standard attributes, there are several custom attributes visible in the code above:

  • Mandatory: Whether a property must have a value
  • StringLength: Maximum allowed length of a string-value property
  • RegularExpression: Whether a property value must comply with a regular expression (or not comply, i.e. excluded characters)
  • Relatable: Whether a class instance is relatable to another class instance, and how
  • Linkable: Whether a class instance can be linked to another class instance (re-used by reference)

Some of these are for validation purposes (and variants can also be found in e.g. the Validation Application Block of the Microsoft Patterns and Practices Enterprise Library). Some are intended to express the characteristics of relations and behavior such as copying, linking, etc.

Validation Pattern

The power of attributes such as illustrated above lies in the very generic way that they can be used, for instance, to apply validation to a domain model such as proposed here. Classes, properties, relations and any other types can be validated uniformly and in a single place, that place being the BasementObject base class. Below is the code for a Validate method that builds up a running ValidationResults collection for any domain class instance (or all at once, namely on a container instance that contains other relatable instances). The template pattern is applied for a protected ValidateCore method that can be used in domain model classes for specific business rules. Violations of the rules can be validated there and added to the ValidationResults collection.

C#
/// <summary>
/// Validates this instance.
/// </summary>
public void Validate()
{
    if (_validationResults != null)
        _validationResults.Clear();
    else
        _validationResults = new ValidationResultCollection();

    AddValidationResults(new ObjectRelationValidator().Validate(this));

    AddValidationResults(new ObjectTypeValidator().Validate(this));
 
    AddValidationResults(new ObjectPropertyValidator().Validate(this));

    ValidateCore();
}

/// <summary>
/// ValidateCore : overridable in derived classes
/// </summary>
protected virtual void ValidateCore()
{
}

Serialization and Repositories

The persistence implementation in the accompanying solution is simple and based on straightforward serialization. The XmlInclude attributes used here are for the standard XmlSerializer. The additional constructor for serialization is required in our case, since the base class contains a custom implementation for the serialization of the (non-standardly implemented) parameters and relations.

In general, a fire and forget all-in-one serialization pattern will not be sufficient of course, bringing a mapping to a relational model or other storage model into play. An important aspect is to have the persistence governed by one or more repositories, possibly in combination with a provider pattern. Here we have the simple implementation of such a repository for straightforward 'container'-based serialization. This will typically have to be replaced by a more extensive repository that can load and save information to and from the domain model, where the 'containers' are to be replaced by well-chosen aggregate boundaries (e.g. customer with order and order lines as a coherent and transactable 'aggregate').

C#
using Codebasement.DomainDrivenDesign.Basement.Enums;
using Codebasement.DomainDrivenDesign.Basement.Serialization;
using Codebasement.DomainDrivenDesign.Model.ModelClasses;

namespace Codebasement.DomainDrivenDesign.Model.Repository
{
    /// <summary>
    /// Repository for access and persistence of the model
    /// </summary>
    public class ModelRepository
    {
        private readonly ISerializationProvider _provider;

        /// <summary>
        /// Initializes a new instance of the ModelRepository class.
        /// </summary>
        public ModelRepository(SerializationType serializationType)
        {
            switch (serializationType)
            {
                case SerializationType.Binary:
                    _provider = new BinarySerializationProvider();
                    break;
                case SerializationType.Soap:
                    _provider = new SoapSerializationProvider();
                    break;
                case SerializationType.Xml:
                    _provider = new XmlSerializationProvider();
                    break;
            }
        }

        /// <summary>
        /// Saves the specified basement object.
        /// </summary>
        public void Save(InformationContainer containerObject,
                         string fileName)
        {
            _provider.Save(containerObject, fileName);
        }

        /// <summary>
        /// Loads the specified file name.
        /// </summary>
        public InformationContainer Load(string fileName)
        {
            return _provider.Load(typeof(InformationContainer), fileName) 
		as InformationContainer;
        }
    }
} 

Factories

Repositories for retrieval and persistence, factories for creation. Below is an example basic factory for creating and deleting domain objects. Note here that these two simple methods create or remove any domain object in the model, they are generic in nature. You could also make CreateObject call itself recursively to create nested relations based solely on the domain model rules for contained relatables.

For a more real-life domain, you would most probably require more specific factory methods that use knowledge about the domain and provide more of a headstart.

C#
/// <summary>
/// Creates the object.
/// </summary>
public static BasementObject CreateObject(
            Type type,
            BasementObject parentObject)
{
    BasementObject newObject =
	Activator.CreateInstance(type) as BasementObject;

    if (newObject != null)
    {
        newObject.ObjectIdentifier = Guid.NewGuid();
        newObject.OnBasementObjectCreated(new BasementObjectEventArgs(newObject));
    }

    if (parentObject != null)
    {
        if (parentObject.Relatables.Contains(type))
            parentObject.Relations.Add(newObject);
        else
            throw new InvalidOperationException(
            String.Format("Type {0} not relatable to object {1}",
            type, parentObject));
    }
    return newObject;
}

/// <summary>
/// Removes the object.
/// </summary>
public static void RemoveObject
	(BasementObject basementObject, BasementObject parentObject)
{
    if (!(parentObject != null &
        basementObject != null)) return;
    parentObject.Relations.Remove(basementObject);
    basementObject.OnBasementObjectRemoved(new BasementObjectEventArgs(basementObject));
}

Sample

The following screenshot shows the sample app that uses the domain model. The treeview is populated using the repository, the factory enables creating and removing instances, and the validation results are visible in the list view.

Sample application

References

Credits

Credits are due for portions of the BasementObject class and several of the custom attributes to Mark Achten and Patrick Peters. I also used a Vista-style progress bar from an article here on CodeProject: Vista Style Progress Bar in C#.

Conclusion

Numerous improvements could and should be made on the code base. However, the point here is the application of some domain driven design principles, making elaborate use of standard and custom attributes, and enabling a domain model that derives from a base assembly that facilitates common ground for any domain model. Essential to Domain Driven Design is to place the business domain back in the spotlight, where it belongs.

History

  • 6th April, 2008: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)