一个简单的 POC,使用 ASP.NET Web API、Entity Framework、Autofac、跨域支持






4.92/5 (43投票s)
本文旨在创建一个概念验证,以演示 ASP.NET Web API Beta 版本的可行性。
引言
本文旨在创建一个概念验证,以演示 ASP.NET WebAPI 正式版本的可行性。
更新
- 修改代码以兼容 Visual Studio 2012
- 将 Autofac 替换为 Castle Windsor 用于依赖注入
先决条件
- AdventureWorksLT2008R2
- ASP.Net WebAPI
Autofac 用于依赖注入Castle Windsor 用于 DI- Log4Net
EntityFramework 4.1Entity Framework 5- jQuery Templates 插件 (jquery-tmpl)
- Visual Studio Express 2012
要实现的目标
- 任何用户都应能查看产品
- 产品将按类别过滤
- 用户应能将产品添加到购物车
- 只有注册用户才能提交购物车
- 用户应能注册到系统中
- 用户在系统中生成订单后将收到确认发票。
此 POC 的架构
我没有遵循任何特定的架构模式,尽管在设计此 POC 时,我尝试遵循一些关键设计原则,例如
我再次重复我的话尝试遵循…… :)。因此,欢迎所有评论/建议进行讨论,我随时准备接受更改,或者您可以自己加入 @CodePlex 来完成。此应用程序的初始骨架如下所示:
逐步教程
- 步骤 1:创建基本应用程序结构布局
- 步骤 2:创建数据访问层
- 步骤 3:创建服务层
- 步骤 4:创建 API 层
- 步骤 5:使用 Castle Windsor 集成依赖注入
- 步骤 6:向解决方案添加视图
- 步骤 7:单元测试和解决问题
- 步骤 8:部署到 IIS
- 步骤 9:提供跨域支持
步骤 1:创建基本应用程序结构布局
有关 ASP.NET WebAPI 的更多信息,请在此处观看视频和教程 。
添加一个名为Application的空白解决方案
添加一个名为Application.DAL的新项目(类库)
重复以上过程,再创建四个名为——的类库项目:
- Application.Model
- Application.Repository
- Application.Common
- Application.Service
接下来,添加一个名为Application.API的新项目,作为 ASP.Net MVC 4 Web 应用程序。单击“确定”后,选择模板为 Web API
这将为我们的主解决方案添加一个功能齐全的 Web API 模板应用程序。在解决方案资源管理器中右键单击 Application.API 项目,然后转到“属性”,然后为该服务应用程序分配一个端口(在本例中为 30000)。按 F5 键,我们就准备好了,如下所示,显示了一些虚拟数据:
步骤 2:准备 Application.DAL、Application.Repository 和 Application.Model 层
有关 Entity Framework 的更多信息,请查看@MSDN
向Application.DAL项目添加实体数据模型。为此,请从数据模板列表中选择 ADO.NET 实体数据模型,并将其命名为AdventureWorks.edmx
选择“从数据库生成”并提供连接字符串以读取数据库架构。这将把 AdventureWorks.edmx 添加到当前项目中。右键单击实体模型表面的空白区域,然后单击“属性”(或在键盘上按 Alt+Enter)。现在将其代码生成策略设置为None。
现在,在Application.Model解决方案中为 AdventureWorks 数据模型创建 POCO 实体。请参阅 MSDN 上有关如何使用T4 模板和 Entity Framework 完成此操作的文章。
然后,在 Application.Repositories 下创建所有必需的存储库。
要构建解决方案,请将Application.Model.dll的引用添加到Application.DAL.dll和Application.Repository.dll项目中,并将Application.DAL.dll的引用添加到Application.Repository.dll项目中。(有关参考,请参见下图)
在继续下一步(设计 Application.Service 和 Application.API 层)之前,让我们看看我们已经设计了什么
步骤 3:向 Application.Service 层添加文件
在向服务层添加文件之前,让我们回顾一下我们想要实现的目标。
- 任何用户都应能查看产品
- 产品将按类别过滤
- 用户应能将产品添加到购物车
- 只有注册用户才能提交购物车
- 用户应能注册到系统中
- 用户在系统中生成订单后将收到确认发票。
为了实现上述目标,我们需要向 Application.Service 添加三个服务类,如下图所示:
我不会深入细节,因为每个方法的实现都清晰易懂。
步骤 4:向 Application.API 层添加文件
为了使 Web API 能够被广泛的客户端访问,例如任何使用 JavaScript 的浏览器,或任何支持 .dll 文件的 Windows/Mobile 平台,我们需要添加继承自ApiController的控制器。 更多详细信息请参见此处
首先,我们删除模板添加的默认控制器(HomeController 和 ValuesController),然后根据下图添加三个新控制器:
同样,这些方法的实现没有什么特别之处,它们都只是将数据推送到服务层。
步骤 5:使用 Castle Windsor 将依赖注入与 WebAPI 集成
实现非常直接且清晰。
在Application.API解决方案资源管理器中,右键单击“引用”节点,然后单击“管理 NuGet 程序包”
这将打开下面的屏幕。
单击“安装”,这将为解决方案添加以下两个引用:
创建一个 Castle Windsor 的适配器WindsorActivator以实现IHttpControllerActivator
public class WindsorActivator : IHttpControllerActivator
{
private readonly IWindsorContainer container;
public WindsorActivator(IWindsorContainer container)
{
this.container = container;
}
public IHttpController Create(
HttpRequestMessage request,
HttpControllerDescriptor controllerDescriptor,
Type controllerType)
{
var controller =
(IHttpController)this.container.Resolve(controllerType);
request.RegisterForDispose(
new Release(
() => this.container.Release(controller)));
return controller;
}
private class Release : IDisposable
{
private readonly Action release;
public Release(Action release)
{
this.release = release;
}
public void Dispose()
{
this.release();
}
}
}
该installer使用container
参数的Install
方法,通过 Windsor 的Fluent Registration API来Register
控制器。
现在我们需要让 Castle Windsor 知道所有的依赖关系,以便它能管理它们。为此,我们创建了一个新的类DependencyInstaller来实现IWindsorInstaller。Installer 使用 Install() 方法的 container 参数,通过 Windsor 的 Fluent Registration API 来注册依赖关系。如下面的实现所示,每当我们向应用程序中的 Controller/Service/Repository 添加新类时,它都会自动注册,我们唯一需要遵循的是命名约定,即所有 Controller 类都应以 Controller 结尾,同样 Service 和 Repository 类也应以 Service 和 Repository 结尾。
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register(
Component.For<ILogService>()
.ImplementedBy<LogService>()
.LifeStyle.PerWebRequest,
Component.For<IDatabaseFactory>()
.ImplementedBy<DatabaseFactory>()
.LifeStyle.PerWebRequest,
Component.For<IUnitOfWork>()
.ImplementedBy<UnitOfWork>()
.LifeStyle.PerWebRequest,
AllTypes.FromThisAssembly().BasedOn<IHttpController>().LifestyleTransient(),
AllTypes.FromAssemblyNamed("Application.Service")
.Where(type => type.Name.EndsWith("Service")).WithServiceAllInterfaces().LifestylePerWebRequest(),
AllTypes.FromAssemblyNamed("Application.Repository")
.Where(type => type.Name.EndsWith("Repository")).WithServiceAllInterfaces().LifestylePerWebRequest()
);
}
}
接下来,在 Global.asax.cs 中,在 Application 的构造函数中创建并配置 Windsor 容器,以便在应用程序退出时自动销毁它。
public class WebApiApplication : System.Web.HttpApplication
{
private readonly IWindsorContainer container;
public WebApiApplication()
{
this.container =
new WindsorContainer().Install(new DependencyInstaller());
}
public override void Dispose()
{
this.container.Dispose();
base.Dispose();
}
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
GlobalConfiguration.Configuration.Services.Replace(
typeof(IHttpControllerActivator),
new WindsorActivator(this.container));
}
}
现在我们可以测试我们的应用程序了。按 F5 键,然后使用以下 URL 在浏览器中进行检查:
https://:30000/api/products : 将显示所有产品类别
https://:30000/api/products/2 : 将显示所有 ProductcategoryID 为 2 的产品
要检查其他方法,首先,我们将向当前解决方案添加一些视图。
步骤 6:向解决方案添加视图
删除 Views 文件夹中的所有默认文件和文件夹,然后添加三个 HTML 及其支持的 JavaScript 文件。对于此 POC,我们将使用 HTML 文件和一些 Jquery 库,例如:
jquery.tmpl.js : 用于创建基于模板的屏幕,以及
jquery.validationEngine.js : 用于验证表单数据
现在结构将如下所示:
有关 HTML 和 JS 文件中的详细代码,请参见代码。下一步,我将解决问题。
步骤 7:解决问题
在运行应用程序之前,右键单击 Products.htm 并单击“设为起始页”。现在按 F5 键。
这意味着 ProductsController.cs 中的两个 WebAPI 方法工作正常。在 JavaScript 中,页面加载时,我们在product.js中调用第一个方法,如下所示:
var productcategoriesAddress = "/api/products/";
$(function () {
$('#tblRight').hide();
$.getJSON(
productcategoriesAddress,
function (data) {
var parents = jQuery.grep(data, function (a) { return a.ParentProductCategoryID == null });
var childs = jQuery.grep(data, function (a) { return a.ParentProductCategoryID != null });
$.each(parents,
function (index, value) {
var categorydata = [];
var subCategory = '';
var subChild = jQuery.grep(childs, function (a) { return a.ParentProductCategoryID == value.ProductCategoryID });
$.each(subChild,
function (index, item) {
var serviceURL = productcategoriesAddress + item.ProductCategoryID;
subCategory = subCategory + " " + "" + item.Name + "";
});
categorydata.push({
'ProductCategoryID': value.ProductCategoryID,
'ParentCategory': value.Name,
'ChildCategory': subCategory
});
$("#categoryTemplate").tmpl(categorydata).appendTo("#categories");
$("#" + value.Name).html(subCategory);
});
GetProducts(1);
}
);
此调用会命中 ProductsController 中的以下方法,并返回所有产品类别,用于创建顶部菜单部分:
public IQueryable GetProductCategories()
{
loggerService.Logger().Info("Calling with null parameter");
return _productservice.GetProductCategories().AsQueryable();
}
ProductService 中的实现是:
public IQueryable GetProductCategories()
{
loggerService.Logger().Info("Calling with null parameter");
return _productservice.GetProductCategories().AsQueryable();
}
第二个调用是根据所选类别获取所有产品。在 JavaScript 中:
function GetProducts(ProductCategoryID) {
var serviceURL = productcategoriesAddress + ProductCategoryID;
$('#categories li h1').css('background', '#736F6E');
$('#mnu' + ProductCategoryID).css('background', '#357EC7');
$("#loader").show();
$.ajax({
type: "GET",
datatype: "json",
url: serviceURL,
context: this,
success: function (value) {
$("#productData").html("");
$("#productTemplate").tmpl(value).appendTo("#productData");
$("#loader").hide();
}
});
return false;
}
在 ProductsController 中,它会执行到:
public IQueryable GetProductByProductCategoryID(int id)
{
loggerService.Logger().Info("Calling with null parameter as : id : " + id);
return _productservice.GetProductByProductCategoryID(id).AsQueryable();
}
ProductService 中的实现是:
public IQueryable GetProductByProductCategoryID(int id)
{
loggerService.Logger().Info("Calling with null parameter as : id : " + id);
return _productservice.GetProductByProductCategoryID(id).AsQueryable();
}
接下来,让我们尝试将一些产品添加到购物车并结账。
单击结账后,它会重定向到 Checkout.htm 页面,并要求使用现有的 CustomerID 登录或注册新客户。
单击注册链接以创建新客户。
单击“注册”按钮后提交表单,并查看发布是否正常工作。再次,成功!!!
现在我们将尝试更新 AddressLine2 的记录,更新工作正常!!!
在这两种情况下(添加或更新客户),我们都调用 CutomerControllers.cs 中的以下方法:
public int PostCustomer(CustomerDTO customer)
{
loggerService.Logger().Info("Calling with parameter as : customer: " + customer);
return _customerService.SaveOrUpdateCustomer(customer);
}
CustomerService 中的实现是:
public int SaveOrUpdateCustomer(CustomerDTO customer)
{
string passwordSalt = CreateSalt(5);
string pasword = CreatePasswordHash(customer.Password, passwordSalt);
Customer objCustomer;
if (customer.CustomerID != 0)
objCustomer = _customerRepository.GetById(customer.CustomerID);
else
objCustomer = new Customer();
objCustomer.NameStyle = customer.NameStyle;
objCustomer.Title = customer.Title;
objCustomer.FirstName = customer.FirstName;
objCustomer.MiddleName = customer.MiddleName;
objCustomer.LastName = customer.LastName;
objCustomer.Suffix = customer.Suffix;
objCustomer.CompanyName = customer.CompanyName;
objCustomer.SalesPerson = customer.SalesPerson;
objCustomer.EmailAddress = customer.EmailAddress;
objCustomer.Phone = customer.Phone;
objCustomer.PasswordHash = pasword;
objCustomer.PasswordSalt = passwordSalt;
objCustomer.ModifiedDate = DateTime.Now;
objCustomer.rowguid = Guid.NewGuid();
if (customer.CustomerID != 0)
_customerRepository.Update(objCustomer);
else
_customerRepository.Add(objCustomer);
_unitOfWork.Commit();
SaveOrUpdateAddress(customer, objCustomer.CustomerID);
return objCustomer.CustomerID;
}
接下来,单击“提交订单”按钮,将先前选择的产品作为订单创建,并接收订单发票。
!!!万岁!!! 订单已在系统中创建,并生成了带有新采购订单的发票。但是,等等,我们发现了一个 Bug
第 1 个问题已在 Asp.Net WebAPI 的最终版本中修复
问题 1:日期格式有问题。
根据 Scott Hanselman 的帖子,这是一个 Bug,将在 ASP.Net WebAPI 的下一个版本中解决。这个问题可以使用 Henrik 的博客中提供的解决方案来解决。
问题 2:另一个问题是,当我尝试使用最后一个创建的客户结账时。错误是:
看到这个错误后,第一个想法是我们在 CustomersController.cs 中遗漏了方法,但方法已经在那里了。进一步的调查表明,导致此错误的原因是此方法名为 ValidateCustomer()。
public CustomerDTO ValidateCustomer(int id, string password)
{
loggerService.Logger().Info("Calling with parameter as : id and password: " + id + " and " + password);
return _customerService.ValidateCustomer(id, password);
}
CustomerService 中的实现是:
public CustomerDTO ValidateCustomer(int id, string password)
{
Customer objCustomer = _customerRepository.GetById(id);
if (objCustomer == null)
return null;
string strPasswordHash = objCustomer.PasswordHash;
string strPasswordSalt = strPasswordHash.Substring(strPasswordHash.Length - 8);
string strPasword = CreatePasswordHash(password, strPasswordSalt);
if (strPasword.Equals(strPasswordHash))
return CreateCustomerDTO(objCustomer);
else
return null;
}
默认情况下,Asp.Net WebAPI 的路由配置遵循 RESTFUL 约定,这意味着它只接受Get、Post、Put 和 Delete操作名称。因此,当我们向 https://:30000/api/customers/ValidateCustomer/30135/test 发送 GET 请求时,我们实际上是在调用 Get(int id) 操作,并将 id=30135 传递给它,这显然会崩溃,因为我们没有以 Get 开头且接受 Id 作为参数的方法。为了解决这个问题,我需要在Global.asax文件中添加一个新的路由定义:
routes.MapHttpRoute(
name: "ValidateCustomer",
routeTemplate: "api/{controller}/{action}/{id}/{password}",
defaults: new { action = "get" }
);
添加此行后,登录功能就开始工作了……:)
步骤 8:部署到 IIS
要将此 API 部署到 IIS 7.5 版本,首先,仅将Application.API解决方案发布到一个文件夹。
接下来,打开 IIS 管理器并创建一个新网站,然后将路径设置为发布文件夹,我们就完成了部署。
现在,检查页面是否可以打开。在本例中,最初我遇到了以下错误,该错误具有自解释性。
出现此错误是因为此应用程序正在使用 Asp.Net 框架版本 2.0 运行。因此,我们需要将其更改为 Asp.Net 框架版本 4.0。
要将 Asp.Net 框架从 2.0 更改为 4.0,请按照以下步骤操作。
这并不是应用程序无法运行的唯一原因,因为在对应用程序池进行了上述更改后,出现了下一个错误:
为了解决此错误,请再次转到应用程序池的高级设置,并将标识从 ApplicationPoolIdentity 更改为 Local System。
将应用程序池修改为使用 Local System 后,应用程序就可以在浏览器中运行了……:)
注意:尽管 IIS 错误取决于服务器/防火墙/网络或系统,因此很难假设您也会遇到与我相同的错误。请查看这些链接,它们在我工作中帮助我解决了一些 IIS 问题。
步骤 9:跨域支持
跨域支持的想法源于我尝试从Application.API解决方案中删除所有 HTML 页面,并创建了一个新的空白 Web 应用程序(Adventure.Website),其中仅包含.html、.Js 和 .css文件以及图像。此应用程序的结构如下所示:
将所有 URL 地址指向 IIS 上部署的代码。构建并运行应用程序后,输出为 null。
为了解决这个问题,我想特别感谢 Carlos 在 MSDN 上的博客 在 ASP.NET Web APIs 中实现 CORS 支持
引用“默认情况下,网页无法调用来自其他域的服务(API)。这是一种安全措施,用于防止跨站点伪造攻击,在这种攻击中,浏览到恶意网站可能会利用浏览器 Cookie 从用户先前登录过的良好站点获取数据(例如您的银行)。然而,在许多情况下,从不同站点获取数据是完全有效的场景,例如需要来自不同来源数据的聚合应用程序。”
代码设置
由于大小限制,我将代码分三个部分附加:
第 1 部分:Application.zip
第 2 部分:Packages.zip
第 3 部分:AdventureSite.zip
第 1 部分和第 3 部分是独立的应用程序。而对于第 2 部分,我们需要将其内容复制到 Application(第 1 部分)的 package 文件夹中。请参考下图:
结论
就是这样!希望您喜欢这篇文章。我不是专家,在撰写本文时(时间限制),也没有遵循完整的行业标准。
所有评论/投票都非常欢迎……:), whenever,我一有时间,就会尝试修改这个解决方案。您也可以加入/贡献到 CodePlex 上的这个项目。
感谢您的时间。