1. 程式人生 > >【轉載】從頭編寫 asp.net core 2.0 web api 基礎框架 (1)

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

asp.net studio 依賴 ole hand 為我 dev 工具 項目

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

工具:

1.Visual Studio 2017 V15.3.5+

2.Postman (Chrome的App)

3.Chrome (最好是)

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

我一直在關註asp.net core 和 angular 2/4, 並在用這對開發了一些比較小的項目. 現在我感覺是時候使用這兩個技術去為企業開發大一點的項目了, 由於企業有時候需要SSO(單點登錄), 所以我一直在等待Identity Server4以及相關庫的正式版, 現在匹配2.0的RC版已經有了, 所以這個可以開始編寫了.

這個系列就是我從頭開始建立我自己的基於asp.net core 2.0 web api的後臺api基礎框架過程, 估計得分幾次才能寫完. 如果有什麽地方錯的, 請各位指出!!,謝謝.

創建項目:

1.選擇asp.net core web application.

技術分享圖片

2.選擇.net core, asp.net core 2.0, 然後選擇Empty (因為是從頭開始):

技術分享圖片

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

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 string 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 :

技術分享圖片

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

轉自:http://www.cnblogs.com/cgzl/p/7637250.html

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