從頭編寫 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 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 = "地下水"
                        }
                    }
                }
            };
        }
    }
}
View Code

創建子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 :

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM