【问题标题】:Ninject DI / ASP.Net MVC - How do I add a business layer?Ninject DI / ASP.Net MVC - 如何添加业务层?
【发布时间】:2014-06-21 04:43:04
【问题描述】:

我正在编写一个愚蠢的程序,试图以一种实用的方式充分理解设计模式中涉及的所有各种概念。例如,我完全了解 DI / IOC,(我认为),但我不完全了解如何在实际的 ASP.Net MVC 4/5 环境中应用它。

我正在编写一个以发票和产品作为我仅有的 2 个表的商店程序。到目前为止,我已经成功地完全应用了 DI/IOC,并完成了以下结构:

Store.Models Store.Interfaces Store.Repositories Store.Web

所有依赖项都已设置并且工作正常。现在这是我的问题。

我想添加一个业务层如下:

商店.商业

出于练习的目的,我决定简单地计算自给定日期以来的年数。当然,在正常情况下我会将它作为计算字段存储在数据库中并检索它。但我这样做是为了学术练习,因为在某些时候,我会遇到一种情况,我将不得不对数据集进行一些复杂的计算。我认为这不应该与模型、存储库一起存储或在控制器中完成。应该有一个单独的“业务”层。现在这是我的问题:

实体框架根据我的模型定义了一个名为 Invoice 的类。这是一个很好的课程,它一直工作到现在。

我定义了一个接口和存储库,设置了 Ninject,让它与 MVC 一起工作。一切都很完美。再开心不过了。

然后我在发票表中添加了一个日期字段。在 EF 中更新了我的模型,更新了我需要更新的其他内容,一切顺利。

接下来我添加了一个 Store.Business 类项目。我设置了一个新的 Invoice 类,它从模型中继承了 Invoice 类,并添加了一个新的属性、构造函数和方法。

namespace Store.Business
{
    //NOTE: Because of limitations in EF you cant declare a subclass of the same name.

    public class InvoiceBL : Store.Models.Invoice
    {
        [NotMapped]
        public int Age { get; set; }

        public InvoiceBL()
        {
            Age = CalcAge(Date);
        }

        private int CalcAge(DateTime? Date)
        {
            Age = 25;
            //TODO: Come back and enter proper logic to work out age
            return Age;
        }
    }
}

然后我修改了我的接口、存储库、控制器、视图等,以使用这个新的 InvoiceBL 类而不是 EF 生成的类。

我开始使用部分类。但我显然对此有麻烦,因为它在不同的项目中。我什至尝试使用相同的命名空间,但没有。对我来说,按项目分开是很重要的。我希望明确定义这些层。因此,由于这不起作用,我选择了继承。无论如何,我更喜欢这种方法,因为我假设部分类是微软的东西,我希望我的理念能够轻松地转移到任何可能没有部分类的 OOP 语言中。另请注意,我将其放回了自己的命名空间中,因此它不再位于命名空间 Store.Models 中,而是在 Store.Business 中。

现在当我运行程序并像以前一样在 url 中输入发票时,我收到以下错误:

Invalid column name 'Discriminator'.
Invalid column name 'Age'.

当我添加 [NotMapped] 属性时,我只收到此错误:

Invalid column name 'Discriminator'.

以下是所有以 EF Auto Generated 模型开头的相关代码:

Store.Models:

namespace Store.Models
{
    using System;
    using System.Collections.Generic;

    public partial class Invoice
    {
        public Invoice()
        {
            this.Products = new HashSet<Product>();
        }

        public int Id { get; set; }
        public string Details { get; set; }
        public Nullable<decimal> Total { get; set; }
        public Nullable<System.DateTime> Date { get; set; }

        public virtual ICollection<Product> Products { get; set; }
    }
}

接下来我们有界面:

namespace Store.Interfaces
{
    public interface IInvoice
    {
        void CreateInvoice(InvoiceBL invoice);
        DbSet<InvoiceBL> Invoices { get; }
        void UpdateInvoice(InvoiceBL invoice);
        InvoiceBL DeleteInvoice(int invoiceId);
    }
}

接下来我们有存储库:

namespace Store.Repositories
{
    public class InvoiceRepository : BaseRepository, IInvoice
    {
        public void CreateInvoice(InvoiceBL invoice)
        {
            ctx.Invoices.Add(invoice);
            ctx.SaveChanges();
        }

        public DbSet<InvoiceBL> Invoices
        {
            get { return ctx.Invoices; }
        }

        public void UpdateInvoice(InvoiceBL invoice)
        {
            ctx.Entry(invoice).State = EntityState.Modified;
            ctx.SaveChanges();    
        }

        public InvoiceBL DeleteInvoice(int invoiceId)
        {
            InvoiceBL invoice = ctx.Invoices.Find(invoiceId);

            if (invoice != null)
            {
                ctx.Invoices.Remove(invoice);
                ctx.SaveChanges();
            }

            return invoice;
        }
    }
}

我已经向您展示了接口层和存储库层都需要的业务层。所以我将继续讨论控制器:

namespace Store.Web.Controllers
{
    public class InvoiceController : Controller
    {
        //---------------------Initialize---------------------------
        private IInvoice _invoiceRepository;
        private IProduct _productRepository;

        public InvoiceController(IInvoice invoiceRepository, IProduct productRepository)
        {
            _invoiceRepository = invoiceRepository;
            _productRepository = productRepository;
        }

        //-----------------------Create-----------------------------

        public ActionResult Create()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(Store.Business.InvoiceBL invoice)
        {
            if (ModelState.IsValid)
            {
                _invoiceRepository.CreateInvoice(invoice);
                return RedirectToAction("Index");
            }

            return View(invoice);
        }

        //-------------------------Read-----------------------------

        [ActionName("Index")]
        public ActionResult List()
        {
            return View(_invoiceRepository.Invoices);
        }

        public ViewResult Details(int id)
        {
            //How is this DI - If your model changes you have to alter the fields
            //addressed here.
            return View(_invoiceRepository.Invoices.FirstOrDefault(i => i.Id == id));
        }

        //-----------------------Update-----------------------------

        [ActionName("Edit")]
        public ActionResult Update(int id)
        {
            //How is this DI - If your model changes you have to alter the fields
            //addressed here.
            var invoice = _invoiceRepository.Invoices.FirstOrDefault(i => i.Id == id);

            if (invoice == null) return HttpNotFound();

            return View(invoice);
        }

        [HttpPost, ActionName("Edit")]
        [ValidateAntiForgeryToken]
        public ActionResult Update(Store.Business.InvoiceBL invoice)
        {
            if (ModelState.IsValid)
            {
                _invoiceRepository.UpdateInvoice(invoice);
                return RedirectToAction("Index");
            }
            else
            {
                return View(invoice);
            }
        }

        //-----------------------Delete-----------------------------

        public ActionResult Delete(int id = 0)
        {
            //Do you really want to always delete only the first one found?? Not cool?
            //Even though in this case, because Id is unique, it will always get the right one.
            //But what if you wanted to delete or update based on name which may not be unique.
            //The other method (Find(invoice) would be better. See products for more.
            //How is this DI - If your model changes you have to alter the fields
            //addressed here.
            var invoice = _invoiceRepository.Invoices.FirstOrDefault(i => i.Id == id);

            if (invoice == null) return HttpNotFound();

            return View(invoice);
        }

        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public ActionResult DeleteConfirmed(int id)
        {
            if(_invoiceRepository.DeleteInvoice(id)!=null)
            {
                //Some code
            }
            return RedirectToAction("Index");
        }

        //-----------------------Master / Detail--------------------
    }
}

终于看到了:

@model IEnumerable<Store.Business.InvoiceBL>

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table>
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Age)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Details)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Total)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Date)
        </th>
        <th></th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Age)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Details)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Total)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Date)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.Id }) |
            @Html.ActionLink("Details", "Details", new { id=item.Id }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.Id })
        </td>
    </tr>
}

</table>

请忽略代码中的任何 cmets,因为它们仅供我自己参考和指导。

再一次,这个问题是关于我为什么会收到提到的错误以及我需要更改什么来解决它。我教过添加 [NotMapped] 属性可以做到这一点,但它没有。

但是,我仍在学习与 MVC 相关的设计模式,所以如果有人对如何更好地构建项目或其他可能有帮助的建议有任何建议,我也很欢迎。

编辑:我忘记了 NinjectControllerFactory:

namespace Store.Web.Ninject
{
    public class NinjectControllerFactory : DefaultControllerFactory
    {
        private IKernel ninjectKernel;

        public NinjectControllerFactory()
        {
            ninjectKernel = new StandardKernel();
            AddBinding();
        }

        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            //return base.GetControllerInstance(requestContext, controllerType);
            return controllerType == null
                ? null
                : (IController)ninjectKernel.Get(controllerType);
        }

        private void AddBinding()
        {
            //TODO FR: Step 4 - Add your interface and repository to the bindings
            ninjectKernel.Bind<IProduct>().To<ProductRepository>(); ;
            ninjectKernel.Bind<IInvoice>().To<InvoiceRepository>(); ;
        }
    }
}

【问题讨论】:

  • 我很快就会在 GitHub 上发布完整的解决方案,供任何想要贡献的人使用。我会在这里发布一个链接。我只想先让它恢复到工作版本,这需要大约一个小时。
  • 我的项目的完整代码现在在 github 上,位于以下链接。 github.com/franasm/Store 请转到损坏的分支以获取上面的代码,因为主分支是我拥有的最后一个工作分支。感谢您提供任何帮助,并随时在此处发表评论并提供任何改进建议。

标签: c# asp.net-mvc entity-framework dependency-injection ninject


【解决方案1】:

您没有提及在添加列后是否让 EF 自动重新生成您的 Invoice 实体。假设您使用代码优先且不通过 T4 模板(.TT 文件)生成实体,则您自己维护实体。生成是一次性的,可帮助您入门,因此您不必从头开始编写所有实体。

在这种情况下,您可以将 Age 字段添加到您的 Invoice 实体中,并让您的业务服务在 CalcAge 函数中获取 Invoice 实体实例,或者只是将 DateTime 传递给该实例功能并恢复年龄。通常,您希望使用视图模型而不是使用 EF 实体来实现此目的,并且您可能会将出生日期存储在数据库中并计算 Age 字段,无论是在数据库中还是在实体逻辑中属性获取器(它会像你已经拥有的 [NotMapped] )。

您不希望将业务层中的类与实际的 EF 实体耦合,而是对实体执行操作,无论是对新创建的实体还是通过存储库层从数据库检索到的实体,就像您一样现在有。

既然你想使用业务层,你可以这样做:

namespace Store.Models
{
    using System;
    using System.Collections.Generic;

    public partial class Invoice
    {
         public Invoice()
         {
             this.Products = new HashSet<Product>();
         }

        public int Id { get; set; }
        public string Details { get; set; }
        public Nullable<decimal> Total { get; set; }

        [NotMapped]
        public int Age {get; set;
    // ...

商业服务:

using Store.Models;

namespace Store.Business
{
    public class InvoiceBL
    {
        public int CalcAge(DateTime? date)
        {
            Age = 25;
            //TODO: Come back and enter proper logic to work out age
            // Something like:
            // return date != null ? <DateTime.Now.Year - date.Year etc.> : null
            return Age;
        }

在控制器中,每次返回 View. 时,您都必须计算年龄字段并将其设置为您的数据模型 Invoice 这不是最佳的,但它确实使用了业务层。

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create(Store.Model invoice)
    {
        if (ModelState.IsValid)
        {
            _invoiceRepository.CreateInvoice(invoice);
            // _service is your business service, injected as a dependency via the constructor, same as the _invoiceRepository is now
            invoice.Age = __service.CalcAge(invoice.BirthDate); // or some such thing
            return RedirectToAction("Index");
        }
        return View(invoice);
    }

您还必须为更新操作等执行此操作;任何返回 Invoice 作为视图模型的操作。

视图的模型将绑定到发票实体:

@model IEnumerable<Store.Models.Invoice>

@{
    ViewBag.Title = "Index";
 }
 // ... and so on

您的 Ninject 容器将绑定该服务,它将成为控制器的依赖项。我个人会将存储库作为依赖项注入到服务中,并将服务注入到控制器中,而不是让服务和存储库在控制器内部分离,但我会选择你所拥有的。

namespace Store.Web.Ninject
{
    public class NinjectControllerFactory : DefaultControllerFactory
    {
        private IKernel ninjectKernel;

        public NinjectControllerFactory()
        {
             ninjectKernel = new StandardKernel();
            AddBinding();
        }

        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            //return base.GetControllerInstance(requestContext, controllerType);
            return controllerType == null
                ? null
                : (IController)ninjectKernel.Get(controllerType);
       }

        private void AddBinding()
        {
            //TODO FR: Step 4 - Add your interface and repository to the bindings
            ninjectKernel.Bind<IProduct>().To<ProductRepository>();
            ninjectKernel.Bind<IInvoice>().To<InvoiceRepository>();
            // Add this, assuming there isn't an interface for your service
            ninjectKernel.Bind<InvoiceBL>().ToSelf();            
        }
    }
}

我没有看到任何关于Discriminator 列的代码,但如果它在实体中并且已映射,则它需要在数据库表中。映射要么通过上下文中使用的类(或直接在上下文中完成),要么使用 DataAnnotation 属性设置,例如 [NotMapped]。

【讨论】:

  • 我没有先使用代码。至少不是发票和产品表。我不知道如何处理登录和注册的东西。我只是将这些表物理复制到我自己的数据库中并更改了 con 字符串,并且它起作用了。所以我把它留在那里。我相信有更好的方法来做到这一点。我知道我可以在数据库表中添加一个计算字段,一切都会正常工作。但我的工作前提是这并不适合所有问题。那么,如果我有一些我想从一个不需要阻塞数据库的数据集计算的东西,我该怎么办......
  • 更多的是关于为模型创建单独的业务层的问题的答案。我知道我使用的示例是人为的,但这足以说明我的观点。我不确定我是否完全理解。您是说视图模型是业务层(或至少应该是)。这在一定程度上是有道理的。所以在这种情况下,他们应该从存储库中读取。不知道你说的是不是这个。
  • 关于依赖的东西。我还在学习这个。因此,我将不胜感激任何建议,例如澄清您所说的内容。我不必使用业务层。但是,如果这要成为我的业务层,我想将视图模型分开,并实际调用它而不是给人一种使用另一种模式(MVVM)的印象的视图模型,即使情况并非如此。实际上没有鉴别器列。这是错误消息的一部分,我不知道它是从哪里得到的。反正我的代码里什么都没有。
  • 再次...我认为我首先使用数据库。但是我需要一些帮助,因为我并不完全意识到我正在分离控制器中与依赖项相关的东西。这很有趣。我想更改我的代码以反映最佳实践。不过,总的来说,感谢您迄今为止提供的所有帮助。
  • 视图模型是视图上使用的模型,通过视图页面上的@model 指令指定。业务层是声明业务服务的地方,例如您的 Store.Business 代码。这绝对与视图模型不同。这个想法是将关注点分开,如视图模型、数据层和业务逻辑层。在这种情况下,MVVM 仅与 ASP.NET MVC 不同,因为您将使用一组特定的类来表示视图使用的模型。我建议您在代码库中搜索 Discriminator 这个词(在文件中查找)。
猜你喜欢
  • 2014-11-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-10
  • 2017-01-07
  • 2014-07-12
  • 2013-11-10
  • 1970-01-01
相关资源
最近更新 更多