第十章 SportsStore:创建一个RESTful Web服务

 
  (译者注:本章中Web服务指Web API,而客户端有时是Web前端程序,有时可以是桌面客户端)
 
  使用Angular或React等框架编写前端时,后端Web服务是非常有用的(为前端提供数据)。前端程序在浏览器中运行,不需要SportsStore提供HTML内容。前端程序与ASP.NET Core MVC应用程序使用HTTP请求进行交互,并使用JSON(Js对象符号)标准格式接收数据。
 
  在本章中,我将通过添加RESTful web服务来完成SportsStore应用程序,该服务可以为前端提供对应用程序数据的访问。ASP.NET Core MVC能极好地支持创建RESTful web服务,但是要小心使用EF Core以获得正确的查询结果(避免影响性能的事情发生)。
 
  对于web服务应该如何工作,没有硬性规定,但是最常用的方式是采用REST模式(Representational State Transfer 表述性状态传递)。世界上没有权威的REST规范,也没有对RESTful模式web服务的组成达成共识,但是web服务广泛使用了一些共同的主题。
     
  REST的核心前提是:web服务通过URL和HTTP方法(如GET和POST)的组合来定义API,这是唯一得到广泛认同的地方。HTTP方法指定了操作的类型(GET、POST等),而URL指定操作哪个或者哪些数据对象。
 
 
10.1 准备工作
在本章,我继续使用上章的SportsStore项目。在SportsStore项目文件夹中运行清单10-1所示的命令来重置数据库。
Listing 10-1. Resetting the Example Application Database
dotnet ef database drop --force
dotnet ef database update

使用dotnet run启动应用程序,导航到http://localhost:5000,单击Seed Data按钮,然后单击Production Seed。会产生少量测试数据存到数据库,如下图10-1所示。

小贴士: 您可以从本书的github知识库中下载本章的SportsStore项目和其他章节的项目:
 
 
10.2 创建Web服务
接下来我将构建一个简单的web服务,该服务提供SportsStore应用程序存储的Product数据。
 
10.2.1 创建仓储
在向应用程序中添加web服务时,最好创建一个单独的存储库,因为客户端程序执行的查询可能与常规ASP.NET Core MVC应用程序执行的不同。对于SportsStore web服务,我在Models文件夹下添加了一个名为IWebServiceRepository.cs的文件。并使用它来定义如清单10-2所示的接口。
1 //Listing 10-2. The Contents of the IWebServiceRepository.cs File in the Models Folder
2 namespace SportsStore.Models
3 {
4     public interface IWebServiceRepository
5     {
6         object GetProduct(long id);
7     }
8 }

 

    我从GetProduct方法开始,它将接受一个主键值并从数据库返回相应的Product对象。注意我代码中GetProduct方法返回一个object,而不是一个Product,因此我可以演示如何从web服务中呈现EF Core查询的数据。
     对于存储库实现类,我添加了一个名为WebServiceRepository.cs的文件。并使用它来定义如清单10-3所示的类。
 1 //Listing 10-3. The Contents of the WebServiceRepository.cs File in the Models Folder
 2 using System.Linq;
 3 namespace SportsStore.Models
 4 {
 5     public class WebServiceRepository : IWebServiceRepository
 6     {
 7         private DataContext context;
 8         public WebServiceRepository(DataContext ctx) => context = ctx;
 9         public object GetProduct(long id)
10         {
11             return context.Products.FirstOrDefault(p => p.Id == id);
12         }
13     }
14 }

 

    该类的GetProduct方法调用了FirstOrDefault LINQ方法来查询首个具有指定Id值,存储在数据库中的对象。创建web服务时,处理“请求不存在的数据”非常重要,这就是为啥我使用FirstOrDefault方法。
    为了在DI(依赖注入)中注册存储库及其实现类,我将清单10-4中所示的语句添加到Startup类中。
 1 //Listing 10-4. Configuring the Repository in the Startup.cs File in the SportsStore Folder
 2 using System;
 3 using System.Collections.Generic;
 4 using System.Linq;
 5 using System.Threading.Tasks;
 6 using Microsoft.AspNetCore.Builder;
 7 using Microsoft.AspNetCore.Hosting;
 8 using Microsoft.AspNetCore.Http;
 9 using Microsoft.Extensions.DependencyInjection;
10 using SportsStore.Models;
11 using Microsoft.EntityFrameworkCore;
12 using Microsoft.Extensions.Configuration;
13 namespace SportsStore
14 {
15     public class Startup
16     {
17         public Startup(IConfiguration config) => Configuration = config;
18         public IConfiguration Configuration { get; }
19         public void ConfigureServices(IServiceCollection services)
20         {
21             services.AddMvc();
22             services.AddTransient<IRepository, DataRepository>();
23             services.AddTransient<ICategoryRepository, CategoryRepository>();
24             services.AddTransient<IOrdersRepository, OrdersRepository>();
25             services.AddTransient<IWebServiceRepository, WebServiceRepository>();
26             string conString = Configuration["ConnectionStrings:DefaultConnection"];
27             services.AddDbContext<DataContext>(options =>
28             options.UseSqlServer(conString));
29             services.AddDistributedSqlServerCache(options =>
30             {
31                 options.ConnectionString = conString;
32                 options.SchemaName = "dbo";
33                 options.TableName = "SessionData";
34             });
35             services.AddSession(options =>
36             {
37                 options.Cookie.Name = "SportsStore.Session";
38                 options.IdleTimeout = System.TimeSpan.FromHours(48);
39                 options.Cookie.HttpOnly = false;
40             });
41         }
42         public void Configure(IApplicationBuilder app, IHostingEnvironment env)
43         {
44             app.UseDeveloperExceptionPage();
45             app.UseStatusCodePages();
46             app.UseStaticFiles();
47             app.UseSession();
48             app.UseMvcWithDefaultRoute();
49         }
50     }
51 }
 
10.2.2 创建API控制器
在应用程序中使用ASP.NET Core MVC的API控制器(APIController),会使添加web服务变得很容易。我添加了一个名为ProductValuesController.cs的文件到Controllers文件夹,并添加清单10-5显示的代码。
 1 //Listing 10-5. The Contents of the ProductValuesController.cs File in the Controllers Folder
 2 using Microsoft.AspNetCore.Mvc;
 3 using SportsStore.Models;
 4 namespace SportsStore.Controllers
 5 {
 6     [Route("api/products")]
 7     public class ProductValuesController : Controller
 8     {
 9         private IWebServiceRepository repository;
10         public ProductValuesController(IWebServiceRepository repo)
11         => repository = repo;
12         [HttpGet("{id}")]
13         public object GetProduct(long id)
14         {
15             return repository.GetProduct(id) ?? NotFound();
16         }
17     }
18 }

 

     这个新控制器类名是ProductValuesController,它按命名约定有一个单词Values,暗示控制器将返回数据给前端,而不是返回HTML文本。web服务控制器的另一个约定是:在路由中创建一个单独的部分,专门用于处理数据请求。 即Web API最常见的URL是:以/api开头,后面写数据类名的复数形式(如Products)。 也就是说,用于处理Product对象的web服务,应该将HTTP请求发送到如下URL: /api/products  因为我将Route路由特性配置为如下所示:
...
[Route("api/products")]
...

  控制器目前只定义了一个操作(Action方法)就是GetProduct方法,该方法根据主键Id值返回单个Product对象,该Action方法用HttpGet特性修饰,HttpGet特性指定让 ASP.NET Core MVC使用该操作(Action方法)处理HTTP GET请求。

...
[HttpGet("{id}")]
public Product GetProduct(long id) {
...

上面[HttpGet("{id}")]特性的参数延续了Route特性定义的URL,就可以通过 /api/products/{id}这样格式的URL访问GetProduct方法(并传递Id值给形参)。Web服务的Action方法返回.NET对象,并自动序列化(JSON)之后发给前端(或客户端)。

当前端(客户端)请求的对象不存在时,为了防止web服务将null序列化来响应请求,Action方法可以使用空合并运算符调用NotFound方法,如下所示:

...
return repository.GetProduct(id) ?? NotFound();
...

这将返回404 - Not Found状态代码,它向前端(客户端)发出请求不能被满足的信号。

 

10.2.3 测试Web服务
要测试新的web服务,请使用dotnet run启动应用程序。打开一个新的PowerShell窗口并执行清单10-6所示的命令,向API控制器发送一个HTTP get请求。
注意: 在本章中,我使用powerShell Invoke-RestMethod命令来模拟来自客户端应用程序的请求。
1 Listing 10-6. Testing the Web Service
2 Invoke-RestMethod http://localhost:5000/api/products/1 -Method GET | ConvertTo-Json

HTTP请求被路由到ProductValues控制器上的GetProduct方法,该方法使用Find方法从数据库检索产品对象。然后Product对象被序列化成JSON格式文本(如下所示)返回给浏览器,浏览器将显示它接收到的数据(前端或客户端程序负责如何展示该数据)。

1 {
2     "id":1,
3     "name":"Kayak",
4     "description":"A boat for one person",
5     "purchasePrice":200.00,
6     "retailPrice":275.00,
7     "categoryId":1,
8     "category":null
9 }

请注意,category导航属性被设置为null,因为我没有要求Entity Framework Core加载Product对象的外键关联的数据。

 
10.2.4 投影排除null导航属性
    发送数据包含null属性会让客户端产生误会,因为这没讲清楚:是不存在外键关联数据?还是有外键关联数据,但是我故意不发给你(即不放到响应中)?
    上节返回的JSON数据,您肯定能理解categoryId值为1,这表明必存在外键关联的数据(只是服务器不想发给你Category的具体内容),如果category值为0说明没指定外键关联数据,但是期望客户端进行这种区分是不对的,尤其是服务器和客户端往往是由不同团队开发的。 如果不想包含外键关联的属性,可以将查询结果使用LINQ的Select方法投射一个对象来避免混淆,该投影对象排除了外键和导航属性,如清单10-7所示。
译者述:简单说应该把服务器上用的实体类(ASP.NET Core MVC、API和EF Core在用)与发给客户端的DTO区分开。服务器可以、也应该将查询到的数据处理后、投影之后发给客户端。而不仅是图省事就直接将实体类发出去,或将外键关联属性设置为null。有时客户端需要更多外键数据时,投影还真的需要把外键关联对象都包含到响应里去。(所以一切应该按照具体需求来决定)
 1 //Listing 10-7. Excluding Properties in the WebServiceRepository.cs File in the Models Folder
 2 using System.Linq;
 3 namespace SportsStore.Models
 4 {
 5     public class WebServiceRepository : IWebServiceRepository
 6     {
 7         private DataContext context;
 8         public WebServiceRepository(DataContext ctx) => context = ctx;
 9         public object GetProduct(long id)
10         {
11             return context.Products
12                 .Select(p => new
13                {
14                  Id = p.Id,
15                  Name = p.Name,
16                  Description = p.Description,
17                  PurchasePrice = p.PurchasePrice,
18                  RetailPrice = p.RetailPrice
19                })
20                .FirstOrDefault(p => p.Id == id);
21               //注意,如果先First..再Select,导致SQL语句查询所有列。
22               //先Select再First使SQL语句中只有想要的列。
23         }
24     }
25 }

我使用LINQ Select方法选择要包含在结果中的属性,并使用FirstOrDefault方法来选择具有指定主键值的对象。使用dotnet run重新启动应用程序,并使用新的PowerShell窗口执行清单10-8所示的命令。

Listing 10-8. Requesting a Product Object
Invoke-RestMethod http://localhost:5000/api/products/1 -Method GET | ConvertTo-Json

这个请求的结果是下面的JSON数据,它排除了相关的数据属性:

1 {
2     "id":1,
3     "name":"Kayak",
4     "description":"A boat for one person",
5     "purchasePrice":200.00,
6     "retailPrice":275.00
7 }

如果检查应用程序生成的日志消息,您将看到EF Core只从数据库中请求指定的属性。

1 ...
2 SELECT TOP(1) [p].[Id], [p].[Name], [p].[Description], [p].[PurchasePrice],
3 [p].[RetailPrice]
4 FROM [Products] AS [p]
5 WHERE [p].[Id] = @__id_0
6 ...
 
 
10.2.5 在Web服务响应中Including关联数据
如果希望在web服务响应中包含外键关联的数据,则需要小心。为了演示这个问题,我修改了存储库使用的查询,使其使用Include方法选择与Product对象外键关联的Category对象,如清单10-9所示。
 1 //Listing 10-9. Including Related Data in the WebServiceRepository.cs File in the Models Folder
 2 using System.Linq;
 3 using Microsoft.EntityFrameworkCore;
 4 namespace SportsStore.Models
 5 {
 6     public class WebServiceRepository : IWebServiceRepository
 7     {
 8         private DataContext context;
 9         public WebServiceRepository(DataContext ctx) => context = ctx;
10         public object GetProduct(long id)
11         {
12             return context.Products.Include(p => p.Category)
13               .FirstOrDefault(p => p.Id == id);
14         }
15     }
16 }
要查看此代码隐藏的问题,请使用dotnet run启动应用程序并使用单独的PowerShell窗口执行清单10-10所示的命令。
Listing 10-10. Requesting Related Data
Invoke-RestMethod http://localhost:5000/api/products/1 -Method GET | ConvertTo-Json

您将看到以下错误消息,而不是JSON数据:

Invoke-RestMethod : Unable to read data from the transport connection: The connection was closed.
 
ASP.NET Core MVC使用一个名为Json.NET的包来做序列化,并且需要更改配置来揭示错误的原因,如清单10-11所示。
 1 //Listing 10-11. Changing the Serializer Configuration in the Startup.cs File in the SportsStore Folder
 2 using System;
 3 using System.Collections.Generic;
 4 using System.Linq;
 5 using System.Threading.Tasks;
 6 using Microsoft.AspNetCore.Builder;
 7 using Microsoft.AspNetCore.Hosting;
 8 using Microsoft.AspNetCore.Http;
 9 using Microsoft.Extensions.DependencyInjection;
10 using SportsStore.Models;
11 using Microsoft.EntityFrameworkCore;
12 using Microsoft.Extensions.Configuration;
13 using Newtonsoft.Json;
14 namespace SportsStore
15 {
16     public class Startup
17     {
18         public Startup(IConfiguration config) => Configuration = config;
19         public IConfiguration Configuration { get; }
20         public void ConfigureServices(IServiceCollection services)
21         {
22             services.AddMvc().AddJsonOptions(opts =>
23                 opts.SerializerSettings.ReferenceLoopHandling
24                     = ReferenceLoopHandling.Serialize);
25             services.AddTransient<IRepository, DataRepository>();
26             services.AddTransient<ICategoryRepository, CategoryRepository>();
27             services.AddTransient<IOrdersRepository, OrdersRepository>();
28             services.AddTransient<IWebServiceRepository, WebServiceRepository>();
29             string conString = Configuration["ConnectionStrings:DefaultConnection"];
30             services.AddDbContext<DataContext>(options =>
31             options.UseSqlServer(conString));
32             services.AddDistributedSqlServerCache(options =>
33             {
34                 options.ConnectionString = conString;
35                 options.SchemaName = "dbo";
36                 options.TableName = "SessionData";
37             });
38             services.AddSession(options =>
39             {
40                 options.Cookie.Name = "SportsStore.Session";
41                 options.IdleTimeout = System.TimeSpan.FromHours(48);
42                 options.Cookie.HttpOnly = false;
43             });
44         }
45         public void Configure(IApplicationBuilder app, IHostingEnvironment env)
46         {
47             app.UseDeveloperExceptionPage();
48             app.UseStatusCodePages();
49             app.UseStaticFiles();
50             app.UseSession();
51             app.UseMvcWithDefaultRoute();
52         }
53     }
54 }

使用dotnet run重新启动应用程序。这次我们打开一个新的浏览器窗口并请求http://localhost:5000/api/products/1 URL。当浏览器请求数据时,您将看到应用程序报告以下错误:

...
Process is terminating due to StackOverflowException
...

浏览器显示的内容揭示了发生了什么。尽管您请求的URL针对单个Product对象及其外键相关Category,但应用程序在崩溃之前发送了大量数据。

 1 ...
 2 {  "id":1,"name":"Kayak","description":"A boat for one person",
 3     "purchasePrice":200.00,"retailPrice":275.00,"categoryId":1,
 4     "category": {"id":1,"name":"Watersports",
 5     "description":"Make a splash",
 6     "products":[{"id":1,"name":"Kayak",
 7     "description":"A boat for one person",
 8     "purchasePrice":200.00,"retailPrice":275.00,
 9     "categoryId":1,
10     "category":{"id":1,"name":"Watersports",
11     "description":"Make a splash",
12     "products":[{"id":1,"name":"Kayak","description":"A boat for one
13 ...
产品对象的Category导航属性指向其相关的类别对象,而类别对象的产品导航属性又包括产品对象,以此类推,这是一个无限循环。这是由一个名为fixing up的EF Core特性引起的,在该特性中,从数据库接收到的对象用作导航属性的值。
 
我在第14章中详细描述了fixing up工作过程,并解释了它啥时候有用,但是对于RESTful web服务,这个特性会带来问题,因为JSON序列化器只是继续跟踪导航属性,直到应用程序堆栈溢出。
 
清单10-11中修改的配置,告诉JSON序列化器即使已经序列化了对象,也要继续跟踪引用。默认行为是在检测到循环时抛出异常,这就是清单10-10中错误的原因。
 
10.2.5.1 避免在关联数据中循环引用
没有办法禁用 fixing up功能,所以避免外键关联引起无限循环的最佳解决方案是:手动为Product对象的Category导航属性投影一个恰当的对象(不要再包含Products导航属性)见清单10-12。
译者述:具体要根据业务决定投影哪些属性,投影到多深的程度。当然要先把能引起循环引用的导航属性剔除掉。
 1 //Listing 10-12. Avoiding an Endless Loop in the WebServiceRepository.cs File in the Models Folder
 2 using System.Linq;
 3 using Microsoft.EntityFrameworkCore;
 4 namespace SportsStore.Models
 5 {
 6     public class WebServiceRepository : IWebServiceRepository
 7     {
 8         private DataContext context;
 9         public WebServiceRepository(DataContext ctx) => context = ctx;
10         public object GetProduct(long id)
11         {
12             return context.Products.Include(p => p.Category)
13                 .Select(p => new
14                  {
15                     Id = p.Id,
16                     Name = p.Name,
17                     PurchasePrice = p.PurchasePrice,
18                     Description = p.Description,
19                     RetailPrice = p.RetailPrice,
20                     CategoryId = p.CategoryId,
21                     Category = new
22                     {
23                         Id = p.Category.Id,
24                         Name = p.Category.Name,
25                         Description = p.Category.Description
26                     }
27                 })
28                 .FirstOrDefault(p => p.Id == id);
29         }
30     }
31 }

 

 
使用dotnet run启动应用程序,并使用单独的PowerShell窗口执行清单10-13所示的命令。
Listing 10-13. Requesting Related Data
Invoke-RestMethod http://localhost:5000/api/products/1 -Method GET | ConvertTo-Json

 

该命令生成以下JSON结果,其中包含所有相关数据,但不包含循环引用:
 1 ...
 2 {
 3     "id": 1,
 4     "name": "Kayak",
 5     "purchasePrice": 200.00,
 6     "description": "A boat for one person",
 7     "retailPrice": 275.00,
 8     "categoryId": 1,
 9     "category":
10     {
11         "id": 1,
12         "name": "Watersports",
13         "description": "Make a splash"
14     }
15 }
16 ...

 

10.2.6 查询多个对象
在处理多个对象的查询时,限制发送给客户端应用程序的数据量非常重要。如果您简单地返回数据库中的所有对象,那么您将增加运行服务器应用程序的成本,甚至可能使客户端应用程序不堪重负。在清单10-14中,我向web服务的存储库接口添加了一个方法,该方法允许客户端指定请求结果的开始索引和请求对象的数量。
1 //Listing 10-14. Adding a Method in the IWebServiceRepository.cs File in the Models Folder
2 namespace SportsStore.Models
3 {
4     public interface IWebServiceRepository
5     {
6         object GetProduct(long id);
7         object GetProducts(int skip, int take);
8     }
9 }

 

在清单10-15中,我在实现类中添加了该方法,并使用上一节中的技术来避免外键关联数据的循环引用。
 1 //Listing 10-15. Adding a Method in the WebServiceRepository.cs File in the Models Folder
 2 using System.Linq;
 3 using Microsoft.EntityFrameworkCore;
 4 namespace SportsStore.Models
 5 {
 6     public class WebServiceRepository : IWebServiceRepository
 7     {
 8         private DataContext context;
 9         public WebServiceRepository(DataContext ctx) => context = ctx;
10         public object GetProduct(long id)
11         {
12             return context.Products.Include(p => p.Category)
13             .Select(p => new
14             {
15                 Id = p.Id,
16                 Name = p.Name,
17                 PurchasePrice = p.PurchasePrice,
18                 Description = p.Description,
19                 RetailPrice = p.RetailPrice,
20                 CategoryId = p.CategoryId,
21                 Category = new
22                 {
23                     Id = p.Category.Id,
24                     Name = p.Category.Name,
25                     Description = p.Category.Description
26                 }
27             })
28             .FirstOrDefault(p => p.Id == id);
29         }
30         public object GetProducts(int skip, int take)
31         {
32             return context.Products.Include(p => p.Category)
33             .OrderBy(p => p.Id)
34             .Skip(skip)
35             .Take(take)
36             .Select(p => new
37             {
38                 Id = p.Id,
39                 Name = p.Name,
40                 PurchasePrice = p.PurchasePrice,
41                 Description = p.Description,
42                 RetailPrice = p.RetailPrice,
43                 CategoryId = p.CategoryId,
44                 Category = new
45                 {
46                     Id = p.Category.Id,
47                     Name = p.Category.Name,
48                     Description = p.Category.Description
49                 }
50             });
51         }
52     }
53 }

 

在清单10-16中,我向web服务控制器添加了一个操作,允许客户端请求多个对象。
 1 //Listing 10-16. Adding an Action in the ProductValuesController.cs File in the Controllers Folder
 2 using Microsoft.AspNetCore.Mvc;
 3 using SportsStore.Models;
 4 namespace SportsStore.Controllers
 5 {
 6     [Route("api/products")]
 7     public class ProductValuesController : Controller
 8     {
 9         private IWebServiceRepository repository;
10         public ProductValuesController(IWebServiceRepository repo)
11                     => repository = repo;
12         [HttpGet("{id}")]
13         public object GetProduct(long id)
14         {
15             return repository.GetProduct(id) ?? NotFound();
16         }
17         [HttpGet]
18         public object Products(int skip, int take)
19         {
20             return repository.GetProducts(skip, take);
21         }
22     }
23 }

 

要测试新的操作方法,请使用dotnet run启动应用程序,并使用单独的PowerShell窗口执行清单10-17所示的命令
Listing 10-17. Querying for Multiple Objects
Invoke-RestMethod http://localhost:5000/api/products?skip=2"&"take=2 -Method GET | ConvertTo-Json

 

HTTP请求令web服务跳过前两个对象,然后返回下两个对象,这将产生以下结果:
 1 ...
 2 {
 3   "value": [
 4     {
 5       "id": 3,
 6       "name": "Soccer Ball",
 7       "purchasePrice": 18.00,
 8       "description": "FIFA-approved size and weight",
 9       "retailPrice": 19.50,
10       "categoryId": 2,
11       "category": {
12         "id": 2,
13         "name": "Soccer",
14         "description": "The World\u0027s Favorite Game"
15       }
16     },
17     {
18       "id": 4,
19       "name": "Corner Flags",
20       "purchasePrice": 32.50,
21       "description": "Give your playing field a professional touch",
22       "retailPrice": 34.95,
23       "categoryId": 2,
24       "category": {
25         "id": 2,
26         "name": "Soccer",
27         "description": "The World\u0027s Favorite Game"
28       }
29     }
30   ],
31   "Count": 2
32 }
33 ...

 

 
10.3 完善Web服务
使用EF Core数据的web服务的复杂性在于序列化响应,如前一节所述。其他标准的数据操作遵循与前几章相同的模式。在清单10-18中,我向存储库添加了一些方法,允许存储、更新和删除对象。
 1 //Listing 10-18. Adding Methods in the IWebServiceRepository.cs File in the Models Folder
 2 namespace SportsStore.Models
 3 {
 4     public interface IWebServiceRepository
 5     {
 6         object GetProduct(long id);
 7         object GetProducts(int skip, int take);
 8         long StoreProduct(Product product);
 9         void UpdateProduct(Product product);
10         void DeleteProduct(long id);
11     }
12 }

 

在清单10-19中,我将新方法添加到存储库实现类中。
 1 //Listing 10-19. Adding Methods in the WebServiceRepository.cs File in the Models Folder
 2 using System.Linq;
 3 using Microsoft.EntityFrameworkCore;
 4 namespace SportsStore.Models
 5 {
 6     public class WebServiceRepository : IWebServiceRepository
 7     {
 8         private DataContext context;
 9         public WebServiceRepository(DataContext ctx) => context = ctx;
10         public object GetProduct(long id)
11         {
12             return context.Products.Include(p => p.Category)
13             .Select(p => new
14             {
15                 Id = p.Id,
16                 Name = p.Name,
17                 PurchasePrice = p.PurchasePrice,
18                 Description = p.Description,
19                 RetailPrice = p.RetailPrice,
20                 CategoryId = p.CategoryId,
21                 Category = new
22                 {
23                     Id = p.Category.Id,
24                     Name = p.Category.Name,
25                     Description = p.Category.Description
26                 }
27             })
28             .FirstOrDefault(p => p.Id == id);
29         }
30         public object GetProducts(int skip, int take)
31         {
32             return context.Products.Include(p => p.Category)
33             .OrderBy(p => p.Id)
34             .Skip(skip)
35             .Take(take)
36             .Select(p => new
37             {
38                 Id = p.Id,
39                 Name = p.Name,
40                 PurchasePrice = p.PurchasePrice,
41                 Description = p.Description,
42                 RetailPrice = p.RetailPrice,
43                 CategoryId = p.CategoryId,
44                 Category = new
45                 {
46                     Id = p.Category.Id,
47                     Name = p.Category.Name,
48                     Description = p.Category.Description
49                 }
50             });
51         }
52         public long StoreProduct(Product product)
53         {
54             context.Products.Add(product);
55             context.SaveChanges();
56             return product.Id;
57         }
58         public void UpdateProduct(Product product)
59         {
60             context.Products.Update(product);
61             context.SaveChanges();
62         }
63         public void DeleteProduct(long id)
64         {
65             context.Products.Remove(new Product { Id = id });
66             context.SaveChanges();
67         }
68     }
69 }

 

注意,StoreProduct方法返回产品对象的主键值(是数据库服务自动分配的)。客户端应用程序通常持有自己的数据模型,重要的是确保它们拥有执行后续操作所需的足够多的信息,而不需要执行额外的查询。
译者述:简单说一次能查询的事,不要分多次去查询。
 
10.3.1 修改控制器
在清单10-20中,我更新了web服务控制器,以添加与新存储库方法对应的操作。
 1 //Listing 10-20. Adding Actions in the ProductValuesController.cs File in the Controllers Folder
 2 using Microsoft.AspNetCore.Mvc;
 3 using SportsStore.Models;
 4 namespace SportsStore.Controllers
 5 {
 6     [Route("api/products")]
 7     public class ProductValuesController : Controller
 8     {
 9         private IWebServiceRepository repository;
10         public ProductValuesController(IWebServiceRepository repo)
11                 => repository = repo;
12         [HttpGet("{id}")]
13         public object GetProduct(long id)
14         {
15             return repository.GetProduct(id) ?? NotFound();
16         }
17         [HttpGet]
18         public object Products(int skip, int take)
19         {
20             return repository.GetProducts(skip, take);
21         }
22         [HttpPost]
23         public long StoreProduct([FromBody] Product product)
24         {
25             return repository.StoreProduct(product);
26         }
27         [HttpPut]
28         public void UpdateProduct([FromBody] Product product)
29         {
30             repository.UpdateProduct(product);
31         }
32         [HttpDelete("{id}")]
33         public void DeleteProduct(long id)
34         {
35             repository.DeleteProduct(id);
36         }
37     }
38 }

 

要测试存储新对象,请使用dotnet run启动应用程序,并使用单独的PowerShell窗口执行清单10-21所示的命令。
Listing 10-21. Storing New Data
Invoke-RestMethod http://localhost:5000/api/products -Method POST -Body (@{Name="Scuba
Mask"; Description="Spy on the Fish"; PurchasePrice=21; RetailPrice=40;CategoryId=1} |
ConvertTo-Json) -ContentType "application/json"

 这个命令太长很难输入,但是它会向服务器发送一个HTTP POST请求,其中包含要存到数据库的Product对象所有属性的值。命令完成后,使用浏览器窗口导航到http://localhost:5000,您将看到新的对象,如图10-2所示。

要测试Update功能,请使用PowerShell窗口运行清单10-22所示的命令,该命令修改Kayak产品。

1 Listing 10-22. Modifying Data
2 Invoke-RestMethod http://localhost:5000/api/products -Method PUT -Body (@{Id=1;Name="Green
3     Kayak"; Description="A Tiny Boat"; PurchasePrice=200; RetailPrice=300;CategoryId=1} |
4     ConvertTo-Json) -ContentType "application/json"

 

重新加载浏览器窗口,您将看到Kayak产品的RetailPrice零售价已经更改为300,现在它的名称是 Green Kayak。
要测试删除数据的功能,请使用PowerShell窗口运行清单10-23所示的命令。
Listing 10-23. Deleting an Object
Invoke-RestMethod http://localhost:5000/api/products/1 -Method DELETE

 

 
 
10.4 常见错误和解决方案
web服务的大多数问题都与我在本章前面描述的JSON序列化问题有关。然而,还有一些不太常见的问题,我将在下面的小节中描述它们。
 
 
10.4.1 当存储和更新对象时属性值为null
Null Property Values When Storing or Updating Objects
如果MVC模型绑定器创建的对象的某些属性的值为空或为零,那么最有可能的原因是您在action方法参数中忽略了FromBody特性。URL只有在默认情况下用于值,当您希望MVC框架使用HTTP请求报文的其他部分时,必须显式地选择数据来源(如frombody)。
 
 
 
10.4.2 web服务请求太慢
Slow Web Service Requests
请求速度慢的最常见原因是遍历(列举)IQueryable<T>对象并意外触发查询。如果您是在JSON序列化之前处理数据,那么这是很容易触发的。重要的是要记住,IQueryable<T>只要被遍历(列举)就会去查询数据库,而不止是在Razor视图中。
 
 
10.4.3 不能显式插入Id列的异常
The “Cannot Insert Explicit Value for Identity Column” Exception
如果您在编写web服务时收到此异常,那么最有可能的原因是客户端在将要存储到数据库的对象,包含了一个主键值。如果您正在编写客户端应用程序,那么您不要在HTTP请求中指定Id主键值。如果您的web服务正在被第三方应用程序使用,那么您要在请求EF Core存储数据之前显式地将action方法中的主键属性清零。
 
 
10.5 本章小结
在本章中,我通过添加一个简单的RESTful web服务完成了SportsStore应用程序,该服务可用于为客户端应用程序提供数据。我向您展示了处理外键关联数据可能导致的常见问题,并展示了如何通过创建动态类型(匿名类)来避免这些问题。在本书的下一部分中,我将更深入地描述EF Core功能。
 
 
 

posted on 2019-01-12 19:05  困兽斗  阅读(231)  评论(0)    收藏  举报

导航