简体   繁体   中英

How can I refactor my database access code outside my MVC project but keep my viewmodels inside?

I have an asp.net-mvc website with the following folders:

  • Controllers
  • Scripts
  • Views
  • ViewModels
  • Models
  • DomainModel

I now want to access a lot of this business logic and database access code and data in another .net app (a windows console app so not web at all) so i am refactoring to remove as much stuff as possible outside of the MVC project and into other projects in the solution so that code could be shared with this other solutions.

I have 2 main issues;

  1. My main issue is that I am struggling to find a place to put the code that generates the ViewModel because a lot of this code I would like to reuse in my console app as the console app sends email which requires the same data that is in the view.

  2. Another main issue is that i am struggling to see how i can move my database access code out of the MVC project while still having the ViewModels inside when many of my functions that instantiate my viewmodels start with a bunch of database access code.

Here is a bit of the detail and my process so far:

Step 1 - Move DomainModel into another project - success

So moving the DomainModel project was simple (as that was a lot of raw objects with some business logic on top - nothing web about it).

Step 2 - Thin out controllers - success

I have thinned out as much of my controllers as possible and moved any business logic or complicated data access logic into the Models folder. When i tried to move the models folder outside the MVC project a few things broke:

Step 3 - Attempt to move Models folder outside MVC Project - struggle

In thinning out the controllers, I have a number of different controller actions that go to a model class and return my ViewModel that i pass back into the view. Something like this (in my controller class):

 public ActionResult ApplicationDetail(int id)
 {
      AppDetailViewModel applicationViewModel = Model.GenerateAppDetailViewModel(id);
      return View(applicationViewModel);
 }

So files in my Model folder are dependency on the ViewModel classes. I do want to centralize the GenerateAppDetailViewModel() function as that is used in multiple different controllers. Also, in my console app (which sends out email, i often want to get all the data that happens to be on some view so my code "wants" to leverage the viewmodel as well .. if i move it out of the MVC project then I can reuse but i think have the dependency issue (clearly i don't need SelectListItem in my console app but in other cases where they are just container objects of different data needed to generate a view I do want to reuse)

or another thing that broke was the dependency on:

System.Web.Mvc

because I have a lot of code that:

  1. queries a table in a database
  2. Converts that into a collection of objects (i am using nhibernate)
  3. Convert that into either some DTO object (which is sitting in ViewModels folder) or a List of SelectListItem objects (to be used to populate dropdowns in the view) which is part of System.web.mvc.

I wanted to look for suggestions on the best way to break out this dependency so i can move as much code out of the MVC project as possible for reuse.

The issue is that if i try to suck my ViewModel code into the Model folder and into another project then again i get stuck because the ViewModel classes have a lot of dependency on

System.Web.Mvc

due to things like SelectListItem.

Should i have 2 view models folders (one in the MVC project that has specific system.web.mvc references and another one that sits in a different project?). It seems like the dependency on SelectListItem is what keeps causing the contention

In most examples that i have seen ViewModels do have a dependency on System.Web.Mvc such as this tutorial

I have seen these questions:

which are sort of related but not sure they answer my specific overall refactoring question stated.

View models are specific to the particular application. I guess that the view models would differ between your web application and your console application. So have each application define its own view models and the corresponding mapping between the domain models and the view models. Don't have your domain models posses methods that convert them to view models because this way you are completely tying your domain layer to the UI layer which is the worst thing to happen. Use a mapping layer (which will be specific to each application type). AutoMapper is a great example of a mapping layer that you could have.

Don't even try to reuse ASP.NET MVC view models in a console application. As you have already found out they will contain references to System.Web.Mvc because for example a dropDownList in ASP.NET MVC is represented with the IEnumerable<SelectListItem> class whereas in a Console Application, god knows, maybe an IEnumerable<SomeItemViewModel> .

Conclusion: View models and mapping back and forth between the domain and view models belong to the UI layer (aka ASP.NET MVC, Console, WPF, ...).

I understand what you want to achieve, and I must tell you: it's absolutely normal to reuse the same ViewModels for different UI layers. After all, VM is part of MVVM pattern, and that pattern is all about separating concerns. And, separation means the ability to replace lower-level layers with another implementations. That is the purpose of separate software layers (among others).

  1. For the beginning, you must assume that your MVC web project is MVVM-based. That will mentally help you make correct decisions. I guess you already did this since you use ViewModel term.
  2. Make ViewModels become platform independent. It's possible, and it has nothing to do with SelectListItem . After all, SelectListItem simply contains the option text/value pair, plus the flag whether it's selected or not. You obviously can express the same information in a different way. After passing this Generic kind of ViewModel to the MVC, you can convert the generic "SelectListItem" to the MVC SelectListItem . Yes, it is sort of mapping, and it does not matter whether it takes place in the MVC view or before passing it to the View. But it must happen on the UI layer (MVC web project), since it's a platform specific mapping concern.
  3. You mentioned data access code: that's a separate software layer, usually abstractly injected into the ViewModels. I foresee no problems with this part.

Hence, you will end up having different software layers (most likely in different .NET libraries): Data Access Layer, ViewModel layer, MVC Web layer.

There is a possibility of having MVC ViewModels as well (one passed from Controller to View) defined in the MVC project, but those will be simply handling (probably accepting) the generic ViewModels and exposing things in the MVC View-specific terms (mapping, inheritance, your imagination?). And this is normal too - after all, MVC is a web-based UI and it definitely has platform-specific differences that must be handled on the platform level, not before. MVC specific ViewModel would be a good place to handle the mapping from generic to MVC SelectListItem .

Once this refactoring is done, there's nothing stopping you from implementing another UI layer - Console application that you are mentioning, by utilizing the same generic ViewModels as the other UI layer - MVC Web project. When using the generic ViewModels in the console application, if you face the Console platform-specific issues, you can come up with the platform-specific ViewModels in the same way as I explained above for the MVC-specific ViewModels.

You can create viewmodels in controller with extension methods:

Controller:

 public ActionResult ApplicationDetail(int id)
 {
      var model = _serviceLayer.GetSomeModel(id); 
      var viewModel = model.CreateInstance(model);      
      return View(viewModel);
 }

Create this SomeModelExtensions in your mvc project

public class SomeModelExtensions {
      public AppDetailViewModel CreateInstance(this SomeModel model) {
           var viewModel = new AppDetailViewModel();
           // here you create viewmodel object from model with logic
           return viewModel;
      }
}

In general I use the following setup/architecture when laying out an MVC app where I may need to reuse the models:

MVC Project: everything web related. I define ViewModels here and map them to the domain models.

Models Project: class library with all your domain logic.

Repository project: This is a class library which access the DB and the Domain Models.

The way it works is that the MVC project will use the (hopefully injected) Repository library to get the Domain Model and map it to its own ViewModel.

If you want to separate the mapping as well, you can put the Mapping Layer, as others have suggested (using AutoMapper eventually), in a separate project. The mapping layer will reference the repositories (and the domain models), while the MVC app will only reference the Mapping layer.

The problem is that the mapping layer, in creating ViewModels, will need a reference to System.Web.Mvc as you found out and you cannot escape this. This is why others have said, and I agree, that you should have a mapping layer per project.

A nice way to get around this is to have a further generic mapping layer with mapping classes defined for the common cases (like the email). Then in the specific child classes you can define the mapping for specific cases (like the ones depending on System.Web.Mvc).

So the end stack would be something like the following, dependencies going down. Of course everything should be interface based.

    MVC App                      Console App
       |                            |
       |                            |
   MVC Specific Mapper         Console Specific Mapper
                 \               /  
                  \             /
                   \           /   
                   GenericMapper <- EmailMapper and EmailViewModel can be implemented here
                     |       |      
                     |       |
                Repository   |
                     |       |
                     |       |
                    DomainModels  

The above is not struggle free and probably the effort of splitting the mapping apart is not worth the candle if there are only one or two common cases. That way, from the Generic Mapper down you are free from the System.Web.Mvc library, while above you are free to forget about DB access code (In a sense the Mapper will act as a Repository for the App).

I assume the MVC web will use the same data as the console app + extra fields, right?

so, what about inheritance your ViewModel from Model? That way you would be able to reuse the model and get custom fields on your ViewModel as needed.

public class AppDetailModel
    {
        public int ID { get; set; }
        public string Name { get; set; }
    }


    public class AppDetailViewModel : AppDetailModel
    {
        public string ViewProperty { get; set; }
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM