ASP.NET Core应用程序2:创建RESTful Web服务

1 准备工作

2 理解RESTful Web服务

Web服务最常见的方法是采用具象状态传输(Representational State Transfer,REST)模式。

REST指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或者设计就是RESTful,核心就是面向资源,REST专门针对网络应用设计和开发方式,以降低开发的复杂性,提高系统的可伸缩性。

REST的核心前提是Web服务通过URL和HTTP方法的组合定义API。常用的HTTP方法有Get,Post、Put、Patch、Delete。Put用于更新现有对象,Patch用于更新现有对象的一部分。

3 使用自定义端点创建Web服务

添加文件WebServiceEndpoint类。

    public static class WebServiceEndpoint
    {
        private static string BASEURL = "api/products";
        public static void MapWebService(this IEndpointRouteBuilder app)
        {
            app.MapGet($"{BASEURL}/{{ProductId}}", async context =>
            {
                long key = long.Parse(context.Request.RouteValues["ProductId"] as string);
                DataContext data = context.RequestServices.GetService<DataContext>();
                Product p = data.Products.Find(key);
                if (p == null)
                {
                    context.Response.StatusCode = StatusCodes.Status404NotFound;
                }
                else
                {
                    context.Response.ContentType = "application/json";
                    var json = JsonSerializer.Serialize(p);
                    await context.Response.WriteAsync(json);
                }
            });

            app.MapGet(BASEURL, async context =>
            {
                DataContext data = context.RequestServices.GetService<DataContext>();
                context.Response.ContentType = "application/json";
                await context.Response.WriteAsync(
                    JsonSerializer.Serialize<IEnumerable<Product>>(data.Products));
            });

            app.MapPost(BASEURL, async context =>
            {
                DataContext data = context.RequestServices.GetService<DataContext>();
                Product p = 
                    await JsonSerializer.DeserializeAsync<Product>(context.Request.Body);
                await data.AddAsync(p);
                await data.SaveChangesAsync();
                context.Response.StatusCode = StatusCodes.Status200OK;
            });

        }

    }

添加配置路由。

endpoints.MapWebService();

WebServiceEndpoint扩展方法创建了三条路由。
第一个路由接收一个值查询单个Product对象。在浏览器输入http://localhost:5000/api/products/1
第二个路由查询所有Product对象,在浏览器输入http://localhost:5000/api/products
第三个路由处理Post请求,添加新对象到数据库。不能使用浏览器发送请求,需要使用命令行,
Invoke-RestMethod http://localhost:5000/api/products -Method POST -Body(@{Name="Swimming Goggles";Price=12.75;CategoryId=1;SupplierId=1}|ConvertTo-Json) -ContentType "application/json"。执行完成后可以使用第二个路由查询一下。

4 使用控制器创建Web服务

端点创建服务的方式有些繁琐,也很笨拙,所以我们使用控制器来创建。

4.1 启用MVC框架

        public void ConfigureServices(IServiceCollection services)
        {
            ......
            services.AddControllers();//定义了MVC框架需要的服务
        }
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, 
                              DataContext context)
        {
            ......
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
                //endpoints.MapWebService();
                endpoints.MapControllers();//定义了允许控制器处理请求的路由
            });
            ...... 
        }

4.1 创建控制器

名称以Controller结尾的公共类都是控制器。
添加Controllers文件,并添加ProductsController类。

    [Route("api/[controller]")]
    public class ProductsController : ControllerBase
    {
        [HttpGet]
        public IEnumerable<Product> GetProducts()
        {
            var productArr = new Product[]
            {
                new Product(){ Name = "Product #1"},
                new Product(){ Name = "Product #2"}
            };
            return productArr;
        }
        [HttpGet("{id}")]
        public Product GetProduct()
        {
            var product = new Product() { ProductId = 1,Name= "测试产品" };
            return product;
        }
    }
(1) 理解基类

控制器是从ControllerBase派生,该类提供对MVC框架和底层ASP.NET Core平台特性的访问。
ControllerBase的属性:

  • HttpContext:返回当前请求的HttpContext对象;
  • ModelState:返回数据验证过程的详细信息。
  • Request:返回返回当前请求的HttpRequest对象;
  • Response:返回返回当前请求的HttpResponse对象;
  • RouteData:返回路由中间件从请求URL中提取的数据;
  • User:返回一个对象,描述于当前请求关联的用户;

每次使用控制器类的一个方法处理请求是,都会创建一个控制器类新实例,这意味着上述属性只描述当前请求。

(2) 理解控制器特性

操作方法支持HTTP方法,URL由应用到控制器的特性组合决定。

控制器的URL由Route特性指定,它应用于类。

    [Route("api/[controller]")]
    public class ProductsController : ControllerBase

参数[controller]部分指从控制器类名派生URL,上述的控制器将URL设置为/api/products。

每个操作方法都用一个属性修饰,指定了HTTP方法。

        [HttpGet]
        public IEnumerable<Product> GetProducts()

访问此方法的URL为/api/products。

应用于指定HTTP方法属性也可以用于控制器URL。

        [HttpGet("{id}")]
        public Product GetProduct()

访问此方法的URL为/api/products/{id}。
在编写控制器务必确保URL只映射到一个操作方法。

(3) 理解控制器方法的返回值

控制器提供的好处之一就是MVC框架负责设置响应头,并序列化发送到客户端的数据对象。
使用端点时,必须直接使用JSON序列化一个可写入响应的字符串,并设置Content-Type头来告诉客户端。而控制器方法只需要返回一个对象,其他都是自动处理的。

(4) 在控制器中使用依赖注入

应用程序的服务通过构造器声明处理,同时仍然允许单个方法声明自己的依赖项。

    [Route("api/[controller]")]
    public class ProductsController : ControllerBase
    {
        private DataContext _context;
        public ProductsController(DataContext dataContext)
        {
            _context = dataContext;
        }
        [HttpGet]
        public IEnumerable<Product> GetProducts()
        {
            return _context.Products;
        }
        [HttpGet("{id}")]
        public Product GetProduct([FromServices] ILogger<ProductsController> logger)
        {
            logger.LogDebug("执行GetProduct");
            return _context.Products.FirstOrDefault();
        }
    }
(5) 使用模型绑定访问路由数据

MVC框架使用请求URL来查找操作方法参数的值,这个过程称为模型绑定。以下通过请求URL访问操作方法http://localhost:5000/api/products/5。

        [HttpGet("{id}")]
        public Product GetProduct([FromServices] ILogger<ProductsController> logger,
            long id)
        {
            logger.LogDebug("执行GetProduct");
            return _context.Products.Find(id);
        }
(6) 在请求主体中进行模型绑定

用于请求体中允许客户端发送容易由操作方法接收的数据。

        [HttpPost]
        public void SaveProduct([FromBody] Product product)
        {
            _context.Products.Add(product);
            _context.SaveChanges();
        }

FromBody属性用于操作参数,它指定应该通过解析请求主体获得该参数值,调用此操作方法是,MVC框架会创建一个新的Product对象,并用参数值填充其属性。

(7) 添加额外的操作
        [HttpPut]
        public void UpdateProduct([FromBody] Product product)
        {
            _context.Products.Update(product);
            _context.SaveChanges();
        }
        [HttpDelete("{id}")]
        public void DeleteProduct(long id)
        {
            _context.Products.Remove(new Product() { ProductId = id });
            _context.SaveChanges();
        }

5 改进Web服务

5.1 使用异步操作

异步操作允许ASP.NET Core线程处理其他可能被阻塞的请求,增加了应用程序可以同时处理HTTP请求的数量。

        [HttpGet]
        public IAsyncEnumerable<Product> GetProducts()
        {
            return _context.Products;
        }
        [HttpGet("{id}")]
        public async Task<Product> GetProduct(
            [FromServices] ILogger<ProductsController> logger,
            long id)
        {
            logger.LogDebug("执行GetProduct");
            return await _context.Products.FindAsync(id);
        }
        [HttpPost]
        public async Task SaveProduct([FromBody] Product product)
        {
            await _context.Products.AddAsync(product);
            await _context.SaveChangesAsync();
        }
        [HttpPut]
        public async Task UpdateProduct([FromBody] Product product)
        {
            _context.Products.Update(product);
            await _context.SaveChangesAsync();
        }
        [HttpDelete("{id}")]
        public async Task DeleteProduct(long id)
        {
            _context.Products.Remove(new Product() { ProductId = id });
            await _context.SaveChangesAsync();
        }

5.2 防止过度绑定

为了防止过度绑定出现的异常,安全的方法是创建单独的数据模型类。

namespace MyWebApp.Models
{
    public class ProductBindingTarget
    {
        public string Name { get; set; }
        public decimal Price { get; set; }
        public long CategoryId { get; set; }

        public long SupplierId { get; set; }

        public Product ToProduct() => new Product()
        {
            Name = this.Name,
            Price = this.Price,
            CategoryId = this.CategoryId,
            SupplierId = this.SupplierId
        };
    }
}

ProductBindingTarget类确保客户端只传递需要的值,而不至于传ProductId属性报错,修改SaveProduct方法参数如下。

        [HttpPost]
        public async Task SaveProduct([FromBody] ProductBindingTarget target)
        {
            await _context.Products.AddAsync(target.ToProduct());
            await _context.SaveChangesAsync();
        }

5.3 使用操作结果

操作方法可以返回一个IActionResult接口的对象,而不必直接使用HttpResponse对象生成它。
ContorllerBase类提供了一组用于创建操作结果对象的方法:

  • OK:返回生成200状态码,并在响应体中发送一个可选的数据对象;
  • NoContent:返回204状态码;
  • BadRequest:返回400状态吗,该方法接收一个可选的模型状态对象向客户端描述问题;
  • File:返回200状态吗,为特定类型设置Content-Type头,并将指定文件发送给客户端;
  • NotFound:返回404状态码;
  • StatusCode:返回会生成一个带有特定状态码的响应;
  • Redirect和RedirectPermanent:返回将客户端重定向到指定URL;
  • RedirectToRoute和RedirectToRoutePermanent:返回将客户端重定向到使用路由系统创建指定URL;
  • LocalRedirect和LocalRedirectPermanent:返回将客户端重定向到应用程序本地的指定URL;
  • RedirectToAction和RedirectToActionPermanent:返回将客户端重定向到一个操作方法。
  • RedirectToPage和RedirectToPagePermanent:返回将客户端重定向到Razor Pages。

修改GetProduct和SaveProduct方法。

        [HttpGet("{id}")]
        public async Task<IActionResult> GetProduct( long id)
        {
            Product p = await _context.Products.FindAsync(id);
            if(p == null)
            {
                return NotFound();
            }
            return Ok(p);   
        }
        [HttpPost]
        public async Task<IActionResult> SaveProduct(
            [FromBody] ProductBindingTarget target)
        {
            Product p = target.ToProduct();
            await _context.Products.AddAsync(p);
            await _context.SaveChangesAsync();
            return Ok(p);
        }
(1)执行重定向
        [HttpGet("redirect")]
        public IActionResult Redirect()
        {
            return Redirect("/api/products/1");
        }
(2)重定向到操作方法
        [HttpGet("redirect")]
        public IActionResult Redirect()
        {
            return RedirectToAction(nameof(GetProduct), new { id = 1 });
        }
(3)路由重定向
        [HttpGet("redirect")]
        public IActionResult Redirect()
        {
            return RedirectToRoute( 
                new { controller = "Products", action = "GetProduct", Id = 1 });
        }

5.4 验证数据

        [Required]
        public string Name { get; set; }

        [Range(1, 1000)]
        public decimal Price { get; set; }

        [Range(1, long.MaxValue)]
        public long CategoryId { get; set; }

        [Range(1, long.MaxValue)]
        public long SupplierId { get; set; }

修改SaveProduct方法,创建对象前作属性验证。ModelState是从ControllerBase类继承来的,如果模型绑定过程生成的数据满足验证标准,那么IsValid返回true。如果验证失败,ModelState对象中会向客户描述验证错误。

        [HttpPost]
        public async Task<IActionResult> SaveProduct(
            [FromBody] ProductBindingTarget target)
        {
            if (ModelState.IsValid)
            {
                Product p = target.ToProduct();
                await _context.Products.AddAsync(p);
                await _context.SaveChangesAsync();
                return Ok(p);
            }
            return BadRequest(ModelState);
        }

5.5 应用API控制器属性

ApiController属性可应用于Web服务控制器类,以更改模型绑定和验证特性的行为。

    [ApiController]
    [Route("api/[controller]")]
    public class ProductsController : ControllerBase

使用ApiController后就不需要使用FromBody从请求中检查ModelState.IsValid属性,自动应用这种普遍的判断,把控制器方法中的代码焦点放在处理应用逻辑。

        [HttpPost]
        public async Task<IActionResult> SaveProduct(ProductBindingTarget target)
        {
            Product p = target.ToProduct();
            await _context.Products.AddAsync(p);
            await _context.SaveChangesAsync();
            return Ok(p);
        }
        [HttpPut]
        public async Task UpdateProduct(Product product)
        {
            _context.Products.Update(product);
            await _context.SaveChangesAsync();
        }

5.6 忽略Null属性

(1)投射选定的属性
     [HttpGet("{id}")]
        public async Task<IActionResult> GetProduct(long id)
        {
            Product p = await _context.Products.FindAsync(id);
            if (p == null)
            {
                return NotFound();
            }
            return Ok(new
            {
                ProductId = p.ProductId,Name = p.Name,
                Price = p.Price,CategoryId = p.CategoryId,
                SupplierId = p.SupplierId
            });
        }

这样做是为了返回有用属性值,以便省略导航属性。

(2)配置JSON序列化器

可将JSON序列化器配置为在序列化对象时省略值为null的属性。
在Stratup类中配置。此配置会影响所有JSON响应

        public void ConfigureServices(IServiceCollection services)
        {
            ......
            services.AddControllers();
            services.Configure<JsonOptions>(opts =>
            {
                opts.JsonSerializerOptions.IgnoreNullValues = true;
            });
        }
posted @ 2024-04-23 10:33  一纸年华  阅读(31)  评论(0编辑  收藏  举报