把Web Api OData移植到Asp.Net Core(1)-重构网站
OData是微软主推的标准化Restful服务端接口,在Asp.Net Web Api中提供了自动化搭建服务端代码的功能,非常强大,开发者可以在1分钟内创建一个OData服务端。但是在Asp.Net Core中,OData没有了,导致以前用OData开发的服务端无法移植到Asp.Net Core,无法跨平台部署到Linux服务器,真是非常头疼。
幸好微软没有抛弃OData,在2017年底发布了Microsoft.AspNetCore.OData 7.0.0-beta1,等不及正式版发布了,先把它用起来。
整个试验过程,包括:
- 采用Asp Net Core重构网站;
- 以docker方式部署网站到Linux;
- 以docker方式使用MySQL数据库;
- Https加密传输;
每一个环节,都是一个艰巨的学习过程。但是Asp.Net Core的最大价值在于跨平台,如果光学会了Asp.Net Core,还是部署在Windows Server的IIS上,有什么价值呢?所以,不管涉及的知识面有多广,都必须要全部打通。
试验过程中还有一个很大的困难是找不到现成可以照搬的例程,有些环节要参考近似的例程,做各种修改试验,然后才找到解决方案。
在开展试验之前,先建立VS2017跨平台开发的整套环境:
安装VS2017最新版本15.5,确认包含了Net Core 2.0开发工具;
安装VMWare,在VMWare上面安装Linux系统,通过SSH工具操控Linux;
所以,电脑配置一定要足够强大,i5四核CPU,16G内存,256G固态硬盘是最低要求。
1.参照官网例程试验
OData官网有一个章节专门介绍这个最新的Beta1的用法,详见:
http://odata.github.io/WebApi/#14-01-netcore-beta1
根据它的介绍,一步步做一个Hello world例程。
新建Asp.NET Core Web项目,选择Web Api模版。
NuGet下载Microsoft.AspNetCore.OData。注意要勾选包括预览发行版。
定义实体类Product.cs。
namespace ODataService.Models
{
public class Product
{
public int ID { get; set; }
public string Name { get; set; }
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
增加控制器ProductsController.cs
using Microsoft.AspNet.OData;
using ODataService.Models;
using System.Collections.Generic;
namespace ODataService.Controllers
{
public class ProductsController : ODataController
{
private List<Product> products = new List<Product>()
{
new Product()
{
ID = 1,
Name = "Bread",
}
};
public List<Product> Get()
{
return products;
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
配置OData服务端。
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
//支持OData
services.AddOData();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//支持OData
var builder = new ODataConventionModelBuilder(app.ApplicationServices);
builder.EntitySet<Product>("Products");
app.UseMvc(routeBuilder =>
{
routeBuilder.MapODataServiceRoute("ODataRoute", "odata", builder.GetEdmModel());
// Work-around for #1175
routeBuilder.EnableDependencyInjection();
});
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
然后按F5运行项目,浏览http://localhost:55292/odata,验证OData V4服务端可以访问。
访问http://localhost:55292/odata/Products,可以返回集合内容。
{“@odata.context”:”http://localhost:55292/odata/$metadata#Products“,”value”:[{“ID”:1,”Name”:”Bread”}]}
试验成功,初步确定OData功能可用。
2.扩充例程支持Restful
官方的例程离Restful还差很远,还要补充大量代码。
参考官方例程新建一个OData服务端项目,把实体类更换为:
public class Book
{
public int ID { get; set; }
public string Name { get; set; }//书名
public DateTime PublishDate { get; set; }//出版日期
public string Author { get; set; }//作者
public float Price { get; set; }//价格
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
然后定义数据库,为了调试方便,使用内存数据库。
public class BookDbContext : DbContext
{
public BookDbContext(DbContextOptions<BookDbContext> options)
: base(options)
{
}
public DbSet<Book> books { get; set; }
}
public static class SeedData
{
public static void SeedDB(BookDbContext context)
{
//重要:如果数据库完全为空,可以自动创建数据库!
context.Database.EnsureCreated();
// DB has been seeded
if (context.books.Any())
return;
context.books.AddRange(
new Book() { Name = "射雕英雄传", PublishDate = new DateTime(1964, 1, 1), Author = "金庸", Price = 9.5f, },
new Book() { Name = "神雕侠侣", PublishDate = new DateTime(1965, 3, 1), Author = "金庸", Price = 10.5f, },
new Book() { Name = "倚天屠龙记", PublishDate = new DateTime(1968, 12, 1), Author = "金庸", Price = 12, }
);
context.SaveChanges();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
初始化配置数据库和OData
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
//开发阶段使用内存数据库调试代码
services.AddDbContext<BookDbContext>(opt => opt.UseInMemoryDatabase("bookdb"));
services.AddMvc();
//支持OData
services.AddOData();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//支持OData
var builder = new ODataConventionModelBuilder(app.ApplicationServices);
builder.EntitySet<Book>("Books");
app.UseMvc(routeBuilder =>
{
routeBuilder.MapODataServiceRoute("ODataRoute", "odata", builder.GetEdmModel());
//允许全部查询操作,替代方法是在实体类上做标注,或者在数据库OnModelCreating时HasAnnotation标注
routeBuilder.Count().Filter().OrderBy().Expand().Select().MaxTop(null);
// Work-around for #1175
routeBuilder.EnableDependencyInjection();
});
//初始化数据库
using (var scope = app.ApplicationServices.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<BookDbContext>();
SeedData.SeedDB(context);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
考虑到Asp.Net Web Api OData自动搭建的控制器代码比较完整,所以先创建一个旧版的Asp.Net Web Api(Net Framework)项目,创建OData控制器,选择模版Web API 2 OData V3 Controller with actions, using Entity Framework,得到控制器类代码,这就是修改的起点。因为这个模版的部分函数封装得太严了,缺少代码,所以又新建了一个Web API 2 OData v3 Controller with read/write actions控制器,互相补充。
然后编译代码,逐一修正错误。
主要更改有:
第一, EnableQuery属性没有了,需要在函数中编写实现查询条件的代码,注意,不要对查询条件参数queryOptions进行检查,要允许全部查询条件,否则总是各种报错;
第二, 部分函数的命名有改变,例如Validate(patch.GetEntity())要改为TryValidateModel(patch.GetInstance());
第三, Post类型的函数的参数,必须加[FromBody],否则娶不到参数值;
最终得到整个控制器类代码如下:
public class BooksController : ODataController
{
private readonly BookDbContext db;
private readonly ILogger _logger;
private static ODataValidationSettings _validationSettings = new ODataValidationSettings();
public BooksController(ILogger<BooksController> logger, BookDbContext context)
{
_logger = logger;
db = context;
}
// GET: odata/Books
public IActionResult GetBooks(ODataQueryOptions<Book> queryOptions)
{
try
{
var items = queryOptions.ApplyTo(db.books);
return Ok(items as IQueryable<Book>);
}
catch (ODataException ex)
{
return BadRequest(ex.Message);
}
}
// GET: odata/Books(5)
public IActionResult GetBook([FromODataUri] int key, ODataQueryOptions<Book> queryOptions)
{
try
{
Book Book = db.books.Find(key);
if (Book == null)
{
return NotFound();
}
return Ok(Book);
}
catch (ODataException ex)
{
return BadRequest(ex.Message);
}
}
// PUT: odata/Books(5)
public IActionResult Put([FromODataUri] int key, [FromBody]Delta<Book> patch)
{
TryValidateModel(patch.GetInstance());
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
Book Book = db.books.Find(key);
if (Book == null)
{
return NotFound();
}
patch.Put(Book);
try
{
db.SaveChanges();
}
catch (DbUpdateConcurrencyException)
{
if (!BookExists(key))
{
return NotFound();
}
else
{
throw;
}
}
return Updated(Book);
}
// POST: odata/Books
public IActionResult Post([FromBody]Book Book)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.books.Add(Book);
int count = db.SaveChanges();
return Created(Book);
}
// PATCH: odata/Books(5)
[AcceptVerbs("PATCH", "MERGE")]
public IActionResult Patch([FromODataUri] int key, [FromBody]Delta<Book> patch)
{
TryValidateModel(patch.GetInstance());
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
Book Book = db.books.Find(key);
if (Book == null)
{
return NotFound();
}
patch.Patch(Book);
try
{
db.SaveChanges();
}
catch (DbUpdateConcurrencyException)
{
if (!BookExists(key))
{
return NotFound();
}
else
{
throw;
}
}
return Updated(Book);
}
// DELETE: odata/Books(5)
public IActionResult Delete([FromODataUri] int key)
{
Book Book = db.books.Find(key);
if (Book == null)
{
return NotFound();
}
db.books.Remove(Book);
db.SaveChanges();
return StatusCode((int)HttpStatusCode.NoContent);
}
private bool BookExists(int key)
{
return db.books.Count(e => e.ID == key) > 0;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
按F5调试,访问http://localhost:56147/odata/Books,确认服务端可用。
http://localhost:56147/odata/Books?$count=true
返回的结果中,包含了数据库记录总数。Fiddler抓包:
{“@odata.context”:”http://localhost:56147/odata/$metadata#Books“,”@odata.count”:3,”value”:[{“ID”:1,”Name”:”\u5c04\u96d5\u82f1\u96c4\u4f20”,”PublishDate”:”1964-01-01T00:00:00+08:00”,”Author”:”\u91d1\u5eb8”,”Price”:9.5},{“ID”:2,”Name”:”\u795e\u96d5\u4fa0\u4fa3”,”PublishDate”:”1965-03-01T00:00:00+08:00”,”Author”:”\u91d1\u5eb8”,”Price”:10.5},{“ID”:3,”Name”:”\u501a\u5929\u5c60\u9f99\u8bb0”,”PublishDate”:”1968-12-01T00:00:00+08:00”,”Author”:”\u91d1\u5eb8”,”Price”:12}]}
浙公网安备 33010602011771号