65.9K
CodeProject 正在变化。 阅读更多。
Home

Signum Framework 教程 第一部分 – Southwind 实体

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (12投票s)

2011年7月12日

LGPL3

15分钟阅读

viewsIcon

43256

downloadIcon

2081

本教程专注于使用 Signum Framework 编写实体。Signum Framework 是一个支持 LINQ 的 Windows/Web 框架,用于编写数据驱动型应用程序。

Signum 框架教程 

内容 

引言

Signum Framework 是一个用于开发实体为中心的 N 层应用程序的开源框架。核心是 Signum.Engine,它是一个带有**完整 LINQ 提供程序**的 ORM,在客户端-服务器(WPF & WCF)和 Web 应用程序(ASP.Net MVC)中都能很好地工作。

Signum Framework 专注于简化“**可组合垂直模块**”的创建,这些模块可以在许多应用程序中使用,并鼓励通过函数式编程和范围模式来实现干净简单的代码。

如果您想了解更多关于 Signum Framework 的独特性,请查看上一篇教程

关于 Signum 框架 2.0 

 

我们很高兴地宣布,我们终于发布了 Signum Framework 2.0 。

这个版本的发布比预期花费了更多时间,但结果也更加雄心勃勃。我们一直在日常使用我们的框架,并认为这个版本已经完成,并将让所有勇于使用它的人感到高兴。

新版本专注于不同的趋势:

  • Signum.Web: 基于 ASP.Net MVC 3.0 和 Razor,力求保持与 Signum.Windows 相同的感受和生产力,同时又不限制 Web 的可能性(jQuery、Ajax、标记控制、友好 URL……)。
  • 跟进最新技术:该框架现在仅在 .Net 4.0/ ASP.Net MVC 3.0 上运行,并且代码片段和模板针对 Visual Studio 2010。
  • 全面改进和修复 bug:完整列表请查看更新日志 http://www.signumframework.com/ChangeLog2.0.ashx。

关于本系列

为了展示框架的功能并加深对架构的理解,我们正在准备一系列教程,其中我们将使用一个稳定的应用程序:Southwind。

Southwind 是 Northwind 的 Signum 版本,Northwind 是 Microsoft SQL Server 提供的著名示例数据库。

在本系列教程中,我们将创建整个应用程序,包括实体、业务逻辑、Windows (WPF) 和 Web (MVC) 用户界面、数据加载以及任何其他值得解释的方面。

如果您想了解更多关于 Signum Framework 原理的内容,请查看上一篇教程

现在是时候动手编写实体了。

安装 

Signum Framework 是开源的(LGPL),可以在 codeplex 免费下载,源代码也可在 github 获取。 

安装程序将复制以下内容:

  • 程序集:{Program files}\Signum Software\Signum Framework 2.0
  • 代码片段:{Documents}\Visual Studio 2010\Code Snippets\Visual C#\My Code Snippets
  • 项目模板:{Documents}\Visual Studio 2010\Templates\ProjectTemplates\Visual C#
  • WPF 控件模板:{Documents}\Visual Studio 2010\Templates\ItemTemplates\Visual C#
  • ASP.Net MVC 3 Razor 控件模板:{Program Files}\Microsoft Visual Studio 10.0\Common7\IDE\ ItemTemplates\CSharp\Web\MVC 3\CodeTemplates\AddView\CSHTML

安装完成后,打开 Visual Studio 2010,并创建一个新项目。在 Visual C#/.Net Framework 4 下,有一个新的 Signum Framework 2.0 Client – Server 模板。创建一个名为 Southwind 的新项目,应该会创建一个包含 5 个项目的解决方案:

  • Southwind.Entities
  • Southwind.Load
  • Southwind.Logic
  • Southwind.Web
  • Southwind.Windows

暂时卸载所有项目,只保留 Southwind.Entities,然后打开 MyEntity.cs。

创建实体

Signum Framework 提倡“代码优先”的方法,您编写的实体会直接映射到数据库表。此外,由于 100% 的 SQL 查询(包括数据库模式修改)都由框架生成,您几乎可以忘记 SQL Management Studio。

然而,为了教学方便,我们将从一个熟悉的图表中展示我们的目标。这是 Northwind 数据库:

我们的第一个实体:Region

我们从简单的开始。要创建 Region 实体,我们需要继承自IdentifiableEntity类并创建描述字段。

我们将它命名为Description,而不是RegionDescription,因为这种冗余仅在手动编写 SQL 时有意义(此处不是)。

另外,我们不必担心 RegionID,因为**每个 IdentifiableEntity 都已经自带 Id 和 ToStr 字段/属性**。

我们已经安装了代码片段,所以只需删除 MyEntityDN 类并按下某个组合键(此处省略了具体按键操作)。

entityWithName [Tab] [Tab] Region [Tab] description [Tab] Description [Enter]
    

完成此操作后,我们应该会看到类似这样的内容:

[Serializable]
public class RegionDN : Entity
{
    [NotNullable, SqlDbType(Size = 100), UniqueIndex]
    string description;
    [StringLengthValidator(AllowNulls = false, Min = 3, Max = 100)]
    public string Description
    {
        get { return description; }
        set { SetToStr(ref description, value, () => Description); }
    }

    public override string ToString()
    {
        return description;
    }
}
    

这是我们的实体类,让我们来分析一下: 

该类是Serializable的,因此您可以将它通过 Web 服务发送或保存在文件中。

它还继承自Entity以启用并发支持。由于此实体可能会被管理员时不时地更改,我们可以改为继承自 IdentifiableEntity 并保存继承的Tick列。 

最后,请注意,我们用单数形式书写实体的名称,以及表的名称。 

字段 

有一个description字段。在 Signum Framework 实体中,**每个字段都会生成一个数据库列**。

该列将尽可能匹配 CLR 类型,因此对于字符串,它将是可空的,并且默认长度为 200 个字符。

字段上的属性会覆盖其中一些默认设置,在本例中,使列不可为空且长度为 100。

仅通过添加UniqueIndex属性,就会在列上创建一个索引。

属性

每个属性都允许用户界面和业务逻辑访问底层字段。

为了在查询中使用该属性,**必须有一个同名但小写的字段**。不幸的是,VS 代码片段不够智能,因此您必须重复两次名称。

通过用一些ValidationAttributes修饰属性,我们可以强制执行实体的简单验证规则。有更灵活的验证选项,并且可以覆盖这些属性。 

此外,我们可以用其他注解来修饰我们的属性,以更改显示名称(DescriptionAttribute)、数字或日期的格式(FormatAttribute)、值的单位(UnitAttribute)等等……

最后,我们可以看到属性的 getter 是简单的,但是 setter 调用了一个受保护的Set方法。这个方法启用了更改跟踪,并执行以下操作:

  • 将字段值设置为新值。
  • 将实体标记为已修改(脏),因此在保存时不会被跳过。
  • 触发 PropertyChanged 事件,以便用户界面(如果存在)得到更新。
  • 如果值已更改则返回 true,如果值相同则返回 false。

带外键的实体:Territory

现在让我们继续处理 Territory。

entityWithName [Tab] [Tab] Territory [Tab] description [Tab] Description [Enter]

我们将基类也更改为IdentifiableEntity,因此TerritoryID是免费的,但我们必须创建 RegionID 列和外键。很简单,只需创建一个 Region 类型的属性:

field [Tab] [Tab] RegionDN [Tab] region [Tab] Region [Enter] 

Territory 属性是必需的,所以让我们在属性上添加一个NotNullValidator。结果应该像这样:

[Serializable]
public class TerritoryDN : Entity
{
    RegionDN region;
    [NotNullValidator]
    public RegionDN Region
    {
        get { return region; }
        set { Set(ref region, value, () => Region); }
    }

    [NotNullable, SqlDbType(Size = 100), UniqueIndex]
    string description;
    [StringLengthValidator(AllowNulls = false, Min = 3, Max = 100)]
    public string Description
    {
        get { return description; }
        set { SetToStr(ref description, value, () => Description); }
    }

    public override string ToString()
    {
        return description;
    }
}

由于RegionDN是一个IdentifiableEntity,框架已经知道它需要为您创建一个idRegion列和一个外键。没有简单的方法可以访问这个底层列(除了通过territory.Region.Id),但处理 ID 是不必要的且容易出错的。 对于 Signum Framework 来说,外键只是一个实现细节。 

 

下一步应该是EmployeeTerritories关联表,但这个表不是一个‘Entity’,而是EmployeesTerritories之间的多对多关系。在 Signum Framework 中,这由Employee实体上的MList<TerritoryDN>字段表示。

在处理Employee表之前,我们可以看到某些字段(AddressCityRegionPostalCodeCountry)也重复出现在CustomersOrdersSupplier表中。 

嵌入式实体:Address 

要创建一个“属于”父表的地址实体,而不是拥有自己的地址,我们只需让它继承自EmbeddedEntity

entity [Tab] [Tab] Address [Enter]
fieldString [Tab] [Tab] address [Tab] Address [Enter] 
fieldString [Tab] [Tab] city [Tab] City [Enter] 
fieldString [Tab] [Tab] region [Tab] Region [Enter] 
fieldString [Tab] [Tab] postalCode [Tab] PostalCode [Enter] 
fieldString [Tab] [Tab] county [Tab] Country [Enter]

现在让我们将基类更改为EmbeddedEntity并对字段大小(以及相应的验证器)进行一些更改,以模仿 Northwind 数据库。

[Serializable]
public class AddressDN : EmbeddedEntity
{
    [NotNullable, SqlDbType(Size = 60)]
    string address;
    [StringLengthValidator(AllowNulls = false, Min = 3, Max = 60)]
    public string Address
    {
        get { return address; }
        set { Set(ref address, value, () => Address); }
    }

    [NotNullable, SqlDbType(Size = 15)]
    string city;
    [StringLengthValidator(AllowNulls = false, Min = 3, Max = 15)]
    public string City
    {
        get { return city; }
        set { Set(ref city, value, () => City); }
    }

    [NotNullable, SqlDbType(Size = 15)]
    string region;
    [StringLengthValidator(AllowNulls = false, Min = 3, Max = 15)]
    public string Region
    {
        get { return region; }
        set { Set(ref region, value, () => Region); }
    }

    [NotNullable, SqlDbType(Size = 10)]
    string postalCode;
    [StringLengthValidator(AllowNulls = false, Min = 3, Max = 10)]
    public string PostalCode
    {
        get { return postalCode; }
        set { Set(ref postalCode, value, () => PostalCode); }
    }

    [NotNullable, SqlDbType(Size = 15)]
    string country;
    [StringLengthValidator(AllowNulls = false, Min = 3, Max = 15)]
    public string Country
    {
        get { return country; }
        set { Set(ref country, value, () => Country); }
    }
}

体量增大:Employee

现在事情变得有趣了,Employee 是 Northwind 中最大的表之一,并包含一些新方面:

  • 一个Address字段,类型为AddressDN,包含我们新的EmbeddedEntity的字段。
  • 一个MList<territory>这将创建关联表。
  • 一个指向自身的关联,以表示Employee层级。
  • 一堆新的值类型,如DateTimeDateTime?(可空)、用于照片的byte[],以及一个无限长度的string(notes)。

通过以下击键操作,我们应该可以在几秒钟内写出实体基础:

entity [Tab] [Tab] Employee [Enter]
fieldString [Tab] [Tab] lastName [Tab] LastName[Enter] 
fieldString [Tab] [Tab] firstName [Tab] FirstNaame [Enter] 
fieldString [Tab] [Tab] title [Tab] Title[Enter] 
fieldString [Tab] [Tab] titleOfCourtesy [Tab] TitleOfCourtesy [Enter] 
field [Tab] [Tab] DateTime? [Tab] birthDate [Tab] BirthDate [Enter]
field [Tab] [Tab] DateTime? [Tab] hireDate [Tab] HireDate [Enter]
field [Tab] [Tab] AddressDN [Tab] address [Tab] Address [Enter]
fieldString [Tab] [Tab] homePhone [Tab] HomePhone [Enter] 
fieldString [Tab] [Tab] extension [Tab] Extension [Enter] 
field [Tab] [Tab] byte[] [Tab] photo [Tab] Photo [Enter]
fieldString [Tab] [Tab] notes [Tab] Notes [Enter] 
field [Tab] [Tab] Lite<EmployeeDN> [Tab] reportsTo [Tab] ReportsTo [Enter]
fieldString [Tab] [Tab] photoPath [Tab] PhotoPath [Enter] 
field [Tab] [Tab] MList<TerritoryDN> [Tab] territories [Tab] Territories [Enter]

这次继承自 Entity 也没问题。

 

字符串字段  

 

让我们手动设置所有字符串字段的大小,并在必要时通过移除字段上的NotNullable来放宽可空性,并设置StringLengthValidator上的AllowNulls = true

 

NVarChar(MAX) 字段  

对于长字符串字段,如‘note’,我们可以通过属性覆盖类型为 NText 类型:

SqlDbType(SqlDbType=SqlDbType.NText)

但由于 NText 已被弃用,而推荐使用NVarChar(MAX),我们将在此使用该属性:

SqlDbType(Size = int.MaxValue)

框架能够理解int.MaxValue并将其替换为NVarChar(MAX)。 

其他字符串字段,如 homePhone 和 extension,为我们提供了使用TelephoneValidator的机会。 

 

DateTime 字段 

请注意,对于DateTime字段,当它不是必需的时,我们仅将其字段类型设为可空。

此外,我们可以放置一个方便的验证器来限制我们的日期: 

[DateTimePrecissionValidator(DateTimePrecision.Minutes)]

这样我们就避免了由于秒、毫秒等引起的舍入误差。 

请注意,所有ValidatorAttributes根据约定都接受 null 值,因此为了防止 null 值,您还需要一个NotNullValidator 

 

VarBinary(MAX) 字段 

就像我们对notes字段所做的那样。对于photo字段也同样适用。默认情况下,byte[]字段被转换为VarBinary。而不是使用SqlDbType.Image(也已弃用),我们将使用Size = int.MaxValue将其设置为VarBinary(MAX)

EmbeddedEntity 字段 

类型为AddressDNaddress字段将包含形式为Address_AddressAddress_CityAddress_Region……的所有相应列。

此外,为了表示地址实体本身是 null(而不是其内部字段的某些值),将创建一个新的Address_HasValue字段,内部字段的类型将被覆盖以支持 null 值。

我们可以通过在address 字段上添加[NotNullable]来禁用此功能,但在本例中这是可以的。

Lite 字段 

Lite<T>是一个泛型类,它创建一个对实体的轻量级引用。在运行时,它只包含TypeIdToStr字段,并可在您的实体中使用以控制延迟加载。

然而,在数据库模式中,Lite<T>字段与类型为T的字段完全相同:一个带有指向另一个实体外键的 ID。 

此外,Lite<T>可以在您的业务逻辑中使用,以将其作为参数传递,从而有效地创建强类型 ID,并且由于它包含原始实体的 ToStr,因此调试起来更容易。  

它也与我们的**LINQ 提供程序**完全集成,因此您可以检索Lite<T>,例如用于填充组合框。在本教程中,我们将多次看到Lite<T>

在这种情况下,将‘reportsTo’设置为Lite<EmployeeDN>类型的字段,可以阻止引擎检索命令链中的所有员工。

MList<TerritoryDN> 字段 

MList 是 List 的一个自动跟踪更改的版本。它包含了 List 的所有便利方法(RemoveAllInvert和索引器...),并且可以轻松地进行数据绑定,因为它实现了INotifyCollectionChanged。  

在数据库中,MList 字段**不会创建任何列,而是创建一个表,其中包含属于一个实体并使用idParent外键关联到该实体的所有项。** 

在我们的例子中,类型为MList<TerritoryDN>territories字段将创建一个名为EmployeeDNTerritories的表,该表看起来与原始表非常相似。 

然而,MList<T> 不仅限于关系表(其他实体的集合),还可以用于创建值集合(MList<int>)或嵌入式实体集合(MList<AddressDN>)。 

速度提升:其余实体

写完Employee实体后,编写其余实体应该很简单。

这可能有点无聊,但这仅用于教学目的。在实际应用程序中,您将创建实体而不是表,而不是之后再创建。

我们抵制了创建自动从旧数据库生成实体的工具。Signum Framework 在某些约定上非常严格,并且很难考虑任何随机的旧数据库。 

但更重要的是,实体将是您应用程序的核心,手工编写它们是重新审视设计和修复旧错误的好机会。

一些小提示

  • Shipper(直接)
  • 客户
    • 使用我们的AddressDNEmbeddedEntity
    • 跳过 Customers demographics(该表为空且没有价值) 
  • Supplier
    • 使用我们的AddressDNEmbeddedEntity
    • HomePage上使用URLValidators
    • PhoneFax上使用TelephoneValidator
  • 产品
    • o 将与CategoryDNSupplierDN的关系,都设置为Lite<T>关系。
  • 类别
    • 为 picture 使用byte[]字段(带有SqlDbType(Size=int.MaxValue))。
  • Order
    • 使用我们的AddressDNEmbeddedEntity
    • 将与 Shipper 和 Customer 的关系设为 Lite。关系

最后,Order 实体中唯一棘手的地方是如何实现OrderDetails。它是一个关系表,但与关系关联了一些信息(UnitPriceQuantityDiscount)。 

正如我们所见,我们可以通过使用MList<T>来实现它,其中 T 是EmbeddedEntity,我们需要先创建一个OrderDetailDN EmbeddedEntity

entity [Tab] [Tab] OrderDetail [Enter]
field [Tab] [Tab] Lite<ProductDN> [Tab] product [Tab] Product [Enter]
field [Tab] [Tab] decimal [Tab] unitPrice [Tab] UnitPrice [Enter]
field [Tab] [Tab] int [Tab] quantity [Tab] Quantity [Enter]
field [Tab] [Tab] float [Tab] discount [Tab] Discount[Enter]

添加了 ValidationAttribute 并更改了基类型后,结果应该像这样:

[Serializable]
public class OrderDetailsDN : EmbeddedEntity
{
    Lite<ProductDN><productdn> product;
    [NotNullValidator]
    public Lite<ProductDN> Product
    {
        get { return product; }
        set { Set(ref product, value, () => Product); }
    }

    decimal unitPrice;
    public decimal UnitPrice
    {
        get { return unitPrice; }
        set { Set(ref unitPrice, value, () => UnitPrice); }
    }

    int quantity;
    public int Quantity
    {
        get { return quantity; }
        set { Set(ref quantity, value, () => Quantity); }
    }

    float discount;
    public float Discount
    {
        get { return discount; }
        set { Set(ref discount, value, () => Discount); }
    }
}
</productdn>

PropertyValidation

到目前为止,我们已经为所有验证使用了ValidationAttributes。简单但不够灵活。  

让我们稍微推进一下验证系统。假设我们想确保折扣是 5%、10%……25% 这样的值,总是 5% 的倍数。

我们没有合适的 Validator 属性来满足这些要求,但我们可以通过创建一个继承自ValidationAttribute的类来创建它。

但是,在本例中,我们仅在实体本身重写PropertyValidation方法。

protected override string PropertyValidation(PropertyInfo pi)
{
    if (pi.Is(() => Discount))
    {
        if ((discount * 100) % 5 != 0)
            return "Discount should be multiple of 5%"; 
    }

    return base.PropertyValidation(pi);
}
        

此方法将为实体的每个属性调用,如果它返回一个字符串,则认为该属性值是错误的。如果一切正常,它应该返回 null。

此技术的优点是可以考虑多个属性值来制定验证逻辑。在这种情况下,我们需要使用Notify(()=>OtherProperty)来强制在更改值后重新评估受影响属性的验证逻辑。

最后,只需在OrderDN实体中创建一个字段MList<orderdetailsdn>我们将获得预期的结果。

MList<OrderDetailsDN> details;
public MList<OrderDetailsDN> Details
{
    get { return details; }
    set { Set(ref details, value, () => Details); }
}
    

此时,Southwind 实体应该能够生成一个与 Northwind 非常相似的数据库,让我们尝试一下。

Southwind.Logic

重新加载Southwind.Logic项目。该项目包含将在服务器上运行的业务逻辑(因此我们可以访问数据库),在不同场景下:

 

  • Web 界面。
  • 通过 WCF 服务的 Windows 界面。
  • Load 应用程序。
  • 单元测试。

 

我们将在下一篇教程中更深入地探讨这个主题,现在我们从简单的开始。

为了创建数据库,我们首先必须告诉引擎哪些实体将包含在数据库模式中。

让我们将示例类MyEntityLogic重命名为OrdersLogic,并将包含MyEntityDN的代码更改为包含OrderDN

其余实体通过遍历OrderDN的依赖关系自动包含。

Southwind.Load

下一步是重新加载Southwind.Load。在此项目中,我们已经创建了一个简单的控制台应用程序,可用于操作数据库模式、加载数据或执行任何其他管理任务。

该模板已提供一个菜单,允许我们创建和同步数据库。让我们转到 SQL Management Studio,连接到 localhost,然后创建一个新数据库。

按约定,数据库名称应与项目名称相同,即‘Southwind’,但您可以在连接字符串中更改它。

创建完成后,将Southwind.Load标记为启动项目并运行。选择第一个选项,通过按“N”键选择New Database,然后……瞧!数据库模式已根据实体创建,看起来应该像这样:

摘要 

在本教程中,我们学习了如何通过继承某些基类来创建实体:

 

  • IdentifiableEntity:适用于带 ID 和表的实体。
  • EmbeddedEntity:适用于存在于父实体中的实体。
  • Entity:适用于带 ID 和表的实体,并且还可以控制并发修改。 

 

在我们的实体中,我们可以创建不同的字段和属性:

 

  • 值:任何标准值类型(stringDateTimeintfloatGuid, byte[]..)都将创建相应的列。 
  • 引用:我们可以通过创建该实体类型的字段来创建对其他实体的引用,或者我们可以使用Lite<T>来实现延迟加载。 虽然我们没有详细说明,但这些引用可以通过继承支持来实现多态。  
  • 集合:我们还可以使用MList<T>来创建集合表,这些表可用于表示实体、值或嵌入式实体的集合。 
  • 枚举:我们在此未详细说明,但您也可以在实体中使用枚举。  

 

我们可以通过在**属性**上放置一些ValidationAttribute来验证我们的实体,或者我们可以重写PropertyValidation方法以获得完全的控制。在其他教程中,我们将看到更多复杂的验证策略。  

正如您所见,Signum Framework 是自顶向下设计的,以提倡“代码优先”的方法,并尝试在旧数据库上工作会很痛苦,因为它有一些严格的约定(例如,每个实体都有IdToStr)。

尽管如此,我们已经看到了生成的数据库如何简单且可预测,并且可以被第三方工具轻松利用。

在下一篇教程中,我们将深入探讨如何编写业务逻辑、加载旧数据以及创建用户界面,以及这些约定如何在长期内简化我们的代码。

© . All rights reserved.