Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    text
    copied!<p>I'm going to answer your second question first. </p> <p>Yes, viewmodels should be tightly coupled with the controller. You shouldn't need a ViewModelFactory though. Something like AutoMapper or ValueInjecter should be good enough for converting domain Product to ProductViewModel. </p> <p>As for your first question, you should keep your domain Product Factory separate from your controller. There are a few different approaches you could use. One would be creating a factory method that only takes scalar values as method arguments -- for example string, bool, etc, other primitives, and pure .NET types.</p> <p>You can then have your controller pass the scalars to the factory method from the viewmodel. This is loosely coupled, and highly cohesive. </p> <p>For example:</p> <pre><code>[HttpPost] public ActionResult CreateProduct(ProductViewModel model) { if (ModelState.IsValid) { // assuming product factory is constructor-injected var domainProduct = _productFactory.BuildProduct( model.Name, model.Price, model.Description); // ... eventually return a result } return View(model); } </code></pre> <p>Another approach is to put the methods for passing viewmodel properties directly on the domain object, but for this approach, it is best to make your property setters non-public:</p> <pre><code>[HttpPost] public ActionResult CreateProduct(ProductViewModel model) { if (ModelState.IsValid) { // assuming no product factory var domainProduct = new Domain.Product(); domainProduct.SetName(model.Name); domainProduct.SetPrice(model.Price); domainProduct.SetDescription(model.Description); // ... eventually return a result } return View(model); } </code></pre> <p>I prefer the first option because it's less verbose, and keeps object creation in your domain layer. However both are loosely coupled, because you are not sharing viewmodel types between your MVC layer and your domain layer. Instead your higher layer (MVC) is taking a dependency in the domain layer, but your domain layer is free from all MVC concerns.</p> <p><strong>Response to first 2 comments</strong></p> <p>Second comment first, re validation: It doesn't necessarily have to be the product factory's responsibility to enforce validation, but if you want to enforce business rules in the domain, validation should happen at the factory <em>or lower</em>. For example, a product factory could instantiate a product and then delegate build operations to methods on the entity -- similar to the SetXyzProperty methods above (difference being those methods might be <code>internal</code> to the domain lib instead of <code>public</code>). In this case, it would be the product entity's responsibility to enforce validation on itself.</p> <p>If you throw exceptions to enforce validation, those would bubble up through the factory and into the controller. This is what I generally try to do. If a business rule ever ends up bubbling to the controller, then it means MVC is missing a validation rule and ModelState.IsValid should be false. Also, this way you don't have to worry about passing messages back from the factory -- business rule violations will come in the form of an exception.</p> <p>As for your first comment, yes: MVC takes a dependency on the domain, not vice versa. If you wanted to pass a viewmodel to the factory, your domain would be taking a dependency on whatever lib the viewmodel class is in (which should be MVC). It's true that you could end up with a lot of factory method args, or factory method overload explosion. If you find this happening, it might be better to expose more granular methods on the entity itself than relying on the factory. </p> <p>For example, you might have a form where the user can quickly click to change just the name or price of a Product, without going through the whole form. That action could even happen over ajax using JSON instead of a full browser POST. When the controller handles it, it might be easier to just invoke <code>myProduct.SetPriceOrName(object priceOrName)</code> instead of <code>productFactory.RePriceOrRename(int productId, object priceOrName)</code>.</p> <p><strong>Response to question update</strong></p> <p>Others may have different opinions, but in mine, the business domain should not expose a validation API. That's not to say you can't have an IsValidPrice method on the entity. However, I don't think it should be exposed as part of the public API. Consider the following:</p> <pre><code>namespace NinetyNineCentStore.Domain { public class Product { public decimal Price { get; protected set; } public void SetPrice(decimal price) { ValidatePrice(price); Price = price; } internal static bool IsPriceValid(decimal price) { return IsPriceAtLeast99Cents(price) &amp;&amp; IsPriceAtMostNineteen99(price) &amp;&amp; DoesPriceEndIn99Cents(price); } private static bool IsPriceAtLeast99Cents(decimal price) { return (price &gt;= 0.99m); } private static bool IsPriceAtMostNineteen99(decimal price) { return (price &lt;= 19.99m); } private static bool DoesPriceEndIn99Cents(decimal price) { return (price % 1 == 99); } private static void ValidatePrice(decimal price) { if (!IsPriceAtLeast99Cents(price)) throw new InvalidOperationException( "Product price must be at least 99 cents."); if (!IsPriceAtMostNineteen99(price)) throw new InvalidOperationException( "Product price must be no greater than 19.99."); if (!DoesPriceEndIn99Cents(price)) throw new InvalidOperationException( "Product price must end with 99 cents."); } } } </code></pre> <p>The above encapsulates validation on the entity, without exposing it in the API. Your factory can still invoke the <code>internal IsPriceValid</code>, but doesn't need to be concerned with every little business rule permutation. When any client, internal or public, tries to violate the rule, an exception is thrown. </p> <p>This pattern might seem like overkill, but consider business rules that involve more than one property on an entity. For example, say you can break the <code>DoesPriceEndIn99Cents</code> rule when the <code>Product.IsOnSale == true</code>. You already have ValidatePrice encapsulated, so you can accommodate that rule without having to expose a new validation API method. </p>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload