This thread looks to be a little on the old side and therefore may no longer be relevant. Please see if there is a newer thread on the subject and ensure you're using the most recent build of any software if your question regards a particular product.
This thread has been locked and is no longer accepting new posts, if you have a question regarding this topic please email us at support@mindscape.co.nz
|
Imagine a domain model that deals with National Highway Projects. A Project is work planned for a particular Highway. Highways are big and therefore many projects may end up being performed on a particular highway. In this case, the list of Highways is a set of "Master Data" that is maintained mainly so that it can be used to qualify a project's scope. Imagine that a User Interface might expect to use the list of Highways inside of a drop down combo box when defining the scope of a project. A Highway has a name as well as some other useful attributes that are to aid in constraining the available options when defining project scope. (Number of Lanes, Has Traffic Lights, Length, Age etc). Now considering how this would be modeled in Lightspeed it seems to fit the pattern of a 1 to many mapping between Highway and Project (Highway being the One side and Project being the Many side). An awkwardness creeps in here in that for our application's purposes there are two objects and technically two aggregate roots in play. The application must retrieve a set of Highways as reference data to populate the drop down list (Highway is the aggregate root here). And the application must perform CRUD operations on Project as an aggregate root where ideally one would want to use a particular Highway instance as a value of an attribute on Project. Now, at runtime, a user opens the form for creating a new Project and the application retrieves the list of Highway instances and uses the list to bind to a combo box on the Project's data entry screen. (This is a client app so we are maintaining the Highway instances as state associated with the form. The user selects a Highway which sets value of the "Highway" attribute on the new Project instance to the particular Highway instance represented by the item in the drop down. Now, Something intersting happens here I'm assuming because of the bi-directionally mapped association between a Highway and Project object (Lightspeed doesn't allow one way mappings). Even though in this case, the logical Aggregate Root is "Project", Lightspeed (legitimately) thinks that the Highway instance is the rightful unit of work and the unit of work on the Highway instance gets propagated to the new Project instance. Project gets assigned an ID (when using GUIDs) even though it hasn't been persisted, and then eventually a null reference exception gets thrown when adding other child objects to a Project. Is there a recommended way of dealing with a scenario like this? Here are the options I can think of: 1) Disconnect the first class domain model relationship between Project and Highway - have an attribute on Project that represents a Highway but is not actually a reference to a Highway object instance. Consequence - In order to navigate to a Highway's properties from a Project instance, a query would need to be performed to look up the Highway by the attribute's value. Consequence - if the attribute's value is a surrogate key such as a GUID it introuces an awkwardness into the domain model. The domain model becomes aware of database implementation details (public ID properties start appearing as attributes). If the 2) Attempt a "one-way mapping" as described in this post https://www.mindscape.co.nz/forums/Thread.aspx?PostID=3478. I think this is just a variation on #1 Any other ideas? Is there a way to use the UnitOfWorkScope to keep the unit of work on the Highway side of the relationship from being propagated somehow? This pattern is a frequently recurring one. |
|
|
We'd be keen to see a repro case for the NullReferenceException. The fact that the Highway is the aggregate root shouldn't stop you creating a new Project associated with that Highway and adding child objects to it, even though the Project has not yet been persisted. So this sounds like it may be a bug rather than a conceptual issue that needs addressing at the domain model level. However, if you do conceptually want the new Project to become an aggregate root, you could do so without polluting the code too much by adding a method something like this: partial class Highway { You can then dispose the UOW containing the initial Highways lookup and start a new one with the new Project as aggregate root. As you rightly observe this is using ID implementation details of Highway and Project but they are tucked away inside the Highway code so this shouldn't be too objectionable. The key thing is that your application code can still navigate from the new Project to the associated Highway and its properties without needing to perform explicit querying. (This will only work once the new Project has been added to a UOW. I didn't do this automatically within the method above because the client code will want to explicitly create the UOW so it has a reference through which to call SaveChanges etc. Note that the new Project will still be assigned an ID when it is added to the new UOW.) The cost of this is that the Highway will need to be reloaded into the new unit of work when the user first accesses the Project.Highway member. You could possibly avoid this by Attaching the initial Highway instance to the new UOW but I doubt it is worth bothering. But as I say none of this should be technically necessary: the fact that the Highway is the aggregate root rather than the Project shouldn't affect the UOW mechanics, and you should still be able to create an object graph under a Project. So if you can help us repro the problem that stops you working this way then we can try to get it fixed for you. |
|
|
Ivan, Attached is a simple repro for the case that I described above. Thanks for taking a look at this. A few notes:
It would be great to know if this is expected behavior. |
|
|
I'm not getting the NullReferenceException (which would be a bug). Instead, I am getting an ObjectDisposedException. This is expected behaviour, though I admit it's not obvious why; and there's arguably a bug in that you should have got the exception much earlier.. Basically what is happening is that when you set activity.Component = component, activity becomes part of the same business transaction as component. In the LightSpeed implementation, this means activity is entered into the same unit of work as component. Next you add some tasks to activity.Tasks, which causes these tasks to get entered into the same unit of work. So far, so good. But when you remove one of the tasks, LightSpeed tries to remove that task from the unit of work. And when that happens, the unit of work checks whether it has been disposed. Because you disposed the unit of work immediately after loading component, this check fails, and the exception is thrown. So there is a confusing behaviour here in that implicit adds *don't* check that the UOW is still live, but implicit removes *do*. However if we were to fix this it would be by ensuring that implicit adds *do* check the UOW, so that you would get the ObjectDisposedException when LightSpeed tried to enter activity into component's disposed UOW. The underlying behaviour -- that LightSpeed will throw an ObjectDisposedException when you manipulate entities in a disposed UOW -- is expected and would remain. Hopefully that explains what's going on and why. More importantly, what can you do about it? There are three possible approaches: 1. Keep the unit of work alive for as long as you are working with the object graph around component -- i.e. as long as you are adding, removing and saving activities and tasks that have an association to component. This is the best approach if each business transaction is short-lived, but can be problematic if the object graph has to be manipulated for a long time (e.g. the user is added activities and tasks interactively through a Windows Forms UI). 2. Do not connect activity directly to component. Instead, set activity.ComponentId = component.Id. This is just a plain integer assignment so it doesn't cause activity to be entered into component's UOW. It surfaces an implementation detail but this can always be wrapped e.g. partial class Activity { 3. Detach component from the stale UOW before associating it with activity. Detaching component means there's no UOW on component for LightSpeed to enter activity into, so no stale UOW to throw an ObjectDisposedException. This would mean modifying LoadComponent as follows: using (var unitOfWork = GetUOW()) { Hope this helps -- please let me know if it doesn't make sense or if none of the proposed remedies will work for your real scenario. |
|
|
Ivan, unifOfWork.Detach(component) seems like a good remedy as we in fact are using a WPF client where not all editing is done within the scope of a unit of work. Although, I cannot find the Detach method in the API documentation for UnitOfWork or UnitOfWorkBase in LightSpeed2. Is this an extension coming from somewhere else? |
|
|
Oops, sorry. Detach was added to UnitOfWork in the 29 May nightly build. I don't think we've rebuilt the help file lately but there should be XML docs for it. |
|
|
Ok, I'm starting to run into some errors that may reflect a misunderstanding of how LS is working. Please help me get some clarity on this. After your suggestion to use the Detach method we have gotten past some errors but it seems have introduced others. The following is the pattern that we are observing: CreateEntity -> Make Modifications in WPF UI -> Attach to UOW -> SaveChanges -> Detach -> Make More Modifications-> Attach to UOW -> SaveChanges -> Detach -> etc... The assumption that we were working under was that the entities themselves were keeping track of state changes and that attaching those entities to a UOW was simply for the purpose of connecting to the DB to do the dirty work of hydration / dehydration / lazy loading etc. The error that I am now seeing that seems to indicate this is an incorrect understanding is described briefly using a simplified sample domain below: Students have a collection of Classes. Using the workflow described above, we create a Student, add new Class entities to that Student's collection of Classes, attach it to a new UOW, and save changes finally detaching. Then we remove a Class from the Student's collection of Classes, again attach to a new UOW instance, and call save changes. This produces a validation error which points to the removed Class instance not having a valid ParentStudentID and an EntityState of 'Modified'. I assumed the EntityState would be Deleted. Is there an extra step needed to make the workflow described above work? |
|
|
Repro attached. Seems that the answer lies in using the Dependent attribute, but for some reason it isn't automatically deleting. |
|