介绍 Rabbit Framework 及其动态理念
Rabbit Framework 是一个用于使用 ASP.NET Web Pages / WebMatrix 构建网站的轻量级新框架。本文介绍了一个在该框架中发现的众多有趣想法之一。
引言
Rabbit Framework 是一个用于使用 ASP.NET Web Pages 构建动态网站的轻量级框架。我创建它是为了测试一些有趣的想法,例如事件驱动、Sinatra 风格 MVC、Monadic Model、 everywhere 使用动态,以及使用 Mocking 进行单元测试。事实上,它非常有趣,让我忍不住要分享这些想法。我在 CodePlex 上发布了该项目,并在 github 上发布了源代码。
本文介绍了“ everywhere 使用动态”的理念,包括将动态用作类引用,使用 ExpandoObject
作为 DTO,以及使用 DynamicObject
进行 Mocking。
背景
传统上,ASP.NET Web 开发仅通过 ASP.NET Web Forms 技术完成。最近,微软为 Web 开发引入了两个新栈。它们是 ASP.NET MVC 和 ASP.NET Web Pages 框架。在我看来,ASP.NET Web Pages 框架是最有趣的。
ASP.NET Web Pages 框架随 WebMatrix 一起推出,WebMatrix 是一个显然更面向初学者的工具,而不是进行硬核开发。Scott Gu 在介绍 WebMatrix 的博客文章中警告读者。
如果您是一位拥有多年 .NET 开发经验的专业开发人员,您可能会看下面的步骤并认为——这个场景太基础了——您需要了解的比这多得多才能构建一个“真正的”应用程序。封装的业务逻辑、数据访问层、ORM 等呢?好吧,如果您正在构建一个关键业务应用程序,并且希望它能够维护多年,那么您确实需要理解并考虑这些场景。 (1)
这让我暂时忽略了 WebMatrix 和 ASP.NET Web Pages 框架几个月,直到我开始在 Visual Studio 中尝试 Razor 页面。那一刻,我发现 ASP.NET Web Pages 框架是一个简单、轻量但功能强大的 ASP.NET 技术,而不仅仅是 WebMatrix 的一部分。
ASP.NET Web Pages 框架为用户提供了一种简单而强大的编写 ASP.NET 应用程序的新方式。它与 WebForms 不同,因为它不使用服务器控件。它也与 MVC 不同,因为它不遵循 MVC 模式。相反,它遵循一个更简单的“内联页面”模型,其中页面基本上是一个 HTML 页面,在需要的地方添加了一些代码。从这个意义上说,它让人想起经典的 ASP,但它也非常不同,因为它拥有 .NET 框架的全部强大功能。它还支持布局页面等概念,这使其比经典的 ASP 更具灵活性。(2)
回到 Scott Gu 的博客文章,事实上,在评论中,他后来澄清了。
从应用程序性能的角度来看,它是相同的 ASP.NET,因此性能非常好。关于项目规模的主要问题将围绕架构和代码组织。在没有清晰关注点分离的世界中(MVC 和 WebForms 都可以提供帮助)更容易陷入麻烦。这并不是说您无法使用单文件方法构建良好且非常大的应用程序,也不是说您无法使用强制执行更分离的体系结构的框架构建糟糕的应用程序。 (1)
这意味着,如果我们能够拥有良好的架构和代码组织,那么基于 ASP.NET Web Pages 框架构建的应用程序的性能将与 Web Forms 和 MVC 一样好。
这鼓励我进行更多探索,并创建了一套工具来构成一个用于使用 ASP.NET Web Pages 构建网站的框架。我称之为 Rabbit 框架,因为兔子轻巧、快速且精致。而今年是兔年。
Using the Code
您可以在 WebMatrix 或任何版本的 Visual Studio 中打开 示例网站 并将其作为演示运行。您也可以将其作为 NuGet 包 下载到您现有的网站中。欢迎您在 CodePlex 上发送反馈,并在 github 上 fork 源代码。
Rabbit Framework 目前处于实验阶段。尚未投入生产使用。请自行承担使用风险。
关注点
ASP.NET Web Pages 是一个构建在 .NET Framework 之上的 Web 技术栈。在 .NET Framework 4 中,最令人印象深刻的新功能是动态。Rabbit Framework 在 everywhere 使用动态,这极大地简化了代码和测试。
基于关注点分离原则,应用程序通常遵循分层设计。有表示层、服务层和数据访问层。ASP.NET MVC 建议将验证放在服务层,并在数据访问层实现存储库。似乎 ASP.NET MVC 将推送到视图的数据称为 Model。对我来说,Model 是其背后服务层和数据访问层的组合。从控制器传递到视图的数据是所谓的 数据传输对象 (DTO),根据领域驱动设计 (DDD)。
在 Rabbit Framework 中,
- C#/.NET 4 的新
dynamic
关键字用于引用其他层中的类。 ExpandoObject
类是 DTO 的类型。DynamicObject
类用于构建 mock 对象。
使用动态关键字
在典型的分层架构中,表示层中的类使用服务层中的类。服务层中的类使用数据访问层中的类。每当类 A 使用类 B 时,A 就依赖于 B。A 和 B 是耦合的。A 没有 B 就无法工作。
类 A 和类 B 之间的依赖关系很强,当 A 只与 B 一起工作时。如果 A 与一个由 B 实现的接口一起工作,依赖关系就很弱。更弱的依赖关系使 A 解耦。
例如,这是耦合且强依赖的。
public class Model
{
public Repository Repository {get; set;}
}
这是解耦且弱依赖的。
public class Model
{
public IRepository Repository {get; set;}
}
解耦依赖的好处是它提供了代码的可重用性。我们可以有接口的不同实现。
public class SqlRepository : IRepository
{
}
public class JsonRepository : IRepository
{
}
Service
类可以重用于 SQL 存储库或 JSON 存储库,而无需更改。而在耦合场景下,这是不可能的。
解耦类的技术是使用接口。这在静态类型编程语言(如 C++、Java 和 C#(4.0 之前))中是众所周知的,并且被广泛使用。虽然它减弱了类之间的依赖关系,但它也为源代码系统引入了更多的工件。可能需要大量的接口,并且需要管理。
回到 C/C++ 时代,您将函数和类写在 .cpp 文件中。您还必须在单独的 .h 文件中声明函数和类。对于 Java/C#,这不需要。因此事情变得更简单了。我们可以节省精力去编写代码,而不是去管理代码的工件。
当 C# 4.0 引入动态时,对抗耦合的斗争可能会进一步改变。我们可以通过使用动态来摆脱接口。
例如,存储库通过 dynamic
引用。
public class Model
{
public dynamic Repository {get; set;}
}
dynamic
关键字本质上禁用了 C# 编译器的编译时类型检查,使其成为所谓的动态类型。这并没有改变 C#/.NET 的强类型特性。这意味着如果您没有分配正确的对象,编译器不会报错。但它会在运行时导致错误。
没有接口,我们节省了更多的编码精力,并且需要管理的东西更少。
编译时类型检查是静态类型语言的一个优点。它防止了无效代码。这是一个权衡。我们用代码的简洁性和可扩展性来换取编译时类型检查和重构。
为了确保对象被正确引用,单元测试很重要。幸运的是,基于动态的单元测试大大简化了。
[TestMethod]
public void SaveAs_Should_Validate_New_Id_Exists()
{
dynamic data = new ExpandoObject(); //create a DTO
data.Id = "old-id";
var repository = new Mock();
repository.Setup("Exists", new object[] { "new-id" }, true); //tell model new id exists
var model = new PageModel();
model.Repository = repository; //push mock repository to model
model.SaveAs(data, "new-id"); //trigger the model
Assert.IsTrue(model.HasError); //make sure model report error
repository.Verify(); //make sure reporsitory.Exists is called
}
上面的示例代码表明,将 mock 存储库推送到模型很容易。
使用 ExpandoObject 作为 DTO
Ruby on Rails 在 Model 中使用 ActiveRecord。ASP.NET MVC 将 Entity Framework 用作存储库,并将 Entity 用作 MVC 中的 M(Model)。Rabbit Framework 使用 ExpandoObject
作为 DTO。
在 Rabbit Framework 中,控制器调用模型。模型调用存储库。模型是一个服务外观和工作单元容器。存储库提供身份映射和内存缓存。它们之间的引用是动态的。DTO 是 .NET 动态对象,即 ExpandoObject
。这种设计使其非常灵活、可扩展且可测试。
这是一个关于控制器如何通过使用 ExpandoObject
将 DTO 传递给视图的示例。
在控制器中,
dynamic model = new ExpandoObject();
model.Message = "hi";
@RenderPage("View", model)
在视图中,
@Page.Message
这与某人可能尝试使用 ASP.NET MVC 的想法相同。
<%@ Page Title="" Language="C#" Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
ASP.NET MVC 具有强类型视图。它还有一个 ViewData
字典来传递额外的数据和 dynamic
数据。在 MVC 3 中,ViewBag
属性公开了一个 dynamic
对象,用于将后期绑定的数据从控制器传递到视图。如果模型是 dynamic
,则无需额外的 ViewBag
。
在数据访问层,Massive 项目是使用 dynamic
进行数据持久化的一个很好的例子。
Mock、Stub 和 Fake,集于一体
由于 Rabbit Framework 尽可能地拥抱动态,Mocking 变得非常容易。下面的示例展示了一个执行了几个操作的测试:
- 将 mock 存储库推送到 Model
- 设置期望
- 触发 Model 执行
- 验证以确保期望得到满足
[TestMethod]
public void SaveAs_Should_Validate_New_Id_Exists()
{
dynamic data = new ExpandoObject(); //create a DTO
data.Id = "old-id";
var repository = new Mock();
repository.Setup("Exists", new object[]
{ "new-id" }, true); ////tell the model new id exists
var model = new PageModel();
model.Repository = repository; //push the mock to model
model.SaveAs(data, "new-id"); //trigger the model
Assert.IsTrue(model.HasError); //make sure model report error
repository.Verify(); //make sure reporsitory.Exists is called
}
这是要测试的 SaveAs
函数。
public PageModel SaveAs(dynamic item, string newId)
{
Assert.IsTrue(item != null);
var oldId = item.Id as string;
Value = item;
Value.Id = newId;
Validate();
if (HasError) return this;
if (oldId != newId)
{
if (Repository.Exists(newId))
{
Errors.Add("Id", string.Format("{0} exisits already.", Value.Id));
}
else
{
Repository.Delete(oldId);
Value.Id = newId;
}
}
if (!HasError) Repository.Save(Value.Id as string, Value);
return this;
}
Mock 实现
Mocking 接口是困难的。它需要动态发射类,就像 Moq 所做的那样。Mocking 具体类更困难。
使用动态对象进行 Mocking 非常容易。Rabbit Framework 中的 Mock
类可能是世界上最简单的 Mock 实现。总共 160 行。不依赖任何第三方库。
Mock
类继承自 DynamicObject
。它允许:
- 验证要调用的方法的顺序
- 验证要调用的方法的次数
- 验证传递给方法的参数数量
- 验证传递给方法的每个参数的类型
- 验证传递给方法的每个参数的值
public class Mock: DynamicObject
{
List<Expectation> expectations = new List<Expectation>();
public void SetupGet(string name, object value)
{
//setup property getter access expectation
}
public void SetupSet(string name, object value)
{
//setup property setter access expectation
}
public void Setup(string name, object[] parameters, object returnValue = null)
{
//setup property method access expectation
}
public void Verify()
{
//verify records against expectations
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
//record property getter access
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
//record property setter access
}
public override bool TryInvokeMember(InvokeMemberBinder binder,
object[] args, out object result)
{
//record method
}
}
下面是 Mock
对象的测试用例。所有用例都有 ExpectedException
属性,因为 Mock
对象期望捕获所有场景。
[TestClass]
public class MockTest
{
/// <summary>
/// Method1 is not called
/// </summary>
[TestMethod]
[ExpectedException(typeof(UnitTestException))]
public void TestMockMethodNotRun()
{
dynamic mock = new Mock();
mock.Setup("Method1", new object[] { "a", It.IsAny<int>() }, 10);
mock.Verify();
}
/// <summary>
/// Method1 has been called already. Add more setup.
/// </summary>
[TestMethod]
[ExpectedException(typeof(UnitTestException))]
public void TestMockMethodCalled()
{
dynamic mock = new Mock();
mock.Setup("Method1", new object[] { "a", It.IsAny<int>() }, 10);
Assert.AreEqual(10, mock.Method1("a", -2));
Assert.AreEqual(10, mock.Method1("b", -2m));
mock.Verify();
}
/// <summary>
/// Method1 is called w/ 0 parameters, expected: 2.
/// </summary>
[TestMethod]
[ExpectedException(typeof(UnitTestException))]
public void TestMockParameterCount()
{
dynamic mock = new Mock();
mock.Setup("Method1", new object[] { "a", 2.5m }, 10);
Assert.AreEqual(10, mock.Method1());
mock.Verify();
}
/// <summary>
/// Method1 parameter #2 type failed, expected: IsNotNull, actual [null].
/// </summary>
[TestMethod]
[ExpectedException(typeof(UnitTestException))]
public void TestMockParameterNotNull()
{
dynamic mock = new Mock();
mock.Setup("Method1", new object[] { "a", It.IsNotNull() }, 10);
Assert.AreEqual(10, mock.Method1("a", null));
mock.Verify();
}
/// <summary>
/// Method1 parameter #2 type failed, expected: IsNull, actual 2.
/// </summary>
[TestMethod]
[ExpectedException(typeof(UnitTestException))]
public void TestMockParameterNull()
{
dynamic mock = new Mock();
mock.Setup("Method1", new object[] { "a", It.IsNull() }, 10);
Assert.AreEqual(10, mock.Method1("a", 2));
mock.Verify();
}
/// <summary>
/// Method1 parameter #2 type failed, expected: System.Int32, actual System.Decim
/// </summary>
[TestMethod]
[ExpectedException(typeof(UnitTestException))]
public void TestMockParameterType()
{
dynamic mock = new Mock();
mock.Setup("Method1", new object[] { "a", It.IsAny<int>() }, 10);
Assert.AreEqual(10, mock.Method1("a", -2m));
mock.Verify();
}
/// <summary>
/// Method1 parameter #1 value failed, a:System.String, actual: b:System.String
/// </summary>
[TestMethod]
[ExpectedException(typeof(UnitTestException))]
public void TestMockParameterValue()
{
dynamic mock = new Mock();
mock.Setup("Method1", new object[] { "a", 2.5m }, 10);
Assert.AreEqual(10, mock.Method1("b", 2.5));
mock.Verify();
}
/// <summary>
/// set_Prop1 parameter #1 type failed, expected:
/// System.String[], actual System.Int32
/// </summary>
[TestMethod]
[ExpectedException(typeof(UnitTestException))]
public void TestMockPropertyType()
{
dynamic mock = new Mock();
mock.SetupSet("Prop1", It.IsAny<string[]>());
mock.Prop1 = 5;
mock.Verify();
}
/// <summary>
/// set_Prop1 parameter #1 value failed, expected:
/// 5:System.String, actual: 5:System.Int32
/// </summary>
[TestMethod]
[ExpectedException(typeof(UnitTestException))]
public void TestMockPropertyValue()
{
dynamic mock = new Mock();
mock.SetupSet("Prop1", "5");
mock.Prop1 = 5;
mock.Verify();
}
}
结论
Dynamic
是 C#/.NET 4 中一项新颖且有趣的功能。使用 dynamic
来用代码的简洁性来换取编译时类型检查是否值得商榷。但摆脱接口(以及可能的泛型)的想法是巨大的。这是一个值得更多研究的课题,因为 dynamic
不仅仅局限于 Rabbit Framework。它适用于所有其他形式的开发。
如果本文引起了您对使用 dynamic
的关注,您可以使用 Rabbit Framework 进行自己的实验。也欢迎您在 CodePlex 上分享您的想法,并在 github 上 fork 源代码。
历史
- 第一个版本,2011 年 3 月,对应的 Rabbit Framework 版本 V0.3.0。