views:

97

answers:

2

I'm trying to figure out the best way to handle loading objects with different graphs (related entities) depending on the context their being used.

For example Here's a sample of my domain objects:

public class Puzzle
{
    public Id{ get; private set; }
    public string TopicUrl { get; set; }
    public string EndTopic { get; set; }
    public IEnumerable<Solution> Solutions { get; set; }
    public IEnumerable<Vote> Votes { get; set; }
    public int SolutionCount { get; set; }
    public User User { get; set; }
}
public class Solution
{
    public int Id { get; private set; }
    public IEnumerable<Step> Steps { get; set; }
    public int UserId { get; set; }
}  
public class Step
{
    public Id { get; set; }
    public string Url { get; set; }
}
public class Vote
{
    public id Id { get; set; }
    public int UserId { get; set; }
    public int VoteType { get; set; }
}

What I'm trying to understand is how to load this information differently depending on how I'm using it.

For example, on the front page I have a list of all puzzles. At this point I don't really care about the solutions for the puzzle or for the steps in those solutions (which can get pretty hefty). All I want are the puzzles. I would load them from my controller like this:

public ActionResult Index(/*  parameters   */)
{
    ...
    var puzzles = _puzzleService.GetPuzzles();
    return View(puzzles);
}

Later on for the puzzle view I now care about only the solutions for the current user. I don't want to load the entire graph with all of the solutions and all of the steps.

public ActionResult Display(int puzzleId)
{
   var puzzle = _accountService.GetPuzzleById(puzzleId);
   //I want to be able to access my solutions, steps, and votes. just for the current user.
}

Inside my IPuzzleService, my methods look like this:

public IEnumerable<Puzzle> GetPuzzles()
{
    using(_repository.OpenSession())
    {
        _repository.All<Puzzle>().ToList();
    }
}
public Puzzle GetPuzzleById(int puzzleId)
{
    using(_repository.OpenSession())
    {
        _repository.All<Puzzle>().Where(x => x.Id == puzzleId).SingleOrDefault();
    }
}

Lazy loading doesn't really work in the real world, because my session is being disposed right after each unit of work. My controllers don't have any concept of the repository and therefore do not manage session state and can't hold on to it until the view is rendered.

I'm trying to figure out what the right pattern to use here is. Do I have different overloads on my service like GetPuzzleWithSolutionsAndVotes or more view specific like GetPuzzlesForDisplayView and GetPuzzlesForListView?

Am I making sense? Am I way off base? Please help.

A: 

I don't think your service should have any knowledge of the Views. Your requirements might change so don't tie their names to view names.

When you call GetPuzzles() you should only load root puzzle, and GetPuzzleById(int id) can eager load the associations.

For example in criteria API: .SetFetchMode("Solutions", NHibernate.FetchMode.Join)

I don't understand why you cant lazy load. If your using nHibernate, the proxy will go back to the database for you when you access the property. Your controller should get all the data you need. You should not pass a non-loaded proxy to your view then your view has to handle the case where it can't load the data.

So you should have controller load the specific pieces of data you need, to load only for a user, I would change the interface such that: GetPuzzle(puzzleId,user)

In this method you can just eager load the data for the one user.

JoshBerke
My mind is a little fuzzy right now I hope this makes sense to you and provides some value
JoshBerke
I can't lazy load because when me session goes out of scope at the end of the GetPuzzle...X() methods it is disposed of. When the proxy tries to fetch the items later I get a "Session is already disposed" error.
Micah
+2  A: 

I had a similar case where I could not use Lazy loading.

If you only need a one or two cases, then the easiest thing as you suggest, create separate GetPuzleWithXYZ() methods.

You could also create a small query object with a fluent interface.

Something like...

public interface IPuzzleQuery
{
    IPuzzleLoadWith IdEquals(int id);
}

public interface IPuzzleLoadWith
{
    ISolutionLoadWith WithSolutions();

    IPuzzleLoadWith WithVotes();
}

public interface ISolutionLoadWith
{
    IPuzzleLoadWith AndSteps();
}

public class PuzzleQueryExpressionBuilder : IPuzzleQuery, IPuzzleLoadWith, ISolutionLoadWith
{
    public int Id { get; private set; }
    public bool LoadSolutions { get; private set; }
    public bool LoadVotes { get; private set; }
    public bool LoadSteps { get; private set; }

    public IPuzzleLoadWith IdEquals(int id)
    { 
     Id = id;
     return this; 
    }

    public ISolutionLoadWith WithSolutions()
    {
        LoadSolutions = true;
        return this;
    }

    public IPuzzleLoadWith WithVotes()
    {
        LoadVotes = true;
        return this;
    }

    public IPuzzleLoadWith AndSteps()
    {
        LoadSteps = true;
        return this;
    }
}

then your Repository Get() method can instantiate the expression builder and pass it to the caller

public Puzzle Get(Action<IPuzzleQuery> expression)
{
    var criteria = new PuzzleQueryExpressionBuilder();

    expression(criteria);

    var query = _repository.All<Puzzle>().Where(x => x.Id == criteria.Id)

    if(criteria.LoadSolutions) ....

    if(criteria.LoadSteps) ....

    if(criteria.LoadVotes) ....

    ...
    ... 

    return query.FirstOrDefault();
}

and typical calls would look like...

Puzzle myPuzzle = Repository.Get(where => where.IdEquals(101).WithSolutions());

Puzzle myPuzzle = Repository.Get(where => where.IdEquals(101).WithSolutions().AndSteps());

Puzzle myPuzzle = Repository.Get(where => where.IdEquals(101).WithVotes().WithSolutions());

it needs a little work, but you can see the basic idea.

Andronicus