问题
I have a simply Class that is intended to be a simple POCO - it just holds data. With one exception: It contains a Collection of Notes. I want to lazy-load this collection so that I don't have to fetch the Notes on Pages that don't need them. The stub for this is this:
public class MyDTOClass
{
private ICollection<Note> _notes = null;
public ICollection<Note> Notes
{
get
{
if(_notes == null)
{
// Get an INoteRepository and initialize the collection
}
return _notes;
}
}
}
Now, I'm wondering how to proceed from here. It's an ASP.net MVC application and I use Dependency Injection to inject the IRepositories in classes that need them, for example my controllers. But as this class here is supposed to be a really simple DTO, I'm reluctant to inject an INoteRepository into it, also because the caller shouldn't worry or care about the fact that this is lazy-loaded.
So I'm thinking of having another Class in my Model that holds a INoteRepository.
public class MyDataAccessClass
{
private INoteRepository _noteRepo;
// Inject is part of Ninject and makes sure I pass the correct
// INoteRepository automatically
[Inject]
public MyDataAccessClass(INoteRepository noteRepository)
{
_noteRepo = noteRepository;
}
public IEnumerable<Note> GetNotes(int projectId)
{
return _noteRepo.GetNotes(projectId);
}
}
This would work of course, but I wonder if this is the correct architecture? I couple the simple DTOClass to another Data Access class and possibly also to my DI mechanism (as I need to create an Instance of the Data Access class in the getter of Notes).
Would you do it differently? Is there a better way to do this, also keeping in mind I already use Ninject?
I'm guessing that this is not a POCO or DTO anymore as it now contains logic, but that's okay. I want it to appear like a POCO to outside caller so I like to have a Property "Notes" rather than methods like "GetNotesForProject" on this or other classes.
My current solution is really ugly, as I need to get the Ninject Kernel from my MvcApplication and use it to spin up the ProjectDataProvider class which takes an INoteRepository in it's constructor, to avoid having to put the INoteRepository somewhere in my "DTO"-Class:
public ICollection<Note> Notes
{
get
{
if(_notes == null)
{
var app = HttpContext.Current.ApplicationInstance as MvcApplication;
if (app == null)
throw new InvalidOperationException("Application couldn't be found");
var pdp = app.Kernel.Get<ProjectDataProvider>();
_notes = new List<Note>(pdp.GetNotes(Id));
}
return _notes;
}
}
Edit: Opened a bounty. Let's ignore the terminology of "POCO" and "DTO", I'll refactor accordingly. So this is about: How should the Lazy-Loading code look in such a situation, and can/should I avoid passing INoteRepository into the MyDTOClass?
回答1:
Your DTO doesn't need to know about the repository itself. All it needs is a delegate that can provide it with the value for notes.
How about something like this:
public class MyDTOClass
{
private ICollection<Note> _notes = null;
public ICollection<Note> Notes
{
get
{
if (_notes == null)
{
if (notesValueProvider == null)
throw new InvalidOperationException("ValueProvider for notes is invalid");
_notes = notesValueProvider();
}
return _notes;
}
}
private Func<ICollection<Note>> notesValueProvider = null;
public MyDTOClass(Func<ICollection<Note>> valueProvider)
{
notesValueProvider = valueProvider;
}
}
Since by definition, your repository is supposed to provide you with an instance of the DTO, we should be able to pass in the value provider delegate like so:
public class Repository
{
public MyDTOClass GetData()
{
MyDTOClass dto = new MyDTOClass(FetchNotes);
return dto;
}
public ICollection<Note> FetchNotes()
{
return new List<Note>(200);
}
}
Will this work for you?
回答2:
If you can wait for .Net 4 (ie your not yet in production) Lazy(of T) is a new Lazy Loading feature in the .Net framework. http://msdn.microsoft.com/en-us/library/dd642331(VS.100).aspx
回答3:
You're defeating the whole purpose of DTOs when you try to add lazy-loading logic to it. I think, you should have two separate objects: one with Note
s, and another -- without them.
回答4:
Another option is to use proxies that inherit from the objects you want to retrieve (following the lead of some object-relational mappers, like NHibernate).
This provides a degree of persistence ignorance by keeping the data access code separate from the domain model:
public class MyLazyDTOClass: MyDTOClass {
// Injected into the constructor by the MyDtoClass repository
private INoteRepository noteRepository;
public ICollection<Note> Notes {
get {
if(base.Notes == null) {
base.Notes = noteRepository.GetNotes(projectId);
}
return base.Notes;
}
}
}
MyDTOClassRepository
declares the base object as its return type but returns the lazy object instead:
public MyDTOClassRepository {
public MyDTOClass GetMyDTOClass(int id) {
// ... Execute data access code to obtain state ...
return new MyLazyDTOClass(this, state);
}
}
MyDTOClass
consumers don't need to know they're dealing with proxies, nor do they have to interact with repositories (except for whatever class makes the initial call of course).
回答5:
After having roamed the astral realms for aeons in desperate search for an answer, I have come to the final conclusion that yes, it's unnecessary to pass a repository into your entity instance, because a repository for an entity type should always be a singleton.
So you could write in your entity class simply something like:
public class Monster
{
public ReadOnlyCollection<Victim> _victims;
public ReadOnlyCollection<Victim> Victims
{
get
{
if (this._victims == null) // Note: Thread-Safety left out for brevity
{
this._victims = VictimRepository.Instance.GetVictimsForMonster(this);
}
return this._victims;
}
}
}
This really solved all headaches for me.
The repository must be implemented in a way that it would always know what to do with the data.
Remember that one repository implementation would for example get the data from the database, while another might get it from a web service. Because of the loose coupling, the repository implementation module is easily replaceable and any data communication is even arbitrarily chainable.
If you have a scenario where you would say "but the repository can't be singleton because I have a complex scenario where I for example access more than one data source and it depends on the actual Monster
instance where to get the Victim
s from", then I say well you must create a repository implementation which knows all data sources and keeps track of where the entity instances come from and where they will go, and so on...
If you feel this approach is not POCO enough, then you must create another loosely coupled Business Logic Layer which wraps or derives from the POCO entity and implements the repository interaction there.
I hope I could give a hint into a direction that feels right for you and anyone. I actually do believe this is the holy grail for multilayer/-tier development. Feel free to further discuss.
回答6:
You can't achieve independence from your repository if you're lazy loading from it. You can keep it at arm's length by using a callback or proxy or letting NHibernate do the dirty work for you, but your DTO has to access the repository to load the Notes.
Your main goal appears to be "I want it to appear like a POCO to outside caller so I like to have a Property "Notes" rather than methods like "GetNotesForProject" on this or other classes." Can't you accomplish this with ninject and constructor injection? Once you've set up ninject you can call kernel.Get() to get a new instance that will not expose any reference to your repository.
回答7:
You could have the Notes property take in an INoteRepository as a parameter. That way the calling code could pass in the correct instance of INoteRepository.
来源:https://stackoverflow.com/questions/1969503/lazy-loading-of-collection-how-to-get-the-items