生成具有业务规则验证功能的模型

Microsoft

下载 PDF

这是免费 “NerdDinner”应用程序教程 的步骤 3,该教程介绍如何使用 ASP.NET MVC 1 生成小型但完整的 Web 应用程序。

步骤 3 演示如何创建可用于查询和更新 NerdDinner 应用程序的数据库的模型。

如果使用 ASP.NET MVC 3,建议遵循入门与 MVC 3MVC 音乐商店教程。

NerdDinner 步骤 3:生成模型

在模型-视图-控制器框架中,术语“模型”是指表示应用程序数据的对象,以及与之集成验证和业务规则的相应域逻辑。 该模型在很多方面都是基于 MVC 的应用程序的“核心”,我们稍后将看到它的行为。

ASP.NET MVC 框架支持使用任何数据访问技术,开发人员可以从各种丰富的 .NET 数据选项中进行选择来实现其模型,包括:LINQ to Entities、LINQ to SQL、NHibernate、LLBLGen Pro、SubSonic、WilsonORM 或仅原始 ADO.NET DataReaders 或数据集。

对于 NerdDinner 应用程序,我们将使用 LINQ to SQL 创建一个与数据库设计非常接近的简单模型,并添加一些自定义验证逻辑和业务规则。 然后,我们将实现一个存储库类,该类可帮助从应用程序的其余部分提取数据持久性实现,并使我们能够轻松地对其进行单元测试。

LINQ to SQL

LINQ to SQL 是作为 .NET 3.5 的一部分附带的 ORM (对象关系映射器) 。

LINQ to SQL 提供了一种将数据库表映射到可对其进行编码的 .NET 类的简单方法。 对于 NerdDinner 应用程序,我们将使用它将数据库中的 Dinners 和 RSVP 表映射到 Dinner 和 RSVP 类。 Dinners 和 RSVP 表的列将对应于 Dinner 和 RSVP 类的属性。 每个 Dinner 和 RSVP 对象将代表数据库中 Dinners 或 RSVP 表中的一个单独的行。

LINQ to SQL 允许我们避免手动构造 SQL 语句以使用数据库数据检索和更新 Dinner 和 RSVP 对象。 相反,我们将定义 Dinner 和 RSVP 类、它们如何映射到数据库以及它们之间的关系。 然后,LINQ to SQL 将负责生成相应的 SQL 执行逻辑,以便我们在运行时交互和使用它们。

我们可以在 VB 和 C# 中使用 LINQ 语言支持来编写从数据库中检索 Dinner 和 RSVP 对象的表达性查询。 这最大限度地减少了需要编写的数据代码量,并使我们能够构建真正干净的应用程序。

将 LINQ to SQL 类添加到项目中

首先,右键单击项目中的“模型”文件夹,然后选择“ 添加新>项 ”菜单命令:

“模型”文件夹的屏幕截图。突出显示了新项。突出显示并选中了模型。

此时会显示“添加新项”对话框。 我们将按“数据”类别进行筛选,并在其中选择“LINQ to SQL 类”模板:

“添加新项”对话框的屏幕截图。突出显示了数据。L I N Q 到 S Q L 类已选中并突出显示。

我们将项目命名为“NerdDinner”,然后单击“添加”按钮。 Visual Studio 将在 \Models 目录下添加 NerdDinner.dbml 文件,然后打开 LINQ to SQL 对象关系设计器:

Visual Studio 中“Nerd 晚餐”对话框的屏幕截图。已选择 Nerd Dinner 点 d b m l 文件。

使用 LINQ to SQL 创建数据模型类

LINQ to SQL 使我们能够从现有数据库架构快速创建数据模型类。 为此,我们将在服务器资源管理器中打开 NerdDinner 数据库,然后选择要在其中建模的表:

服务器资源管理器的屏幕截图。表已展开。突出显示了晚餐和 R S V P。

然后,我们可以将表拖到 LINQ to SQL 设计器图面上。 执行此操作时,LINQ to SQL 将使用表的架构自动创建 Dinner 和 RSVP 类 (,类属性映射到数据库表列) :

“书呆子晚餐”对话框的屏幕截图。显示 Dinner 和 R S V P 类。

默认情况下,LINQ to SQL 设计器在基于数据库架构创建类时会自动“复数”表和列名。 例如:上述示例中的“Dinners”表生成了“Dinner”类。 此类命名有助于使我们的模型与 .NET 命名约定保持一致,我通常发现让设计器修复此问题很方便 (尤其是在) 添加大量表时。 但是,如果不喜欢设计器生成的类或属性的名称,则始终可以重写它并将其更改为所需的任何名称。 可以通过在设计器中内联编辑实体/属性名称或通过属性网格对其进行修改来执行此操作。

默认情况下,LINQ to SQL 设计器还会检查表的主键/外键关系,并基于它们自动创建它创建的不同模型类之间的默认“关系关联”。 例如,当我们将 Dinners 和 RSVP 表拖到 LINQ to SQL 设计器上时,根据 RSVP 表对 Dinners 表具有外键这一事实推断出两者之间的一对多关系关联, (设计器中的箭头) 指示这一点:

晚餐和 R S V P 表的屏幕截图。突出显示了一个箭头,指向 Dinner 属性树和 R S V P 属性树。

上述关联将导致 LINQ to SQL 向 RSVP 类添加强类型“Dinner”属性,开发人员可以使用该属性访问与给定 RSVP 关联的 Dinner。 它还会导致 Dinner 类具有“RSVP”集合属性,使开发人员能够检索和更新与特定 Dinner 关联的 RSVP 对象。

下面,当我们创建新的 RSVP 对象并将其添加到 Dinner 的 RSVP 集合时,可以看到 Visual Studio 中的智能感知示例。 请注意 LINQ to SQL 如何自动在 Dinner 对象上添加“RSVP”集合:

Visual Studio 中 intellisense 的屏幕截图。突出显示了 R S V Ps。

通过将 RSVP 对象添加到 Dinner 的 RSVP 集合,我们告诉 LINQ to SQL 将 Dinner 与数据库中的 RSVP 行之间的外键关系相关联:

R S V P 对象和 Dinner 的 R S V P 集合的屏幕截图。

如果不喜欢设计器对表关联进行建模或命名的方式,可以重写它。 只需单击设计器中的关联箭头,并通过属性网格访问其属性即可重命名、删除或修改它。 不过,对于 NerdDinner 应用程序,默认关联规则适用于要生成的数据模型类,并且只能使用默认行为。

NerdDinnerDataContext 类

Visual Studio 将自动创建表示使用 LINQ to SQL 设计器定义的模型和数据库关系的 .NET 类。 还会为添加到解决方案的每个 LINQ to SQL 设计器文件生成 LINQ to SQL DataContext 类。 由于我们将 LINQ to SQL 类项命名为“NerdDinner”,因此创建的 DataContext 类将称为“NerdDinnerDataContext”。 此 NerdDinnerDataContext 类是我们与数据库交互的主要方式。

我们的 NerdDinnerDataContext 类公开了两个属性-“Dinners”和“RSVP”,这两个属性表示我们在数据库中建模的两个表。 可以使用 C# 针对这些属性编写 LINQ 查询,以便从数据库中查询和检索 Dinner 和 RSVP 对象。

以下代码演示如何实例化 NerdDinnerDataContext 对象,并对其执行 LINQ 查询以获取将来发生的 Dinners 序列。 Visual Studio 在编写 LINQ 查询时提供完整的 intellisense,并且从它返回的对象是强类型且还支持 intellisense:

Visual Studio 的屏幕截图。突出显示了说明。

除了允许我们查询 Dinner 和 RSVP 对象之外,NerdDinnerDataContext 还会自动跟踪我们随后对 Dinner 和 RSVP 对象所做的任何更改,通过它检索。 我们可以使用此功能轻松地将更改保存回数据库 ,而无需编写任何显式 SQL 更新代码。

例如,下面的代码演示如何使用 LINQ 查询从数据库中检索单个 Dinner 对象,更新两个 Dinner 属性,然后将更改保存回数据库:

NerdDinnerDataContext db = new NerdDinnerDataContext();

// Retrieve Dinner object that reprents row with DinnerID of 1
Dinner dinner = db.Dinners.Single(d => d.DinnerID == 1);

// Update two properties on Dinner 
dinner.Title = "Changed Title";
dinner.Description = "This dinner will be fun";

// Persist changes to database
db.SubmitChanges();

上述代码中的 NerdDinnerDataContext 对象自动跟踪从中检索到的 Dinner 对象的属性更改。 调用“SubmitChanges () ”方法时,它将对数据库执行相应的 SQL“UPDATE”语句,以保留更新的值。

创建 DinnerRepository 类

对于小型应用程序,有时最好让控制器直接针对 LINQ to SQL DataContext 类工作,并在控制器中嵌入 LINQ 查询。 但是,随着应用程序变大,此方法的维护和测试变得很麻烦。 它还可能导致我们在多个位置复制相同的 LINQ 查询。

使应用程序更易于维护和测试的一种方法是使用“存储库”模式。 存储库类有助于封装数据查询和持久性逻辑,并从应用程序中抽象化数据暂留的实现详细信息。 除了使应用程序代码更简洁外,使用存储库模式还可以更轻松地在将来更改数据存储实现,并且有助于在不需要真实数据库的情况下对应用程序进行单元测试。

对于 NerdDinner 应用程序,我们将使用以下签名定义 DinnerRepository 类:

public class DinnerRepository {

    // Query Methods
    public IQueryable<Dinner> FindAllDinners();
    public IQueryable<Dinner> FindUpcomingDinners();
    public Dinner             GetDinner(int id);

    // Insert/Delete
    public void Add(Dinner dinner);
    public void Delete(Dinner dinner);

    // Persistence
    public void Save();
}

注意:在本章的后面部分,我们将从此类中提取 IDinnerRepository 接口,并在控制器上使用它启用依赖项注入。 不过,首先,我们将从简单开始,直接使用 DinnerRepository 类。

若要实现此类,我们将右键单击“模型”文件夹,然后选择 “添加新>项 ”菜单命令。 在“添加新项”对话框中,我们将选择“类”模板并将文件命名为“DinnerRepository.cs”:

“模型”文件夹的屏幕截图。“添加新项”突出显示。

然后,我们可以使用以下代码实现 DinnerRepository 类:

public class DinnerRepository {
 
    private NerdDinnerDataContext db = new NerdDinnerDataContext();

    //
    // Query Methods

    public IQueryable<Dinner> FindAllDinners() {
        return db.Dinners;
    }

    public IQueryable<Dinner> FindUpcomingDinners() {
        return from dinner in db.Dinners
               where dinner.EventDate > DateTime.Now
               orderby dinner.EventDate
               select dinner;
    }

    public Dinner GetDinner(int id) {
        return db.Dinners.SingleOrDefault(d => d.DinnerID == id);
    }

    //
    // Insert/Delete Methods

    public void Add(Dinner dinner) {
        db.Dinners.InsertOnSubmit(dinner);
    }

    public void Delete(Dinner dinner) {
        db.RSVPs.DeleteAllOnSubmit(dinner.RSVPs);
        db.Dinners.DeleteOnSubmit(dinner);
    }

    //
    // Persistence 

    public void Save() {
        db.SubmitChanges();
    }
}

使用 DinnerRepository 类检索、更新、插入和删除

创建 DinnerRepository 类后,让我们看一些代码示例,这些代码示例演示了我们可以使用它执行的常见任务:

查询示例

下面的代码使用 DinnerID 值检索单个 Dinner:

DinnerRepository dinnerRepository = new DinnerRepository();

// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);

下面的代码将检索所有即将到来的晚餐,并循环访问它们:

DinnerRepository dinnerRepository = new DinnerRepository();

// Retrieve all upcoming Dinners
var upcomingDinners = dinnerRepository.FindUpcomingDinners();

// Loop over each upcoming Dinner and print out its Title
foreach (Dinner dinner in upcomingDinners) {
   Response.Write("Title" + dinner.Title);
}

插入和更新示例

下面的代码演示如何添加两个新晚餐。 在数据库上调用“Save () ”方法之前,不会向数据库提交对存储库的添加/修改。 LINQ to SQL 会自动包装数据库事务中的所有更改 ,因此,在我们的存储库保存时,要么发生所有更改,要么不执行任何更改:

DinnerRepository dinnerRepository = new DinnerRepository();

// Create First Dinner
Dinner newDinner1 = new Dinner();
newDinner1.Title = "Dinner with Scott";
newDinner1.HostedBy = "ScotGu";
newDinner1.ContactPhone = "425-703-8072";

// Create Second Dinner
Dinner newDinner2 = new Dinner();
newDinner2.Title = "Dinner with Bill";
newDinner2.HostedBy = "BillG";
newDinner2.ContactPhone = "425-555-5151";

// Add Dinners to Repository
dinnerRepository.Add(newDinner1);
dinnerRepository.Add(newDinner2);

// Persist Changes
dinnerRepository.Save();

下面的代码检索现有的 Dinner 对象,并修改其上的两个属性。 在存储库上调用“Save () ”方法时,更改将提交回数据库:

DinnerRepository dinnerRepository = new DinnerRepository();

// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);

// Update Dinner properties
dinner.Title = "Update Title";
dinner.HostedBy = "New Owner";

// Persist changes
dinnerRepository.Save();

下面的代码检索晚餐,然后向其中添加 RSVP。 它使用 LINQ to SQL 为我们 (创建的 Dinner 对象上的 RSVP 集合执行此操作,因为数据库) 中的两者之间存在主键/外键关系。 在存储库上调用“Save () ”方法时,此更改将作为新的 RSVP 表行保留回数据库:

DinnerRepository dinnerRepository = new DinnerRepository();

// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);

// Create a new RSVP object
RSVP myRSVP = new RSVP();
myRSVP.AttendeeName = "ScottGu";

// Add RSVP to Dinner's RSVP Collection
dinner.RSVPs.Add(myRSVP);

// Persist changes
dinnerRepository.Save();

删除示例

下面的代码检索现有的 Dinner 对象,然后将其标记为要删除。 在存储库上调用“Save () ”方法时,它会将删除内容提交回数据库:

DinnerRepository dinnerRepository = new DinnerRepository();

// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);

// Mark dinner to be deleted
dinnerRepository.Delete(dinner);

// Persist changes
dinnerRepository.Save();

将验证和业务规则逻辑与模型类集成

集成验证和业务规则逻辑是处理数据的任何应用程序的关键部分。

架构验证

使用 LINQ to SQL 设计器定义模型类时,数据模型类中属性的数据类型对应于数据库表的数据类型。 例如:如果 Dinners 表中的“EventDate”列是“datetime”,则 LINQ to SQL 创建的数据模型类的类型为“DateTime”, (它是内置的 .NET 数据类型) 。 这意味着,如果尝试从代码中为其分配整数或布尔值,则会收到编译错误;如果尝试在运行时将无效字符串类型隐式转换为该字符串类型,则会自动引发错误。

使用字符串时,LINQ to SQL 还会自动处理转义 SQL 值 ,这有助于在使用它时防止 SQL 注入攻击。

验证和业务规则逻辑

架构验证作为第一步很有用,但很少足够。 大多数实际方案都需要能够指定更丰富的验证逻辑,这些逻辑可以跨越多个属性、执行代码,并且通常能够感知模型的状态 (例如:创建/更新/删除模型,或在域特定的状态(如“archived”) )。 有各种不同的模式和框架可用于定义验证规则并将其应用于模型类,并且有几个基于 .NET 的框架可用于帮助执行此操作。 可以在 ASP.NET MVC 应用程序中使用其中几乎任何一个。

对于 NerdDinner 应用程序,我们将使用相对简单且直接的模式,其中我们在 Dinner 模型对象上公开 IsValid 属性和 GetRuleViolations () 方法。 IsValid 属性将返回 true 或 false,具体取决于验证和业务规则是否全部有效。 GetRuleViolations () 方法将返回任何规则错误的列表。

我们将通过向项目添加“分部类”,为 Dinner 模型实现 IsValid 和 GetRuleViolations () 。 分部类可用于向 VS 设计器维护的类添加方法/属性/事件, (例如 LINQ to SQL 设计器) 生成的 Dinner 类,并帮助避免工具弄乱代码。 可以通过右键单击 \Models 文件夹,然后选择“添加新项”菜单命令,将新的分部类添加到项目。 然后,我们可以在“添加新项”对话框中选择“类”模板,并将其命名为 Dinner.cs。

“模型”文件夹的屏幕截图。“添加新项”处于选中状态。晚餐点 c s 写入“添加新项”对话框中。

单击“添加”按钮会将 Dinner.cs 文件添加到项目并在 IDE 中打开它。 然后,我们可以使用以下代码实现基本的规则/验证强制框架:

public partial class Dinner {

    public bool IsValid {
        get { return (GetRuleViolations().Count() == 0); }
    }

    public IEnumerable<RuleViolation> GetRuleViolations() {
        yield break;
    }

    partial void OnValidate(ChangeAction action) {
        if (!IsValid)
            throw new ApplicationException("Rule violations prevent saving");
    }
}

public class RuleViolation {

    public string ErrorMessage { get; private set; }
    public string PropertyName { get; private set; }

    public RuleViolation(string errorMessage, string propertyName) {
        ErrorMessage = errorMessage;
        PropertyName = propertyName;
    }
}

有关上述代码的一些说明:

  • Dinner 类的前面有一个“部分”关键字 (keyword) –这意味着它中包含的代码将与 LINQ to SQL 设计器生成/维护的类组合在一起,并编译为单个类。
  • RuleViolation 类是我们将添加到项目的帮助程序类,用于提供有关规则冲突的更多详细信息。
  • Dinner.GetRuleViolations () 方法会导致验证和业务规则 (我们将很快) 实现它们。 然后,它返回一系列 RuleViolation 对象,这些对象提供有关任何规则错误的更多详细信息。
  • Dinner.IsValid 属性提供了一个方便的帮助程序属性,该属性指示 Dinner 对象是否具有任何活动 RuleViolations。 开发人员可以随时使用 Dinner 对象 (对其进行主动检查,并且不会) 引发异常。
  • Dinner.OnValidate () 分部方法是 LINQ to SQL 提供的挂钩,它允许我们在每当 Dinner 对象即将保留在数据库中时收到通知。 上述 OnValidate () 实现可确保 Dinner 在保存之前没有 RuleViolation。 如果它处于无效状态,则会引发异常,这将导致 LINQ to SQL 中止事务。

此方法提供了一个简单的框架,我们可以将验证和业务规则集成到中。 现在,让我们将以下规则添加到 GetRuleViolations () 方法:

public IEnumerable<RuleViolation> GetRuleViolations() {

    if (String.IsNullOrEmpty(Title))
        yield return new RuleViolation("Title required","Title");

    if (String.IsNullOrEmpty(Description))
        yield return new RuleViolation("Description required","Description");

    if (String.IsNullOrEmpty(HostedBy))
        yield return new RuleViolation("HostedBy required", "HostedBy");

    if (String.IsNullOrEmpty(Address))
        yield return new RuleViolation("Address required", "Address");

    if (String.IsNullOrEmpty(Country))
        yield return new RuleViolation("Country required", "Country");

    if (String.IsNullOrEmpty(ContactPhone))
        yield return new RuleViolation("Phone# required", "ContactPhone");

    if (!PhoneValidator.IsValidNumber(ContactPhone, Country))
        yield return new RuleViolation("Phone# does not match country", "ContactPhone");

    yield break;
}

我们使用 C# 的“yield return”功能返回任何 RuleViolation 的序列。 上面的前六个规则检查只是强制要求 Dinner 上的字符串属性不能为 null 或空。 最后一个规则更有趣一点,并调用 PhoneValidator.IsValidNumber () 帮助程序方法,我们可以添加到项目,以验证 ContactPhone 号码格式是否与 Dinner 的国家/地区匹配。

可以使用 。NET 的正则表达式支持来实现此电话验证支持。 下面是一个简单的 PhoneValidator 实现,我们可以添加到我们的项目,使我们能够添加特定于国家/地区的正则表达式模式检查:

public class PhoneValidator {

    static IDictionary<string, Regex> countryRegex = new Dictionary<string, Regex>() {
           { "USA", new Regex("^[2-9]\\d{2}-\\d{3}-\\d{4}$")},
           { "UK", new Regex("(^1300\\d{6}$)|(^1800|1900|1902\\d{6}$)|(^0[2|3|7|8]{1}[0-9]{8}$)|(^13\\d{4}$)|(^04\\d{2,3}\\d{6}$)")},
           { "Netherlands", new Regex("(^\\+[0-9]{2}|^\\+[0-9]{2}\\(0\\)|^\\(\\+[0-9]{2}\\)\\(0\\)|^00[0-9]{2}|^0)([0-9]{9}$|[0-9\\-\\s]{10}$)")},
    };

    public static bool IsValidNumber(string phoneNumber, string country) {

        if (country != null && countryRegex.ContainsKey(country))
            return countryRegex[country].IsMatch(phoneNumber);
        else
            return false;
    }

    public static IEnumerable<string> Countries {
        get {
            return countryRegex.Keys;
        }
    }
}

处理验证和业务逻辑冲突

添加上述验证和业务规则代码后,每当尝试创建或更新 Dinner 时,都会评估并强制执行验证逻辑规则。

开发人员可以编写如下所示的代码来主动确定 Dinner 对象是否有效,并在不引发任何异常的情况下检索其中所有冲突的列表:

Dinner dinner = dinnerRepository.GetDinner(5);

dinner.Country = "USA";
dinner.ContactPhone = "425-555-BOGUS";

if (!dinner.IsValid) {

    var errors = dinner.GetRuleViolations();
    
    // do something to fix the errors
}

如果尝试将 Dinner 保存在无效状态,则会在 DinnerRepository 上调用 Save () 方法时引发异常。 这是因为 LINQ to SQL 在保存 Dinner 的更改之前自动调用 Dinner.OnValidate () 分部方法,并且我们在 Dinner.OnValidate () 中添加了代码,以便在 Dinner 中存在任何规则冲突时引发异常。 我们可以捕获此异常,并被动地检索要修复的冲突列表:

Dinner dinner = dinnerRepository.GetDinner(5);

try {

    dinner.Country = "USA";
    dinner.ContactPhone = "425-555-BOGUS";

    dinnerRepository.Save();
}
catch {

    var errors = dinner.GetRuleViolations();

    // do something to fix errors
}

由于验证和业务规则是在模型层中实现的,而不是在 UI 层中实现的,因此它们将在应用程序中的所有方案中应用和使用。 我们稍后可以更改或添加业务规则,并让与 Dinner 对象一起使用的所有代码都遵循这些规则。

能够灵活地在一个位置更改业务规则,而不会让这些更改在整个应用程序和 UI 逻辑中产生波纹,是编写良好的应用程序的标志,也是 MVC 框架所鼓励的一个好处。

下一步

现在,我们有了一个可用于查询和更新数据库的模型。

现在,让我们向项目添加一些控制器和视图,我们可以使用这些控制器和视图围绕该项目构建 HTML UI 体验。