第十章 SportsStore:创建一个RESTful Web服务
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所示。

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 }
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 }
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 }
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 }
... [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状态代码,它向前端(客户端)发出请求不能被满足的信号。
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对象的外键关联的数据。
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 ...
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 }
Listing 10-10. Requesting Related Data
Invoke-RestMethod http://localhost:5000/api/products/1 -Method GET | ConvertTo-Json
您将看到以下错误消息,而不是JSON数据:
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 ...
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 }
Listing 10-13. Requesting Related Data
Invoke-RestMethod http://localhost:5000/api/products/1 -Method GET | ConvertTo-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 ...
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 }
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 }
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 }
Listing 10-17. Querying for Multiple Objects Invoke-RestMethod http://localhost:5000/api/products?skip=2"&"take=2 -Method GET | ConvertTo-Json
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 ...
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 }
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 }
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 }
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"
Listing 10-23. Deleting an Object
Invoke-RestMethod http://localhost:5000/api/products/1 -Method DELETE
浙公网安备 33010602011771号