views:

595

answers:

2

Hi, in this MVC tutorial it showed how to decouple Controller from database logic using Repository and a Service layer. However the example is for a very simple model of pure properties. What about associations?

Say if Product model has an association to the Users table through Owner association. The controller calls the service or the repository to get a list of users, pass it to View, and View displays the users as a drop-down list.

Then on a standard POST: product/Create, controller needs to get the chosen userID, then grab the User entity of that id, and associate it with the to be created Product.

  if (!String.IsNullOrEmpty(Request["SelectedUserId"]))
        {
            int selectedUserId = Convert.ToInt16(Request["SelectedUserId"]);
            newProduct.Owner = _productService.GetUserById(ownerId);
        }

  _productService.AddProduct(newProduct);

But this process complicates the controller logic. Even worse if we need to validate the associations (since there wouldn't be OnChanging events for the associations, we can't do it in the Model partial class).

My question is, is there any better way to handle associations? Is it better to pass the Request parameters to the service layers as well and do everything there? What about validations?

A: 

Update: (after the comment on it not being linq2sql)

In your specific scenario (based on your desc + the code you posted), I would definitely move the load down to the service layer. This doesn't mean moving the request down to it. I would refactor as:

string userIdStr = Request["SelectedUserId"];
var userId = !String.IsNullOrEmpty(userIdStr) ? 
     Convert.ToInt16(userIdStr) : 
     default(int?);
_productService.AddProduct(userId, newProduct);

I also usually have an extension method like ToNullable<int>(), that makes it:

var userId = Request["SelectedUserId"].ToNullable<int>();
_productService.AddProduct(userId, newProduct);

Depending on the emphasis you give to models, you can have something that contains both fields, like CustomerProduct.


If you are persisting with linq2sql, you don't need to get the User to do that in this scenario. Linq2sql will generate an OwnerId property, which you can use without having to go the db to get it.

Having the model tied for different sub-systems is not always the best approach when dealing with complex models. This is more important if you have parts of the system that are appropriate for their own bounded context. When that's the case, the above has even more importance, as you want to have really clear and specific places to integrate the 2 bounded contexts.

Besides the above, there are some times when it is appropriate to have some extra glue for these type of processes. Consider adding a service class to help you on it. This doesn't mean that everything needs to go through a service class or that the domain objects wouldn't have the logic, its just there for the specific scenarios where you need the extra actions that isn't appropriate for either the domain objects or the controllers.

eglasius
Thanks for answering. The NerdDinner example from ScottGu uses Linq2Sql with MVC which was well understood. However in the case of Linq2Entities it seems not possible to have Both an OwnerID property and an Owner association. Therefore my question of where to place to logic for retrieving the User.
rokeyge
@rokeyge y, somehow I missed the linq2entities. Added an update more focused on moving the load of user away of the controller. I am also interested on the other related question - can we add/insert in linq2entities without loading the related entity.
eglasius
Ah, I like concrete answers! Have you ever thought of where to place the Validation on the non-nullable userId (which is now Owner association). Can't figure out how to use the IErrorDataInfo method described on the official tutorial.(Off topic I know, open another thread?)
rokeyge
@rokeyge probably another thread. I have limited experience with the asp.net MVC (although have plenty using MVP with regular asp.net). Look for xVal which is a validation framework for MVC - ideally you want it checked client side i.e. before ever reaching the server (+ in the server of course).
eglasius
A: 

This is the perfect book you should read about how to properly model (the "M" in MVC) the repositories, models, and he includes direct Linq associations. As a bonus, he tackles how to auto-wire up all of your custom controllers with the repository they expect. Yes, it was written for a version of ASP.NET MVC that is 4 versions old (before the release), but the concepts stick with you. The information applies to all MVC (or any DDD project w/Linq rather) projects.

In short, the book introduces DDD concepts to inject a repository into your custom Controller.

public class ContentController : Controller
{
  IContentRepository _repository;
  public ContentController(IContentRepository repo)
  {
    _repository = repo;
  }

  public ActionResult Add(Content c)
  {
    _repository.Add(c);
    _repository.SubmitChanges();

    return View(c);
  }
}

Notice the dependency injection concept in use here by forcing a repository into the constructor. Now, you could just wire it up yourself without the constructor. But, when you get into a large project (50+ repositories), it helps to use an inversion of control container that facilitates the creation of these objects for you, like Castle Windsor (again, all in that 100 page book above).

For your use-case, you'd simple access the User object from a user repository and then let the rest follow-through.

The book goes into how to model, say your User and Product entities, using LinqToSql with EntityRef and EntitySet, so all of your associations are honored. Which is too much code to write here. Basically, link a User() object to your Product() object by the UserID and decorate each of those entities with Linq attributes:

[Table(Name="ut_Content")]
public class Content : EntityBase
{
  [Column(IsPrimaryKey = true
      , IsDbGenerated = true
      , AutoSync = AutoSync.OnInsert)]
  public int ContentID { get; set; }

  [Column(CanBeNull = false)]
  public string ContentKey { get; set; }

  [Column(CanBeNull = false)]
  public string Title { get; set; }

  [Column]
  public string Description { get; set; }

  [Column]
  public string Body { get; set; }

  [Column]
  public string BodyFormatted { get; set; }
}

He tackles the different LinqToSql use cases, and shows why "Create your model first, do not use Schema-Generated Linq objects" works here.

And, a repository example I guess for good measure:

public class ContentRepository : IContentRepository
{
  private Table<Content> _contents;

  public ContentRepository(string connectionString)
  {
    DataContext db = new DataContext(connectionString);
    _contents = db.GetTable<Content>();
  }

  public void Add(Content entity)
  {
    _contents.InsertOnSubmit(entity);
  }
}

Remember, one of the best things about Linq is its deferred execution feature. You are not operating on an entire Linq Table here. Your query gets deferred so you can filter your IQueryable with Where and Joins and order bys. Only when you request the .ToList() or alike methods is when the query is executed.

Use a DDD Service

Finally, to address your original issue of "getting a user, then adding the product", the DDD (Domain Driven Design) playbook has the perfect solution: Services.

You'd create a UserProductService() class in your Domain that does exactly what you what you did above. But it has the advantage of abstracting the business logic out into your Domain, so other locations can access and use the same business rule.

public class UserProductService
{
  private IProductRepository _productRepository;
  private IUserRepository _userRepository;
  public UserProductService()
  {
    // this is where CastleWindsor can really help
    _productRepository = new ProductRepository();
    _userRepository = new UserRepository();
  }

  public void AddProduct(int userID, Product product)
  {
    User user = userRepository.GetUserByID(userID);

    // your logic above, abstracted into a service
    if (user.IsValid && product.IsValid)
    {
      product.Owner = user.UserID;        
      _productRepository.AddProduct(product);
    }  
  }
}

The DDD rule for services is: When you have two entities, but you need to perform a common action on both, use a service to act upon both entities.

eduncan911
An affiliate marketing link? Is that really OK here?
Craig Stuntz
I don't know where you get that "DDD rule" from but I don't agree.There is nothing wrong with actions between entities without services. The service you show here is an application service, not a domain service. I recommend Eric Evans book about DDD.
Paco
Why don't you put the user in the constructor of a product when it's required?
Paco
@Paco: You can mod the constructor as you need for dependency injection. My qas quick and dirty using your example. My code was from a working CMS (the Content stuff). As for the "book", the book the general Service rule comes from is "Domain-Driven Design Quickly, a summation of E.Evans DDD"
eduncan911
@Paco: As for "nothing wrong with actions between entities without services", that violates the rule if each aggregate root (Product and User) talk to each other. Now if Product is within the User Aggregate boundry, then your statement is correct:actions within aggregates can cross boundries within.
eduncan911
@Craig: I thought it was. I see it QUITE A LOT in comments and answers. Even in questions! It's my personal account, trying to support my coding habits. :)
eduncan911
I cannot remember reading that "rule". I wouldn't recommend to inject dependencies in entities, because it will make layering impossible because of the bidirectional associations. Remember the separation between domain, infrastructure and application services.
Paco
What is your problem about an aggregate root containing other aggregate roots? I use domain services when the some action does not naturally fit in an entity. Domain services can use multiple entities sometimes, but sometimes only one.
Paco
rokeyge
I wish there is a good (and bit complex) tutorial on MVC with Linq2Entities, there are plenty for Linq2Sql already!
rokeyge