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

Master Chef (第一部分) ASP.NET Core MVC 结合 Fluent NHibernate 和 AngularJS

starIconstarIconstarIconstarIconstarIcon

5.00/5 (19投票s)

2016年9月21日

CPOL

12分钟阅读

viewsIcon

58713

downloadIcon

1198

在本文中,我想展示如何使用 ASP.NET Core MVC、Fluent Hibernate 和 Angular JS 构建一个单页应用程序 – MasterChef。

引言

ASP.NET 5 已被弃用,并更名为 ASP.NET Core 1.0。然而,虽然命名为新的、完全从头开始编写的 ASP.NET 框架“ASP.NET 5”是一个糟糕的主意,但有一个主要原因:5 使它看起来比 ASP.NET 4.6 更大、更好,并且取代了 ASP.NET 4.6。事实并非如此。所以 ASP.NET 5 现在是 ASP.NET Core 1.0,.NET Core 5 现在是 .NET Core 1.0。为什么是 1.0?因为这些是新的。整个 .NET Core 概念是新的。.NET Core 1.0 CLI 非常新。不仅如此,.NET Core 也不如完整的 .NET Framework 4.6 完整。

使用 ASP.NET Core 的一大新功能是跨平台兼容性。从这个版本开始,我们既可以在 Windows 上开发和运行 ASP.NET Core(这始终是事实),也可以在 Mac OS X 和 Linux 操作系统上进行开发和运行。

ASP.NET Core 和 MVC 是服务器端的东西,但正如我们之前提到的,在单页应用程序中,还有一个客户端组件。Angular 实际上是最流行的框架之一。它建立在 Web 技术之上——HTML、CSS 和 JavaScript——并遵循模型视图(某种东西)模式,这基本上允许我们创建表示层和业务逻辑之间解耦的应用程序。

NHibernate 是 Microsoft .NET 平台的一个对象关系映射 (ORM) 解决方案。它提供了一个将面向对象领域模型映射到传统关系数据库的框架。Fluent NHibernate 提供了 NHibernate 标准 XML 映射文件的替代方案。Fluent NHibernate 允许您使用强类型 C# 代码编写映射,而不是编写 XML 文档(*hbm.xml* 文件)。这可以轻松重构、提高可读性并使代码更简洁。

在本文中,我想展示如何使用 ASP.NET Core MVC、Fluent Hibernate 和 Angular JS 构建一个单页应用程序 – MasterChef。

Master Chef 菜谱数据模型 UML

在您的本地 SQL Express 中,只需创建一个名为“MasterChef”的数据库。然后运行 *sql* 文件夹下的 *schema.sql*。

在 Visual Studio 2015 Update 3 中创建 MasterChef 应用程序

要使用 ASP.NET Core,您需要更新 ASP.NET Web 工具。最新版本是 2.0.2,从此链接下载。

从 Visual C#/Web 中,选择 ASP.NET core Web Application (.NET Framework)。

ASP.NET Core 有两种应用程序

  1. ASP.NET Core .NET Framework 应用程序是在 Windows 上使用 .NET Framework 运行的应用程序。
  2. ASP.NET Core .NET Core 应用程序是使用 .NET Core 在 Windows、Linux 和 OS X 上运行的跨平台应用程序。

选择“Empty”模板并取消选中“Host in cloud”。

看看 ASP.NET Core Web 解决方案结构。它创建一个“src”文件夹,实际项目位于“src”文件夹下。在此 *src* 文件夹内有一个特殊的文件夹——*wwwroot*,它将保存我们所有活动的 Web 文件。所以任何 HTML、我们最终的 *angularapp.js*、任何其他被压缩的脚本、图片资源或类似的东西,都会放在这里。但是我们所有的源代码——实际运行这个 ASP.NET 5 MVC 6 Angular 应用程序的代码——都永远不会放在 *wwwroot* 文件夹中。

添加 Fluent NHibernate 数据模型

1) 安装 Fluent NHibernate

从 Nuget 程序包管理器添加 Fluent NHibernate 程序包。

打开 *project.json* 并添加“FluentNHibernate”: “2.0.3”

2) 添加数据模型类

在我们的解决方案中,创建一个“Models”文件夹。将 RecipeRecipeStepRecipeItem 模型类添加到 *Models* 文件夹。

public class Recipe
 {
        public virtual Guid RecipeId { get; set; }
        public virtual string Name { get; set; }
        public virtual string Comments { get; set; }
        public virtual DateTime ModifyDate { get; set; } 
        public virtual IList<RecipeStep>  Steps{ get; set; }
}

public class RecipeStep
 {
        public virtual Guid RecipeStepId { get; set; }
        public virtual int StepNo { get; set; }
        public virtual string Instructions { get; set; }
        public virtual IList<RecipeStep> RecipeItems { get; set; }
 }

public class RecipeItem
{
        public virtual Guid ItemId { get; set; }
        public virtual string Name { get; set; }
        public virtual float Quantity { get; set; }
        public virtual string MeasurementUnit { get; set; }
 }

3) 添加 Fluent NHibernate 映射类

如前所述,NHibernate 映射 XML(*.hbm*)被 Fluent Hibernate 中的映射类取代。所以我们需要为每个数据模型类提供映射类。

public class RecipeMap : ClassMap<Recipe>
    {
        public RecipeMap()
        {
            Id(x => x.RecipeId);
            Map(x => x.Name);
            Map(x => x.Comments);
            Map(x => x.ModifyDate);
            HasMany(x => x.Steps).KeyColumn("RecipeId").Inverse().OrderBy("StepNo Asc");
            Table("Recipes");
        }
}

如何映射一个列表?在 RecipeMap 类中,使用 HasMany(x=>x.Steps).KeyCoumn("RecipeId")RecipeSteps 表中的引用键。另外,菜谱步骤需要按 StepNo 排序,这使用了 OrderBy。在 RecipeStepMap 类中发生相同的映射。

public class RecipeStepMap : ClassMap<RecipeStep>
    {
        public RecipeStepMap()
        {
            Id(x => x.RecipeStepId);

            Map(x => x.StepNo);
            Map(x => x.Instructions);
            HasMany(x => x.RecipeItems).KeyColumn("RecipeStepId").Inverse();
            Table("RecipeSteps");
        }
}
  public class RecipeItemMap : ClassMap<RecipeItem>
    {
        public RecipeItemMap()
        {
            Id(x => x.ItemId);
            Map(x => x.Name);
            Map(x => x.Quantity);
            Map(x => x.MeasurementUnit);
            Table("RecipeItems");
        }
}

4) 添加存储库类

我们使用存储库模式来分离检索数据并将其映射到实体模型中的逻辑与对模型进行操作的业务逻辑。业务逻辑应该独立于数据源层所构成的数据类型。

存储库在数据源层和应用程序的业务层之间进行中介。它查询数据源中的数据,将数据从数据源映射到业务实体,并将业务实体中的更改持久化到数据源。存储库将业务逻辑与底层数据源的交互分离开来。

Repository 类中,我们需要配置 Fluent NHibernate 会话。

_sessionFactory = Fluently.Configure()
                    .Database(MsSqlConfiguration.MsSql2012
                    .ConnectionString("Server=.\\sqlexpress; 
                     Database=MasterChef; Integrated Security=SSPI;"))
                    .Mappings(m => m
                    .FluentMappings.AddFromAssemblyOf<Repository>())
                    .BuildSessionFactory();
                _session = _sessionFactory.OpenSession();

您可以通过 .FluentMappings.Add(…) 添加一个映射类列表。另外,您可以通过 .FluentMappings.AddFromAssembly(…) 添加一个程序集中的所有映射类。

添加 Web API 控制器

1) 安装 ASP.NetCore.Mvc

我们在 Nuget 程序包管理器、*project.json* 中添加 Asp.NetCore.Mvc 程序包。

2) 添加 API RecipesController

创建一个“api”文件夹,然后右键单击 API 文件夹添加一个新项。在 ASP.NET 中,选择 Web API Controller Class 模板。我们将类命名为 *RecipesController.cs*。

RecipesController 类中,我们设置了处理基本 CRUD 请求的函数。我们这里有一个 GET 请求,用于获取所有菜谱。我们这里还有一个 Get 函数,它接受一个 id,以便用户可以请求我们返回的特定菜谱。我们这里还有更多函数,例如 POST,它允许用户创建新菜谱。还有 PUT,我们可以更新现有菜谱。最后是 DELETE,可以删除特定菜谱。所以所有这些都是 Web API controller 类提供的样板代码,我们将在此基础上进行扩展以创建我们的实际应用程序。

RecipesController 类中,我们添加 _repository 成员来处理数据库事务。

HttpGet 获取所有菜谱。

    	[HttpGet]
        public IEnumerable<Recipe> Get()
        {
            return _repository.GetAllRecipes();
        }

HttpGet(id) 获取特定菜谱。

[HttpGet("{id}")]
        public IActionResult Get(Guid id)
        {
            var recipe = _repository.GetRecipe(id);
            if (recipe != null)
                return new ObjectResult(recipe);
            else
                return new NotFoundResult();

        }

HttpPost(Recipe) 添加或更新菜谱。如果输入的菜谱 id 为空,则添加新菜谱;否则,则更新现有菜谱。

       [HttpPost]
        public IActionResult Post([FromBody]Recipe recipe)
        {
            if (recipe.RecipeId == Guid.Empty)
            {
                return new ObjectResult(_repository.AddRecipe(recipe));
            }
            else
            {
                var existingOne = _repository.GetRecipe(recipe.RecipeId);
                existingOne.Name = recipe.Name;
                existingOne.Comments = recipe.Comments;
                _repository.UpdateRecipe(existingOne);
                return new ObjectResult(existingOne);
            }
        }

HttpPut(id, recipe) 更新菜谱。

       [HttpPut("{id}")]
        public IActionResult Put(Guid id, [FromBody]Recipe recipe)
        {
            var existingOne = _repository.GetRecipe(recipe.RecipeId);
            existingOne.Name = recipe.Name;
            existingOne.Comments = recipe.Comments;
            _repository.UpdateRecipe(recipe);
            return new ObjectResult(existingOne);
        }

HTTPDelete 删除菜谱。

      [HttpDelete("{id}")]
        public IActionResult Delete(Guid id)
        {
            _repository.DeleteRecipe(id);
            return new StatusCodeResult(200);
        }

3) 配置 MVC

添加 recipes controller 后,我们期望 https:///api/recipes 返回所有菜谱。让我们试试。

不起作用。为什么?这是因为我们还没有配置 MVC。

转到 *Startup.cs*。

ConfigureServices(…) 中添加 services.AddMvc(),并在 Configure(…) 中添加 app.UseMvc

public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        // This method gets called by the runtime. 
        // Use this method to configure the HTTP request pipeline.
        public void Configure
         (IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole();
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseMvc();
            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("Hello World!");
            });
        }

添加这些之后,让我们再试一次,点击菜单栏中的“IIS Express”。

现在 URL 可以访问了,并且返回了一个 JSON 结果。

Grunt

1) 添加 Grunt

添加一个名为“scripts”的新文件夹,用于存放我们所有的脚本文件。正如我们之前提到的,所有活动的脚本和 HTML 都位于 *wwwroot* 文件夹下。所以现在,我们需要安装 Grunt 来帮助我们自动将“scripts”文件夹下的所有脚本安装到 *wwwroot* 文件夹。最终,我们希望 Grunt 帮助我们监视此文件夹,合并并压缩其中的所有脚本,然后再将结果移至我们的活动 *wwwroot* 文件夹。

要安装 Grunt,我们将使用 ASP.NET Core 内置的 NPM 包管理器支持。这 بالإضافة 到对 Bower 包管理器和 NuGet 包管理器的支持。

右键单击我的项目以添加一个新项。我将转到 Client-side 并滚动直到看到 NPM Configuration File 选项,*package.json* 是一个合适的名称。

打开 *package.json*,将 grunt 添加到 dependencies。

{
	"version": "1.0.0",
	"name": "asp.net",
	"private": true,
    "devDependencies": {
    "grunt": "1.0.1",
    "grunt-contrib-uglify": "2.0.0",
    "grunt-contrib-watch": "1.0.0",
    "bower": "1.7.9"
  }
}

2) 配置 Grunt

接下来是实际配置 Grunt。右键单击项目,转到 **Add** - **New Item**,然后在 **Client-side** 部分选择 **Grunt Configuration file**。将名称保留为 *gruntfile.js* 并 **Add** 该文件。

我们单独配置 uglify 插件和 watch 插件。对于 uglify 插件(它帮助我们将 *scripts* 文件夹中的所有 JavaScript 文件压缩成另一个文件——*app.js*),该文件将位于 *wwwroot* 文件夹中。第二项配置是设置监视 *scripts* 文件夹。并且任何时候该文件夹中的任何 JavaScript 文件发生更改,它都会自动运行 uglify,确保我们在 *wwwroot* 文件夹的 *app.js* 中拥有最新版本的脚本。此 Grunt 配置文件中的最后一项是注册要运行的任务。

打开 *Gruntfile.js*

module.exports = function (grunt) {
    grunt.loadNpmTasks("grunt-contrib-uglify");
    grunt.loadNpmTasks("grunt-contrib-watch");
    grunt.initConfig({
        uglify: {
            my_target: {
                files: {
                    'wwwroot/app.js': ['scripts/app.js', 'scripts/**/*.js']
                }
            }
        },
        watch: {
            scripts: {
                files: ['scripts/**/*.js'],
                tasks: ['uglify']
            }
        }
    });
    grunt.registerTask('default', ['uglify', 'watch']);
};

现在,我们应该能够实际运行此文件了。转到 **View** 菜单 - Other Windows。选择将在底部显示的 Task Runner Explorer。刷新它以查看保存的任务。然后运行默认任务。现在您可以看到输出。现在,结果是我们没有向 *wwwroot/app.js* 写入任何内容。这正是我们所期望的,因为我们在脚本文件中没有任何可以压缩并从中创建 *app.js* 的脚本。

Angular JS

1) 安装 Angular JS

我们使用 bower 包管理器来获取我们的客户端 Angular JavaScript 文件。所以第一步是右键单击我们的项目来添加一个新项。在 Client-side 中选择一个 Bower Configuration File,“*bower.json*”。

完成后,*bower.json* 不会在我们的解决方案中显示。发生了什么?我不知道。这应该是 Visual Studio 的一个 bug,需要 Microsoft 来修复。目前,我们必须从文件系统打开 *bower.json*。

在 *bower.json* 中,将“jquery”、“bootstrap”、“angular”、“angular-route”和“angular-resource”添加到 dependencies 部分。

{
	"name": "asp.net",
	"private": true,
    "dependencies": {
    "jquery": "3.1.0",
    "bootstrap": "3.1.0",
    "angular": "1.5.8",
    "angular-route": "1.5.8",
    "angular-resource": "1.5.8"
  }

保存后,Visual Studio 会自动开始恢复所有这些程序包。

恢复完成后,Visual Studio 会将这些程序包安装在 *wwwroot\lib* 文件夹下。

2) 添加 index.html

现在我们需要在 *wwwroot* 文件夹下添加一个 index.html 作为我们的主页。

我们创建一个非常简单的 *index.html*,确保它被 ASP.NET Core 使用。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
    <h1>Master Chef Recipes</h1>
</body>
</html>

通过单击“IIS Express”启动我们的 Web 应用程序。

它似乎不起作用。这是因为我们还没有告诉 ASP.NET core 使用静态文件。

转到 *startup.cs*。

Configure(…) 方法中添加以下两行:

app.UseDefaultFiles();
app.UseStaticFiles();

然后再次启动我们的 Web 应用程序。

现在它奏效了。

3) 添加 Angular JS App 模块

现在我们要创建一个 Angular 模块,它将成为我们的应用程序。记住我们将所有 JavaScript 都放在“scripts”文件夹下。所以右键单击我们项目中的“scripts”文件夹。添加一个新项。在 Client-side 模板部分,选择 AngularJs Module。默认名称是 *app.js*。

在 *app.js* 中,将应用程序名称更改为“masterChefApp”。

(function () {
    'use strict';

    angular.module('masterChefApp', [
        // Angular modules 
        //'ngRoute'

        // Custom modules 

        // 3rd Party Modules
        
    ]);
})();

4) 添加 Service Factory

从 Angular JS,我们需要某种方式调用服务器 Web API。这可以通过服务完成。所以我们创建一个服务工厂来发出 HTTP 调用。

首先,我们在 *scripts* 下创建一个“service”文件夹。然后右键单击“service”添加一个新项。在 Client Side 中,选择 AngularJs Factory 模板。将名称更改为 *recipesFactory.js*。

在 *recipesFactory.cs* 中,将 app 更改为 masterChefApp,然后在 getData() 中调用 $http.get(‘api/recipes/’)

(function () {
    'use strict';

    angular
        .module('masterChefApp')
        .factory('recipesFactory', recipesFactory);

    recipesFactory.$inject = ['$http'];

    function recipesFactory($http) {
        var service = {
            getData: getData
        };

        return service;

        function getData() {
            return $http.get('/api/recipes');
        }
    }
})();

5) 添加 Angular JS Controller

现在我们想创建一个客户端控制器来在浏览器中显示菜谱。

首先,我们在 scripts 下创建一个 controller 文件夹。然后,右键单击 controller,添加一个新项。在 Client-Side 中,选择使用 $scope 模板的 AngularJs controller。将名称更改为 *recipesController.js*。

在 controller 函数的函数体内,设置一个作用域变量来调用 recipes。我们通过使用 recipesFactory 及其 getData 函数来设置它。

function () {
    'use strict';

    angular
        .module('masterChefApp')
        .controller('recipesController', recipesController);
     

    recipesController.$inject = ['$scope', 'recipesFactory'];

    function recipesController($scope, recipesFactory) {
            $scope.recipes = [];
            recipesFactory.getData().success(function (data) {
                $scope.recipes = data;
            }).error(function (error) {
                //log errors
        });
    }
})();

保存后,您可以查看 *wwwroot* 文件夹,一个压缩的 *app.js* 已自动创建,其中包含 *app.js*、*recipesFactory.js* 和 *recipesController.js* 中的所有脚本。

6) 更改 index.html 以使用 Angular 模块

第一件事是使用 ng-app 指令来激活我们的 Angular 应用程序。所以我们必须确保在使用 ng-app 指令时使用我们在 *app.js* 中定义的相同名称,即 masterChefApp。另外,我们需要导入一些我们需要的脚本文件。最后,引用 *app.js*——您可以看到所有这些 JavaScript 文件都在我的 *wwwroot* 文件夹中。继续,在我的 <body> 标签上,使用另一个指令。这就是 ng-cloak 指令。它将保持 body 隐藏,直到 Angular 完全加载数据并呈现我的模板。在 <body> 内部,定义一个 div。使用 ng-controller 指令来指示这个 div

<!DOCTYPE html>
<html ng-app="masterChefApp">
<head>
    <base href="/">
    <meta charset="utf-8" />
    <title>Master Chef Recipes</title>
    <script src="lib/angular/angular.min.js"></script>
    <script src="lib/angular-resource/angular-resource.min.js"></script>
    <script src="lib/angular-route/angular-route.min.js"></script>
    <script src="app.js"></script>
    <link href="lib/bootstrap/dist/css/bootstrap.min.css" 
    rel="stylesheet" media="screen">
</head>
<body ng-cloak>
    <div ng-controller="recipesController">
        <h1>Master Chef Recipes</h1>
        <ul>
            <li ng-repeat="recipe in recipes">
                <p> {{recipe.name}} - {{recipe.comments}}</p>
                <ul>
                    <li ng-repeat="step in recipe.steps">
                        <p> step {{step.stepNo}} : {{step.instructions}}</p>
                        <ul>
                            <li ng-repeat="item in step.recipeItems">
                                <p> {{item.name}} 
                                    {{item.quantity}} {{item.measurementUnit}}</p>
                            </li>
                        </ul>
                    </li>
                </ul>
            </li>
        </ul>
    </div>
</body>
</html>

好的。现在,通过单击 **IIS Express** 来运行它。

浏览器上显示了两个 master chef 菜谱。一个是 Honey Chicken,另一个是 Mongolian Lamb。非常容易学习,就像 Angular JS 一样。

Angular Resource 让 Master Chef 更好

recipesFactory 中,我们显式调用 $http.get。它有效。但是我们可以通过 ngResource 使它变得更简单、更好。ngResource 模块通过 $resource 服务提供与 RESTful 服务的交互支持。在 $resource 服务中,所有 http get、post、put 和 delete 操作都已内置。您只需传递 URL,无需显式调用它们。

首先在 *app.js* 中,我们注册一个自定义模块,recipesService

(function () {
    'use strict';

    angular.module('masterChefApp', [
        // Angular modules 
        //'ngRoute'

        // Custom modules 
        'recipesService'
        // 3rd Party Modules
        
    ]);
})();

然后添加另一个 AngularJS Factory 类,我们将其命名为 *recipesService.cs*。

在 *recipesService.cs* 中,我们注入 ngresource 并传递“/api/recipes/:id”。

(function () {
    'use strict';

    var recipesService = angular.module('recipesService', ['ngResource']);

    recipesService.factory('Recipe', ['$resource', function ($resource) {
        return $resource('/api/recipes/:id');
    }]);

})();

在 *recipesController.cs* 中,我们可以直接使用 Recipe.query()

(function () {
    'use strict';

    angular
        .module('masterChefApp')
        .controller('recipesController', recipesController);

    recipesController.$inject = ['$scope', 'Recipe'];

    function recipesController($scope, Recipe) {
        $scope.recipes = Recipe.query();
    }
})();

现在我们不再需要 *recipesFactory.cs* 了。我们将其删除。然后再次运行我们的 master chef Web 应用程序。

它运行得非常完美。

结论

在本文中,我向您展示了如何集成 ASP.NET Core MVC、Fluent NHibernate 和 AngularJS 来构建一个简单的 Web 应用程序。在Master Chef 第二部分中,我将讨论 Angular route 并使用 Angular route 构建一个 SPA CRUD(列表、添加、编辑、删除)应用程序。另外,我将向您展示如何使用 bootstrap 样式使您的 Web 应用程序看起来更好。

历史

  • 2020年2月6日:初始版本
© . All rights reserved.