Collections of entities

66 views
Skip to first unread message

Henrik Olsson

unread,
Dec 1, 2011, 2:15:05 AM12/1/11
to ncqr...@googlegroups.com
I'm trying to use ncqrs for a prototype. If I simplify the domain I have three entities: healthcare facility, booking and person. I chose healthcare facility as an AR with a collection of bookings, and person as another AR. Booth healtcare facility and person have external IDs (non-guids), but the also have internal IDs (guids) which are generated by the client when register commands (RegisterHealthcareFacility, RegisterPerson) are constructed. Bookings have references to persons (using the guid). So far so good. But then I wish to have a rule in my domain model that stops the same person from being registered twice, so I would like to check that the external ID (social security number) is unique. Then the problem is how to do that since instances of persons don't know about each other.
 
I thought about creating a new AR Persons that would have a collection of Person instances. In a Persons method, say void RegisterPerson(Guid personId, string ssn, string name), I could easily do the uniqueness check, but would create two other problems:
 
1. The booking entity would reference an entity (person) in another aggregate other than the aggregate root. That is not good.
2. In the RegisterPersonCommandExecutor, I would have to look up the only Persons instance in the context using a Guid (context.GetById<Persons>(command.PersonsId)), which feels awkward.
 
This problem feels like a very common one, so I hope there is simple solution.

Wayne Douglas

unread,
Dec 1, 2011, 2:42:06 AM12/1/11
to ncqr...@googlegroups.com

This is a standard Cqrs issue. Look up set based validation on Greg youngs site.

Also, why two IDs? It doesn't sound right.

Henrik Olsson

unread,
Dec 1, 2011, 4:22:37 AM12/1/11
to ncqr...@googlegroups.com
The distilled version is that the client should do the validation before sending the command. If that is not sufficient, I could have an event handler detecting the situation and perhaps sending a ResolveDuplicatePerson command. If that is not sufficent, I could have a domain service that maintains the set and performs validation.
 
By two IDs, do you mean both Guid (internal ID) and SSN (external ID)? If yes, the answer is that users use SSN, but ncqrs requires that the AR ID is of type Guid. Maybe I don't need to have SSN in the domain model, only in the read model - I have to think about that.

Greg Young

unread,
Dec 1, 2011, 4:27:08 AM12/1/11
to ncqr...@googlegroups.com
For these problems the easiest way is to make the key of the aggregate
natural. That post was intended to show more difficult problems (like
when we have something like only 22000 customers can be gold
customers) for uniqueness its relatively simple

--
Le doute n'est pas une condition agréable, mais la certitude est absurde.

Henrik Olsson

unread,
Dec 1, 2011, 4:49:03 AM12/1/11
to ncqr...@googlegroups.com
How do I make the key of the aggregate natural with ncqrs?

Wayne Douglas

unread,
Dec 1, 2011, 5:05:01 AM12/1/11
to ncqr...@googlegroups.com

You already have one

Henrik Olsson

unread,
Dec 1, 2011, 5:14:23 AM12/1/11
to ncqr...@googlegroups.com
Yes, but doesn't ncqrs require a Guid as the AR key?

Wayne Douglas

unread,
Dec 1, 2011, 5:19:12 AM12/1/11
to ncqr...@googlegroups.com
this was my issue.

Greg Young

unread,
Dec 1, 2011, 5:20:49 AM12/1/11
to ncqr...@googlegroups.com
not sure ... most support it ... eg: user:gregoryyoung1

On Thu, Dec 1, 2011 at 4:49 AM, Henrik Olsson <u.henri...@live.se> wrote:
> How do I make the key of the aggregate natural with ncqrs?

--

Wayne Douglas

unread,
Dec 1, 2011, 5:32:51 AM12/1/11
to ncqr...@googlegroups.com
He has a social sec number.

we need to make the ID in NCQRS a genric or something so we can set it to whatever we want and then we can set the ID in the ctor of the AR

Is this possible?

IMO that would make NCQRS much more viable.

Henrik Olsson

unread,
Dec 1, 2011, 4:24:35 PM12/1/11
to ncqr...@googlegroups.com
That would be great. The question is how to do that without breaking existing code. Is it possible to create a generic, or should an "alternate ID" be added as a new field in code and in the event store?

Olly Lite

unread,
Nov 21, 2012, 4:40:33 PM11/21/12
to ncqr...@googlegroups.com
 
 
I believe I had the same problem. here is a section of code that I've used as a test in order to validate this. There was also a couple internal NCQRS bugs I had to work around when using JSON serialization to RavenDB for snapshots, there are comments for those, and if need be I can post the changes I had to make to NCQRS inorder to get this to function as expected
 
namespace Test.Domain
{
 
    #region Root and Entity Classes
 
    /// <summary>
    /// JsonObject is required to mitigate the self referencing loop error from Newtonsoft JSON serializer
    /// Since we are referencing an Entity
    /// </summary>
    [JsonObject(IsReference = true)]
    public class TestAr : AggregateRootMappedWithExpressionsISnapshotable<TestAr>
    {
        [NonSerializedprivate List<TestEntity_entities = new List<TestEntity>();
 
        [AggregateRootId]
        public Guid TestArId { getset; }
 
        public string TestArData { getset; }
        public DateTime CreatedonUtc { getset; }
        public DateTime ModifiedOnUtc { getset; }
 
        //Entities
        public List<TestEntityTestEntities
        {
            get { return _entities; }
            set { _entities = value; }
        }
 
        #region Constructors
 
        public TestAr()
        {
            IsArCreated();
            EventApplied += ArEventApplied;
        }
 
        public TestAr(Guid testarid, string testardata)
            : this(testarid)
        {
            CreateTestAr(testarid, testardata);
        }
 
        private TestAr(Guid testarid)
            : base(testarid)
        {
            TestArId = testarid;
            EventApplied += ArEventApplied;
        }
 
        #endregion
 
        #region Private Methods
 
        /// <summary>
        /// Fired after NCQRS events are applied
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        /// <remarks>
        /// Can be used to fire subsequent commands/events
        /// Pumped from the <see cref="IEventHandler{TEvent}"/> event queue on the <see cref="IPublishedEvent{TEvent}"/> bus
        /// </remarks>
        private static void ArEventApplied(object sender, EventAppliedEventArgs e)
        {
            //Display the event
            Console.WriteLine(e.Event);
        }
 
        /// <summary>
        /// Validated that the business AR id exists for use
        /// </summary>
        private void IsArCreated()
        {
            if (EventSourceId.Equals(Guid.Empty))
            {
                throw new CoreException(
                    "The Test AR has not been created and no operations can be executed on it").
                    GetException<ApplicationException>();
            }
        }
 
        #endregion
 
        public override void InitializeEventHandlers()
        {
            Map<TestArCreated>().ToHandler(OnTestArCreated);
            Map<TestArChanged>().ToHandler(OnTestArChanged);
            Map<TestEntityCreated>().ToHandler(OnTestEntityCreated);
        }
 
        #region Snapshot
 
        public TestAr CreateSnapshot()
        {
            return this;
        }
 
        public void RestoreFromSnapshot(TestAr snapshot)
        {
            TestArData = snapshot.TestArData;
            CreatedonUtc = snapshot.CreatedonUtc;
            ModifiedOnUtc = snapshot.ModifiedOnUtc;
 
            foreach (var entity in snapshot.TestEntities)
                entity.SetParent(this);
 
            TestEntities = snapshot.TestEntities;
        }
 
        #endregion
 
        #region CQRS Commands and Events
 
        protected void CreateTestAr(Guid testarid, string testardata)
        {
            var clock = NcqrsEnvironment.Get<IClock>();
            var e = new TestArCreated
                {
                    TestArData = testardata,
                    CreatedonUtc = clock.UtcNow()
                };
            ApplyEvent(e);
        }
 
        protected void OnTestArCreated(TestArCreated e)
        {
            TestArData = e.TestArData;
            CreatedonUtc = e.CreatedonUtc;
        }
 
        protected void ChangeTestAr(string testardata)
        {
            var clock = NcqrsEnvironment.Get<IClock>();
            var e = new TestArChanged
                {
                    TestArData = testardata,
                    ModifiedonUtc = clock.UtcNow()
                };
            ApplyEvent(e);
        }
 
        protected void OnTestArChanged(TestArChanged e)
        {
            TestArData = e.TestArData;
            ModifiedOnUtc = e.ModifiedonUtc;
        }
 
        protected void CreateTestEntity(Guid testentityid, string entitydata)
        {
            var clock = NcqrsEnvironment.Get<IClock>();
            var e = new TestEntityCreated
                {
                    TestEntityId = testentityid,
                    EntityData = entitydata,
                    CreatedonUtc = clock.UtcNow()
                };
            ApplyEvent(e);
        }
 
        protected void OnTestEntityCreated(TestEntityCreated e)
        {
            var o = new TestEntity(this, e.TestEntityId);
            TestEntities.Add(o);
        }
 
        protected void ChangeTestEntity(Guid testentityid, string entitydata)
        {
            var o = TestEntities.First(x => x.EntityId == testentityid);
            o.ChangeTestEntity(entitydata);
        }
 
        #endregion
    }
 
    /// <summary>
    /// Test Entity Class
    /// </summary>
    public class TestEntity : EntityMappedByConvention
    {
        public string EntityData { getset; }
        public DateTime CreatedOnUtc { getset; }
        public DateTime ModifiedOnUtc { getset; }
 
        public TestEntity(AggregateRoot parent, Guid entityId)
            : base(parent, entityId)
        {
        }
 
        /// <summary>
        /// Used to establish the event handlers from the parent AR
        /// when restored from a snapshot
        /// </summary>
        /// <param name="parent"></param>
        public void SetParent(AggregateRoot parent)
        {
            ParentAggregateRoot = parent;
            SetParentHandlers(parent);
        }
 
        public void ChangeTestEntity(string entitydata)
        {
 
            var clock = NcqrsEnvironment.Get<IClock>();
            var e = new TestEntityChanged
                {
                    EntityData = entitydata,
                    ModifiedOnUtc = clock.UtcNow()
                };
            ApplyEvent(e);
        }
 
        protected void OnTestEntityChanged(TestEntityChanged e)
        {
            EntityData = e.EntityData;
            ModifiedOnUtc = e.ModifiedOnUtc;
        }
    }
 
    #endregion
 
    #region Command and Event Classes
 
    [MapsToAggregateRootConstructor(typeof (TestAr))]
    public class CreateTestAr : CommandBase
    {
        public Guid TestArId { getset; }
        public string TestArData { getset; }
    }
 
    [Serializable]
    public class TestArCreated
    {
        public string TestArData { getset; }
        public DateTime CreatedonUtc { getset; }
    }
 
    [MapsToAggregateRootMethod(typeof (TestAr), "ChangeTestAr")]
    public class ChangeTestAr : CommandBase
    {
        [AggregateRootId]
        public Guid TestArId { getset; }
 
        public string TestArData { getset; }
    }
 
    [Serializable]
    public class TestArChanged
    {
        public string TestArData { getset; }
        public DateTime ModifiedonUtc { getset; }
    }
 
    [MapsToAggregateRootMethod(typeof (TestAr), "CreateTestEntity")]
    public class CreateTestEntity : CommandBase
    {
        [AggregateRootId]
        public Guid TestArId { getset; }
 
        public Guid TestEntityId { getset; }
        public string EntityData { getset; }
    }
 
    [Serializable]
    public class TestEntityCreated
    {
        public Guid TestEntityId { getset; }
        public string EntityData { getset; }
        public DateTime CreatedonUtc { getset; }
    }
 
    [MapsToAggregateRootMethod(typeof (TestAr), "ChangeTestEntity")]
    public class ChangeTestEntity : CommandBase
    {
        [AggregateRootId]
        public Guid TestArId { getset; }
 
        public Guid TestEntityId { getset; }
        public string EntityData { getset; }
    }
 
    [Serializable]
    public class TestEntityChanged : EntitySourcedEventBase
    {
        public string EntityData { getset; }
        public DateTime ModifiedOnUtc { getset; }
    }
 
    #endregion
}
Reply all
Reply to author
Forward
0 new messages