Images in this post missing? We recently lost them in a site migration. We're working to restore these as you read this. Should you need an image in an emergency, please contact us at imagehelp@codebetter.com
Using Interceptors To Enforce Aggregate Boundaries

An AGGREGATE is a cluster of associated objects that we treat as a unit for the purpose of data changes. Each AGGREGATE has a root and a boundary. The boundary defines what is inside the AGGREGATE. Domain-Driven Design, Eric Evans

This pattern is at the root of decision making while breathing life into the abstraction of business concerns, yet how to apply these principles in the corners of an application can throw me into a quandary.

Aggregates referencing other aggregates is one such quandary, especially when it comes time to employ the repository(s) associated with each aggregate root.

Using NHibernate can seduce you away from abiding by the aggregate rule with its cascading benefits. I am okay with that, perhaps requiring lazy-loading across aggregate boundaries, since the benefits are great and I am not sure it muddies the aggregate water too much.

But I have a situation where I have the following AGGREGATES:

public class MaterialsTest
{
    public Sample Sample {get;set;}
}
 
public class Sample
{
    public string SampleNumber{get;set;}
}

 

Note that MaterialsTest references Sample. It turns out that MaterialsTest will have several implementations, creating an n...1 relationship to Sample.

As I mentioned, both are aggregates with their respective repositories and NHibernate could let me just cascade my persistence to Sample via mapping. However, there is another rule that popped up that requires the 'SampleNumber' to be set/updated for EACH sample that is saved. That is a Sample specification, though, not a MaterialsTest specification so I want to be sure to keep it in the right camp without munging up my MaterialsTestRepository with references to SampleRepository...I agree it could be argued that the reference is okay but I wanted to keep the rule separate from the repository concerns.

Currently,  I am only doing In Memory representations of my persistence, so while I could leap in to NHibernate and employ an IInterceptor.OnFlushDirty call to sniff changes on the properties that determine the value for 'SampleNumber', I thought I'd see if Castle Project's IInterceptor could hep' a brutha out and it turns out I have a cleaner division between my aggregate roots by using interceptors on my repositories.

First my test...I had to learn how to test interceptors along the way. Note that 'SandCone' and 'NuclearDensity' is my MaterialsTest implementations here:

[TestFixture]
[TestsOn(typeof (SampleRepositoryInterceptor))]
public class SampleRepositoryInterceptorTest
{
    private IFieldDensityRepository mockRepos;
    private MockRepository mocks;
    private ProxyGenerator proxyGenerator;
    private ISampleRepository sampleRepository;
    private SampleRepositoryInterceptor sut;
    private IFieldDensityRepository target;
 
    [SetUp]
    public void BeforeTest()
    {
        mocks = new MockRepository();
        proxyGenerator = new ProxyGenerator();
        sampleRepository = mocks.CreateMock<ISampleRepository>();
        mockRepos = mocks.DynamicMock<IFieldDensityRepository>();
        sut = CreateSUT();
 
        target =
            proxyGenerator.CreateInterfaceProxyWithTarget<IFieldDensityRepository>(mockRepos, sut);
    }
 
    [TearDown]
    public void AfterTest()
    {
        mocks.ReplayAll();
        mocks.VerifyAll();
    }
 
    public SampleRepositoryInterceptor CreateSUT()
    {
        return new SampleRepositoryInterceptor(sampleRepository);
    }
 
    [Test]
    public void INterceptSaveMethod_ShouldSaveEnumerableSamples()
    {
        SandCone sc1 = new SandConeTestDataBuilder().AsNew().Build();
        SandCone sc2 = new SandConeTestDataBuilder().AsNew().Build();
        Sample[] samplesToSave = new Sample[] {sc1.Sample, sc2.Sample};
        using (mocks.Record())
        {
            Expect.Call(sampleRepository.Save(samplesToSave)).Return(samplesToSave);
        }
        mocks.ReplayAll();
 
        target.Save(new FieldDensity.FieldDensity[] {sc1, sc2});
    }
 
    [Test]
    public void INterceptSaveMethod_ShouldSaveSingleSample()
    {
        SandCone sc1 = new SandConeTestDataBuilder().AsNew().Build();
        using (mocks.Record())
        {
            Expect.Call(sampleRepository.Save(sc1.Sample)).Return(sc1.Sample);
        }
        mocks.ReplayAll();
 
        target.Save(sc1);
    }
}
 
Now the code that makes it pass:
public class SampleRepositoryInterceptor : IInterceptor
{
    private readonly ISampleRepository sampleRepository;
    public bool Intercepted;
 
    public SampleRepositoryInterceptor(ISampleRepository sampleRepository)
    {
        this.sampleRepository = sampleRepository;
    }
 
    #region IInterceptor Members
 
    public void Intercept(IInvocation invocation)
    {
        if (Equals(invocation.Method.Name, "Save"))
        {
            if (invocation.Arguments[0] is IEnumerable)
            {
                List<Sample> samples = new List<Sample>();
                IEnumerator enumerator = ((IEnumerable) invocation.Arguments[0]).GetEnumerator();
                while (enumerator.MoveNext())
                {
                    if(enumerator.Current is IReferenceSample)
                    samples.Add(((IReferenceSample) enumerator.Current).Sample);
                }
                sampleRepository.Save(samples.ToArray());
            }
            else
            {
                if(invocation.Arguments[0] is IReferenceSample)
                {
                    sampleRepository.Save(((IReferenceSample)invocation.Arguments[0]).Sample);
                }
            }
        }
        invocation.Proceed();
    }
 
    #endregion
}

 

Finally, I can decorate my InMemoryFieldDensityRepository with [Interceptor(typeof(SampleRepositoryInterceptor))] and register the interceptor in my container (using Binsor)

Component("sample_repository_interceptor",SampleRepositoryInterceptor,SampleRepositoryInterceptor,LifestyleType.Transient)

 

Now my client code can just call the repository method for the MaterialsTestRepository implementation (InMemoryFieldDensityRepository above) and my interceptor will enforce my aggregate boundary rule.

fieldDensityRepository.Save(myMaterialsTest);

This gives me freedom to slip in the appropriate SampleNumber rule I mentioned above just before items are delivered to my SampleRepository.

So I learned how to test interceptors, implement them to help me enforce DDD constraints, and simplify my management in the meantime...not bad!

When I implement my NHibernate repositories, I'll be getting even more granular in generating this SampleNumber using NHibernate's built in interception to avoid unnecessary Updates on Samples. Here is another benefit of this interception technique...I get to avoid changing client code to deal with an implementation change.


Posted 02-06-2008 11:16 PM by Michael Nichols

[Advertisement]

Comments

Scott Bellware wrote re: Using Interceptors To Enforce Aggregate Boundaries
on 02-07-2008 4:41 AM

Mike,

What kind of problems in your work were you facing that is solved by using strong runtime constraints to enforce the semantics of the pattern?

Mike wrote re: Using Interceptors To Enforce Aggregate Boundaries
on 02-07-2008 4:15 PM

@Scott,

I glazed over this in the post, but the requirement for the Sample.SampleNumber generation requires guaranteed incrementing with no gaps according to the state of all samples matching the signature of each sample sorted on their (mutable) SampledOn property.

Another identifier which has similar requirements is going to be built in too. I struggled figuring out how I could enforce reqs like these within the domain without letting implicit enforcement of these rules seep into client code.

I considered simply pushing the specs down into a trigger on the db to enforce but I really really don't like that.

Is there a potential problem you see I could create in my implementation in the post?

About The CodeBetter.Com Blog Network
CodeBetter.Com FAQ

Our Mission

Advertisers should contact Brendan

Subscribe
Google Reader or Homepage

del.icio.us CodeBetter.com Latest Items
Add to My Yahoo!
Subscribe with Bloglines
Subscribe in NewsGator Online
Subscribe with myFeedster
Add to My AOL
Furl CodeBetter.com Latest Items
Subscribe in Rojo

Member Projects
DimeCasts.Net - Derik Whittaker

Friends of Devlicio.us
Red-Gate Tools For SQL and .NET

NDepend

SlickEdit
 
SmartInspect .NET Logging
NGEDIT: ViEmu and Codekana
LiteAccounting.Com
DevExpress
Fixx
NHibernate Profiler
Unfuddle
Balsamiq Mockups
Scrumy
JetBrains - ReSharper
Umbraco
NServiceBus
RavenDb
Web Sequence Diagrams
Ducksboard<-- NEW Friend!

 



Site Copyright © 2007 CodeBetter.Com
Content Copyright Individual Bloggers

 

Community Server (Commercial Edition)