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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (43投票s)

2012 年 3 月 20 日

CPOL

11分钟阅读

viewsIcon

356356

downloadIcon

8384

本文旨在创建一个概念验证,以演示 ASP.NET Web API Beta 版本的可行性。

下载 Application.zip

下载 packages.zip

下载 AdventureSite.zip

或 

下载 CodePlex  

引言 

本文旨在创建一个概念验证,以演示 ASP.NET WebAPI 正式版本的可行性。

更新  

  • 修改代码以兼容 Visual Studio 2012
  • 将 Autofac 替换为 Castle Windsor 用于依赖注入   

先决条件 

要实现的目标 

  • 任何用户都应能查看产品
  • 产品将按类别过滤
  • 用户应能将产品添加到购物车
  • 只有注册用户才能提交购物车
  • 用户应能注册到系统中
  • 用户在系统中生成订单后将收到确认发票。

此 POC 的架构

我没有遵循任何特定的架构模式,尽管在设计此 POC 时,我尝试遵循一些关键设计原则,例如

我再次重复我的话尝试遵循…… :)。因此,欢迎所有评论/建议进行讨论,我随时准备接受更改,或者您可以自己加入 @CodePlex 来完成。此应用程序的初始骨架如下所示:

Architecture

逐步教程

步骤 1:创建基本应用程序结构布局

有关 ASP.NET WebAPI 的更多信息,请在此处观看视频和教程

添加一个名为Application的空白解决方案

添加一个名为Application.DAL的新项目(类库)

重复以上过程,再创建四个名为——的类库项目:

  • Application.Model
  • Application.Repository
  • Application.Common
  • Application.Service
现在,应用程序将显示如下屏幕截图

Solution 1

接下来,添加一个名为Application.API的新项目,作为 ASP.Net MVC 4 Web 应用程序。单击“确定”后,选择模板为 Web API

这将为我们的主解决方案添加一个功能齐全的 Web API 模板应用程序。

Solution 1

在解决方案资源管理器中右键单击 Application.API 项目,然后转到“属性”,然后为该服务应用程序分配一个端口(在本例中为 30000)。按 F5 键,我们就准备好了,如下所示,显示了一些虚拟数据:

Solution 1

步骤 2:准备 Application.DAL、Application.Repository 和 Application.Model 层

有关 Entity Framework 的更多信息,请查看@MSDN

Application.DAL项目添加实体数据模型。为此,请从数据模板列表中选择 ADO.NET 实体数据模型,并将其命名为AdventureWorks.edmx

选择“从数据库生成”并提供连接字符串以读取数据库架构。这将把 AdventureWorks.edmx 添加到当前项目中。右键单击实体模型表面的空白区域,然后单击“属性”(或在键盘上按 Alt+Enter)。现在将其代码生成策略设置为None

Solution 1

现在,在Application.Model解决方案中为 AdventureWorks 数据模型创建 POCO 实体。请参阅 MSDN 上有关如何使用T4 模板和 Entity Framework 完成此操作的文章。
然后,在 Application.Repositories 下创建所有必需的存储库。

Solution 1

要构建解决方案,请将Application.Model.dll的引用添加到Application.DAL.dllApplication.Repository.dll项目中,并将Application.DAL.dll的引用添加到Application.Repository.dll项目中。(有关参考,请参见下图)
在继续下一步(设计 Application.Service 和 Application.API 层)之前,让我们看看我们已经设计了什么

Solution 1

步骤 3:向 Application.Service 层添加文件

在向服务层添加文件之前,让我们回顾一下我们想要实现的目标。

  • 任何用户都应能查看产品
  • 产品将按类别过滤
  • 用户应能将产品添加到购物车
  • 只有注册用户才能提交购物车
  • 用户应能注册到系统中
  • 用户在系统中生成订单后将收到确认发票。

为了实现上述目标,我们需要向 Application.Service 添加三个服务类,如下图所示:

Solution 1

Solution 1

我不会深入细节,因为每个方法的实现都清晰易懂。

步骤 4:向 Application.API 层添加文件

为了使 Web API 能够被广泛的客户端访问,例如任何使用 JavaScript 的浏览器,或任何支持 .dll 文件的 Windows/Mobile 平台,我们需要添加继承自ApiController的控制器。 更多详细信息请参见此处

首先,我们删除模板添加的默认控制器(HomeController 和 ValuesController),然后根据下图添加三个新控制器:

Solution 1

同样,这些方法的实现没有什么特别之处,它们都只是将数据推送到服务层。

步骤 5:使用 Castle Windsor 将依赖注入与 WebAPI 集成 

实现非常直接且清晰。  

Application.API解决方案资源管理器中,右键单击“引用”节点,然后单击“管理 NuGet 程序包”

Solution 1

这将打开下面的屏幕。 

 

 单击“安装”,这将为解决方案添加以下两个引用:

 

创建一个 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 APIRegister控制器。 

现在我们需要让 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 : 将显示所有产品类别

Solution 1

https://:30000/api/products/2 : 将显示所有 ProductcategoryID 为 2 的产品

Solution 1

要检查其他方法,首先,我们将向当前解决方案添加一些视图。

步骤 6:向解决方案添加视图

删除 Views 文件夹中的所有默认文件和文件夹,然后添加三个 HTML 及其支持的 JavaScript 文件。对于此 POC,我们将使用 HTML 文件和一些 Jquery 库,例如:

jquery.tmpl.js : 用于创建基于模板的屏幕,以及

jquery.validationEngine.js : 用于验证表单数据

现在结构将如下所示:

Solution 1

有关 HTML 和 JS 文件中的详细代码,请参见代码。下一步,我将解决问题。

步骤 7:解决问题

在运行应用程序之前,右键单击 Products.htm 并单击“设为起始页”。现在按 F5 键。

Solution 1

这意味着 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();
                    }
                

接下来,让我们尝试将一些产品添加到购物车并结账。

Solution 1

单击结账后,它会重定向到 Checkout.htm 页面,并要求使用现有的 CustomerID 登录或注册新客户。
单击注册链接以创建新客户。

Solution 1

单击“注册”按钮后提交表单,并查看发布是否正常工作。再次,成功!!!

现在我们将尝试更新 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;
                    }
                

接下来,单击“提交订单”按钮,将先前选择的产品作为订单创建,并接收订单发票。

Solution 1

!!!万岁!!! 订单已在系统中创建,并生成了带有新采购订单的发票。但是,等等,我们发现了一个 Bug 

第 1 个问题已在 Asp.Net WebAPI 的最终版本中修复  

问题 1:日期格式有问题。

Solution 1

根据 Scott Hanselman 的帖子,这是一个 Bug,将在 ASP.Net WebAPI 的下一个版本中解决。这个问题可以使用 Henrik 的博客中提供的解决方案来解决。

Solution 1

问题 2:另一个问题是,当我尝试使用最后一个创建的客户结账时。错误是:

Solution 1

看到这个错误后,第一个想法是我们在 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。

Solution 1

Solution 1

要将 Asp.Net 框架从 2.0 更改为 4.0,请按照以下步骤操作。

Solution 1

Solution 1

这并不是应用程序无法运行的唯一原因,因为在对应用程序池进行了上述更改后,出现了下一个错误:

Solution 1

为了解决此错误,请再次转到应用程序池的高级设置,并将标识从 ApplicationPoolIdentity 更改为 Local System。

Solution 1

将应用程序池修改为使用 Local System 后,应用程序就可以在浏览器中运行了……:)

Solution 1

注意:尽管 IIS 错误取决于服务器/防火墙/网络或系统,因此很难假设您也会遇到与我相同的错误。请查看这些链接,它们在我工作中帮助我解决了一些 IIS 问题。

步骤 9:跨域支持

跨域支持的想法源于我尝试从Application.API解决方案中删除所有 HTML 页面,并创建了一个新的空白 Web 应用程序(Adventure.Website),其中仅包含.html、.Js 和 .css文件以及图像。此应用程序的结构如下所示:

Solution 1

将所有 URL 地址指向 IIS 上部署的代码。构建并运行应用程序后,输出为 null。

Solution 1

为了解决这个问题,我想特别感谢 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 上的这个项目。

感谢您的时间。

© . All rights reserved.