把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,等不及正式版发布了,先把它用起来。

整个试验过程,包括:

  1. 采用Asp Net Core重构网站;
  2. 以docker方式部署网站到Linux;
  3. 以docker方式使用MySQL数据库;
  4. Https加密传输;

每一个环节,都是一个艰巨的学习过程。但是Asp.Net Core的最大价值在于跨平台,如果光学会了Asp.Net Core,还是部署在Windows Server的IIS上,有什么价值呢?所以,不管涉及的知识面有多广,都必须要全部打通。

试验过程中还有一个很大的困难是找不到现成可以照搬的例程,有些环节要参考近似的例程,做各种修改试验,然后才找到解决方案。

在开展试验之前,先建立VS2017跨平台开发的整套环境:

  1. 安装VS2017最新版本15.5,确认包含了Net Core 2.0开发工具;
    这里写图片描述

  2. 安装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}]}

原文链接: https://blog.csdn.net/woodsun2008/article/details/78976232
posted on 2018-05-25 22:39  SunnyTrudeau  阅读(219)  评论(0)    收藏  举报