在学习Asp.net Mvc中,今天第一次听了基架,哈哈!
常用的首字母缩略词 CRUD 恰当地传达了根据数据存储编写例程创建、检索、更新和删除操作的普通任务。Microsoft 提供由 T4 模板强力驱动的实用基架引擎,可为使用实体框架的 ASP.NET MVC 应用程序中的模型自动创建基本的 CRUD 控制器和视图(目前也可使用不带实体框架基架的 WebAPI 和 MVC)。
基架生成可导航和使用的页面。总而言之,它们减少了您在构建 CRUD 页面时需要执行的单调工作。不过,基架生成的结果提供的功能具有限制性,会让您立即根据自己的需求稍稍调整所生成的控制器逻辑和视图。
这样做的风险在于构建基架是一个单向过程。您无法重新使用基架生成可以反映模型变化的控制器和视图,而又不覆盖调整。因此,您必须注意跟踪您已自定义的模块,这样才能知道哪些模型可以安全地重新使用基架生成,哪些模型不可以。
在团队环境中,很难保持这样的警惕性。最重要的是,编辑控制器在“编辑”视图中显示大部分的模型属性,因此可能会揭示敏感信息。它会盲目地对视图提交的所有属性进行模型绑定和暂留,这增加了大规模赋值攻击的风险。
在本文中,我将介绍如何创建项目专用的自定义 T4 模板,从而强力驱动 ASP.NET MVC 实体框架 CRUD 基架子系统。与此同时,我还将介绍如何扩展控制器的创建和编辑回发处理程序,这样您便可以在回发模型绑定和数据存储暂留之间插入您自己的代码。
为了解决对大规模赋值的担忧,我将创建一个自定义属性,让您能够完全控制应该以及不应该暂留的模型属性。然后,我将另外添加一个自定义属性,以便您可以将属性显示为“编辑”视图上的只读标签。
此后,您将对以下内容拥有前所未有的控制力:您的 CRUD 页面以及如何显示和暂留模型,同时将降低应用程序受到攻击的风险。最棒的一点是,您将可以对 ASP.NET MVC 项目中的所有模型利用这些技术,并能在模型改变时安全地重新生成控制器和视图。
项目设置
我已使用 Visual Studio 2013 Ultimate、ASP.NET MVC 5、Entity Framework 6 和 C#(此处所讨论的技术也与 Visual Studio 2013 Professional、Premium 和 Express for Web 以及 Visual Basic .NET 兼容)开发出此解决方案。我创建了两个解决方案以供下载:第一个是基线解决方案,您可以使用该解决方案从正在处理的项目入手,然后手动实施这些技术。第二个是完整解决方案,其中包括本文提及的所有改进。
每个解决方案都包含三个项目:一个适用于 ASP.NET MVC 网站、一个适用于实体模型和 T4 基架函数,另一个适用于数据上下文。解决方案的数据上下文指向 SQL Server Express 数据库。除了已提及的依赖关系,我使用 NuGet 添加了启动,以设置基架生成的视图的主题。
如果您选中 Microsoft Web Developer Tools 设置选项,那么基架子系统便会在设置期间安装。后续的 Visual Studio Servive Pack 将自动更新基架文件。在最新的 Microsoft Web 平台安装程序中,您可以获取在 Visual Studio Servive Pack 之间发布的基架子系统的所有更新。您可以从 bit.ly/1g42AhP 下载 Microsoft Web 平台安装程序。
如果您在使用本文随附的下载代码时遇到任何问题,请确保您使用的是最新版本,并且已仔细阅读 ReadMe.txt 文件。我将根据需要更新代码。
定义业务规则
为了说明生成 CRUD 视图所涉及的完整工作流程并减少干扰,我打算使用一个非常简单的实体模型,称为“产品”。
public class Product { public int ProductId { get; set; } public string Description { get; set; } public DateTime?CreatedDate { get; set; } public DateTime?ModifiedDate { get; set; } }
依据惯例,MVC 知道 ProductId 是主键,但它却不知道我对 CreatedDate 和 ModifiedDate 属性有特殊要求。顾名思义,我希望 CreatedDate 传递相关产品(由 ProductId 表示)插入数据库的时间。我还希望 ModifiedDate 传递产品的最后一次修改时间(我将使用 UTC 日期时间值)。
我想将 ModifiedDate 值作为只读文本显示在“编辑”视图上(如果记录从未经过修改,则 ModifiedDate 等于 CreatedDate)。我不希望在任何视图上显示 CreatedDate。我也不希望用户能够提供或修改这些日期值,因此我不想呈现任何表单控件来收集用户在“创建”或“编辑”视图上的输入值。
为了防止这些值受到攻击,我希望确保不在回发中暂留这些值,这样纵使聪明的黑客可以将它们作为表单域或查询字符串值提供,也无法发起攻击。由于我考虑到这些业务逻辑层规则,因此我不希望数据库负责维护这些列值(例如,我不想创建任何触发器或嵌入任何表格列定义逻辑)。
探索 CRUD 基架工作流程
首先,让我们检查一下默认基架的功能。我将右键单击 Web 项目的“控制器”文件夹并选择“添加控制器”,从而添加一个控制器。此时,“添加基架”对话框启动(请参见图 1)。
图 1:MVC 5“添加基架”对话框
我将使用“具有视图的 MVC 5 控制器,使用实体框架”条目,因为它可以使用基架为模型生成 CRUD 控制器和视图。选择该条目,然后单击“添加”。接下来出现的对话框为您提供了几个选项,这些选项最终会成为随后转换的 T4 模板的参数(请参见图 2)。
图 2:“添加控制器”对话框
输入“ProductController”作为控制器名称。为了方便本文进行论述,请取消选中“使用异步控制器操作”复选框(异步操作不在本文的讨论范围之内)。接下来,选择产品模型类。由于您使用的是实体框架,因此需要数据上下文类。下拉列表中显示继承自
System.Data.Entity.DbContext
的类,因此如果您的解决方案使用多个数据库上下文,请选择一个正确类。对于视图选项,请选中“生成视图”和“使用布局页面”。将布局页面文本框留空。
在您单击“添加”后,多个
T4 模板会进行转换,从而提供基架生成的结果。此过程生成控制器 (ProductController.cs)
和五个视图(Create.cshtml、Delete.cshtml、Details.cshtml、Edit.cshtml 和
Index.cshtml)的代码。前者写入 Web 项目的“控制器”文件夹,后者写入 Web
项目的“视图”文件夹。此时,您拥有一个可正常运行的控制器,以及管理产品实体中的数据所需的全部 CRUD
视图。您可以立即开始使用这些网页(从索引视图入手)。
您可能希望 CRUD 页面的外观和行为与项目中的其他所有模型都相似。使用 T4
模板通过基架生成 CRUD 页面有助于增强这种一致性。也就是说,您应该拒绝采用直接修改控制器和视图的做法,而是应该修改负责生成它们的 T4
模板。遵循此做法可确保基架生成的文件无需进一步修改,可以随时使用。
检视控制器存在的缺陷
虽然基架子系统能够让您以相当快的速度运行,但是它生成的控制器仍存在一些缺陷。我将说明如何进行一些改进。请参见图 3,了解用于处理创建和编辑操作的基架生成的控制器操作方法。
图 3:用于处理创建和编辑操作的基架生成的控制器操作方法
public ActionResult Create( [Bind(Include="ProductId,Description,CreatedDate,ModifiedDate")] Product product) { if (ModelState.IsValid) { db.Products.Add(product); db.SaveChanges(); return RedirectToAction("Index"); } return View(product); } public ActionResult Edit( [Bind(Include="ProductId,Description,CreatedDate,ModifiedDate")] Product product) { if (ModelState.IsValid) { db.Entry(product).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); } return View(product); }
每个方法的绑定属性明确包括产品模型的每个属性。如果 MVC 控制器在回发后对所有模型属性进行模型绑定,这就是所谓的大规模赋值。这也称为过发布,属于严重的安全漏洞。黑客可以利用这一漏洞,因为在数据库上下文中后续会调用 SaveChanges。这样就可以确保模型在数据存储中暂留。默认情况下,MVC 5 中的 CRUD 基架系统使用的控制器模板为创建和编辑操作回发方法生成大规模赋值代码。
如果您选择修饰模型上的某些属性,使得这些属性不会呈现给“创建”或“编辑”视图,也会导致大规模赋值发生。在进行模型绑定后,这些属性会设置为空(请参阅“使用属性抑制 CRUD 视图上的属性”,其中包含的属性可用于指定您是否应将基架生成的属性呈现给所生成的视图)。为了进行说明,我将先向产品模型添加两个属性:
public class Product { public int ProductId { get; set; } public string Description { get; set; } [ScaffoldColumn(false)] public DateTime?CreatedDate { get; set; } [Editable(false)] public DateTime?ModifiedDate { get; set; } }
当我使用“添加控制器”重新运行基架过程时,[Scaffold(false)] 属性可确保 CreatedDate 不显示在任何视图上(如前所述)。[Editable(false)] 属性可确保 ModifiedDate 将显示在“删除”、“详细信息”和“索引”视图上,但不会显示在“创建”或“编辑”视图上。如果属性不呈现给“创建”或“编辑”视图,则不会显示在回发的 HTTP 请求流中。
这就带来了问题,因为您在这些由 MVC 强力驱动的 CRUD 页面中为模型属性赋值的最后一次机会就是在回发期间。因此,如果属性回发值为空,那么该空值将进行模型绑定。然后,当 SaveChanges 在数据上下文对象中执行时,该模型将暂留在数据存储中。如果这是在编辑回发操作方法中完成,则该属性将替换为空值。这实际上删除了数据存储中的当前值。
在我的示例中,CreatedDate 在数据存储中暂留的值将会丢失。事实上,没有呈现给“编辑”视图的所有属性都将导致数据存储中的值被空值覆盖。如果模型属性或数据存储不允许赋空值,那么您会在回发时看到错误。为了克服这些缺陷,我将修改负责生成控制器的 T4 模板。
替换基架模板
若要修改使用基架生成控制器和视图的方式,您必须修改负责生成它们的 T4 模板。您可以修改原始模板,这将对所有 Visual Studio 项目的基架产生全局影响。您还可以修改 T4 模板的项目专用副本,这将只影响包含相应副本的项目。我将执行后一修改。
原始 T4 基架模板位于 %programfiles%\Microsoft Visual Studio 12.0\Common7\IDE\Extensions\Microsoft\Web\Mvc\Scaffolding\Templates 文件夹中(此类模板依赖位于 %programfiles%\-Microsoft Visual Studio 12.0\Common7\IDE\Extensions\Microsoft\Web Tools\Scaffolding 文件夹中的多个 .NET 程序集)。我将把重点放在使用基架生成实体框架 CRUD 控制器和视图的特定模板。图 4 对这些机制进行了总结。
图 4:使用基架生成实体框架 CRUD 控制器和视图的 T4 模板
基架模板的子文件夹名称 |
模板文件名 (.cs 表示 C#;.vb 表示 Visual Basic .NET) |
生成此文件 (.cs 表示 C#;.vb 表示 Visual Basic .NET) |
MvcControllerWithContext |
Controller.cs.t4 Controller.vb.t4 |
Controller.cs Controller.vb |
MvcView |
Create.cs.t4 Create.vb.t4 |
Create.cshtml Create.vbhtml |
MvcView |
Delete.cs.t4 Delete.vb.t4 |
Delete.cshtml Delete.vbhtml |
MvcView |
Details.cs.t4 Details.vb.t4 |
Details.cshtml Details.vbhtml |
MvcView |
Edit.cs.t4 Edit.vb.t4 |
Edit.cshtml Edit.vbhtml |
MvcView |
Index.cshtml Index.vbhtml |
Index.cshtml Index.vbhtml |
若要创建项目专用模板,请将要替换的文件从原始 T4 基架文件夹复制到 ASP.NET MVC Web 项目中的 CodeTemplates 文件夹(必须与该名称完全一致)。依据惯例,基架子系统首先会在 MVC 项目的 CodeTemplates 文件夹中查找匹配模板。
为了使用此功能,您必须精确复制原始模板文件夹中显示的特定子文件夹名称和文件名。我已从实体框架基架子系统的 CRUD 中复制我打算替换的 T4 文件。图 5 展示了我的 Web 项目的 CodeTemplates 文件夹。
图 5:Web 项目的 CodeTemplates 文件夹
我还复制了
Imports.include.t4 和
ModelMetadataFunctions.cs.include.t4。项目需要这些文件才能使用基架生成视图。此外,我只复制了 C#
(.cs) 版本的文件(如果您使用的是 Visual Basic .NET,则不妨复制文件名中包含 .vb
的文件)。基架子系统将转换这些项目专用文件,而不是这些文件的全局版本。
扩展创建和编辑操作方法
现在我已经有项目专用
T4
模板,可以根据需要修改这些模板了。首先,我将扩展控制器的创建和编辑操作方法,以便能够在模型暂留前进行检查和修改。为了尽可能保持模板生成的代码的通用性,我不想在模板中添加任何模型专用逻辑,而是希望调用与模型绑定的外部函数。这样,控制器的创建和编辑操作就得到了扩展,同时还可以模拟模型上的多形性。为此,我将创建一个接口,并将其命名为“IControllerHooks”:
namespace JW_ScaffoldEnhancement.Models { public interface IControllerHooks { void OnCreate(); void OnEdit(); } }
接下来,我将修改 Controller.cs.t4 模板(位于 CodeTemplates\-MVCControllerWithContext 文件夹中)。这样,如果模型已实施 IControllerHooks,则模板的创建和编辑回发操作方法将各自调用模型的 OnCreate 和 OnEdit 方法。图 6 展示了控制器的创建操作回发方法,图 7 展示了控制器的编辑操作回发方法。
图 6:控制器的创建操作回发方法的扩展版本
public ActionResult Create( [Bind(Include="ProductId,Description,CreatedDate,ModifiedDate")] Product product) { if (ModelState.IsValid) { if (product is IControllerHooks) { ((IControllerHooks)product).OnCreate(); } db.Products.Add(product); db.SaveChanges(); return RedirectToAction("Index"); } return View(product); }
图 7:控制器的编辑操作回发方法的扩展版本
public ActionResult Edit( [Bind(Include="ProductId,Description,CreatedDate,ModifiedDate")] Product product) { if (ModelState.IsValid) { if (product is IControllerHooks) { ((IControllerHooks)product).OnEdit(); } db.Entry(product).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); } return View(product); }
现在,我将把产品类修改为实施 IController-Hooks。然后,我将添加要在控制器调用 OnCreate 和 OnEdit 时执行的代码。图 8 展示了新的产品模型类。
图 8:实施 IControllerHooks 以扩展控制器的产品模型
public class Product :IControllerHooks { public int ProductId { get; set; } public string Description { get; set; } public DateTime?CreatedDate { get; set; } public DateTime?ModifiedDate { get; set; } public void OnCreate() { this.CreatedDate = DateTime.UtcNow; this.ModifiedDate = this.CreatedDate; } public void OnEdit() { this.ModifiedDate = DateTime.UtcNow; } }
不可否认,实施此“扩展”逻辑的方法有许多,但是通过对控制器模板的创建和编辑方法使用这种一行式修改,我现在可以在模型绑定之后、但在暂留之前修改产品模型实例。我甚至可以设置未发布给“创建”和“编辑”视图的模型属性的值。
您会发现模型的 OnEdit 函数没有设置 CreatedDate 的值。如果 CreatedDate 没有呈现给“编辑”视图且调用的是 SaveChanges,那么它会在控制器的编辑操作方法暂留模型后替换为空值。为防止这种情况发生,我将需要进一步修改控制器模板。
加强编辑操作方法
我已提到过一些与大规模赋值相关的问题。修改模型绑定行为的一种方法是将绑定属性修改为排除不要绑定的属性。不过,这种方法实际上仍会将空值写入数据存储。更好的策略是进行其他编程,不过这样做很值得。
我将使用实体框架附加方法将模型附加到数据库上下文。然后,我可以跟踪实体条目,并能根据需要设置 IsModified 属性。为了驱动此逻辑,我将在 JW_Scaffold-Enhancement.Models 项目中新建名为 CustomAttributes.cs 的类模块(请参见图 9)。
图 9:新的类模块 CustomAttributes.cs
using System; namespace JW_ScaffoldEnhancement.Models { public class PersistPropertyOnEdit :Attribute { public readonly bool PersistPostbackDataFlag; public PersistPropertyOnEdit(bool persistPostbackDataFlag) { this.PersistPostbackDataFlag = persistPostbackDataFlag; } } }
我将使用此属性指明我不希望从“编辑”视图暂留到数据存储中的属性(未修饰的属性将包含隐式 [PersistPropertyOnEdit(true)] 属性)。我是有意防止 CreatedDate 属性得到暂留,因此我已将新属性只添加到产品模型的 CreatedDate 属性。新修饰的模型类如下所示:
public class Product :IControllerHooks { public int ProductId { get; set; } public string Description { get; set; } [PersistPropertyOnEdit(false)] public DateTime?CreatedDate { get; set; } public DateTime?ModifiedDate { get; set; } }
现在,我将需要修改 Controller.cs.t4 模板,使其接受新属性。增强 T4 模板时,您可以选择是在模板内部还是外部进行相关更改。我建议尽可能将代码添加到外部代码模块中,除非您使用的是一种第三方模板编辑器工具。这样做可以提供纯 C# 画布(而不是散布 T4 标记的画布),您可以在其中重点关注代码。它还可以协助您进行测试,并便于您在更广泛地使用测试工具时使用函数。最后,由于从 T4 基架引用程序集的方式存在一些缺陷,您将在绑定所有内容时遇到较少的技术问题。
我的模型项目包含名为 GetPropertyIsModifiedList 的公共函数,它可返回用于循环访问的 List<String>,从而为所传递的程序集和类型生成 IsModified 设置。图 10 展示了 Controller.cs.t4 模板中的此代码。
图 10:用于生成改进的控制器编辑回发处理程序的 T4 模板代码
在图 11
展示的 GetPropertyIsModifiedList
中,我使用反射获取对所提供的模型属性的访问权限。然后,我循环访问它们,以确定哪些属性是使用 PersistPropertyOnEdit
属性进行修饰。您最有可能希望暂留模型上的大部分属性,因此我构建了模板代码,将属性的 IsModified 值默认设置为 True。这样,您只需将
[PersistPropertyOnEdit(false)] 添加到您不想暂留的属性即可。
图 11:模型项目 ScaffoldFunctions.GetPropertyIsModifiedList 静态函数
static public List<string> GetPropertyIsModifiedList(string ModelNamespace, string ModelTypeName, string ModelVariable) { List<string> OutputList = new List<string>(); // 获取模型对象的属性 string aqn = Assembly.CreateQualifiedName(ModelNamespace + ", Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", ModelNamespace + "."+ ModelTypeName); // 根据程序集限定名获取类型对象 Type typeModel = Type.GetType(aqn); // 获取类型的属性 PropertyInfo[] typeModelProperties = typeModel.GetProperties(); PersistPropertyOnEdit persistPropertyOnEdit; foreach (PropertyInfo propertyInfo in typeModelProperties) { persistPropertyOnEdit = (PersistPropertyOnEdit)Attribute.GetCustomAttribute( typeModel.GetProperty(propertyInfo.Name), typeof(PersistPropertyOnEdit)); if (persistPropertyOnEdit == null) { OutputList.Add(ModelVariable + "Entry.Property(e => e." + propertyInfo.Name + ").IsModified = true;"); } else { OutputList.Add(ModelVariable + "Entry.Property(e => e." + propertyInfo.Name + ").IsModified = " + ((PersistPropertyOnEdit)persistPropertyOnEdit).PersistPostbackDataFlag.ToString().ToLower() + ";"); } } return OutputList; }
已修订的控制器模板生成重新构思的编辑回发操作方法(如图 12 所示)。我的 GetPropertyIsModifiedList 函数生成此源代码的部分代码。
图 12:基架新生成的控制器编辑处理程序
if (ModelState.IsValid) { if (product is IControllerHooks) { ((IControllerHooks)product).OnEdit(); } db.Products.Attach(product); var productEntry = db.Entry(product); productEntry.Property(e => e.ProductId).IsModified = true; productEntry.Property(e => e.Description).IsModified = true; productEntry.Property(e => e.CreatedDate).IsModified = false; productEntry.Property(e => e.ModifiedDate).IsModified = true; db.SaveChanges(); return RedirectToAction("Index"); }
使用属性抑制 CRUD 视图上的属性
ASP.NET MVC 仅提供三个属性,以便于您在某种程度上控制是否将模型的属性呈现给基架生成的视图(请参见图 A)。前两个属性的用途相同(尽管它们驻留在不同的命名空间):[Editable(false)] 和 [ReadOnly(true)]。这些属性会导致经过修饰的属性不呈现给“创建”和“编辑”视图。第三个属性 [ScaffoldColumn(false)] 会导致经过修饰的属性不在所呈现的任何视图中显示。
图 A:防止属性呈现的三个属性
模型元数据属性 | 属性命名空间 | 受影响的视图 | 会发生什么 |
无 | 无 | 无其他属性只带来普通结果。 | |
[Editable(false)] [ReadOnly(true)] |
可编辑: System.ComponentModel.DataAnnotations 只读: System.ComponentModel |
Create 编辑 |
经过修饰的模型属性未呈现。 |
[ScaffoldColumn(false)] | System.ComponentModel.DataAnnotations |
Create 删除 详细信息 编辑 索引 |
经过修饰的模型属性未呈现。 |
自定义视图
有时,您希望在“编辑”视图上显示您不希望用户修改的值。ASP.NET MVC 提供的属性并不支持您这样做。我想在“编辑”视图上看到 ModifiedDate,但不想用户认为这是一个可编辑的字段。为了实施该做法,我将在 CustomAttributes.cs 类模块中创建另一个名为 DisplayOnEditView 的自定义属性,如下所示:
public class DisplayOnEditView :Attribute { public readonly bool DisplayFlag; public DisplayOnEditView(bool displayFlag) { this.DisplayFlag = displayFlag; } }
这样,我就可以修饰模型属性,使其呈现为“编辑”视图上的标签。然后,我将可以在“编辑”视图上显示 ModifiedDate,无需担心有人在回发期间篡改它的值。
现在,我可以使用该属性进一步修饰产品模型。我将把新属性放置在 ModifiedDate 属性上。我将使用 [Editable(false)] 确保它不会显示在“创建”视图上,并使用 [DisplayOnEditView(true)] 确保它作为标签显示在“编辑”视图上:
public class Product :IControllerHooks { public int ProductId { get; set; } public string Description { get; set; } [PersistPropertyOnEdit(false)] [ScaffoldColumn(false)] public DateTime?CreatedDate { get; set; } [Editable(false)] [DisplayOnEditView(true)] public DateTime?ModifiedDate { get; set; } }
最后,我将修改负责生成“编辑”视图的 T4 模板,使其接受 DisplayOnEditView 属性:
HtmlForDisplayOnEditViewAttribute = JW_ScaffoldEnhancement.Models.ScaffoldFunctions. GetHtmlForDisplayOnEditViewAttribute( ViewDataTypeName, property.PropertyName, property.IsReadOnly);
此外,我还将把 GetHtmlForDisplayOnEditViewAttribute 函数添加到 ScaffoldFunctions 类中,如图 13 所示。
当属性为 False 时,GetHtmlForDisplayOnEditViewAttribute 函数返回 Html.EditorFor;当属性为 True 时,该函数返回 Html.Display-TextFor。“编辑”视图将把 ModifiedDate 显示为标签,并把其他所有非键字段显示为可编辑的文本框(如图 14 所示)。
图 13:用于支持自定义 DisplayOnEditViewFlag 属性的 ScaffoldFunctions.GetHtmlForDisplayOnEditViewAttribute 静态函数
static public string GetHtmlForDisplayOnEditViewAttribute( string ViewDataTypeName, string PropertyName, bool IsReadOnly) { string returnValue = String.Empty; Attribute displayOnEditView = null; Type typeModel = Type.GetType(ViewDataTypeName); if (typeModel != null) { displayOnEditView = (DisplayOnEditView)Attribute.GetCustomAttribute(typeModel.GetProperty( PropertyName), typeof(DisplayOnEditView)); if (displayOnEditView == null) { if (IsReadOnly) { returnValue = String.Empty; } else { returnValue = "@Html.EditorFor(model => model."+ PropertyName + ")"; } } else { if (((DisplayOnEditView)displayOnEditView).DisplayFlag == true) { returnValue = "@Html.DisplayTextFor(model => model."+ PropertyName + ")"; } else { returnValue = "@Html.EditorFor(model => model."+ PropertyName + ")"; } } } return returnValue; }
图 14:显示只读 ModifiedDate 字段的“编辑”视图
总结
我只是简要介绍了使用基架子系统可以实现的功能。我侧重的是为实体框架提供 CRUD 控件和视图的基架,但也有其他基架可以为其他类型的网页和 Web API 操作生成代码。