ASP.NET WEB API
与WEB API有关的类型
HttpMessageHandler(System.Net.Http)(消息处理器)
表示Http请求的处理程序,处理程序类似于Http管道,它们是链式调用,所以可以自定义更多的处理程序。
HttpClient(System.Net.Http)(Http客户端)
表示客户端请求的类,可以配置请求的WEB API地址、Http报头、异步发送请求和读取服务端响应的Http报文等操作。HttpClient默认的构造函数就是利用HttpMessageHandler处理Http请求,而开发人员也可以在初始化HttpClient的时候向其构造函数传递一个自定义的消息处理器。
//请求的地址主机名和端口号或域名
DefaultRequestHeaders
//请求报头的集合,提供了Add方法用于添加报头,报头以键值对的方式添加
//示例:
request.DefaultRequestHeaders.Add( "Accept", "application/json" );
//另一种方式是先创建报头对象,然后像下面这样添加:
request.DefaultRequestHeaders.Accept.Add( new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue( "application/json" ) ); //MediaTypeWithQualityHeaderValue表示Http报文的Accept请求头,更多报头可参考MSDN。
GetAsync( string uri )
//异步发起对WEB API的调用,此方法会自动开启一个新的线程Task,返回一个Task<HttpResponseMessage>
PostAsJsonAsync( string uri, T value )
//将value序列化为json字符串,异步提交Post请求到参数指定的地址,此方法会自动开启一个新的线程Task,返回一个Task<HttpResponseMessage>
//示例:
Product product = new Product { Name = "Hex", Category = "音乐", Price = 180 };
HttpResponseMessage response = request.PostAsJsonAsync( "/api/products", product ).Result; //调用Task的Result属性提取异步任务的返回值,这会阻塞所有线程直到异步任务成功返回数据
PutAsJsonAsync( )
//异步修改数据,此方法会自动开启一个新的线程Task,返回一个Task<HttpResponseMessage>
//示例:
Product product = new Product { Name = "万有引力之虹", Category = "图书", Price = 89 };
HttpResponseMessage response = request.PutAsJsonAsync( "/api/products/1", product ).Result; //服务端接收一个产品id和一个Product实体
DeleteAsync( )
//异步删除数据,此方法会自动开启一个新的线程Task,返回一个Task<HttpResponseMessage>
HttpResponseMessage(System.Net.Http)(Http响应流)
表示服务端返回的消息
//获取Http请求是否成功返回了状态码,状态码在200-299之间时返回true
StatusCode
//获取或设置服务端返回的Http状态码,(int)response.StatusCode
ReasonPhrase
//获取或设置服务端返回的Http状态码相关的信息
Content
//获取或设置报文主体,即服务端返回的数据,返回一个HttpContent,可以实例化一个StringContent来创建响应的内容,因为StringContent从HttpContent派生
HttpRequestMessage(System.Net.Http)(Http请求流)
表示客户端请求的消息
//创建一个响应流对象,value表示报文主体内容
Content
//获取或设置报文主体,即客户端提交的数据,返回一个HttpContent
Headers
//获取客户端请求的头部信息集合,可通过在其上调用GetValues(string headerKey)来获取请求头信息
HttpContent(System.Net.Http)(Http报文主体)
表示客户端或服务端发送的报文主体内容
//异步读取报文数据,返回一个Task,要取出数据需要Task.Result
//示例:
string json = response.Content.ReadAsAsync<string>( ).Result; //读取http报文
Headers
//报文主体内容的头信息集合,可通过在其上调用GetValues(string headerKey)来获取报文内容中的头信息,比如获取Content-Type
//示例:
IEnumerable<string> contentType = response.Content.Headers.GetValues( "Content-Type" );
foreach(var str in contentType)
{
Console.WriteLine( str);
}
创建ASP.NET WEB API服务
选择ASP.NET WEB应用程序,勾选WEB API
或选择空,这样就可以取消勾选MVC,值创建一个不包含MVC的API项目:
项目创建完成后可以看到Controllers目录有两个控制器,一个用于ASP.NET MVC,另一个ValueController就是用于API服务的API控制器,每一个API控制器都从ApiController派生。Api控制器的方法返回类型按约定最好是使用HttpResponseMessage,每个方法按约定的前缀名称定义,Get前缀是获取数据,Post前缀是以Post提交方式提交数据,Put前缀是修改数据,Delete前缀是删除数据,按照这四种约定来定义你的Http处理函数即可。而客户端在调用API服务时,其请求的地址不能包含处理Http请求的函数名称,只包含Api控制器的名称即可,因为客户端会使用HttpClient的以Get、Post、Put、Delete作为前缀的函数名来发起Http请求,所以服务端会自动根据前缀约定找到Api控制器下对应的处理函数来处理请求。
配置Web.Config允许其它服务端返回的页面使用Ajax跨域请求
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="*" />
<add name="Access-Control-Allow-Headers" value="*" />
<add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE" />
</customHeaders>
</httpProtocol>
</system.webServer>
首先需要在Models目录定义模型类、处理模型的接口、处理模型的类型,它们分别是Product、IProductRepository、ProductRepository。
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
}
}
{
public interface IProductRepository
{
IEnumerable<Product> GetAll( );
Product Get( int id );
Product Add( Product item );
void Remove( int id );
bool Update( Product item);
}
}
{
public class ProductRepository : IProductRepository
{
private List<Product> products = new List<Product>( );
private int nextId = 0;
public ProductRepository( )
{
Add( new Product { Id = nextId++, Category = "图书", Name = "寂静的春天", Price = 58 } );
Add( new Product { Id = nextId++, Category = "音乐", Name = "King Of Sweet", Price = 150 } );
Add( new Product { Id = nextId++, Category = "音乐", Name = "Sweet Glow Of Silence", Price = 12 } );
Add( new Product { Id = nextId++, Category = "图书", Name = "精神分析导论", Price = 12 } );
Add( new Product { Id = nextId++, Category = "音乐", Name = "Zebra", Price = 12 } );
}
//根据id查询产品
public Product Get( int id )
{
return products.Single( p => p.Id == id );
}
//查询所有产品
public IEnumerable<Product> GetAll( )
{
return products;
}
//添加产品
public Product Add( Product item )
{
item.Id = nextId++;
products.Add( item );
return item;
}
//删除产品
public void Remove( int id )
{
products.RemoveAt( id );
}
//修改产品
public bool Update( Product item )
{
var product=products.Single( p => p.Id == item.Id );
products.Remove( product );
products.Add( item );
return true;
}
}
}
新建一个Api控制器
using Newtonsoft.Json;
namespace API.Controllers
{
public class ProductsController : ApiController
{
static readonly IProductRepository productRepository = new ProductRepository( );
//查询所有产品
public HttpResponseMessage GetAll( )
{
return Request.CreateResponse( HttpStatusCode.OK, JsonConvert.SerializeObject( productRepository.GetAll( ) ) );
}
//根据id查询产品
public HttpResponseMessage GetProduct( int id )
{
Product item = productRepository.Get( id );
return item == null ? throw new HttpResponseException( HttpStatusCode.NotFound ) : Request.CreateResponse( HttpStatusCode.OK, JsonConvert.SerializeObject( item ) );
}
//根据分类查询产品
public HttpResponseMessage GetProductCategory( string category )
{
var list = productRepository.GetAll( ).Where( p => p.Category == category );
return Request.CreateResponse( HttpStatusCode.OK, JsonConvert.SerializeObject( list ) );
}
//添加产品
public HttpResponseMessage PostProduct( Product item )
{
item = productRepository.Add( item );
var response = Request.CreateResponse( HttpStatusCode.Created, item );
string uri = Url.Link( "DefaultApi", new { id = item.Id } );
response.Headers.Location = new Uri( uri );
return response;
}
//修改产品
public HttpResponseMessage PutProduct( int id, Product product )
{
product.Id = id;
bool update = productRepository.Update( product );
return Request.CreateResponse( HttpStatusCode.OK );
}
//删除产品
public HttpResponseMessage DeleteProduct( int id )
{
productRepository.Remove( id );
return new HttpResponseMessage( HttpStatusCode.NoContent ); //删除后可以返回http204以表示再无此条目
}
}
}
WEB API默认返回xml的数据,为了能返回json格式,可通过修改App_Start目录的WebApiConfig.cs文件,在Register方法中作如下配置:
using Newtonsoft.Json.Serialization;
namespace API
{
public static class WebApiConfig
{
public static void Register( HttpConfiguration config )
{
// Web API 配置和服务
// Web API configuration and services
var json = config.Formatters.JsonFormatter;
// 解决json序列化时的循环引用问题
json.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
// 干掉XML序列化器
config.Formatters.Remove( config.Formatters.XmlFormatter );
var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>( ).First( );
jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver( );
}
}
}
新建一个CUI程序来表示客户端
using Newtonsoft.Json;
namespace ClientCallWebAPI
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
}
class Program
{
static void Main( string[] args )
{
HttpClient request = new HttpClient( ); //创建发送请求的http对象
request.BaseAddress = new Uri( "http://localhost:54838" ); //请求的地址主机名和端口号或域名
request.DefaultRequestHeaders.Add( "Accept", "application/json" );//添加Accept报头,定义我能接受的从服务端返回的数据类型
//发起异步请求,等待返回一个http报文
//request.GetAsync( "/api/products/1" ); 根据id查询产品
//request.GetAsync( "/api/products" ); 查询所有产品
//request.GetAsync( "/api/products?category=图书" ); 根据类别查询产品
HttpResponseMessage response = request.GetAsync( "/api/products?category=图书" ).Result;
if (!response.IsSuccessStatusCode) Console.WriteLine( "服务端无响应" );
string json= response.Content.ReadAsAsync<string>( ).Result; //读取http报文
IEnumerable<Product> products = JsonConvert.DeserializeObject<IEnumerable<Product>>( json );
if (!products.Any( ))
{
Console.WriteLine( $"{response.StatusCode}{response.ReasonPhrase}" );
return;
}
foreach (var item in products)
{
Console.WriteLine( $"{item.Id} {item.Name} {item.Category} {item.Price}" );
}
}
}
}
例子中调用HttpClient的异步操作方法向服务端发起请求,而在WEB应用程序中,除了可以使用Result阻塞所有线程等待异步任务完成以外,还可以使用await操作符达到同样的效果,如:
{
HttpResponseMessage response = request.GetAsync( "/api/products?category=图书" ).Result;
}
或:
{
HttpResponseMessage response =await request.GetAsync( "/api/products?category=图书" );
}
API控制器的Action方法也可以直接返回string,如:
public string Add( [FromBody] Product pro )
{
return $"{pro.Name},{pro.Category},{pro.Price}";
}
添加API路由模板
默认情况下Http请求调用API的Url都是以api开头,带api控制器名,但不带Action名称,这是在WebApiConfig.cs中默认的路由模板所定义的,你可以更改这个模板,以便可以像下面那样调用API
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}", //没有action
defaults: new { id = RouteParameter.Optional }
);
//调用时的url请求格式:api/products
config.Routes.MapHttpRoute(
name:"myApi",
routeTemplate: "api/{controller}/{action}/{id}", //定义了api的action占位符
defaults: new { id = RouteParameter.Optional }
);
//调用时的url请求格式:api/products/insertProduct
自定义客户端的消息处理器
调用API的客户端可以自定义消息处理器,消息处理器从System.Net.Http.DelegatingHandler派生,然后重写基类的SendAsync和Dispose方法,因为请求发出时会进入SendAsync方法,而服务端的响应同样也会进入SendAsync方法,所以该方法可以处理即将发送到远程API的请求,也可以处理远程API返回的数据。
using Newtonsoft.Json;
using System.Diagnostics;
namespace ClientCallWebAPI
{
public class MyMessageHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, System.Threading.CancellationToken cancellationToken )
{
//发送Http请求,如果没有得到服务端API的响应,则输出一个日志记录,最后将HttpResponseMessage(响应消息)返回以便传递给其它的Http消息处理器
HttpResponseMessage response = await base.SendAsync( request, cancellationToken );
if (!response.IsSuccessStatusCode)
{
Trace.Listeners.Add( new TextWriterTraceListener( "f:/log.txt" ) );
Trace.AutoFlush = true;
Trace.WriteLine( $"响应错误:请求时间 { DateTime.Now.ToString( ) } 错误码:{ response.StatusCode } 错误详细信息:{ response.ReasonPhrase }" );
}
return response;
}
protected override void Dispose( bool disposing )
{
base.Dispose( disposing );
}
}
class Program
{
static void Main( string[] args )
{
HttpClient request = HttpClientFactory.Create( new MyMessageHandler( ) );
}
}
}
自定义服务端的消息处理器
与自定义客户端的消息处理器是一样的,以下实现当请求进入服务端后,MyMessageHandler 将处理请求,将客户端IP写入日志,最后调用基类的消息处理器正常处理请求。
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Net.Http;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace API.App_Start
{
public class MyMessageHandler : DelegatingHandler
{
protected async override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken )
{
//自定义处理请请求:请求到达后将客户端IP写入日志
Trace.Listeners.Add( new TextWriterTraceListener( @"F:log.txt" ) );
Trace.AutoFlush = true;
Trace.WriteLine( $"{DateTime.Now.ToString( "yyyy年-MM月-dd日" )} 请求到达 IP:{ IP.GetIP( )}" );
// 调用内置的消息处理器处理请求
var response = await base.SendAsync( request, cancellationToken );
//向客户端输出响应:得到响应的输出对象后可以自定义一个响应头并添加到输出流返回给客户端
response.Headers.Add( "myMessageHeader", "你好,你的请求已经成功处理" );
return response;
}
protected override void Dispose( bool disposing )
{
base.Dispose( disposing );
}
}
public class IP
{
/// <summary>
/// 获取客户端IP地址
/// </summary>
/// <returns>若失败则返回回送地址</returns>
public static string GetIP( )
{
//如果客户端使用了代理服务器,则利用HTTP_X_FORWARDED_FOR找到客户端IP地址
var var = HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
string userHostAddress = var != null ? var.ToString( ).Split( ',' )[0].Trim( ) : "";
//否则直接读取REMOTE_ADDR获取客户端IP地址
if (string.IsNullOrEmpty( userHostAddress ))
{
userHostAddress = HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];
}
//前两者均失败,则利用Request.UserHostAddress属性获取IP地址,但此时无法确定该IP是客户端IP还是代理IP
if (string.IsNullOrEmpty( userHostAddress ))
{
userHostAddress = HttpContext.Current.Request.UserHostAddress;
}
//最后判断获取是否成功,并检查IP地址的格式(检查其格式非常重要)
if (!string.IsNullOrEmpty( userHostAddress ) && IsIP( userHostAddress ))
{
return userHostAddress;
}
return "本机IP";
}
/// <summary>
/// 检查IP地址格式
/// </summary>
/// <param name="ip"></param>
/// <returns></returns>
public static bool IsIP( string ip )
{
return System.Text.RegularExpressions.Regex.IsMatch( ip, @"^((2[0-4]d|25[0-5]|[01]?dd?).){3}(2[0-4]d|25[0-5]|[01]?dd?)$" );
}
}
}
将消息处理器插入处理管道,需要在App_Start的WebApiConfig.cs中注册
{
public static void Register( HttpConfiguration config )
{
config.MessageHandlers.Add( new MyMessageHandler( ) );
}
}
注册单路由消息处理器
{
public static class WebApiConfig
{
public static void Register( HttpConfiguration config )
{
// Web API 路由
config.MapHttpAttributeRoutes( );
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.MessageHandlers.Add( new MyMessageHandler1( ) ); //全局消息处理器
config.Routes.MapHttpRoute(
name: "SpecialApi",
routeTemplate: "specialApi/{controller}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: null, //必须提供约束,哪怕是null,否则会提示参数个数不完整,没有采用4个参数重载
handler:new MyMessageHandler2( ) //注册特定路由的消息处理器,只针对此路由使用此消息处理器
);
}
}
}
创建API帮助文档
右击API项目属性 - 生成 - 勾选生成xml文档,为xml文档命名,这会为项目生成一个xml格式的说明性文档
打开项目目录,Areas - HelpPage - App_Start - HelpPageConfig.cs,在Register方法中注册xml说明文档
{
config.SetDocumentationProvider( new XmlDocumentationProvider( HttpContext.Current.Server.MapPath( "~/App_Data/APIHelp.xml" ) ) );
}
在你的Api控制器中为每一个Action操作添加注释,帮助文档页面会自动显示这些注释信息。
/// 根据id查询产品
/// </summary>
/// <param name="id">提供产品ID</param>
/// <returns></returns>
public HttpResponseMessage GetProduct( int id )
{
Product item = productRepository.Get( id );
return item == null ? throw new HttpResponseException( HttpStatusCode.NotFound ) : Request.CreateResponse( HttpStatusCode.OK, JsonConvert.SerializeObject( item ) );
}
打开以下项目目录的文件可以修改帮助文档页面及其详细页面的部分英文说明为中文:
Areas - HelpPage - Views - Help - Index.cshtml,可修改帮助文档页面顶部的文字描述。
Areas - HelpPage - Views - Help - Api.cshtml,可修改帮助文档详细页面顶部的回到帮助主页的超链接。
Areas - HelpPage - Views - Help - DisplayTemplates - ApiGroup.cshtml,可修改帮助文档页面的列头为中文。
Areas - HelpPage - Views - Help - DisplayTemplates - HelpPageApiModel.cshtml,可修改帮助文档API页面的列头为中文。
Areas - HelpPage - Views - Help - DisplayTemplates - Parameters.cshtml,可修改帮助文档API详细页面的列头为中文。
异常处理
为了便于开发人员定位http错误,也为了向客户端显示更加友好的http错误信息,你可以手动定义http异常信息,这样可以把友好的异常信息响应给客户端,也可以定义一个http异常过滤器,异常过滤器应从ExceptionFilterAttribute派生。
手动定义http异常
手动定义http异常,使用这种方式必须注意,WEB API的Http异常机制是最早开始执行的,像下面这种由开发人员编写的异常抛出逻辑是后来才执行的,也即,如果一个明显的Http异常被WEB API的异常机制捕获,比如发起请求的客户端并未提供category参数,那么以下代码的测试逻辑根本不会执行,因为未提供category参数的异常已经在手动测试的代码执行前被执行,如果没有明显的异常被WEB API捕获,则以下测试category参数的值是否在两个值的范围之内的代码逻辑才会得到执行。
{
HttpResponseMessage httpResponseMessage = null;
if (!category.Contains( "图书" ) || !category.Contains( "音乐" ))
{
httpResponseMessage = Request.CreateResponse( HttpStatusCode.NotFound, JsonConvert.SerializeObject( new { msg = "提供的参数值不在可查询范围之内" } ) );
//CreateResponse方法会自动将参数2提供的value序列化为json格式:message:value
//httpResponseMessage = Request.CreateResponse( HttpStatusCode.NotFound, "提供的参数值不在可查询范围之内" );
}
else
{
var list = productRepository.GetAll( ).Where( p => p.Category == category );
httpResponseMessage = Request.CreateResponse( HttpStatusCode.OK, JsonConvert.SerializeObject( list ) );
}
return httpResponseMessage;
}
定义全局异常过滤器
你可以直接将异常过滤器应用在api控制器或api控制器的action方法上,但是,如果你创建API项目时勾选了MVC,那么过滤器先必须注册在WebApiConfig.cs中,否则无效。单经过我的测试,自定义的Http异常过滤器无效,下断点不会进入,原因不明。
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using System.Net;
using System.Net.Http;
using System.Web.Http.Filters;
using System.Diagnostics;
using Newtonsoft.Json;
namespace API.App_Start
{
/// <summary>
/// Http错误过滤器
/// </summary>
public class HttpErrorFilterAttribute: ExceptionFilterAttribute
{
/// <summary>
/// 过滤Http错误
/// </summary>
/// <param name="context"></param>
public override void OnException( HttpActionExecutedContext context )
{
var ex = context.Exception;
//将异常写入日志记录
Trace.Listeners.Add( new TextWriterTraceListener( "f:/logforHttpError.txt" ) );
Trace.AutoFlush = true;
Trace.WriteLine( $"异常发生时间:{DateTime.Now.ToString( "yyyy-MM-dd HH:mm:ss" )} " +
$"异常类型:{context.Exception.GetType( ).ToString( )}"+
$"堆栈信息:{context.Exception.Message}{context.Exception.StackTrace}"
);
//无实现
if (ex is NotImplementedException)
{
object jsonMsg = new { errorType = ex.GetType( ).ToString( ), message = ex.Message, statusCode = (int)HttpStatusCode.NotImplemented };
string jsonMsgStr = JsonConvert.SerializeObject( jsonMsg );
context.Exception= new HttpResponseException( new HttpResponseMessage( HttpStatusCode.NotImplemented )
{
Content = new StringContent( jsonMsgStr ),
ReasonPhrase = "请求无实现"
} );
}
//超时
else if (ex is TimeoutException)
{
object jsonMsg = new { errorType = ex.GetType( ).ToString( ), message = ex.Message, statusCode = (int)HttpStatusCode.RequestTimeout };
string jsonMsgStr = JsonConvert.SerializeObject( jsonMsg );
context.Exception = new HttpResponseException( new HttpResponseMessage( HttpStatusCode.RequestTimeout )
{
Content = new StringContent( jsonMsgStr ),
ReasonPhrase = "请求超时"
} );
}
//服务器内部错误
else
{
object jsonMsg = new { errorType = ex.GetType( ).ToString( ), message = ex.Message, statusCode = (int)HttpStatusCode.InternalServerError };
string jsonMsgStr = JsonConvert.SerializeObject( jsonMsg );
context.Exception = new HttpResponseException( new HttpResponseMessage( HttpStatusCode.InternalServerError )
{
Content = new StringContent( jsonMsgStr ),
ReasonPhrase = "内部服务器错误"
} );
}
base.OnException( context );
}
}
}
{
public static class WebApiConfig
{
public static void Register( HttpConfiguration config )
{
config.Filters.Add( new API.App_Start.HttpErrorFilterAttribute( ) );
}
}
}
Javascript调用API
$(document).ready(function () {
//查询数据
$.ajax({
url: "http://localhost:58594/api/employeemsg/Get",
type: "get",
success: (data) => {
var json = $.parseJSON(data);
$(json).each((index, item) =>alert(item.ID + item.Name)); }
});
//添加新数据
$.ajax({
url: "http://localhost:58594/api/employeemsg/Add",
data:{ ID:15,Name:"lily",Gender:"男",Birthday:"2016-11-12",Age:32 },
type: 'post',
success: (data) => {
//console.log(data);
alert(data);
}
});
});
</script>
服务端调用API得到数据存入ViewBag后,可以通过如下方式将数据取出来放进Js代码中:
var jsonstr=@Html.Raw(ViewBag.msg);
var json = $.parseJSON(jsonstr);
$(json).each(function(index,item){
alert(item.ID+item.Name+item.Gender+item.Birthday);
});
});
附:下载远程数据
WebClient wc = new WebClient();
wc.Encoding = Encoding.GetEncoding("utf-8");
string content = wc.DownloadString(url);