在 MVC 中使用通用存储库模式和工作单元进行 CRUD 操作






4.82/5 (110投票s)
本文介绍在 ASP.NET MVC 应用程序中使用的通用存储库模式和工作单元。
引言
本文介绍在 ASP.NET MVC 应用程序中使用的通用存储库模式和工作单元。我们正在为一个 Book(图书)实体开发一个应用程序,在该应用程序上我们可以执行创建、读取、更新和删除操作。为了使文章保持简单易懂,对于通用存储库模式和工作单元,我们在应用程序中使用单个 Book 实体。
在上一篇文章“使用存储库模式在 MVC 中执行 CRUD 操作”中,我们为 Book 实体创建了一个 Book 存储库,对于执行 CRUD 操作的单个实体来说这是很好的。但是,假设我们有一个企业级应用程序,并且该应用程序有多个实体,那么我们需要为每个实体创建一个存储库。简而言之,我们可以说我们正在为每个实体的存储库重复代码,这违背了软件工程中的 DRY(每一个知识都应该在系统中拥有单一、明确、权威的表示)原则;这就是为什么我们需要通用存储库模式。如图 1.1 所示,两位开发者有一个问题:“是创建一个新部分还是重用现有部分?”一位开发者选择了创建新部分,如文章 使用存储库模式在 MVC 中执行 CRUD 操作 中所述。但另一位开发者选择了第二个选项,重用现有代码以减少代码量。所以现在你是第二位开发者,你的方法是通用存储库模式和工作单元,这也是本文的主要目标。
学习使用 Entity Framework 进行 MVC 的路线图
- 使用 Code First 方法和 Fluent API 在 Entity Framework 中建立关系
- Entity Framework 的 Code First 迁移
- 使用 MVC 中的 Entity Framework 5.0 Code First 方法进行 CRUD 操作
- 在 MVC 中使用存储库模式进行 CRUD 操作
- 在 MVC 中使用通用存储库模式和工作单元进行 CRUD 操作
- 在 MVC 中使用通用存储库模式和依赖注入进行 CRUD 操作
存储库(Repository)模式
存储库模式旨在创建应用程序的数据访问层和业务逻辑层之间的抽象层。它是一种数据访问模式,鼓励更松耦合的数据访问方法。我们将数据访问逻辑创建在一个单独的类或一组类中,称为存储库,负责持久化应用程序的业务模型。
在本文中,我们设计了一个通用的通用存储库类和一个工作单元类。工作单元类为每个实体创建一个存储库实例,然后使用该存储库执行 CRUD 操作。我们在控制器中创建 `UnitOfWork` 类的一个实例,然后根据实体创建一个存储库实例,之后根据操作使用存储库的方法。
下图显示了存储库与 Entity Framework 数据上下文之间的关系,其中 MVC 控制器通过工作单元而不是直接通过 Entity Framework 与存储库进行交互。
为什么需要工作单元
正如其名称所示,工作单元是执行某项操作的。在本文中,工作单元会执行我们创建它实例时需要完成的任务。它会实例化我们的 `DbContext`,之后每个存储库实例都使用相同的 `DbContext` 来执行数据库操作。因此,工作单元是一种确保所有存储库使用相同数据库上下文的模式。
实现一个通用存储库和一个工作单元类
注意:在本文中,您的用户界面使用的是具体类对象,而不是接口,因为这个概念我将在下一篇文章中介绍。为了保持代码简洁并仅描述概念,我从控制器中移除了错误处理代码,但您应该始终在控制器中使用错误处理。
在本节文章中,我们创建了两个项目,一个是 `EF.Core`,另一个是 `EF.Data`。在本文中,我们使用 Entity Framework Code First 方法,因此 `EF.Core` 项目包含应用程序数据库所需的实体。在 `EF.Data` 项目中,我们创建了两个实体:一个是 `BaseEntity` 类,它包含将由每个实体继承的公共属性;另一个是 `Book` 实体。让我们看看每个实体。以下是 `BaseEntity` 类的代码片段。
using System;
namespace EF.Core
{
public abstract class BaseEntity
{
public Int64 ID { get; set; }
public DateTime AddedDate { get; set; }
public DateTime ModifiedDate { get; set; }
public string IP { get; set; }
}
}
现在,我们在 `EF.Core` 项目的 *Data* 文件夹下创建一个 `Book` 实体,该实体继承自 `BaseEntity` 类。以下是 `Book` 实体的代码片段。
using System;
namespace EF.Core.Data
{
public class Book : BaseEntity
{
public string Title { get; set; }
public string Author { get; set; }
public string ISBN { get; set; }
public DateTime Published { get; set; }
}
}
`EF.Data` 项目包含 `DataContext`、Book 实体映射、存储库和工作单元类。ADO.NET Entity Framework Code First 数据访问方法要求我们创建一个继承自 `DbContext` 类的“数据访问上下文”类,因此我们创建了一个上下文类 `EFDbContext`(*EFDbContext.cs*)。在此类中,我们重写了 `OnModelCreating()` 方法。当上下文类(`EFDbContext`)的模型被初始化但尚未锁定并用于初始化上下文之前,会调用此方法,以便在模型被锁定之前可以进一步配置模型。以下是上下文类的代码片段。
using System;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration;
using System.Linq;
using System.Reflection;
using EF.Core;
namespace EF.Data
{
public class EFDbContext : DbContext
{
public EFDbContext()
: base("name=DbConnectionString")
{
}
public new IDbSet<TEntity> Set<TEntity>() where TEntity : BaseEntity
{
return base.Set<TEntity>();
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
var typesToRegister = Assembly.GetExecutingAssembly().GetTypes()
.Where(type => !String.IsNullOrEmpty(type.Namespace))
.Where(type => type.BaseType != null && type.BaseType.IsGenericType
&& type.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>));
foreach (var type in typesToRegister)
{
dynamic configurationInstance = Activator.CreateInstance(type);
modelBuilder.Configurations.Add(configurationInstance);
}
base.OnModelCreating(modelBuilder);
}
}
}
如您所知,EF Code First 方法遵循约定优于配置的原则,因此在构造函数中,我们只需传递连接字符串名称,这与 *App.Config* 文件中的名称相同,它会连接到该服务器。在 `OnModelCreating()` 方法中,我们使用反射将实体映射到该项目中的配置类。
现在,我们定义 book 实体的配置,该配置将在实体创建数据库表时使用。此配置定义了类库项目 `EF.Data` 中的 *Mapping* 文件夹。现在为实体创建配置类。对于 `Book` 实体,我们创建 `BookMap` 实体。
以下是 `BookMap` 类的代码片段。
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.ModelConfiguration;
using EF.Core.Data;
namespace EF.Data.Mapping
{
public class BookMap : EntityTypeConfiguration<Book>
{
public BookMap()
{
HasKey(t => t.ID);
Property(t => t.ID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(t => t.Title).IsRequired();
Property(t => t.Author).IsRequired();
Property(t => t.ISBN).IsRequired();
Property(t => t.Published).IsRequired();
ToTable("Books");
}
}
}
现在,我们创建一个通用存储库类。我们不为存储库创建接口,以便使我们的文章易于理解。这个通用存储库包含了所有的 CRUD 操作方法。该存储库包含一个带 `Context` 参数的构造函数,因此当我们创建存储库的实例时,我们会传递一个上下文,这样每个实体的存储库都将使用相同的上下文。我们使用上下文的 `saveChanges()` 方法,但您也可以使用工作单元类的 `save` 方法,因为它们具有相同的上下文。以下是通用存储库的代码片段。
using System;
using System.Data.Entity;
using System.Data.Entity.Validation;
using System.Linq;
using EF.Core;
namespace EF.Data
{
public class Repository<T> where T : BaseEntity
{
private readonly EFDbContext context;
private IDbSet<T> entities;
string errorMessage = string.Empty;
public Repository(EFDbContext context)
{
this.context = context;
}
public T GetById(object id)
{
return this.Entities.Find(id);
}
public void Insert(T entity)
{
try
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
this.Entities.Add(entity);
this.context.SaveChanges();
}
catch (DbEntityValidationException dbEx)
{
foreach (var validationErrors in dbEx.EntityValidationErrors)
{
foreach (var validationError in validationErrors.ValidationErrors)
{
errorMessage += string.Format("Property: {0} Error: {1}",
validationError.PropertyName, validationError.ErrorMessage) + Environment.NewLine;
}
}
throw new Exception(errorMessage, dbEx);
}
}
public void Update(T entity)
{
try
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
this.context.SaveChanges();
}
catch (DbEntityValidationException dbEx)
{
foreach (var validationErrors in dbEx.EntityValidationErrors)
{
foreach (var validationError in validationErrors.ValidationErrors)
{
errorMessage += Environment.NewLine + string.Format("Property: {0} Error: {1}",
validationError.PropertyName, validationError.ErrorMessage);
}
}
throw new Exception(errorMessage, dbEx);
}
}
public void Delete(T entity)
{
try
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
this.Entities.Remove(entity);
this.context.SaveChanges();
}
catch (DbEntityValidationException dbEx)
{
foreach (var validationErrors in dbEx.EntityValidationErrors)
{
foreach (var validationError in validationErrors.ValidationErrors)
{
errorMessage += Environment.NewLine + string.Format("Property: {0} Error: {1}",
validationError.PropertyName, validationError.ErrorMessage);
}
}
throw new Exception(errorMessage, dbEx);
}
}
public virtual IQueryable<T> Table
{
get
{
return this.Entities;
}
}
private IDbSet<t> Entities
{
get
{
if (entities == null)
{
entities = context.Set<t>();
}
return entities;
}
}
}
}
现在创建一个工作单元类;该类名为 `UnitOfWork`。该类继承自 `IDisposable` 接口,以便在其实例将在每个控制器中被释放。该类初始化应用程序的 `DataContext`。该类的核心是 `Repository
using System;
using System.Collections.Generic;
using EF.Core;
namespace EF.Data
{
public class UnitOfWork : IDisposable
{
private readonly EFDbContext context;
private bool disposed;
private Dictionary<string,object> repositories;
public UnitOfWork(EFDbContext context)
{
this.context = context;
}
public UnitOfWork()
{
context = new EFDbContext();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public void Save()
{
context.SaveChanges();
}
public virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
context.Dispose();
}
}
disposed = true;
}
public Repository<T> Repository<T>() where T : BaseEntity
{
if (repositories == null)
{
repositories = new Dictionary<string,object>();
}
var type = typeof(T).Name;
if (!repositories.ContainsKey(type))
{
var repositoryType = typeof(Repository<>);
var repositoryInstance = Activator.CreateInstance(repositoryType.MakeGenericType(typeof(T)), context);
repositories.Add(type, repositoryInstance);
}
return (Repository<t>)repositories[type];
}
}
}
使用通用存储库模式的 MVC 应用程序
现在,我们创建一个 MVC 应用程序(`EF.Web`),如图 1.3 所示。这是我们应用程序的第三个项目,该项目包含 `Book` 实体 CRUD 操作的用户界面以及执行这些操作的控制器。首先,我们转到控制器。在应用程序的 *Controllers* 文件夹下创建一个 `BookController`。该控制器包含每个 CRUD 操作用户界面的所有 `ActionResult` 方法。我们首先创建一个工作单元类实例,然后控制器的构造函数根据需要的实体初始化存储库。以下是 `BookController` 的代码片段。
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using EF.Core.Data;
using EF.Data;
namespace EF.Web.Controllers
{
public class BookController : Controller
{
private UnitOfWork unitOfWork = new UnitOfWork();
private Repository<book> bookRepository;
public BookController()
{
bookRepository = unitOfWork.Repository<book>();
}
public ActionResult Index()
{
IEnumerable<book> books = bookRepository.Table.ToList();
return View(books);
}
public ActionResult CreateEditBook(int? id)
{
Book model = new Book();
if (id.HasValue)
{
model = bookRepository.GetById(id.Value);
}
return View(model);
}
[HttpPost]
public ActionResult CreateEditBook(Book model)
{
if (model.ID == 0)
{
model.ModifiedDate = System.DateTime.Now;
model.AddedDate = System.DateTime.Now;
model.IP = Request.UserHostAddress;
bookRepository.Insert(model);
}
else
{
var editModel = bookRepository.GetById(model.ID);
editModel.Title = model.Title;
editModel.Author = model.Author;
editModel.ISBN = model.ISBN;
editModel.Published = model.Published;
editModel.ModifiedDate = System.DateTime.Now;
editModel.IP = Request.UserHostAddress;
bookRepository.Update(editModel);
}
if (model.ID > 0)
{
return RedirectToAction("Index");
}
return View(model);
}
public ActionResult DeleteBook(int id)
{
Book model = bookRepository.GetById(id);
return View(model);
}
[HttpPost,ActionName("DeleteBook")]
public ActionResult ConfirmDeleteBook(int id)
{
Book model = bookRepository.GetById(id);
bookRepository.Delete(model);
return RedirectToAction("Index");
}
public ActionResult DetailBook(int id)
{
Book model = bookRepository.GetById(id);
return View(model);
}
protected override void Dispose(bool disposing)
{
unitOfWork.Dispose();
base.Dispose(disposing);
}
}
}
现在我们已经开发了 `BookController` 来处理 book 实体的 CRUD 操作请求。现在,开发 CRUD 操作的用户界面。我们为添加和编辑图书、图书列表、图书删除和图书详情创建视图。我们逐一来看。
创建/编辑图书视图
我们在视图的 *Book* 文件夹下创建了一个用于创建和编辑图书的通用视图,名为 *CreateEditBook.cshtml*。我们使用日期选择器来选择图书的出版日期。这就是为什么我们编写 JavaScript 代码来实现日期选择器。
(function ($) {
function Book() {
var $thisthis = this;
function initializeAddEditBook() {
$('.datepicker').datepicker({
"setDate": new Date(),
"autoclose": true
});
}
$this.init = function () {
initializeAddEditBook();
}
}
$(function () {
var self = new Book();
self.init();
});
}(jQuery))
现在定义一个创建/编辑图书视图,以下是 *CreateEditBook.cshtml* 的代码片段。
@model EF.Core.Data.Book
@{
ViewBag.Title = "Create Edit Book";
}
<div class="book-example panel panel-primary">
<div class="panel-heading panel-head">Add / Edit Book</div>
<div class="panel-body">
@using (Html.BeginForm())
{
<div class="form-horizontal">
<div class="form-group">
@Html.LabelFor(model => model.Title,
new { @class = "col-lg-1 control-label" })
<div class="col-lg-9">
@Html.TextBoxFor(model => model.Title,
new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.ISBN,
new { @class = "col-lg-1 control-label" })
<div class="col-lg-9">
@Html.TextBoxFor(model => model.ISBN,
new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Author,
new { @class = "col-lg-1 control-label" })
<div class="col-lg-9">
@Html.TextBoxFor(model => model.Author,
new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Published,
new { @class = "col-lg-1 control-label" })
<div class="col-lg-9">
@Html.TextBoxFor(model => model.Published,
new { @class = "form-control datepicker" })
</div>
</div>
<div class="form-group">
<div class="col-lg-8"></div>
<div class="col-lg-3">
@Html.ActionLink("Back to List", "Index",
null, new { @class = "btn btn-default" })
<button class="btn btn-success"
id="btnSubmit" type="submit">
Submit
</button>
</div>
</div>
</div>
}
</div>
</div>
@section scripts
{
<script src="~/Scripts/bootstrap-datepicker.js" type="text/javascript"></script>
<script src="~/Scripts/book-create-edit.js" type="text/javascript"></script>
}
现在运行应用程序并使用 `HttpGet` 请求调用 `CreateEditBook()` 操作方法,然后我们就会看到如图 1.4 所示的用户界面,用于向应用程序添加一本新图书。
图书列表视图
这是应用程序被访问或入口点执行时的第一个视图。它显示了如图 1.5 所示的图书列表。我们在表格格式中显示图书数据,并且在此视图上,我们创建了用于添加新图书、编辑图书、删除图书和查看图书详情的链接。此视图是索引视图,以下是视图的 *Book* 文件夹下 *index.cshtml* 的代码片段。
@model IEnumerable<EF.Core.Data.Book>
@using EF.Web.Models
<div class="book-example panel panel-primary">
<div class="panel-heading panel-head">Books Listing</div>
<div class="panel-body">
<a id="createEditBookModal"
href="@Url.Action("CreateEditBook")" class="btn btn-success">
<span class="glyphicon glyphicon-plus"></span>Book
</a>
<table class="table" style="margin: 4px">
<tr>
<th>
@Html.DisplayNameFor(model => model.Title)
</th>
<th>
@Html.DisplayNameFor(model => model.Author)
</th>
<th>
@Html.DisplayNameFor(model => model.ISBN)
</th>
<th>Action
</th>
<th></th>
</tr>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Author)
</td>
<td>
@Html.DisplayFor(modelItem => item.ISBN)
</td>
<td>
@Html.ActionLink("Edit", "CreateEditBook",
new { id = item.ID }, new { @class = "btn btn-success" }) |
@Html.ActionLink("Details", "DetailBook",
new { id = item.ID }, new { @class = "btn btn-primary" }) |
@Html.ActionLink("Delete", "DeleteBook",
new { id = item.ID }, new { @class = "btn btn-danger" })
</td>
</tr>
}
</table>
</div>
</div>
当我们运行应用程序并使用 `HttpGet` 请求调用 `index()` 操作时,我们就会在如图 1.5 所示的用户界面中看到所有列出的图书。此用户界面提供了 CRUD 操作的选项。
如图所示,每本图书都有一个“编辑”选项,当我们点击“编辑”按钮时,会使用 `HttpGet` 请求调用 `CreateEditBook()` 操作方法,并显示如图 1.6 所示的用户界面。
现在,我们更改输入字段数据并点击提交按钮,然后使用 `HttpPost` 请求调用 `CreateEditBook()` 操作方法,该图书数据将成功更新到数据库中。
图书详情视图
当我们点击图书列表数据中的“详情”按钮时,我们创建一个显示特定图书详情的视图。我们使用 `HttpGet` 请求调用 `DetailBook()` 操作方法,该方法显示一个“`Details`”视图,如图 1.7 所示。因此,我们创建一个名为 `DetailBook` 的视图,以下是 *DetailBook.cshtml* 的代码片段。
@model EF.Core.Data.Book
@{
ViewBag.Title = "Detail Book";
}
<div class="book-example panel panel-primary">
<div class="panel-heading panel-head">Book Detail</div>
<div class="panel-body">
<div class="form-horizontal">
<div class="form-group">
@Html.LabelFor(model => model.Title, new { @class = "col-lg-1 control-label" })
<div class="col-lg-9">
@Html.DisplayFor(model => model.Title, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Author, new { @class = "col-lg-1 control-label" })
<div class="col-lg-9">
@Html.DisplayFor(model => model.Author, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.ISBN, new { @class = "col-lg-1 control-label" })
<div class="col-lg-9">
@Html.DisplayFor(model => model.ISBN, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Published, new { @class = "col-lg-1 control-label" })
<div class="col-lg-9">
@Html.DisplayFor(model => model.Published, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.AddedDate, new { @class = "col-lg-1 control-label" })
<div class="col-lg-9">
@Html.DisplayFor(model => model.AddedDate, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.ModifiedDate, new { @class = "col-lg-1 control-label" })
<div class="col-lg-9">
@Html.DisplayFor(model => model.ModifiedDate, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.IP, new { @class = "col-lg-1 control-label" })
<div class="col-lg-9">
@Html.DisplayFor(model => model.IP, new { @class = "form-control" })
</div>
</div>
@using (Html.BeginForm())
{
<div class="form-group">
<div class="col-lg-1"></div>
<div class="col-lg-9">
@Html.ActionLink("Edit", "CreateEditBook",
new { id = Model.ID }, new { @class = "btn btn-primary" })
@Html.ActionLink("Back to List", "Index",
null, new { @class = "btn btn-success" })
</div>
</div>
}
</div>
</div>
</div>
删除图书
删除图书是本文的最后一个操作。为了删除图书,我们按照以下步骤操作:点击图书列表中的“删除”按钮,然后会显示图书详情视图,询问“您确定要删除此项吗?”。点击删除视图中的“删除”按钮后,会发送一个 `HttpPost` 请求,调用 `ConfirmDeleteBook()` 操作方法,该方法会删除该图书。当我们点击图书列表中的“删除”按钮时,它会发送一个 `HttpGet` 请求,调用 `DeleteBook()` 操作方法,该方法会显示一个删除视图,然后点击该视图中的“删除”按钮,就会发送一个 `HttpPost` 请求,调用 `ConfirmDeleteBook()` 操作方法,该方法会删除该图书。
以下是 *DeleteBook.cshtml* 的代码片段。
@model EF.Core.Data.Book
@{
ViewBag.Title = "Delete Book";
}
<div class="book-example panel panel-primary">
<div class="panel-heading panel-head">Delete Book</div>
<div class="panel-body">
<h3>Are you sure you want to delete this?</h3>
<h1>@ViewBag.ErrorMessage</h1>
<div class="form-horizontal">
<div class="form-group">
@Html.LabelFor(model => model.Title,
new { @class = "col-lg-1 control-label" })
<div class="col-lg-9">
@Html.DisplayFor(model => model.Title,
new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Author,
new { @class = "col-lg-1 control-label" })
<div class="col-lg-9">
@Html.DisplayFor(model => model.Author,
new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.ISBN,
new { @class = "col-lg-1 control-label" })
<div class="col-lg-9">
@Html.DisplayFor(model => model.ISBN,
new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Published,
new { @class = "col-lg-1 control-label" })
<div class="col-lg-9">
@Html.DisplayFor(model => model.Published,
new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.AddedDate,
new { @class = "col-lg-1 control-label" })
<div class="col-lg-9">
@Html.DisplayFor(model => model.AddedDate,
new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.ModifiedDate,
new { @class = "col-lg-1 control-label" })
<div class="col-lg-9">
@Html.DisplayFor(model => model.ModifiedDate,
new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.IP,
new { @class = "col-lg-1 control-label" })
<div class="col-lg-9">
@Html.DisplayFor(model => model.IP,
new { @class = "form-control" })
</div>
</div>
@using (Html.BeginForm())
{
<div class="form-group">
<div class="col-lg-1"></div>
<div class="col-lg-9">
<input type="submit" value="Delete"
class="btn btn-danger" />
@Html.ActionLink("Back to List", "Index",
null, new { @class = "btn btn-success" })
</div>
</div>
}
</div>
</div>
</div>
结论
本文介绍了带有工作单元的通用存储库模式。我在这个应用程序的用户界面设计中使用了 Bootstrap CSS 和 JavaScript。您对这篇文章有什么看法?请提供您的反馈。