從頭編寫 asp.net core 2.0 web api 基礎框架 (1)

工具:html

1.Visual Studio 2017 V15.3.5+linux

2.Postman (Chrome的App)nginx

3.Chrome (最好是)web

關於.net core或者.net core 2.0的相關知識就不介紹了, 這裏主要是從頭編寫一個asp.net core 2.0 web api的基礎框架.chrome

我一直在關注asp.net core 和 angular 2/4, 並在用這對開發了一些比較小的項目. 如今我感受是時候使用這兩個技術去爲企業開發大一點的項目了, 因爲企業有時候須要SSO(單點登陸), 因此我一直在等待Identity Server4以及相關庫的正式版, 如今匹配2.0的RC版已經有了, 因此這個能夠開始編寫了.apache

這個系列就是我從頭開始創建我本身的基於asp.net core 2.0 web api的後臺api基礎框架過程, 估計得分幾回才能寫完. 若是有什麼地方錯的, 請各位指出!!,謝謝.json

 

建立項目:

1.選擇asp.net core web application.windows

2.選擇.net core, asp.net core 2.0, 而後選擇Empty (由於是從頭開始):設計模式

下面看看項目生成的代碼:api

Program.cs

複製代碼
namespace CoreBackend.Api
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .Build();
    }
}
複製代碼

這個Program是程序的入口, 看起來很眼熟, 是由於asp.net core application實際就是控制檯程序(console application).

它是一個調用asp.net core 相關庫的console application. 

Main方法裏面的內容主要是用來配置和運行程序的.

由於咱們的web程序須要一個宿主, 因此 BuildWebHost這個方法就建立了一個WebHostBuilder. 並且咱們還須要Web Server.

看一下WebHost.CreateDefaultBuilder(args)的源碼:

複製代碼
public static IWebHostBuilder CreateDefaultBuilder(string[] args)
        {
            var builder = new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    var env = hostingContext.HostingEnvironment;

                    config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                          .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

                    if (env.IsDevelopment())
                    {
                        var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
                        if (appAssembly != null)
                        {
                            config.AddUserSecrets(appAssembly, optional: true);
                        }
                    }

                    config.AddEnvironmentVariables();

                    if (args != null)
                    {
                        config.AddCommandLine(args);
                    }
                })
                .ConfigureLogging((hostingContext, logging) =>
                {
                    logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
                    logging.AddConsole();
                    logging.AddDebug();
                })
                .UseIISIntegration()
                .UseDefaultServiceProvider((context, options) =>
                {
                    options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
                });

            return builder;
        }
複製代碼

asp.net core 自帶了兩種http servers, 一個是WebListener, 它只能用於windows系統, 另外一個是kestrel, 它是跨平臺的.

kestrel是默認的web server, 就是經過UseKestrel()這個方法來啓用的.

可是咱們開發的時候使用的是IIS Express, 調用UseIISIntegration()這個方法是啓用IIS Express, 它做爲Kestrel的Reverse Proxy server來用.

若是在windows服務器上部署的話, 就應該使用IIS做爲Kestrel的反向代理服務器來管理和代理請求.

若是在linux上的話, 可使用apache, nginx等等的做爲kestrel的proxy server.

固然也能夠單獨使用kestrel做爲web 服務器, 可是使用iis做爲reverse proxy仍是由不少有點的: 例如,IIS能夠過濾請求, 管理證書, 程序崩潰時自動重啓等.

UseStartup<Startup>(), 這句話表示在程序啓動的時候, 咱們會調用Startup這個類.

Build()完以後返回一個實現了IWebHost接口的實例(WebHostBuilder), 而後調用Run()就會運行Web程序, 而且阻止這個調用的線程, 直到程序關閉.

BuildWebHost這個lambda表達式最好不要整合到Main方法裏面, 由於Entity Framework 2.0會使用它, 若是把這個lambda表達式去掉以後, Add-Migration這個命令可能就很差用了!!!

Startup.cs

複製代碼
namespace CoreBackend.Api
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
        }

        // 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();
            }

            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("Hello World!");
            });
        }
    }
}
複製代碼

其實Startup算是程序真正的切入點.

ConfigureServices方法是用來把services(各類服務, 例如identity, ef, mvc等等包括第三方的, 或者本身寫的)加入(register)到container(asp.net core的容器)中去, 並配置這些services. 這個container是用來進行dependency injection的(依賴注入). 全部注入的services(此外還包括一些框架已經註冊好的services) 在之後寫代碼的時候, 均可以將它們注入(inject)進去. 例如上面的Configure方法的參數, app, env, loggerFactory都是注入進去的services.

Configure方法是asp.net core程序用來具體指定如何處理每一個http請求的, 例如咱們可讓這個程序知道我使用mvc來處理http請求, 那就調用app.UseMvc()這個方法就行. 可是目前, 全部的http請求都會致使返回"Hello World!".

這幾個方法的調用順序: Main -> ConfigureServices -> Configure

請求管道和中間件(Request Pipeline, Middleware)

請求管道: 那些處理http requests並返回responses的代碼就組成了request pipeline(請求管道).

中間件: 咱們能夠作的就是使用一些程序來配置那些請求管道 request pipeline以便處理requests和responses. 好比處理驗證(authentication)的程序, 連MVC自己就是個中間件(middleware).

每層中間件接到請求後均可以直接返回或者調用下一個中間件. 一個比較好的例子就是: 在第一層調用authentication驗證中間件, 若是驗證失敗, 那麼直接返回一個表示請求未受權的response.

app.UseDeveloperExceptionPage(); 就是一個middleware, 當exception發生的時候, 這段程序就會處理它. 而判斷env.isDevelopment() 表示, 這個middleware只會在Development環境下被調用.

能夠在項目的屬性Debug頁看到這個設置: 

須要注意的是這個環境變量Development和VS裏面的Debug Build沒有任何關係.

在正式環境中, 咱們遇到exception的時候, 須要捕獲並把它記錄(log)下來, 這時候咱們應該使用這個middleware: Exception Handler Middleware, 咱們能夠這樣調用它:

複製代碼
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler();
            }
複製代碼

UseExceptionHandler是能夠傳參數的, 但暫時先這樣, 咱們在app.Run方法裏拋一個異常, 而後運行程序, 在Chrome裏按F12就會發現有一個(或若干個, 多少次請求, 就有多少個錯誤)500錯誤.

用來建立 Web Api的middleware:

 原來的.net使用asp.net web api 和 asp.net mvc 分別來建立 web api和mvc項目. 可是 asp.net core mvc把它們整合到了一塊兒.

MVC Pattern

model-view-controller 它的定義是: MVC是一種用來實現UI的架構設計模式. 可是網上有不少解釋, 有時候有點分不清究竟是幹什麼的. 可是它確定有這幾個有點: 鬆耦合, Soc(Separation of concerns), 易於測試, 可複用性強等.

可是MVC絕對不是完整的程序架構, 在一個典型的n層架構裏面(presentation layer 展現層, business layer 業務層, data access layer數據訪問層, 還有服務處), MVC一般是展現層的. 例如angular就是一個客戶端的MVC模式.

在Web api裏面的View就是指數據或者資源的展現, 一般是json.

註冊並使用MVC

由於asp.net core 2.0使用了一個大而全的metapackage, 因此這些基本的services和middleware是不須要另外安裝的.

首先, 在ConfigureServices裏面向Container註冊MVC: services.AddMvc();

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(); // 註冊MVC到Container
        }

而後再Configure裏面告訴程序使用mvc中間件:

複製代碼
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler();
            }

            app.UseMvc();
複製代碼

注意順序, 應該在處理異常的middleware後邊調用app.UseMvc(), 因此處理異常的middleware能夠在把request交給mvc之間就處理異常, 更總要的是它還能夠捕獲並處理返回MVC相關代碼執行中的異常.

而後別忘了把app.Run那部分代碼去掉. 而後改回到Develpment環境, 跑一下, 試試效果:

Chrome顯示了一個空白頁, 按F12, 顯示了404 Not Found錯誤.

這是由於我只添加了MVC middleware, 可是它啥也沒作, 也沒有找到任何可用於處理請求的代碼, 因此咱們要添加Controller來返回數據/資源等等.

Asp.net Core 2 Metapackage 和 Runtime Store

Asp.net core 2 metapackage, asp.net core 2.0開始, 全部必須的和經常使用的庫也包括少量第三方庫都被整和到了這個大而全的asp.net core 2 metapackage裏面, 因此開發者就沒必要本身挨個庫安裝也沒有版本匹配問題了.

Runtime Store, 有點像之前的GAC, 在系統裏有一個文件夾裏面包含全部asp.net core 2程序須要運行的庫(我電腦的是: C:\Program Files\dotnet\store\x64\netcoreapp2.0), 每一個在這臺電腦上運行的asp.net core 2應用只需調用這些庫便可. 

它的優勢是:

  1. 部署快速, 不須要部署這裏麪包含的庫;
  2. 節省硬盤空間, 多個應用程序都使用同一個store, 而沒必要每一個程序的文件夾裏面都部署這些庫文件. 
  3. 程序啓動更快一些. 由於這些庫都是預編譯好的.

缺點是: 服務器上須要安裝.net core 2.0

可是, 也能夠不引用Runtime Store的庫, 本身在部署的時候挨個添加依賴的庫.

Controller

首先創建一個Controllers目錄, 而後創建一個ProductController.cs, 它須要繼承Microsoft.AspNetCore.Mvc.Controller

咱們先創建一個方法返回一個Json的結果.

先創建一個Dto(Data Transfer Object) Product:

複製代碼
namespace CoreBackend.Api.Dtos
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public float Price { get; set; }
    }
}
複製代碼

而後在Controller裏面寫這個Get方法:

複製代碼
namespace CoreBackend.Api.Controllers
{
    public class ProductController: Controller
    {
        public JsonResult GetProducts()
        {
            return new JsonResult(new List<Product>
            {
                new Product
                {
                    Id = 1,
                    Name = "牛奶",
                    Price = 2.5f
                },
                new Product
                {
                    Id = 2,
                    Name = "麪包",
                    Price = 4.5f
                }
            });
        }
    }
}
複製代碼

而後運行, 並使用postman來進行請求:

請求的網址返回404 Not Found, 由於尚未配置路由 Routing, 因此MVC不知道如何處理/映射這些URI.

Routing 路由

路由有兩種方式: Convention-based (按約定), attribute-based(基於路由屬性配置的). 

其中convention-based (基於約定的) 主要用於MVC (返回View或者Razor Page那種的).

Web api 推薦使用attribute-based.

這種基於屬性配置的路由能夠配置Controller或者Action級別, uri會根據Http method而後被匹配到一個controller裏具體的action上.

經常使用的Http Method有:

  • Get, 查詢, Attribute: HttpGet, 例如: '/api/product', '/api/product/1'
  • POST, 建立, HttpPost, '/api/product'
  • PUT 總體修改更新 HttpPut, '/api/product/1'
  • PATCH 部分更新, HttpPatch, '/api/product/1'
  • DELETE 刪除, HttpDelete, '/api/product/1

還有一個Route屬性(attribute)也能夠用於Controller層, 它能夠控制action級的URI前綴.

複製代碼
namespace CoreBackend.Api.Controllers
{
    //[Route("api/product")]
    [Route("api/[controller]")]
    public class ProductController: Controller
    {
        [HttpGet]
        public JsonResult GetProducts()
        {
            return new JsonResult(new List<Product>
            {
                new Product
                {
                    Id = 1,
                    Name = "牛奶",
                    Price = 2.5f
                },
                new Product
                {
                    Id = 2,
                    Name = "麪包",
                    Price = 4.5f
                }
            });
        }
    }
}
複製代碼

使用[Route("api/[controller]")], 它使得整個Controller下面全部action的uri前綴變成了"/api/product", 其中[controller]表示XxxController.cs中的Xxx(實際上是小寫).

也能夠具體指定, [Route("api/product")], 這樣作的好處是, 若是ProductController重構之後更名了, 只要不改Route裏面的內容, 那麼請求的地址不會發生變化.

而後在GetProducts方法上面, 寫上HttpGet, 也能夠寫HttpGet(). 它裏面還能夠加參數,例如: HttpGet("all"), 那麼這個Action的請求的地址就變成了 "/api/product/All".

運行結果:

咱們把獲取數據的代碼整理成一個ProductService, 而後保證程序運行的時候, 操做的是同一批數據:

複製代碼
namespace CoreBackend.Api.Services
{
    public class ProductService
    {
        public static ProductService Current { get; } = new ProductService();

        public List<Product> Products { get; }

        private ProductService()
        {
            Products = new List<Product>
            {
                new Product
                {
                    Id = 1,
                    Name = "牛奶",
                    Price = 2.5f
                },
                new Product
                {
                    Id = 2,
                    Name = "麪包",
                    Price = 4.5f
                },
                new Product
                {
                    Id = 3,
                    Name = "啤酒",
                    Price = 7.5f
                }
            };
        }
    }
}
複製代碼

而後修改一下Controller裏面的代碼:

複製代碼
namespace CoreBackend.Api.Controllers
{
    [Route("api/[controller]")]
    public class ProductController: Controller
    {
        [HttpGet]
        public JsonResult GetProducts()
        {
            return new JsonResult(ProductService.Current.Products);
        }
    }
}
複製代碼

也是一樣的運行效果.

再寫一個查詢單筆數據的方法:

        [Route("{id}")]
        public JsonResult GetProduct(int id)
        {
            return new JsonResult(ProductService.Current.Products.SingleOrDefault(x => x.Id == id));
        }

這裏Route參數裏面的{id}表示該action有一個參數名字是id. 這個action的地址是: "/api/product/{id}"

測試一下:

若是請求一個id不存在的數據:

Status code仍是200, 內容是null. 由於框架找到了匹配uri的action, 因此不會返回404, 可是咱們若是找不到數據的話, 應該返回404錯誤才比較好.

Status code

http status code 是reponse的一部分, 它提供了這些信息: 請求是否成功, 失敗的緣由. 

web api 能涉及到的status codes主要是這些:

200: OK

201: Created, 建立了新的資源

204: 無內容 No Content, 例如刪除成功

400: Bad Request, 指的是客戶端的請求錯誤.

401: 未受權 Unauthorized.

403: 禁止操做 Forbidden. 驗證成功, 可是無法訪問相應的資源

404: Not Found 

409: 有衝突 Conflict.

500: Internal Server Error, 服務器發生了錯誤.

返回Status Code

目前咱們返回的JsonResult繼承與ActionResult, ActionResult實現了IActionResult接口.

由於web api不必定返回的都是json類型的數據, 也不必定只返回一堆json(可能還要包含其餘內容). 因此JsonResult並不合適做爲Action的返回結果.

例如: 咱們想要返回數據和Status Code, 那麼能夠這樣作:

複製代碼
        [HttpGet]
        public JsonResult GetProducts()
        {
            var temp = new JsonResult(ProductService.Current.Products)
            {
                StatusCode = 200
            };
            return temp;
        }
複製代碼

可是每一個方法都這麼寫太麻煩了.

asp.net core 內置了不少方法均可以返回IActionResult.

Ok, NotFound, BadRequest等等.

因此改一下方法:

複製代碼
namespace CoreBackend.Api.Controllers
{
    [Route("api/[controller]")]
    public class ProductController : Controller
    {
        [HttpGet]
        public IActionResult GetProducts()
        {
            return Ok(ProductService.Current.Products);
        }

        [Route("{id}")]
        public IActionResult GetProduct(int id)
        {
            var product = ProductService.Current.Products.SingleOrDefault(x => x.Id == id);
            if (product == null)
            {
                return NotFound();
            }
            return Ok(product);
        }
    }
}
複製代碼

如今, 請求id不存在的數據時, 就返回404了.

若是咱們用chrome直接進行這個請求, 它的效果是這樣的:

StatusCode Middleware

asp.net core 有一個 status code middleware, 使用一下這個middleware看看效果:

複製代碼
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler();
            }

            app.UseStatusCodePages(); // !!!

            app.UseMvc();
        }
複製代碼

如今更友好了一些.

子資源 Child Resources

有時候, 兩個model之間有主從關係, 會根據主model來查詢子model.

先改一下model: 添加一個Material做爲Product子model. 並在Product裏面添加一個集合導航屬性.

複製代碼
namespace CoreBackend.Api.Dtos
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public float Price { get; set; }
        public ICollection<Material> Materials { get; set; }
    }

    public class Material
    {
        public int Id { get; set; }
        public int Name { get; set; }
    }
}
複製代碼

改下ProductService:

複製代碼
namespace CoreBackend.Api.Services
{
    public class ProductService
    {
        public static ProductService Current { get; } = new ProductService();

        public List<Product> Products { get; }

        private ProductService()
        {
            Products = new List<Product>
            {
                new Product
                {
                    Id = 1,
                    Name = "牛奶",
                    Price = 2.5f,
                    Materials = new List<Material>
                    {
                        new Material
                        {
                            Id = 1,
                            Name = "水"
                        },
                        new Material
                        {
                            Id = 2,
                            Name = "奶粉"
                        }
                    }
                },
                new Product
                {
                    Id = 2,
                    Name = "麪包",
                    Price = 4.5f,
                    Materials = new List<Material>
                    {
                        new Material
                        {
                            Id = 3,
                            Name = "麪粉"
                        },
                        new Material
                        {
                            Id = 4,
                            Name = "糖"
                        }
                    }
                },
                new Product
                {
                    Id = 3,
                    Name = "啤酒",
                    Price = 7.5f,
                    Materials = new List<Material>
                    {
                        new Material
                        {
                            Id = 5,
                            Name = "麥芽"
                        },
                        new Material
                        {
                            Id = 6,
                            Name = "地下水"
                        }
                    }
                }
            };
        }
    }
}
複製代碼

建立子Controller

MaterialController:

複製代碼
namespace CoreBackend.Api.Controllers
{
    [Route("api/product")] // 和主Model的Controller前綴同樣
    public class MaterialController : Controller
    {
        [HttpGet("{productId}/materials")]
        public IActionResult GetMaterials(int productId)
        {
            var product = ProductService.Current.Products.SingleOrDefault(x => x.Id == productId);
            if (product == null)
            {
                return NotFound();
            }
            return Ok(product.Materials);
        }

        [HttpGet("{productId}/materials/{id}")]
        public IActionResult GetMaterial(int productId, int id)
        {
            var product = ProductService.Current.Products.SingleOrDefault(x => x.Id == productId);
            if (product == null)
            {
                return NotFound();
            }
            var material = product.Materials.SingleOrDefault(x => x.Id == id);
            if (material == null)
            {
                return NotFound();
            }
            return Ok(material);
        }
    }
}
複製代碼

測試一下, 很成功:

結果的格式

asp.net core 2.0 默認返回的結果格式是Json, 並使用json.net對結果默認作了camel case的轉化(大概可理解爲首字母小寫). 

這一點與老.net web api 不同, 原來的 asp.net web api 默認不適用任何NamingStrategy, 須要手動加上camelcase的轉化.

我很喜歡這樣, 由於大多數前臺框架例如angular等都約定使用camel case.

若是非得把這個規則去掉, 那麼就在configureServices裏面改一下:

複製代碼
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc()
                .AddJsonOptions(options =>
                {
                    if (options.SerializerSettings.ContractResolver is DefaultContractResolver resolver)
                    {
                        resolver.NamingStrategy = null;
                    }
                });
        }
複製代碼

如今就是這樣的結果了:

可是仍是默認的比較好.

內容協商 Content Negotiation

若是 web api提供了多種內容格式, 那麼能夠經過Accept Header來選擇最好的內容返回格式: 例如:

application/json, application/xml等等

若是設定的格式在web api裏面沒有, 那麼web api就會使用默認的格式.

asp.net core 默認提供的是json格式, 也能夠配置xml等格式.

目前只考慮 Output formatter, 就是返回的內容格式.

試試: json:

xml:

設置header爲xml後,返回的仍是json, 這是由於asp.net core 默認只實現了json.

能夠在ConfigureServices裏面修改Mvc的配置來添加xml格式:

複製代碼
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc()
                .AddMvcOptions(options =>
                {
                    options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
                });
        }
複製代碼

而後試試:

首先不寫Accept Header:

而後試試accept xml :

 

先寫這些..............................

博客文章能夠轉載,但不能夠聲明爲原創. 

個人.NET Core公衆號:

相關文章
相關標籤/搜索