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






4.50/5 (12投票s)
本教程专注于使用 Signum Framework 编写实体。Signum Framework 是一个支持 LINQ 的 Windows/Web 框架,用于编写数据驱动型应用程序。
- 下载 Signum Framework 2.0 二进制文件 - 2.17 MB(也可在 codeplex.com 获取)
- 在 github.com 下载 Signum Framework 2.0 源代码
- 下载 Southwind 第一部分实体 - 165.63 KB(也可在 github.com 获取)
- Microsoft Northwind 示例数据库备份(也可在 microsoft.com 获取)
- 生成的 Southwind 数据库备份
Signum 框架教程
- Signum Framework 原理 (SF 1.0)
- Signum Framework 教程 第一部分 - Southwind 实体 (SF 2.0)
- Signum Framework 教程 第二部分 - Southwind 逻辑 (SF 2.0)
- Signum Framework 教程 第三部分 - Southwind 加载 (SF 2.0)
内容
- 引言
- 关于 Signum 框架 2.0
- 关于本系列
- 安装
- 创建实体
- 我们的第一个实体:Region
- 带外键的实体:Territory
- 嵌入式实体:Address
- 体量增大:Employee 实体
- 速度提升:其余实体
- 属性验证
- Southwind.Logic
- Southwind.Load
- 摘要
引言
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
’,而是Employees
和Territories
之间的多对多关系。在 Signum Framework 中,这由Employee
实体上的MList<TerritoryDN>
字段表示。
在处理Employee
表之前,我们可以看到某些字段(Address
、City
、Region
、PostalCode
和Country
)也重复出现在Customers
、Orders
和Supplier
表中。
嵌入式实体: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
层级。 - 一堆新的值类型,如
DateTime
、DateTime?
(可空)、用于照片的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 字段
类型为AddressDN
的address
字段将包含形式为Address_Address
、Address_City
、Address_Region
……的所有相应列。
此外,为了表示地址实体本身是 null(而不是其内部字段的某些值),将创建一个新的Address_HasValue
字段,内部字段的类型将被覆盖以支持 null 值。
我们可以通过在address
字段上添加[NotNullable]
来禁用此功能,但在本例中这是可以的。
Lite 字段
Lite<T>
是一个泛型类,它创建一个对实体的轻量级引用。在运行时,它只包含Type
、Id
和ToStr
字段,并可在您的实体中使用以控制延迟加载。
然而,在数据库模式中,Lite<T>
字段与类型为T
的字段完全相同:一个带有指向另一个实体外键的 ID。
此外,Lite<T>
可以在您的业务逻辑中使用,以将其作为参数传递,从而有效地创建强类型 ID,并且由于它包含原始实体的 ToStr,因此调试起来更容易。
它也与我们的**LINQ 提供程序**完全集成,因此您可以检索Lite<T>
,例如用于填充组合框。在本教程中,我们将多次看到Lite<T>
。
在这种情况下,将‘reportsTo’设置为Lite<EmployeeDN>
类型的字段,可以阻止引擎检索命令链中的所有员工。
MList<TerritoryDN> 字段
MList
是 List 的一个自动跟踪更改的版本。它包含了 List 的所有便利方法(RemoveAll
、Invert
和索引器...),并且可以轻松地进行数据绑定,因为它实现了INotifyCollectionChanged
。
在数据库中,MList 字段**不会创建任何列,而是创建一个表,其中包含属于一个实体并使用idParent
外键关联到该实体的所有项。**
在我们的例子中,类型为MList<TerritoryDN>
的territories
字段将创建一个名为EmployeeDNTerritories
的表,该表看起来与原始表非常相似。
然而,MList<T> 不仅限于关系表(其他实体的集合),还可以用于创建值集合(MList<int>
)或嵌入式实体集合(MList<AddressDN>
)。
速度提升:其余实体
写完Employee
实体后,编写其余实体应该很简单。
这可能有点无聊,但这仅用于教学目的。在实际应用程序中,您将创建实体而不是表,而不是之后再创建。
我们抵制了创建自动从旧数据库生成实体的工具。Signum Framework 在某些约定上非常严格,并且很难考虑任何随机的旧数据库。
但更重要的是,实体将是您应用程序的核心,手工编写它们是重新审视设计和修复旧错误的好机会。
一些小提示
- Shipper(直接)
- 客户
- 使用我们的
AddressDN
EmbeddedEntity
。 - 跳过 Customers demographics(该表为空且没有价值)
- 使用我们的
- Supplier
- 使用我们的
AddressDN
EmbeddedEntity
。 - 在
HomePage
上使用URLValidators
。 - 在
Phone
和Fax
上使用TelephoneValidator
。
- 使用我们的
- 产品
- o 将与
CategoryDN
和SupplierDN
的关系,都设置为Lite<T>
关系。
- o 将与
- 类别
- 为 picture 使用
byte[]
字段(带有SqlDbType(Size=int.MaxValue)
)。
- 为 picture 使用
- Order
- 使用我们的
AddressDN
EmbeddedEntity
。 - 将与 Shipper 和 Customer 的关系设为 Lite。
关系
- 使用我们的
最后,Order 实体中唯一棘手的地方是如何实现OrderDetails
。它是一个关系表,但与关系关联了一些信息(UnitPrice
、Quantity
、Discount
)。
正如我们所见,我们可以通过使用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 和表的实体,并且还可以控制并发修改。
在我们的实体中,我们可以创建不同的字段和属性:
- 值:任何标准值类型(
string
、DateTime
、int
、float
、Guid, byte[]
..)都将创建相应的列。 - 引用:我们可以通过创建该实体类型的字段来创建对其他实体的引用,或者我们可以使用
Lite<T>
来实现延迟加载。 虽然我们没有详细说明,但这些引用可以通过继承支持来实现多态。 - 集合:我们还可以使用
MList<T>
来创建集合表,这些表可用于表示实体、值或嵌入式实体的集合。 - 枚举:我们在此未详细说明,但您也可以在实体中使用枚举。
我们可以通过在**属性**上放置一些ValidationAttribute
来验证我们的实体,或者我们可以重写PropertyValidation
方法以获得完全的控制。在其他教程中,我们将看到更多复杂的验证策略。
正如您所见,Signum Framework 是自顶向下设计的,以提倡“代码优先”的方法,并尝试在旧数据库上工作会很痛苦,因为它有一些严格的约定(例如,每个实体都有Id
和ToStr
)。
尽管如此,我们已经看到了生成的数据库如何简单且可预测,并且可以被第三方工具轻松利用。
在下一篇教程中,我们将深入探讨如何编写业务逻辑、加载旧数据以及创建用户界面,以及这些约定如何在长期内简化我们的代码。