环境:VS2017 , Win10 64bit ,
实现功能:
1. 提供index.html,能实现人机简单交互(通过JQuery)。复杂的可以考虑Angular1.x或ASP.Net的Single Page App,我还没实践。
2. 提供RESTful的web service,方便其他工具调用。场景例如:a. 你希望通知远程机器干掉某个process或获取机器信息 b. 在本机上,你希望从Windows Service发一些显示信息出来
方案:本案例是convention-based routing的实现。通过阅读Routing in ASP.NET Web API 与 Attribute Routing in ASP.NET Web API 2这两个例子,灵活性上和易理解上Attribute Routing > convention-based ,根据Routing and Action Selection in ASP.NET Web API,要完全理解convention-based如何选择controller与action反而非常吃力。推荐使用Attribute Routing.
效果图:
用浏览器访问http://localhost:9095/返回
更新(2021/01/23): 今天在梳理web service与web API概念。发现官方例子文档是webAPI2,就想难道还有webAPI3,4,5?和.Net或Core有什么关系? 目前也可以用Core实现webAPI ,步骤和.net很接近,只是在VS2019中有ASP.NET Core Web API template.
步骤:
1. 参考Use OWIN to Self-Host ASP.NET Web API,重复内容就不说了。要注意的是tutorial提供的sample只监听了localhost,本地通过Edge|IE|Chrome访问没问题,其他机器访问会报Error 400 , invalid hostname错误,所以我的Program.cs添加了监听所有9095端口的部分(参考了Hosting WebAPI using OWIN in a windows service与HTTP Error 400 - OWIN with Topshelf)。
using Microsoft.Owin.Hosting;
namespace demo
{
static void Main()
{
StartOptions options = new StartOptions();
options.Urls.Add("http://localhost:9095");//这3行保证了本机和其他机器都能访问
options.Urls.Add("http://127.0.0.1:9095");
options.Urls.Add("http://*:9095");
// Start OWIN host
using (WebApp.Start<Startup>(options))
{
// Create HttpClient and make a request to api/values
HttpClient client = new HttpClient();
var response = client.GetAsync( "http://localhost:9095/api/values/AllProducts").Result;
Console.WriteLine(response);
Console.WriteLine(response.Content.ReadAsStringAsync().Result);
Console.ReadLine();
}
}
}
2. 提供index.html给客户端,参考How to serve index.html with web api selfhosted with OWIN ,所以我的Startup.cs有变化. 原文的链接文章提到了Katana,这原本是ASP.Net团队开发的OWIN实现2019年起又被整合回.Net Core了。
记得安装nuget包Microsoft.Owin.StaticFiles
using Owin;
using System.Web.Http;
using Microsoft.Owin.StaticFiles;
using Microsoft.Owin.FileSystems;
public class Startup
{
// This code configures Web API. The Startup class is specified as a type
// parameter in the WebApp.Start method.
public void Configuration(IAppBuilder appBuilder)
{
// Configure Web API for self-host.
HttpConfiguration config = new HttpConfiguration();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
appBuilder.UseWebApi(config);
//下面这部分就是提供index.html的能力
const string rootFolder = ".";
var fileSystem = new PhysicalFileSystem(rootFolder);
var options = new FileServerOptions
{
EnableDefaultFiles = true,
FileSystem = fileSystem
};
appBuilder.UseFileServer(options);
}
}
3. Tutoral是没提供index.html,html我参考了Get Started with ASP.NET Web API 2 。创建index.html后记得设置,这样工程编译后才会把它放在debug/release目录下
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Product App</title>
</head>
<body>
<div>
<h2>All Products</h2>
<ul id="products" />
</div>
<div>
<h2>Search by ID</h2>
<input type="text" id="prodId" size="5" />
<input type="button" value="Search" onclick="find();" />
<p id="product" />
</div>
<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.3.min.js"></script>
<script>
var uri = 'api/values';
var uri_rel_getAllProducts = "AllProducts";
$(document).ready(function () {
// Send an AJAX request
$.getJSON(uri + "/" + uri_rel_getAllProducts)
.done(function (data) {
// On success, 'data' contains a list of products.
$.each(data, function (key, item) {
// Add a list item for the product.
$('<li>', { text: formatItem(item) }).appendTo($('#products'));
});
});
});
function formatItem(item) {
//return item.Name + ': $' + item.Price;
return item;
}
var uri_rel_Product = "Product";
function find() {
var id = $('#prodId').val();
$.getJSON(uri + "/" + uri_rel_Product + '/' + id)
.done(function (data) {
$('#product').text(formatItem(data));
})
.fail(function (jqXHR, textStatus, err) {
$('#product').text('Error: ' + err);
});
}
</script>
</body>
</html>
4. 默认tutoral的routing是使用了/api/{controller}/{id}形式,即系统收到Get请求默认就找Get开头的方法,不管方法名是什么。我为了调用清楚,参考了Routing in ASP.NET Web API ,所以我的Startup.cs里是 routeTemplate: "api/{controller}/{action}/{id}",同时Controller也略有不同
public class ValuesController : ApiController
{
//只适用于/api/{controller}/{id}的routing
//由于我使用了/api/{controller}/{action}/{id}的routing,这部分不会被调用
// GET api/values
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
//适用于/api/{controller}/{action}/{id}的routing,客户端访问/api/values/AllProducts/{id}时会被调用
[HttpGet]
[ActionName("AllProducts")]
public IEnumerable<string> AllProducts()
{
return new string[] { "value1", "value2" };
}
//适用于/api/{controller}/{action}/{id}的routing,客户端访问/api/values/Product/{id}时会被调用
[HttpGet]
[ActionName("Product")]
public string Product(int id)
{
return "haha";
}
//只适用于/api/{controller}/{id}的routing
//由于我使用了/api/{controller}/{action}/{id}的routing,这部分不会被调用
// GET api/values/5
public string Get(int id)
{
return "value";
}
//只适用于/api/{controller}/{id}的routing
//由于我使用了/api/{controller}/{action}/{id}的routing,这部分不会被调用
// POST api/values
public void Post([FromBody]string value)
{
}
//只适用于/api/{controller}/{id}的routing
//由于我使用了/api/{controller}/{action}/{id}的routing,这部分不会被调用
// PUT api/values/5
public void Put(int id, [FromBody]string value)
{
}
//只适用于/api/{controller}/{id}的routing
//由于我使用了/api/{controller}/{action}/{id}的routing,这部分不会被调用
// DELETE api/values/5
public void Delete(int id)
{
}
}
5. (Optional) 要是self-host的Webservice所在机器要开firewall,确保netsh http add urlacl url=http://*:9000/ user=<your user> 在被外部机器访问前被执行过。这个要求在2处都有提到Hosting WebAPI using OWIN in a windows service | OWIN-WebAPI-Service ,当然这是Microsoft官方也提及了Configuring HTTP and HTTPS ,不过是在WCF里提到的。
后续学习:从Microsoft的Create a REST API with Attribute Routing in ASP.NET Web API 2中我们能看到,Response的返回有返回IEnumerable的,也有返回IQueryable 。IQueryable是服务于oData和filter descriptor的,比如:我要查询100个工人,希望返回的结果是按照工龄排序了的。又或者,工人有很多,我希望查询到工龄最大的一个人。这些排序或过滤的功能,要是数据量大必须在Server端完成。
http://localhost:18950/api/products?$orderby=price desc
http://localhost:18950/api/products?$filter=name eq 'Gizmo 3'
http://localhost:18950/api/products?$orderby=price%20desc&$top=1
有个问题: 要是数据量大,理应数据库去完成过滤与排序,放在serverlet上是否合适?毕竟数据库很有可能是独立服务器。