Ok, after a long review of the various versions of my forum app, I have come to some alarming conclusions.
While I like Linq2Sql’s ability to define separate namespaces for the generated context and entities, I’d be stuck with SqlServer. That isn’t going to fly.
Context and Entities must be in the same namespace, and poco support is currently not up to par. This makes it impossible to put entities in a domain project and the mappings in another. This isn’t going to fly either.
I simply prefer the data model over the domain driven model, and there are simply too many considerations, and too much overhead, in working with NHibernate for me to build my entities in a way that works with me. This isn’t going to fly either!
What alternatives do I have left that offer a true code first approach, along with persistence ignorance?
The UI should NEVER dictate how your domain is structured.
Let’s take what you want to build and put it back to it’s basic components
You have a Forum
A forum has many Posts
A post can have many Replies
So a rough pseudo structure would be something like this:
public class Forum {
public virtual IList<Thread> Threads { get; set; }
}
public class Thread {
public virtual IList<Post> Posts { get; set; }
}
public class Post {
public virtual IList<Reply> Replies { get; set; }
}
public class Reply {
}
So if you’re in a ThreadView, get the Thread object, which holds the list of your Posts, giving you all posts, and gives you access to the Thread Id.
If you want to make the Thread aware of it’s parent, add the association which gives you access to the parent object and also it’s ID, no need to have a separate field to store the ID.
Well that was my first inclination actually, which was why I said I “needed” them in the first place.
It’s kind of a relief, after accidentally irritating you, to find you are in agreement with me on using back references for this task.
Normally, I prefer not to use them, in order to avoid accidentally updating a parent via a child, but who’s going to code something up that silly anyway?
public class Forum {
public virtual Forum Parent {get; set;}
}
and your Mapping (FluentNH)
public class ForumMap : ClassMap<Forum> {
// all the forum guff
References(x => x.Parent);
}
Er… I wasn’t being condescending. I was trying to verify I understood you correctly in your suggestion that I should use a threadRepository instead of trying to make the cateogryRepository work. =/
Mate what you choose to implement is up to you, the above code wasn’t “Copy, paste and it will work”, it’s pseudo code.
The principals on how to do this is there and yes it should work fine given the structure.
It’ll just be a matter of setting up your mappings accordingly and set relationships as Inverse where required and managing your Cascades.
Also, next time you want to post a reply with a condescending tone and try to woo someone with your knowledge of DDD, make sure the reply you were “correcting” wasn’t based on your previous post! :x
// ReplyController
[httppost, valid…]
public ActionResult Move(ReplyMoveModel model)
{
// problem one: using a single repository per aggregate chain
// the only logical thing to use here is the ForumRepository
Reply entity = ForumRepository.Getxxxx(model.Id);
// but I have no clue how to fetch the entity in this fashion
// due to the variable depth at which the entity might be.
// I’d much rather do…
Reply entity = ReplyRepository.SelectById(id);
entity.SetThread(ThreadRepository.SelectById(model.ThreadId);
ReplyRepository.Update(entity);
// but this breaks ddd and subsequent selects won’t reflect any changes.
}
The following line suggests the use a thread repository…
var threadYouWantToMoveTo = repository.Get(id);
So let me rewrite it…
var threadYouWantToMoveTo = threadRepository.Get(id);
The problem is, in DDD, you should only expose repositories for your aggregate roots. In my case, it’s actualy the CategoryRepository (category is basically a forum, without the Moderated and Enabled properties, and also maps to the forum table).
An example would be in a typical ddd app (the estore)…
{controller}/{customerName}/Order/{orderId}/{action}/{id}
==
Customer/Andrew/Order/42/Add/99
public ActionResult Add(string customerName, int orderId, int id)
{
Customer c = customerRepository.GetByName(customerName);
Order o = c.FindOrder(x => x.Id == orderId);
o.AddItem(new OrderLineItem(id));
customerRepository.Update(c);
}
Ok, in the above example, only the aggregate root repository is ever used. The problem comes in when there are so many levels of nesting possible in my app. It isn’t a simple Category -> Forum -> Thread -> Reply scenario. If it were, I could use a fancy route like the one above.
public class Forum {
public virtual IList<Thread> Threads { get; set; }
public virtual IList<Forum> Forums { get; set; }
}
public class Thread {
public virtual IList<Post> Posts { get; set; }
}
public class Post {
public virtual IList<Reply> Replies { get; set; }
}
public class Reply {
public virtual IList<Reply> Replies { get; set; }
}
You’re almost there, just change your thinking slightly.
Moving shouldn’t be a hassle because all you have to do is take a single entity and add it to another.
lets say you want move a thread
var originalThread = blah;
var threadYouWantToMoveTo = repository.Get(id);
threadYouWantToMoveTo.AddThread(originalThread); // Simply adds it to the IList<Thread> of another thread
Just make sure you have your mappings set up properly (HasMany vs. HasManyToMany vs. References, HasOne, HasAny etc)
My general approach is always code first and will generally use NH along with SqlLite to start development and testing by simply using SchemaExport to create the database in SqlLite on the fly and worry about my application first, then move to the database and do the tweeking then, rather than worry too much about the database upfront.
I’ve written a quick article on how to quickly move from in-memory to integration tests which gives you a quick insight how I wire that up.
Now, I want to split the thread on reply4 which happens to have an Id of 42 and move everything below that to a new thread.
// ReplyController
[httppost, valid…]
public ActionResult Move(ReplyMoveModel model)
{
// problem one: using a single repository per aggregate chain
// the only logical thing to use here is the ForumRepository
Reply entity = ForumRepository.Getxxxx(model.Id);
// but I have no clue how to fetch the entity in this fashion
// due to the variable depth at which the entity might be.
// I’d much rather do…
Reply entity = ReplyRepository.SelectById(id);
entity.SetThread(ThreadRepository.SelectById(model.ThreadId);
ReplyRepository.Update(entity);
// but this breaks ddd and subsequent selects won’t reflect any changes.
}
Ok, that was about the size of it. I really DO need these elements. If you can explain how to do such an operation without them, I’d be overjoyed.
As for the ParentId property. That makes it easier for me to select by parentid and not have to use an Expression.Sql in the criteria. If I added the back reference only, I could use entity.Parent.Id == parentId, but that might actually cause problems if a given entity has null for Parent, which can happen.
This whole thing is just SO confusing to me. Data Models were much easier to work with. =/
I don’t have a problem with NH / FNH. It works great, and does what it’s designed to do. Aside from not being able to use SqlLite (running a 64 bit machine), I am now quite familiar with how to use it, right down to Projections and Expression.Sql() to get at things using table fields not present in entities. I even have a unit test that uses config.SchemaExport Drop and Create to setup the database. So db is the the least of my concerns.
That is not an issue.
The issue is that my UI design requires the presence of both back references as well as their keys. This is something NH dissallows. That’s pretty clear cut. I can’t use it.
And before you ask, I already posted on that topic in another recent thread here, so I won’t restate the whole thing.
I’ve always had a problem articulating my problems, especially when I can’t exactly pinpoint the problem myself. You’ll have to forgive for that. I know it can be frustrating.
Anyway, moving on…
I will give it one more try, and hopefully explain it a little better this time.
Forget my entities and my mapping for the minute. Assume they are all correctly designed and appropriately mapped.
Let’s look at an actual example from my app.
ForumController
[HttpGet]
public ActionResult Edit(int id)
{
return View(forumService.GetForumEditModel(id));
}
[HttpPost, ValidateAntiForgeryToken]
public ActionResult Edit(ForumEditModel model)
{
// merge validate error collection into ModelState
ConsumeErrors(forumService.ValidateForumAddModel(model));
// exit now if there were any validation errors
if (!ModelState.IsValid) return View(model);
// forward the model to the service for processing
forumService.ProcessForumEditModel(model);
// return to manager unless HandleError has already redirected us
return RedirectToAction("Manager");
}
ForumService
public void ProcessForumEditModel(ForumEditModel model)
{
// first get the target entity
// and yes, we are breaking away from the root
// repository only idiom. I could not figure out how
// to select a forum by id using the cateogry repository.
Forum target = forumRepository.SelectById(model.Id);
// set protected properties that are allowed to change
target.SetTitle(model.Title);
// update the rest of the properties
target.Description = model.Description;
target.Moderated = model.Moderated;
target.Enabled = model.Enabled;
target.Visible = model.Visible;
// are we assigning a parent or unparented?
// model.ParentId is chosen from a selectlist.
if (model.ParentId.HasValue)
{
Forum parent = forumRepository.SelectById(model.ParentId.Value);
parent.AddForum(target);
forumRepository.Update(parent);
}
else
{
// I'm not sure what to do here because the entity has no
// knowledge of it's current parent. What I need to do
// though is ensure that the entity isn't currently assigned
// to another forum. If it is, remove it and save/update.
}
}
Now, if I added the either the back ref, or the ref id, I could do it. And I can do this quite easily. What I am trying to find out is if there is a way to do so with my entities as-is without grabbing a whole wad of forums from the repository and recursively searching them for the one that owns the entity (if any).