Search code examples
linq-to-sqlasp.net-mvc-2business-logicmulti-layer

ASP.NET MVC - Multilayer doubts


I've been working on a ASP.NET MVC project with LinqToSql. The application has 3 layers: UI, Business and Data.

The last few days I was implementing (I'm still are) a Excel file upload. So my Controller receives the file uploaded, does some stuff, pass info to Business and then to Data. But, along with the development of that, some doubts came up.

Here are some of my doubts (I think the bullet is the easiest way to show):

  1. The Excel file must be validated. The application must validate if the worksheet values are correct and if they are, insert/update to database. Should I validate the Excel in Controller or in the Business?

  2. This Excel may insert data to DB, for example, a new Product(); Is there a problem in creating new instances in UI layer or is it better to do in Business? Is it better to pass an object from UI to Business or is it better to pass all Class properties and create the object in the Business?

  3. In this Excel action, I have some helper methods, like verify if the worksheet reached its end, verify if the cell has value, generate a DataTable for the uploaded file and some others. Where these helper methods should be placed? At the moment, they are in UI Layer (same as Controller).

  4. Forgetting about the Excel thing, imagine a simple Product form page. On POST, Controller will receive a FormCollection. Should this FormCollection be treated on Controller or should it be pass to Business and Business do all of the stuff?

Sorry about some many questions. I'm also trying to refactor my code and the "Fat Controller" issue is right on my door!

Thanks in advance!


Solution

  • You should indeed avoid having fat controllers. But as always easier said than done.

    So let me try to answer your questions with an example. As always you would start by designing a view model which will represent the data that the user sends to this action (don't use any weakly typed FormCollection or ViewData)

    public class UploadViewModel
    {
        [Required]
        public HttpPostedFileBase File { get; set; }
    }
    

    then we move on to the controller:

    public ProductsController: Controller
    {
        private readonly IProductsService _service;
        public ProductsController(IProductsService service)
        {
            _service = service;
        }
    
        public ActionResult Upload()
        {
            var model = new UploadViewModel();
            return View(model);
        }
    
        [HttpPost]
        public ActionResult Upload(UploadViewModel model)
        {
            if (!ModelState.IsValid)
            {
                // The model was not valid => redisplay the form 
                // so that the user can fix his errors
                return View(model);
            }
    
            // at this stage we know that the model passed UI validation
            // so let's hand it to the service layer by constructing a
            // business model
            string error;
            if (!_service.TryProcessFile(model.File.InputStream, out error))
            {
                // there was an error while processing the file =>
                // redisplay the view and inform the user
                ModelState.AddModelError("file", error);
                return View(model);
            }
    
            return Content("thanks for submitting", "text/plain");
        }
    }
    

    and the last bit is the service layer. It will have 2 dependencies: the first one will take care of parsing the input stream and returning a list of Products and the second will take care of persisting those products to the database.

    Just like this:

    public class ProductsService: IProductsService
    {
        private readonly IProductsParser _productsParser;
        private readonly IProductsRepository _productsRepository;
        public ProductsService(IProductsParser productsParser, IProductsRepository productsRepository)
        {
            _productsParser = productsParser;
            _productsRepository = productsRepository;
        }
    
        public bool TryProcessFile(Stream input, out string error)
        {
            error = "";
            try
            {
                // Parse the Excel file to extract products
                IEnumerable<Product> products = _productsParser.Parse(input);
    
                // TODO: Here you may validate whether the products that were
                // extracted from the Excel file correspond to your business
                // requirements and return false if not
    
                // At this stage we have validated the products => let's persist them
                _productsRepository.Save(products);
                return true;
            }
            catch (Exception ex)
            {
                error = ex.Message;
            }
            return false;
        }
    }
    

    Then of course you would have two implementations of those dependencies:

    public class ExcelProductsParser: IProductsParser
    {
        public IEnumerable<Product> Parse(Stream input)
        {
            // parse the Excel file and return a list of products
            // that you might have extracted from it
            ...
        }
    }
    

    and the repository:

    public class Linq2SqlProductsRepository: IProductsRepository
    {
        public void Save(IEnumerable<Product> products)
        {
            // save the products to the database
            ...
        }
    }
    

    Remark: You could enrich the view model with additional properties that will represent some metadata that we could associate to this file upload and which might have some corresponding input fields on the form. Then you could define a business model to pass to the TryProcessFile method instead of a simple Stream. In this case AutoMapper could be used in the controller action to map between the UploadViewModel and this new business model that you would define.