When you say load in "How do I load just one of the 1000 line items?" do you mean "load from the database"? In other words, how do I load just one child entity of an aggregate root from the database?
This is a bit complex, but you can have your repositories return a derivation of the aggregate root, whose fields are lazy-loaded. E.g.
namespace Domain
{
public class LineItem
{
public int Id { get; set; }
// stuff
}
public class Order
{
public int Id { get; set; }
protected ReadOnlyCollection<LineItem> LineItemsField;
public ReadOnlyCollection<LineItem> LineItems { get; protected set; }
}
public interface IOrderRepository
{
Order Get(int id);
}
}
namespace Repositories
{
// Concrete order repository
public class OrderRepository : IOrderRepository
{
public Order Get(int id)
{
Func<IEnumerable<LineItem>> getAllFunc = () =>
{
Collection<LineItem> coll;
// { logic to build all objects from database }
return coll;
};
Func<int, LineItem> getSingleFunc = idParam =>
{
LineItem ent;
// { logic to build object with 'id' from database }
return ent;
};
// ** return internal lazy-loading derived type **
return new LazyLoadedOrder(getAllFunc, getSingleFunc);
}
}
// lazy-loading internal derivative of Order, that sets LineItemsField
// to a ReadOnlyCollection constructed with a lazy-loading list.
internal class LazyLoadedOrder : Order
{
public LazyLoadedOrder(
Func<IEnumerable<LineItem>> getAllFunc,
Func<int, LineItem> getSingleFunc)
{
LineItemsField =
new ReadOnlyCollection<LineItem>(
new LazyLoadedReadOnlyLineItemList(getAllFunc, getSingleFunc));
}
}
// lazy-loading backing store for LazyLoadedOrder.LineItems
internal class LazyLoadedReadOnlyLineItemList : IList<LineItem>
{
private readonly Func<IEnumerable<LineItem>> _getAllFunc;
private readonly Func<int, LineItem> _getSingleFunc;
public LazyLoadedReadOnlyLineItemList(
Func<IEnumerable<LineItem>> getAllFunc,
Func<int, LineItem> getSingleFunc)
{
_getAllFunc = getAllFunc;
_getSingleFunc = getSingleFunc;
}
private List<LineItem> _backingStore;
private List<LineItem> GetBackingStore()
{
if (_backingStore == null)
_backingStore = _getAllFunc().ToList(); // ** lazy-load all **
return _backingStore;
}
public LineItem this[int index]
{
get
{
if (_backingStore == null) // bypass GetBackingStore
return _getSingleFunc(index); // ** lazy-load only one from DB **
return _backingStore[index];
}
set { throw new NotSupportedException(); }
}
// "getter" implementations that use lazy-loading
public IEnumerator<LineItem> GetEnumerator() { return GetBackingStore().GetEnumerator(); }
IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
public bool Contains(LineItem item) { return GetBackingStore().Contains(item); }
public void CopyTo(LineItem[] array, int arrayIndex) { GetBackingStore().CopyTo(array, arrayIndex); }
public int Count { get { return GetBackingStore().Count; } }
public bool IsReadOnly { get { return true; } }
public int IndexOf(LineItem item) { return GetBackingStore().IndexOf(item); }
// "setter" implementations are not supported on readonly collection
public void Add(LineItem item) { throw new NotSupportedException("Read-Only"); }
public void Clear() { throw new NotSupportedException("Read-Only"); }
public bool Remove(LineItem item) { throw new NotSupportedException("Read-Only"); }
public void Insert(int index, LineItem item) { throw new NotSupportedException("Read-Only"); }
public void RemoveAt(int index) { throw new NotSupportedException("Read-Only"); }
}
}
Callers of OrderRepository.Get(int)
would receive something that is effectively just an Order object, but is actually a LazyLoadedOrder. Of course to do this your aggregate roots must provide a virtual member or two, and be designed around those extension points.
Edit to address question updates
In the case of an address, I would treat it as a value object, i.e. immutable compositions of data that are together treated as a single value.
public class Address
{
public Address(string street, string city)
{
Street = street;
City = city;
}
public string Street {get; private set;}
public string City {get; private set;}
}
Then, in order to modify the aggregate, you create a new instance of Address. This is analogous to the behavior of DateTime. You can also add methods methods to Address such as SetStreet(string)
but these should return new instances of Address, just as the methods of DateTime return new instances of DateTime.
In your case, immutable Address value objects have to be coupled with some kind of observation of the Addresses collection. A straightforward and clean technique is to track added and removed AddressValues in separate collections.
public class Customer
{
public IEnumerable<Address> Addresses { get; private set; }
// backed by Collection<Address>
public IEnumerable<Address> AddedAddresses { get; private set; }
// backed by Collection<Address>
public IEnumerable<Address> RemovedAddresses { get; private set; }
public void AddAddress(Address address)
{
// validation, security, etc
AddedAddresses.Add(address);
}
public void RemoveAddress(Address address)
{
// validation, security, etc
RemovedAddresses.Add(address);
}
// call this to "update" an address
public void Replace(Address remove, Address add)
{
RemovedAddresses.Add(remove);
AddedAddresses.Add(add);
}
}
Alternatively you could back Addresses with an ObservableCollection<Address>
.
This is indeed a pure DDD solution, but you mentioned NHibernate. I'm not an NHibernate expert, but I imagine you will have to add some code to let NHibernate know where changes to Addresses are being stored.