天天看点

2. 创建服务

首先创建服务。使用.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,如下所示:

2. 创建服务

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方法返回的结果:

2. 创建服务

下表显示了重要的HTTP状态码、Controller方法和返回状态码的实例化对象。要返回任何HTTP状态码,可以返回一个Http StatusCodeResult对象,用所需的状态码初始化:

2. 创建服务

所有成功状态码都以2开头,错误代码以4开头。状态码列表在RFC 7231中可以找到:https://tools.ietf.org/html/rfc7231#section-6.3。

继续阅读