MVC的最佳架构

发布时间 2023-08-20 08:51:16作者: 吾心依旧

原文地址

https://chsakell.com/2015/02/15/asp-net-mvc-solution-architecture-best-practices/

  

  为Web应用程序选择正确的架构是必须的,尤其是对于大型应用程序。使用默认的Visual Studio ASP。NET MVC Web应用程序项目模板,添加带有脚手架选项的控制器,只需引导应用程序并在几分钟内创建页面和数据,听起来确实很棒,但老实说,这并不总是正确的选择。查看所有默认选项,将业务、数据和表示逻辑保持在同一个项目中,将影响解决方案中的几个因素,如可伸缩性、可用性或可测试性。在这篇文章中,我们将看到如何保持事物的整洁,创建一个高度松散耦合的ASP。NET MVC解决方案,其中数据访问、业务和表示层以正确的方式定义。为此,我们将使用几种模式和框架,其中一些将在下面介绍。

 

实体框架代码优先开发

通用存储库模式

使用Autofac框架的依赖注入

自动映射器

让我们开始吧。假设我们想构建一个名为“Store”的电子商店Web应用程序,请创建一个同名的空白解决方案。

 

模型

将类库项目添加到解决方案中,名为Store.Model。这个库是我们保存所有域对象的地方。实体框架将依靠它们来构建数据库,但我们不会在此项目上使用DataAnnotations属性配置CodeFirst。相反,我们将使用Fluent API将所有代码优先配置放在特定的配置类中。添加一个名为Models的文件夹,并添加以下两个简单类。

Gadget.cs
public class Gadget
{
public int GadgetID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public string Image { get; set; }

public int CategoryID { get; set; }
public Category Category { get; set; }
}

Category.cs
public class Category
{
public int CategoryID { get; set; }
public string Name { get; set; }
public DateTime? DateCreated { get; set; }
public DateTime? DateUpdated { get; set; }

public virtual List<Gadget> Gadgets { get; set; }

public Category()
{
DateCreated = DateTime.Now;
}
}

Data Access Layer and Repositories
数据访问层和存储库
该层的目的是直接访问数据库。它是唯一负责与数据库通信的层。如果其他层想要访问数据库,那么这将通过我们将在本项目中定义的一些类(存储库)来完成。这将是添加名为Store的新类库项目的唯一方法。数据,并确保添加对先前创建的项目Store.Model的引用。使用Nuget包管理器安装实体框架。我们要做的第一件事是为我们的域对象定义实体类型配置。添加名为Configuration的文件夹,其中包含以下两个继承EntityTypeConfiguration类的类。

GadgetConfiguration.cs
public class GadgetConfiguration: EntityTypeConfiguration<Gadget>
{
public GadgetConfiguration()
{
ToTable("Gadgets");
Property(g => g.Name).IsRequired().HasMaxLength(50);
Property(g => g.Price).IsRequired().HasPrecision(8, 2);
Property(g => g.CategoryID).IsRequired();
}
}

CategoryConfiguration.cs
public class CategoryConfiguration : EntityTypeConfiguration<Category>
{
public CategoryConfiguration()
{
ToTable("Categories");
Property(c => c.Name).IsRequired().HasMaxLength(50);
}
}

没有任何困难的配置需要解释,关键是要理解在哪里放置正确的对象。接下来我们要做的是创建DbContext类,该类将负责访问数据库。在当前项目的根下添加以下类。
StoreEntities.cs
lic class StoreEntities : DbContext
{
public StoreEntities() : base("StoreEntities") { }

public DbSet<Gadget> Gadgets { get; set; }
public DbSet<Category> Categories { get; set; }

public virtual void Commit()
{
base.SaveChanges();
}

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new GadgetConfiguration());
modelBuilder.Configurations.Add(new CategoryConfiguration());
}
}
我们希望在应用程序第一次启动时为数据库添加种子,因此也将以下类添加到项目的根目录中。
StoreSeedData
public class StoreSeedData : DropCreateDatabaseIfModelChanges<StoreEntities>
{
protected override void Seed(StoreEntities context)
{
GetCategories().ForEach(c => context.Categories.Add(c));
GetGadgets().ForEach(g => context.Gadgets.Add(g));

context.Commit();
}

private static List<Category> GetCategories()
{
return new List<Category>
{
new Category {
Name = "Tablets"
},
new Category {
Name = "Laptops"
},
new Category {
Name = "Mobiles"
}
};
}

private static List<Gadget> GetGadgets()
{
return new List<Gadget>
{
new Gadget {
Name = "ProntoTec 7",
Description = "Android 4.4 KitKat Tablet PC, Cortex A8 1.2 GHz Dual Core Processor,512MB / 4GB,Dual Camera,G-Sensor (Black)",
CategoryID = 1,
Price = 46.99m,
Image = "prontotec.jpg"
},
new Gadget {
Name = "Samsung Galaxy",
Description = "Android 4.4 Kit Kat OS, 1.2 GHz quad-core processor",
CategoryID = 1,
Price = 120.95m,
Image= "samsung-galaxy.jpg"
},
new Gadget {
Name = "NeuTab® N7 Pro 7",
Description = "NeuTab N7 Pro tablet features the amazing powerful, Quad Core processor performs approximately Double multitasking running speed, and is more reliable than ever",
CategoryID = 1,
Price = 59.99m,
Image= "neutab.jpg"
},
new Gadget {
Name = "Dragon Touch® Y88X 7",
Description = "Dragon Touch Y88X tablet featuring the incredible powerful Allwinner Quad Core A33, up to four times faster CPU, ensures faster multitasking speed than ever. With the super-portable size, you get a robust power in a device that can be taken everywhere",
CategoryID = 1,
Price = 54.99m,
Image= "dragon-touch.jpg"
},
new Gadget {
Name = "Alldaymall A88X 7",
Description = "This Alldaymall tablet featuring the incredible powerful Allwinner Quad Core A33, up to four times faster CPU, ensures faster multitasking speed than ever. With the super-portable size, you get a robust power in a device that can be taken everywhere",
CategoryID = 1,
Price = 47.99m,
Image= "Alldaymall.jpg"
},
new Gadget {
Name = "ASUS MeMO",
Description = "Pad 7 ME170CX-A1-BK 7-Inch 16GB Tablet. Dual-Core Intel Atom Z2520 1.2GHz CPU",
CategoryID = 1,
Price = 94.99m,
Image= "asus-memo.jpg"
},
// Code ommitted
};
}
}
您可以看到,这个接口继承了IDisposable接口,因此将实现IDbFactory接口的Concrete类也必须实现IDisposble接口。要以干净的方式做到这一点,请添加一个将实现IDisposable接口的Disposable类。那么任何将实现IDbFactory接口的类都只想继承这个类。
Disposable.cs
public class Disposable : IDisposable
{
private bool isDisposed;

~Disposable()
{
Dispose(false);
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!isDisposed && disposing)
{
DisposeCore();
}

isDisposed = true;
}

// Ovveride this to dispose custom objects
protected virtual void DisposeCore()
{
}
}

我已经强调了DisposeCore虚拟方法,因为这个方法将使其他类继承这个方法,以按需要的方式处理自己的对象。现在添加IDbFactory接口的实现类。

DbFactory.cs
public class DbFactory : Disposable, IDbFactory
{
StoreEntities dbContext;

public StoreEntities Init()
{
return dbContext ?? (dbContext = new StoreEntities());
}

protected override void DisposeCore()
{
if (dbContext != null)
dbContext.Dispose();
}
}
现在是时候创建一个通用的IRepository接口了,我们将在这里声明存储库将支持的默认操作。我在这里添加了一些我认为最常用的操作,但您可以根据需要扩展这些操作。
IRepository.cs
public interface IRepository<T> where T : class
{
// Marks an entity as new
void Add(T entity);
// Marks an entity as modified
void Update(T entity);
// Marks an entity to be removed
void Delete(T entity);
void Delete(Expression<Func<T, bool>> where);
// Get an entity by int id
T GetById(int id);
// Get an entity using delegate
T Get(Expression<Func<T, bool>> where);
// Gets all entities of type T
IEnumerable<T> GetAll();
// Gets entities using delegate
IEnumerable<T> GetMany(Expression<Func<T, bool>> where);
}


请注意,CRUD操作被注释为Mark to do something…这意味着当存储库实现添加、更新或删除实体时,此时不会向数据库发送命令。相反,调用者(服务层)将负责通过IUnitOfWork注入的实例向数据库发送Commit命令。为此,将使用名为UnitOfWork的模式。将以下两个文件添加到Infrastructure文件夹中

IUnitOfWork.cs
public interface IUnitOfWork
{
void Commit();
}

UnitOfWork.cs
public class UnitOfWork : IUnitOfWork
{
private readonly IDbFactory dbFactory;
private StoreEntities dbContext;

public UnitOfWork(IDbFactory dbFactory)
{
this.dbFactory = dbFactory;
}

public StoreEntities DbContext
{
get { return dbContext ?? (dbContext = dbFactory.Init()); }
}

public void Commit()
{
DbContext.Commit();
}
}
与我们使用Disposable类的方式相同,我们将使用一个抽象类,它具有IRepository接口的虚拟实现。这个基类将从所有特定的存储库继承,因此将实现IRepository接口。添加以下类。
RepositoryBase.cs
public abstract class RepositoryBase<T> where T : class
{
#region Properties
private StoreEntities dataContext;
private readonly IDbSet<T> dbSet;

protected IDbFactory DbFactory
{
get;
private set;
}

protected StoreEntities DbContext
{
get { return dataContext ?? (dataContext = DbFactory.Init()); }
}
#endregion

protected RepositoryBase(IDbFactory dbFactory)
{
DbFactory = dbFactory;
dbSet = DbContext.Set<T>();
}

#region Implementation
public virtual void Add(T entity)
{
dbSet.Add(entity);
}

public virtual void Update(T entity)
{
dbSet.Attach(entity);
dataContext.Entry(entity).State = EntityState.Modified;
}

public virtual void Delete(T entity)
{
dbSet.Remove(entity);
}

public virtual void Delete(Expression<Func<T, bool>> where)
{
IEnumerable<T> objects = dbSet.Where<T>(where).AsEnumerable();
foreach (T obj in objects)
dbSet.Remove(obj);
}

public virtual T GetById(int id)
{
return dbSet.Find(id);
}

public virtual IEnumerable<T> GetAll()
{
return dbSet.ToList();
}

public virtual IEnumerable<T> GetMany(Expression<Func<T, bool>> where)
{
return dbSet.Where(where).ToList();
}

public T Get(Expression<Func<T, bool>> where)
{
return dbSet.Where(where).FirstOrDefault<T>();
}

#endregion

}

由于实现被标记为虚拟,因此任何存储库都可以根据需要对特定操作进行验证。现在混凝土存储库:添加一个名为repositories的新文件夹,并添加以下两个类:
GadgetRepository.cs
public class GadgetRepository : RepositoryBase<Gadget>, IGadgetRepository
{
public GadgetRepository(IDbFactory dbFactory)
: base(dbFactory) { }
}

public interface IGadgetRepository : IRepository<Gadget>
{

}

 

CategoryRepository.cs
public class CategoryRepository : RepositoryBase<Category>, ICategoryRepository
{
public CategoryRepository(IDbFactory dbFactory)
: base(dbFactory) { }

public Category GetCategoryByName(string categoryName)
{
var category = this.DbContext.Categories.Where(c => c.Name == categoryName).FirstOrDefault();

return category;
}

public override void Update(Category entity)
{
entity.DateUpdated = DateTime.Now;
base.Update(entity);
}
}
public interface ICategoryRepository : IRepository<Category>
{
Category GetCategoryByName(string categoryName);
}

您可以看到,GadgetRepository支持使用默认行为的默认操作,当然这是可以的。另一方面,您可以看到一个示例,其中特定的存储库需要扩展其操作(GetCategoryByName)或覆盖默认操作(Update)。通常,您为每个Model类添加一个存储库,因此每个类型为T的存储库都负责通过DbContext.Set<T>操作特定的DbSet。我们已经完成了数据访问层的实现,因此可以继续下一个。

 

服务层
您希望公开MVC控制器的哪些操作?业务逻辑将在哪里实现?是的,你猜对了,就在这一层。添加名为Store.Service的新类库项目。您必须添加对先前创建的两个项目Store的引用。模型和存储数据。请注意,我还没有告诉您在这个项目中安装Entity Framework。我不会这样做,因为所需的任何数据库操作都将通过我们之前创建的注入存储库完成。将第一个服务文件添加到此项目。
GadgetService.cs
// operations you want to expose
public interface IGadgetService
{
IEnumerable<Gadget> GetGadgets();
IEnumerable<Gadget> GetCategoryGadgets(string categoryName, string gadgetName = null);
Gadget GetGadget(int id);
void CreateGadget(Gadget gadget);
void SaveGadget();
}

public class GadgetService : IGadgetService
{
private readonly IGadgetRepository gadgetsRepository;
private readonly ICategoryRepository categoryRepository;
private readonly IUnitOfWork unitOfWork;

public GadgetService(IGadgetRepository gadgetsRepository, ICategoryRepository categoryRepository, IUnitOfWork unitOfWork)
{
this.gadgetsRepository = gadgetsRepository;
this.categoryRepository = categoryRepository;
this.unitOfWork = unitOfWork;
}

#region IGadgetService Members

public IEnumerable<Gadget> GetGadgets()
{
var gadgets = gadgetsRepository.GetAll();
return gadgets;
}

public IEnumerable<Gadget> GetCategoryGadgets(string categoryName, string gadgetName = null)
{
var category = categoryRepository.GetCategoryByName(categoryName);
return category.Gadgets.Where(g => g.Name.ToLower().Contains(gadgetName.ToLower().Trim()));
}

public Gadget GetGadget(int id)
{
var gadget = gadgetsRepository.GetById(id);
return gadget;
}

public void CreateGadget(Gadget gadget)
{
gadgetsRepository.Add(gadget);
}

public void SaveGadget()
{
unitOfWork.Commit();
}

#endregion

}

第一个和最后一个突出显示的代码行提醒您为什么创建IUnitOfWork接口。如果我们想通过这个服务类创建一个小工具对象,我们可以这样写。
// init a gadget object..
gadgetService.CreateGadget(gadget);
gadgetService.SaveGadget();

其他突出显示的代码行表示此服务所需的任何存储库都将通过其构造函数注入。这将通过依赖容器完成,我们将使用Autofac框架在MVC项目的启动类中设置依赖容器。以同样的方式,我创建了GadgetService。cs文件。
// operations you want to expose
public interface ICategoryService
{
IEnumerable<Category> GetCategories(string name = null);
Category GetCategory(int id);
Category GetCategory(string name);
void CreateCategory(Category category);
void SaveCategory();
}

public class CategoryService : ICategoryService
{
private readonly ICategoryRepository categorysRepository;
private readonly IUnitOfWork unitOfWork;

public CategoryService(ICategoryRepository categorysRepository, IUnitOfWork unitOfWork)
{
this.categorysRepository = categorysRepository;
this.unitOfWork = unitOfWork;
}

#region ICategoryService Members

public IEnumerable<Category> GetCategories(string name = null)
{
if (string.IsNullOrEmpty(name))
return categorysRepository.GetAll();
else
return categorysRepository.GetAll().Where(c => c.Name == name);
}

public Category GetCategory(int id)
{
var category = categorysRepository.GetById(id);
return category;
}

public Category GetCategory(string name)
{
var category = categorysRepository.GetCategoryByName(name);
return category;
}

public void CreateCategory(Category category)
{
categorysRepository.Add(category);
}

public void SaveCategory()
{
unitOfWork.Commit();
}

#endregion
}
我们也完成了服务层的工作。让我们从最后一个ASP开始。NET MVC Web应用程序。

 

演示文稿层
添加新ASP。NET Web应用程序名为Store。Web选择选中MVC选项的空模板。我们还需要通过Nuget Packages添加对以前所有类库项目和实体框架安装的引用。你可能想知道,我们是否会在这个项目中编写任何与实体框架相关的查询?根本不需要,我们需要它的一些名称空间,这样我们就可以为应用程序设置数据库配置,例如数据库初始化器。自从我们开始做这个,打开Global.asax。cs文件,并添加下面一行代码来设置我们在Store中创建的种子初始化器。数据项目。
Glbal.asax.cs
protected void Application_Start()
{
// Init database
System.Data.Entity.Database.SetInitializer(new StoreSeedData());

AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
您还需要创建一个连接字符串元素来定义要在哪里创建数据库。在Web中添加以下元素。配置文件,并根据您的开发环境要求对其进行更改。
Web.config
<connectionStrings>
<add name="StoreEntities" connectionString="Data Source=(localdb)\v11.0;Initial Catalog=StoreDb;Integrated Security=True" providerName="System.Data.SqlClient" />
</connectionStrings>
我们在前面的步骤中已经做了这样的努力来创建存储库和服务,但现在是时候让它们一起工作了。如果你还记得,所有服务构造器都有必须注入的存储库接口。服务主题稍后将注入控制器构造器,这就是我们的应用程序的工作方式。为了实现这一点,我们需要设置依赖注入,因此我决定使用Autofac。确保安装Autofac ASP。NET MVC 5通过Nuget包集成。
Create a Bootstrapper.cs file under the Start_App folder and paste the following code.
Bootstrapper.cs
public static void Run()
{
SetAutofacContainer();
}

private static void SetAutofacContainer()
{
var builder = new ContainerBuilder();
builder.RegisterControllers(Assembly.GetExecutingAssembly());
builder.RegisterType<UnitOfWork>().As<IUnitOfWork>().InstancePerRequest();
builder.RegisterType<DbFactory>().As<IDbFactory>().InstancePerRequest();

// Repositories
builder.RegisterAssemblyTypes(typeof(GadgetRepository).Assembly)
.Where(t => t.Name.EndsWith("Repository"))
.AsImplementedInterfaces().InstancePerRequest();
// Services
builder.RegisterAssemblyTypes(typeof(GadgetService).Assembly)
.Where(t => t.Name.EndsWith("Service"))
.AsImplementedInterfaces().InstancePerRequest();

IContainer container = builder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
}
}
代码本身是不言自明的。我希望你已经跟随我,并且你已经像我一样命名了你的存储库和服务类,否则,这是行不通的。完成指导还有两件重要的事情要做。第一个定义ViewModel类并将Automapper设置为将域实体映射到视图模型并向后映射。第二个是了解如何在我们的web应用程序中设置CSS Bootstrap。我想大多数人都会从Nuget Packages安装引导程序,并开始向项目添加css和脚本引用。在这里,我们将采用不同的方法。

CSS引导

首先,从官方网站下载Boostrap发行版。在应用程序中分别添加三个名为css、fonts和js的文件夹。在css文件夹中粘贴引导程序。从您下载的css文件中,在字体文件夹中粘贴所有内容都在相应的字体文件夹中,在js文件夹中,只需粘贴引导程序即可。js文件。我们将使用捆绑和小型化进行引导,为了实现这一点,您需要安装Microsoft ASP。NET Web优化框架。
完成安装后,将名为BundleConfig的新类添加到App_Start文件夹中,如下所示:
BundleConfig.cs
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bootstrap/js").Include("~/js/bootstrap.js", "~/js/site.js"));
bundles.Add(new StyleBundle("~/bootstrap/css").Include("~/css/bootstrap.css", "~/css/site.css"));

BundleTable.EnableOptimizations = true;
}
}
正如你所看到的,我也参考了这个网站。js和站点。css javascript和css文件。这些文件可以承载您想要执行的任何引导css自定义或任何javascript相关代码。请随意添加相应的文件并将其保留为空。现在我们需要声明,我们希望MVC使用捆绑和小型化,因此在Global.asax中添加以下行。cs文件。
Global.asax.cs
protected void Application_Start()
{
// Init database
System.Data.Entity.Database.SetInitializer(new StoreSeedData());

AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);

// Autofac and Automapper configurations
Bootstrapper.Run();
}
请注意,我还调用了Bootstrapper。Run()函数,用于设置我们之前所做的Autofac配置。此函数还将配置Automapper,稍后我们将看到这一点。让我们暂时结束Bootrap。我们需要一个Layout用于我们的应用程序,所以在Views文件夹下创建一个Shared文件夹,并添加一个名为_Layout.cshtml的MVC5LayoutPage(Razor)类型的新项目。
_Layout.cshtml
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@ViewBag.Title</title>
<!-- Bootstrap -->
@Styles.Render("~/bootstrap/css")
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/
html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/
respond.min.js"></script>
<![endif]-->
</head>
<body>
<nav id="myNavbar" class="navbar navbar-default navbar-inverse navbar-fixed-top" role="navigation">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbarCollapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
@Html.ActionLink("Store", "Index", "Home", new { }, new { @class = "navbar-brand" })
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="nav navbar-nav">
<li class="active">
@Html.ActionLink("Tablets", "Index", "Home", new { category = "Tablets" }, null)
</li>
<li class="active">
@Html.ActionLink("Laptops", "Index", "Home", new { category = "Laptops" }, null)
</li>
<li class="active">
@Html.ActionLink("Mobiles", "Index", "Home", new { category = "Mobiles" }, null)
</li>
</ul>
</div>
</nav>
@RenderBody()
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
@Scripts.Render("~/bootstrap/js")
</body>
</html>
页面可能会抱怨无法解析Razor语法,因此您必须在web中添加以下using语句。位于Views文件夹下的config文件(而不是应用程序的web.config)。以下是该文件的一部分。
web.config
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
<add namespace="Store.Web" />
<add namespace="System.Web.Optimization" />
</namespaces>
自动映射器

在实际应用程序中,您的域对象可能有很多属性,但您只需要在浏览器中显示其中的一些属性。更重要的是,当发布回服务器时,例如当通过表单元素创建对象时,您还希望只发布域对象的一些属性。为此,您定义了ViewModel对象,并使用它们来代替真正的域对象。确保从Nuget Packages安装Automapper。
添加一个名为ViewModels的新文件夹,其中包含以下类。
GadgetViewModel.cs
public class GadgetViewModel
{
public int GadgetID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public string Image { get; set; }

public int CategoryID { get; set; }
}

CategoryViewModel.cs
public class CategoryViewModel
{
public int CategoryID { get; set; }
public string Name { get; set; }

public List<GadgetViewModel> Gadgets { get; set; }
}

GadgetFormViewModel.cs
public class GadgetFormViewModel
{
public HttpPostedFileBase File { get; set; }
public string GadgetTitle { get; set; }
public string GadgetDescription { get; set; }
public decimal GadgetPrice { get; set; }
public int GadgetCategory { get; set; }
}

当ViewModel类的属性被命名为各自的域对象时,Automapper足够聪明,可以通过默认约定进行映射。否则,您必须自己手动设置映射。请注意我添加的最后一个类,GadgetFormViewModel。我们可以进行一个转换,在“ViewModel”之前添加一个“Form”单词,这样我们就知道这种类型的视图模型是通过表单元素发布回服务器的。现在让我们配置映射。添加新文件夹Mappings并添加以下类文件。
AutoMapperConfiguration.cs
public class AutoMapperConfiguration
{
public static void Configure()
{
Mapper.Initialize(x =>
{
x.AddProfile<DomainToViewModelMappingProfile>();
x.AddProfile<ViewModelToDomainMappingProfile>();
});
}
}
我们还没有创建所需的配置文件,但我们稍后会创建。我想向您展示的是,您可以创建任意多个自动映射器配置文件,然后将它们添加到Mapper中。初始化函数。这里我们将定义两个配置文件,一个用于将域模型映射到ViewModel,另一个用于向后。在与前一个相同的文件夹中添加以下类。

DomainToViewModelMappingProfile.cs
public class DomainToViewModelMappingProfile : Profile
{
public override string ProfileName
{
get { return "DomainToViewModelMappings"; }
}

protected override void Configure()
{
Mapper.CreateMap<Category,CategoryViewModel>();
Mapper.CreateMap<Gadget, GadgetViewModel>();
}
}

ViewModelToDomainMappingProfile.cs
public class ViewModelToDomainMappingProfile : Profile
{
public override string ProfileName
{
get { return "ViewModelToDomainMappings"; }
}

protected override void Configure()
{
Mapper.CreateMap<GadgetFormViewModel, Gadget>()
.ForMember(g => g.Name, map => map.MapFrom(vm => vm.GadgetTitle))
.ForMember(g => g.Description, map => map.MapFrom(vm => vm.GadgetDescription))
.ForMember(g => g.Price, map => map.MapFrom(vm => vm.GadgetPrice))
.ForMember(g => g.Image, map => map.MapFrom(vm => vm.File.FileName))
.ForMember(g => g.CategoryID, map => map.MapFrom(vm => vm.GadgetCategory));
}
}
对于Domain->ViewModels映射,我们不需要设置任何内容。自动映射器将使用默认约定,这很好。对于我们的GadgetFormViewModel->Gadget映射,我们手动设置了如上所示的配置。Automapper要完成的最后一件事是在Bootstrapper类中添加以下行。
Bootsrapper.cs
public static class Bootstrapper
{
public static void Run()
{
SetAutofacContainer();
//Configure AutoMapper
AutoMapperConfiguration.Configure();
}
// Code ommitted

Controllers and Views
我们快完成了。添加名为HomeController的新MVC控制器并粘贴以下代码。
HomeController.cs
public class HomeController : Controller
{
private readonly ICategoryService categoryService;
private readonly IGadgetService gadgetService;

public HomeController(ICategoryService categoryService, IGadgetService gadgetService)
{
this.categoryService = categoryService;
this.gadgetService = gadgetService;
}

// GET: Home
public ActionResult Index(string category = null)
{
IEnumerable<CategoryViewModel> viewModelGadgets;
IEnumerable<Category> categories;

categories = categoryService.GetCategories(category).ToList();

viewModelGadgets = Mapper.Map<IEnumerable<Category>, IEnumerable<CategoryViewModel>>(categories);
return View(viewModelGadgets);
}
}
现在,您可以实际了解为什么我们如此努力地设置存储库、服务、Autofac和Automapper。对于每个请求,服务将被注入控制器中,并且它们的数据将在发送到客户端之前映射到ViewModels。右键单击“索引”操作并添加一个名为“索引”的视图,代码如下。我必须在这里提到,我们使用的小工具对象具有对Web应用程序项目中名为images的文件夹的图像引用。你可以使用你的图片,也可以在文章的最后下载这个项目。

Views/Home/Index.cshtml
@model IEnumerable<Store.Web.ViewModels.CategoryViewModel>

@{
ViewBag.Title = "Store";
Layout = "~/Views/Shared/_Layout.cshtml";
}

<p>

</p>
<div class="container">
<div class="jumbotron">

@foreach (var item in Model)
{
<div class="panel panel-default">
<div class="panel-heading">
@*@Html.DisplayFor(modelItem => item.Name)*@
@Html.ActionLink("View all " + item.Name, "Index", new { category = item.Name }, new { @class = "pull-right" })
@using (Html.BeginForm("Filter", "Home", new { category = item.Name }, FormMethod.Post, new { @class = "navbar-form" }))
{
@Html.TextBox("gadgetName", null, new { @class = "form-control", placeholder = "Search in " + item.Name })
}


</div>
@foreach (var gadget in item.Gadgets)
{
@Html.Partial("Gadget", gadget)
}
<div class="panel-footer">
@using (Html.BeginForm("Create", "Home", FormMethod.Post,
new { enctype = "multipart/form-data", @class = "form-inline" }))
{
@Html.Hidden("GadgetCategory", item.CategoryID)
<div class="form-group">
<label class="sr-only" for="file">File</label>
<input type="file" class="form-control" name="file" placeholder="Select picture..">
</div>
<div class="form-group">
<label class="sr-only" for="gadgetTitle">Title</label>
<input type="text" class="form-control" name="gadgetTitle" placeholder="Title">
</div>
<div class="form-group">
<label class="sr-only" for="gadgetName">Price</label>
<input type="number" class="form-control" name="gadgetPrice" placeholder="Price">
</div>
<div class="form-group">
<label class="sr-only" for="gadgetName">Description</label>
<input type="text" class="form-control" name="gadgetDescription" placeholder="Description">
</div>
<button type="submit" class="btn btn-primary">Upload</button>
}
</div>
</div>
}

</div>

</div>
这里有两件事需要注意。第一个是我们需要创建一个Partial视图来显示GadgetViewModel对象,第二个是窗体的控件元素的名称。您可以看到,它们使用了我们的GadgetFormViewModel属性。在共享文件夹下,创建以下部分视图以显示GadgetViewModel对象。
Views/Shared/Gadget.cshtml
@model Store.Web.ViewModels.GadgetViewModel

<div class="panel-body">
<div class="media">
<a class="pull-left" href="#">
<img class="media-object" src="../../images/@Model.Image" />
</a>
<div class="media-body">
<h3 class="media-heading">
@Model.Name
</h3>
<p>@Model.Description</p>
</div>
</div>
</div>
In the Index.cshtml page I have added search and filter functionality and Create gadget as well. To achieve that you need to add the following Action methods to the HomeController.

HomeController.cs
public ActionResult Filter(string category, string gadgetName)
{
IEnumerable<GadgetViewModel> viewModelGadgets;
IEnumerable<Gadget> gadgets;

gadgets = gadgetService.GetCategoryGadgets(category, gadgetName);

viewModelGadgets = Mapper.Map<IEnumerable<Gadget>, IEnumerable<GadgetViewModel>>(gadgets);

return View(viewModelGadgets);
}

[HttpPost]
public ActionResult Create(GadgetFormViewModel newGadget)
{
if (newGadget != null && newGadget.File != null)
{
var gadget = Mapper.Map<GadgetFormViewModel, Gadget>(newGadget);
gadgetService.CreateGadget(gadget);

string gadgetPicture = System.IO.Path.GetFileName(newGadget.File.FileName);
string path = System.IO.Path.Combine(Server.MapPath("~/images/"), gadgetPicture);
newGadget.File.SaveAs(path);

gadgetService.SaveGadget();
}

var category = categoryService.GetCategory(newGadget.GadgetCategory);
return RedirectToAction("Index", new { category = category.Name });
}
I am sure that at this point you understand the purpose of all the above code so I won’t explain anything. You need to add a Filter page so right click in the Filter action and create the following View.

Home/Views/Filter.cshtml
@model IEnumerable<Store.Web.ViewModels.GadgetViewModel>

@{
ViewBag.Title = "Filter";
Layout = "~/Views/Shared/_Layout.cshtml";
}

<div class="container">
<div class="jumbotron">

@foreach (var item in Model)
{
<div class="panel panel-default">
<div class="panel-heading">
@Html.Label(item.Name)
</div>
@Html.Partial("Gadget", item)
</div>
}

</div>
</div>