首先创建服务。使用.NET Core时,需要从ASP.NET Web Application开始,并在如下图所示的对话框中选择Web Api。这个模板添加了Web API需要的文件夹和引用。如果需要Web页面和服务,还可以使用模板Web Applicaiton(Model-View-Controller)。使用示例代码,这个项目在解决方案BooksServiceSample中命名为BooksServiceSampleHost。
用这个模板创建的目录结构包含创建服务所需要的文件夹。Controllers目录包含Web API控制器。事实上,Web API和ASP.NET Core MVC使用相同的基础设施。ASP.NET 的.NET Framework版本不是这样。使用默认的模板,WeatherForecastController是通过一个简单的示例实现创建的。在较大的应用程序中,最好将其分离为多个库。如果创建一个包含服务和模型的库,那么使用来自不同的技术(例如,来自Web API项目和Azure Functions)的相同类型是很容易的。Web API(控制器)的实现也可以位于与托管应用程序分离的库中。在Web应用程序中,可以使用在应用程序自己的库中实现的控制器。服务(BookServices)和Web API(APIBookService)的库都实现为.NET 标准2.0库。有了库APIBookServices,需要添加NuGet包Microsoft.AspNetCore.Mvc.Core和Microsoft.AspNetCore.Mvc.ViewFeatures来实现控制器。在.NET标准2.0库中不可能使用前几章中用过的元数据包Microsoft.AspNetCore.All,而需要一个.NET Core 2.0库。
现在,从模型开始。在项目BooksService中,Models目录用于数据模型。可以将实体类型添加到此目录,以及返回模型类型的存储库。
所创建的服务返回图书的章节列表,并允许动态添加和删除章节。
1. 定义模型
首先需要一个类型来表示要返回和修改的数据。在Models目录中顶一顶额类的名称是BookChapter,它包含表示一章的简单属性:
public class BookChapter
{
public Guid Id { get; set; }
public int Number { get; set; }
public string Title { get; set; }
public int Pages { get; set; }
}
2. 创建服务
接下来,创建一个服务。服务提供的方法由接口IBookChaptersService定义,用于检索、添加和更新图书章节:
public interface IBookChaptersService
{
void Add(BookChapter bookChapter);
void AddRange(IEquatable<BookChapter> chanters);
IEquatable<BookChapter> GerAll();
BookChapter Find(Guid id);
BookChapter Remove(Guid id);
void Update(BookChapter bookChapter);
}
服务的实现由类BookChaptersService定义。书的章节保存在一个集合类中。由于不同客户机请求的多个任务可以并发访问该集合,因此在图书章节中使用类型ConcurrencyDictionary。这个类时线程安全的。Add、Remove和Update方法使用集合来添加、删除和更新图书章节:
public class BookChaptersService:IBookChaptersService
{
private readonly ConcurrentDictionary<Guid,BookChapter> _chapters =
new ConcurrentDictionary<Guid,BookChapter>();
public void Add(BookChapter bookChapter)
{
bookChapter.Id = Guid.NewGuid();
_chapters[bookChapter.Id] = bookChapter;
}
public void AddRange(IEnumerable<BookChapter> chapters)
{
foreach (var chapter in chapters)
{
chapter.Id = Guid.NewGuid();
_chapters[chapter.Id] = chapter;
}
}
public BookChapter Find(Guid id)
{
_chapters.TryGetValue(id,out BookChapter bookChapter);
return bookChapter;
}
public IEnumerable<BookChapter> GerAll() => _chapters.Values;
public BookChapter Remove(Guid id)
{
_chapters.TryRemove(id,out BookChapter remove);
return remove;
}
public void Update(BookChapter bookChapter)
{
_chapters[bookChapter.Id] = bookChapter;
}
}
注意:
通过示例代码,TryRemove方法确保id参数传递的BookChapter在字典中。如果字典已经不包含书的章节,那没关系。
如果找不到所传递的图书章节,Remove方法的另一种实现可以抛出异常。控制器可以更改此错误,以返回HTTP未找到的状态码(404)。
Mircrosoft REST API指南(https://github.com/microsoft/apiguidelines/blob/master/Guidelines.md)指定DELETE请求为幂等性的,因此它应该在多个请求中返回相同的结果。(url无效)
因此,第一次访问服务时,可以使用一些示例章节,类SampleChapter用章节信息填充图书章节服务:
public class SampleChapters
{
private readonly IBookChaptersService _bookChaptersService;
public SampleChapters(IBookChaptersService bookChaptersService)
{
_bookChaptersService = bookChaptersService;
}
private string[] sampleTitles =
{
".NET Application Architectures",
"Core C#",
"Objects and Types",
"Object-Oriented Programming with C#",
"Generics",
"Operators and Casts",
"Arrays",
"Deelgates, Lambdas, and Events",
"Windows Communication Foundation"
};
private int[] chapterNumbers =
{
1,2,3,4,5,6,7,8,44
};
private int[] numberPaes =
{
35,42,33,20,24,38,20,32,44
};
public void CreateSampleChapters()
{
var chapters = new List<BookChapter>();
for (int i = 0; i < 8; i++)
{
chapters.Add(new BookChapter
{
Id = Guid.NewGuid(),
Title = sampleTitles[i],
Number = chapterNumbers[i],
Pages = numberPaes[i]
});
}
_bookChaptersService.AddRange(chapters);
}
}
在托管应用程序中,引用了库。要使服务可用,需要用依赖注入(DI)容器注册。这是在Startup类中完成的。
在启动之后,BookChaptersService和SampleChapters服务通过ID容器的AddSingleton方法注册,为请求服务的所有客户端只创建一个实例。由于BookChaptersService注册为单例,因此可以同时从多个线程中访问它;这就是为什么在其实现代码中需要ConcurrentDictionary的原因:
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSingleton<IBookChaptersService, BookChaptersService>();
services.AddSingleton<SampleChapters>();
}
创建示例章节的调用在Configure方法中完成。在这里,将注入SampleChapters对象,在方法的实现中,将调用CreateSampleChapters创建示例章节:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env,SampleChapters sampleChapters)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
sampleChapters.CreateSampleChapters();
}
3. 创建控制器
Web API控制器使用图书章节服务。控制器可以通过Solution Explorer上下文菜单Add New Item | Web API Controller Class 创建。管理图书章节的控制器类被命名为BookChaptersConttoller。这个类派生自基类Controller。到控制器的路由由使用Route特性定义。该路由以api开头,其后是控制器的名称,这是没有Conttroller后缀的控制器类名。BooksChaptersController的构造函数需要一个实现IBookChapterRepository接口的对象。这个对象是通过依赖注入功能注入的:
[Produces("application/json","application/xml")]
[Route("api/[controller]")]
public class BookChaptersController : Controller
{
private readonly IBookChaptersService _bookChaptersService;
public BookChaptersController(IBookChaptersService bookChaptersService)
{
_bookChaptersService = bookChaptersService;
}
}
模板中创建的Get方法被重命名,并被修改为返回类型为IEnumerable<BookChapter>的完整集合:
[HttpGet]
public IEnumerable<BookChapter> GetBookChapters()=> _bookChaptersService.GetAll();
带一个参数的Get方法被重命名为GetBookChapterById,用Find方法过滤存储库的字典。过滤器的参数id从URL中检索。如果没有找到章节,存储库的Find方法就返回null。在这种情况下,返回NotFound。NotFound返回一个404(未找到)响应。找到对象时,创建一个新的ObjectResult并返回它:ObjectResult返回一个状态码200,其中包含图书的章节:
// GET api/values/5
[HttpGet("{id}",Name =nameof(GetBookChapterById))]
public IActionResult GetBookChapterById(Guid id)
{
var chapter = _bookChaptersService.Find(id);
if (chapter == null)
{
return NotFound();
}
else
{
return new ObjectResult(chapter);
}
}
要添加图书的新章节,应添加PostBookChapter。该方法接收一个BookChapter作为HTTP体的一部分,反序列化后分配给方法的参数。如果参数chapter为null,就返回一个BadRequest(HTTP 400 错误)。如果添加BookChapter,这个方法就返回CreatedAtRoute。CreateAtRoute返回HTTTP状态码201(已创建)及序列化的对象。返回的标题信息包含到资源的链接,其id设置为新建对象的标识符:
// POST api/values
[HttpPost]
public IActionResult PostBookChapter([FromBody] BookChapter chapter)
{
if (chapter == null)
{
return BadRequest();
}
_bookChaptersService.Add(chapter);
return CreatedAtRoute(nameof(GetBookChapterById),new { chapter.Id},chapter);
}
更新条目需要基于HTTP PUT请求。PutBookChapter方法在集合中更新已有的条目。如果对象还不在集合中,就返回NotFound。如果找到了对象,就更新它并返回一个成功的结果状态码204,其中没有内容:
// PUT api/values/5
[HttpPut("{id}")]
public IActionResult PutBookChapter(Guid id, [FromBody] BookChapter chapter)
{
if (chapter == null || id != chapter.Id)
{
return BadRequest();
}
if (_bookChaptersService.Find(chapter.Id) == null)
{
return NotFound();
}
_bookChaptersService.Update(chapter);
return NoContent();
}
对于HTTP DELETE请求,从字典中删除图书的章节:
// DELETE api/values/5
[HttpDelete("{id}")]
public IActionResult DeleteBookChapter(Guid id)
{
var chapter = _bookChaptersService.Remove(id);
if (chapter == null)
{
return NotFound();
}
//return CreatedAtAction("removed!",new { id, },chapter);
return Ok();
}
有了这个控制器,就可以在浏览器上进行第一组测试了。打开链接https://localhost:5001/api/BookChapters(端口号可能不同),返回JSON,如下所示:
4. 修改响应格式
ASP.NET Web API的.NET Framework版本返回JSON或XML,这取决于由客户端请求的格式。在ASP.NET Core MVC中,当返回ObjectResult时,默认情况下返回JSON。如果也需要返回XML,可以添加一个对Startup类的AddXmlSerializerFormatters的调用。AddXmlSerializerFormatters是IMcBuilder接口的一个扩展方法,可以使用流利API添加到AddMvc方法中:
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddXmlSerializerFormatters();
services.AddControllers();
services.AddSingleton<IBookChaptersService, BookChaptersService>();
services.AddSingleton<SampleChapters>();
}
在控制器中,使用Produces特性可以指定允许的内容类型和可选的结果:
[Produces("application/json","application/xml")]
[Route("api/[controller]")]
public class BookChaptersController : Controller
{
//...
}
5. REST结果和状态码
下表总结了服务基于HTTP方法返回的结果:
下表显示了重要的HTTP状态码、Controller方法和返回状态码的实例化对象。要返回任何HTTP状态码,可以返回一个Http StatusCodeResult对象,用所需的状态码初始化:
所有成功状态码都以2开头,错误代码以4开头。状态码列表在RFC 7231中可以找到:https://tools.ietf.org/html/rfc7231#section-6.3。