janinaj
janinaj

Reputation: 154

Does this violate the DRY principle?

I have 3 domain models - Item, ItemProductLine, and ProductLine. Each of these map to already existing database tables. I also have a view model that I use in my view.

Domain models:

public class Item
{
    public string itemId { get; set; }
    public string itemDescription { get; set; }
    public float unitPrice { get; set; }
    // more fields
    public virtual ItemProductLine itemProductLine { get; set; }
}

public class ItemProductLine
{
    public string itemId { get; set; }
    public String productLineId { get; set; }
    // more fields
    public virtual ProductLine productLine { get; set; }
}

public class ProductLine
{
    public string productLineId { get; set; }
    public string productLine { get; set; }
    // more fields
}

View model:

public class ItemViewModel
{
    public string itemNumber { get; set; }
    public String itemDescription { get; set; }
    public Double unitPrice { get; set; }
    public string productLine { get; set; }
}

My current query is:

from item in dbContext.Items
where unitPrice > 10
select new ItemViewModel()
{
    itemNumber = item.itemNumber
    itemDescription = item.itemDescription
    unitPrice = item.unitPrice
    productLine = item.itemProductLine.productLine.productLine
}

I currently have this query in the controller, but I am refactoring the code. I want to put the query code in a repository class in a data access layer. From what I've read, I should not reference any view models in that layer. If I change select new ItemViewModel() to select new Item(), it will return the error:

The entity or complex type 'proj.DAL.Item' cannot be constructed in a LINQ to Entities query.

A solution I have seen is to create a data transfer object (DTO) to transfer data from my domain model to my view model.

However, by doing this, I would have 3 copies of the data. If I need to add another database field and display it, I need to update 3 files. I believe I am violating the DRY principle. Is it inevitable to violate the DRY principle when using DTOs and view models? If not, can you provide an example of how to refactor this to have DRY code?

Upvotes: 0

Views: 664

Answers (3)

Karhgath
Karhgath

Reputation: 1809

Unlike ramiramulu, I would refrain from introducing too many abstractions.

If you use EF, your DAL is actually Entity Framework, no need to abstract that. A lot of people attempts to do this but this only complicates your code a lot, for no gain. If you were doing SQL requests and calling stored procedures directly, then a DAL would be helpful, but building an abstraction on top of EF (which is another abstraction, or over NHibernate) is a bad idea.

Also, pure DTOs as an abstraction are more and more frown upon, but they can be used if you have a middleware and do not directly access the database - for example, a message bus like NServiceBus: messages would be considered DTOs in that case.

Unless you do very simple and pure CRUD (in which case, go ahead, put the logic in controllers - no reason to add complexity for pretty straightforward business), you should move business logic outside of your controllers for sure. For this you have many options, but 2 of the most popular are : a rich domain model with domain driven design or rich business services with service oriented design. They are a lot of ways to do this, but these 2 illustrates very different approaches.

Rich Domain (Controller per Aggregate)

In the first case, your controller would be responsible for acquiring the domain object, calling the logic, and returning a View Model. They do the bridge between the View world and the Model world. How to acquire the domain object(s) needs to be somewhat abstracted, often simple virtual methods works great - keep it simple.

Aggregate Root:

public class Item
{
    public string itemId { get; set; }
    public string itemDescription { get; set; }
    public float unitPrice { get; set; }
    // more fields
    public virtual ItemProductLine itemProductLine { get; set; }

    // Example of logic, should always be in your aggregate and not in ItemProductLine for example
    public void UpdatePrice(float newPrice)
    {
       // ... Implement logic
    }
}

View Model:

public class ItemViewModel
{
    public int id { get; set; }
    public string itemNumber { get; set; }
    public String itemDescription { get; set; }
    public Double unitPrice { get; set; }
    public string productLine { get; set; }
}

Controller:

public class ItemController : Controller
{
    [HttpGet]
    public ActionResult Edit(int id)
    {
       var item = GetById(id);
       // Some logic to map to the VM, maybe automapper, valueinjector, etc.
       var model = item.MapTo<ItemViewModel>();
       return View(model); 
    }

    [HttpPost]
    public ActionResult Update(int id, ItemViewModel model)
    {
       // Do some validation
       if (!model.IsValid)
       {
           View("Edit", model); // return edit view
       }

       var item = GetById(model.id);

       // Execute logic
       item.UpdatePrice(model.unitPrice);
       // ... maybe more logic calls

       Save(item);

       return RedirectToAction("Edit");
    }

    public virtual Item GetById(int id)
    {
        return dbContext.Items.Find(id);
    }

    public virtual bool Save(Item item)
    {
        // probably could/should be abstracted in a Unit of Work
        dbContext.Items.Update(item);
        dbContext.Save();
    }
}

This works great with logic that trickles down and are very model specific. It is also great when you do not use CRUD and are very action-based (e.g. a button to update only the price compared to an edit page where you can change all item values). It is pretty decoupled and the separation of concerns is there - you can edit and test business logic on their own, you can test controllers without a backend (by overriding the virtual functions), and you do not have hundreds of abstractions built on one another. You might roll out the virtual function in a repository class, but by experience you always have very specific filters and concerns that are controller/view dependent, and often you end up with one controller per aggregate root, so controllers are a good place for them (e.g. .GetAllItemsWithAPriceGreaterThan(10.0))

In an architecture like that, you have to be careful about boundaries. For example, you could have a Product controller/aggregate and want to list all Items related to that product, but it should be read-only - you couldn't call any business on Items from Products - you need to navigate to the Item controller for that. The best way to do this is to automatically map to the ViewModel :

public class ProductController : Controller
{
    // ...

    public virtual IEnumerable<ItemViewModel> GetItemsByProductId(int id)
    {
        return dbContext.Items
            .Where(x => ...)
            .Select(x => x.MapTo<ItemViewModel>())
            .ToList();
        // No risks of editing Items
    }
}

Rich Services (Controller per Service)

With rich services, you build a more service oriented abstraction. This is great when business logic spawns multiple boundaries and models. Services play the role of the bridge between the View and the Model. They should NEVER expose the underlying Models, only specific ViewModels (which play the role of DTO in that case). This is very good when you have a MVC site and some REST WebApi working on the same dataset for example, they can reuse the same services.

Model:

public class Item
{
    public string itemId { get; set; }
    public string itemDescription { get; set; }
    public float unitPrice { get; set; }
    // more fields
    public virtual ItemProductLine itemProductLine { get; set; }
}

View Model:

public class ItemViewModel
{
    public int id { get; set; }
    public string itemNumber { get; set; }
    public String itemDescription { get; set; }
    public Double unitPrice { get; set; }
    public string productLine { get; set; }
}

Service:

public class ItemService
{
    public ItemViewModel Load(int id)
    {
        return dbContext.Items.Find(id).MapTo<ItemViewModel>();
    }

    public bool Update(ItemViewModel model)
    {
        var item = dbContext.Items.Find(model.id);

        // update item with model and check rules/validate
        // ...

        if (valid)
        {            
            dbContext.Items.Update(item);
            dbContext.Save();
            return true;
        }

        return false;
    }
}

Controller:

public class ItemController : Controller
{
    public ItemService Service { get; private set; }

    public ItemController(ItemService service)
    {
        this.Service = service;
    }

    [HttpGet]
    public ActionResult Edit(int id)
    {
       return View(Service.Load(id)); 
    }

    [HttpPost]
    public ActionResult Update(int id, ItemViewModel model)
    {
       // Do some validation and update
       if (!model.IsValid || !Service.Update(model))
       {
           View("Edit", model); // return edit view
       }

       return RedirectToAction("Edit");
    }
}

Controllers are only there to call the Service(s) and compose the results for the Views. They are "dumb" compared to domain oriented controllers, but if you have a lot of views complexities (tons of composed views, ajax, complex validation, json/xml processing along side html, etc.), this is the preferred approach.

Also, in this case, services do not have to related to only one model. The same service could manipulate multiple model types if they share business logic. So an OrderService could access the inventory and make adjustments there, etc. They are more process-based than model-based.

Upvotes: 2

ramiramilu
ramiramilu

Reputation: 17182

I would do it this way -

My Domain Model -

public class Item
{
    // more fields
    public virtual ItemProductLine itemProductLine { get; set; }
}

public class ItemProductLine : ProductLine
{
    // more fields
}

public class ProductLine
{
    // more fields
}

DAL Would be -

    public class ItemRepository
    {
        public Item Fetch(int id)
        {
           // Get Data from Database into Item Model
        }
    }

BAL would be -

public class ItemBusinessLayer
{
    public Item GetItem(int id)
    {
       // Do business logic here
       DAL.Fetch(10);
    }
}

Controller would be -

public class ItemController : Controller
{
    public ActionResult Index(int id)
    {
       Item _item = BAL.GetItem(10);
       ItemViewModel _itemViewModel = AutomapperExt.Convert(_item); // something where automapper will be invoked for conversion process
       return View(_itemViewModel);
    }
}

Automapper will be maintained in a separate class library.

The main reason why I choose this way is that, for a particular business there can be any number of applications/frontends, but their business domain models shouldn't change. So my BAL is not going to change. It returns business domains itself. Thats doesn't mean everytime I need to return Item model, instead I will have MainItemModel, MiniItemModel etc., all these models will server business requirements.

Now it is the responsibility of frontend (probably controllers) to decide which BAL method to be invoked and how much data to be used on frontend.

Now some devs might argue, that UI shouldn't be having that judgement capacity to decide how much data to use and what data to see, instead BAL should have that power to make decision. I agree and that happens in BAL itself if our domain model is strong and flexible. If security is main constraint and domain models are very rugged, then we can have the automapper conversion at BAL itself. Or else simply have it on UI side. End of the day, MVC is all about making code more manageable, cleaner, reusable and comfortable.

Upvotes: 0

MikeSW
MikeSW

Reputation: 16348

Having multiple models is not a DRY violation however your code breaks the Separation of Concerns principle because the domain model is the same with (or built upon, read: coupled to) persistence model. You should keep your models separated for each layer and use a tool like automapper to map them. This prevents the model to serve more than one purpose.

It looks like repeating yourself, but in fact you are keeping your layers decoupled and ensuring code maintainability.

Upvotes: 2

Related Questions