REST:一个简单的 REST 框架






4.93/5 (73投票s)
一个使用 .NET 基础类库从头开始编写的简单 REST 框架
- 引言
- 代码在哪里?
- 它是如何工作的
- 就这些
引言
在我工作的地方,我很幸运地接触到各种技术,包括 Web 开发、桌面开发、消息传递、线程等等。但我一直对人们如何创建完整的 REST 堆栈很感兴趣,并想知道只用自己的智慧和 .NET BCL 来做到这一点需要什么。
所以我想了想,并提出了以下要求/问题
- 如何监听 HTTP 请求?
- 如何让用户代码尽可能简单?
- 如何让用户代码通过 IOC/依赖注入来使用其他依赖项?
- 能否支持 Json/Xml 和其他可能的序列化偏好?
因此,带着这些要点,我开始着手从头开始创建一个 REST 框架。
对于那些不知道 REST 是什么/代表什么的人,维基百科是这样说的:
表示状态转移 (REST) 是万维网体系结构的抽象;更具体地说,REST 是一种架构风格,它由一组协调的架构约束组成,这些约束应用于分布式超媒体系统中的组件、连接器和数据元素。REST 忽略组件实现和协议语法的细节,以便专注于组件的角色、它们与其他组件交互的约束以及它们对重要数据元素的解释。
表示状态转移一词于 2000 年由 Roy Fielding 在其加州大学欧文分校的博士论文中首次提出并定义。REST 已被应用于描述期望的 Web 体系结构、识别现有问题、比较替代解决方案以及确保协议扩展不会违反使 Web 成功的核心约束。Fielding 在开发 HTTP 1.1 和统一资源标识符 (URI) 的同时,与同事合作开发了 REST。
REST 架构风格也适用于 Web 服务的开发,作为 SOAP 等其他分布式计算规范的替代方案。如果 Web 服务符合架构约束部分中描述的约束,则可以将它们称为“RESTful”。如果您只对 REST 在 Web API 中的应用感兴趣,请参阅应用于 Web 服务的章节。
http://en.wikipedia.org/wiki/Representational_state_transfer 最后更新日期 03/10/14
代码在哪里?
代码可在我的 GitHub 页面找到:https://github.com/sachabarber/rest
如何运行演示代码?
代码包含在一个解决方案中,这是一个 Visual Studio 2013 解决方案,打开后应如下所示。
为了正确运行,您应该确保以下 2 个项目被启动
RESTServerConsoleHost
RESTClientConsoleApp
您可以手动执行此操作,或者通过在 Visual Studio 中运行多项目来完成。如下图所示:
当您正确运行时,您应该会看到一些来自 RESTClientConsoleApp
的输出,大致如下:
它是如何工作的
本节概述了我编写的简单 REST 框架的工作原理
总体思路
我编写的 REST 框架实际上可以分解为几个简单的步骤:
- 监听传入的 HTTP 请求
- 对于传入的 HTTP 请求,尝试识别一个
IHandler
(稍后会详细介绍,别担心)来处理该请求 - 根据请求的 HTTP 方法以及请求 URL 提供的的内容/URL,识别要调用的处理程序中的正确方法
- 将正确的输入参数从请求流传递给处理程序方法,并获取结果
- 获取结果并使用响应流将其返回给调用代码
托管/IOC 支持 (最终用户代码)
我想创建一个可以非常轻松地托管在几乎任何应用程序中的东西,而且我不想依赖任何其他框架。
托管(最终用户代码)
Task.Run(() =>
{
IWindsorContainer container = new WindsorContainer();
container.Install(new DomainInstaller(), new HandlerInstaller());
//run the server
IDependencyResolver dependencyResolver = new WindsorDependencyResolver(container);
HttpServer httpServer = new HttpServer(dependencyResolver);
httpServer.Run(new string[] { @"https://:8001/" });
});
这里有几点需要注意
- 我们使用一个 IOC 容器(我选择了 Castle Windsor,但您可以使用任何您喜欢的)
- 您的代码需要实现一个
IDependencyResolver
,然后将其交给 REST 框架,以便它能够解析自身的依赖项以及用户代码(即您的代码)的依赖项
IOC 代码 (最终用户代码)
您的代码应该提供以下内容,可能值得查看演示的 RESTServerConsoleHost
项目。正如我所说,我使用的是 Castle Windsor,所以根据您选择的 IOC 容器,以下内容可能会有所不同
依赖项注册 (最终用户代码)
注册由 IHandler
基于的代码使用的任何依赖项(我们稍后将介绍)
public class DomainInstaller : IWindsorInstaller
{
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register(Component
.For(typeof(IRepository<Person,int>))
.ImplementedBy(typeof(InMemoryPersonRepository))
.LifestyleSingleton());
container.Register(Component
.For(typeof(IRepository<Account,int>))
.ImplementedBy(typeof(InMemoryAccountRepository))
.LifestyleSingleton());
container.Register(Component
.For(typeof(IRepository<User,int>))
.ImplementedBy(typeof(InMemoryUserRepository))
.LifestyleSingleton());
}
}
处理程序注册 (最终用户代码)
注册任何 IHandler
public class HandlerInstaller : IWindsorInstaller
{
public void Install(IWindsorContainer container, IConfigurationStore store)
{
foreach (var item in this.GetType().Assembly.GetTypes())
{
if (item.GetInterfaces().Any(x => x.FullName.Contains("IHandler")))
{
AddHandler(container, item);
}
}
}
private void AddHandler(IWindsorContainer container, Type item)
{
container.Register(Component
.For(typeof(IHandler))
.ImplementedBy(item)
.LifestyleTransient());
}
}
自定义 IDependencyResolver (最终用户代码)
创建一个自定义 IDependencyResolver
public class WindsorDependencyResolver : IDependencyResolver
{
private readonly IWindsorContainer container;
public WindsorDependencyResolver(IWindsorContainer container)
{
this.container = container;
}
public object GetService(Type serviceType)
{
return container.Kernel.HasComponent(serviceType) ? container.Resolve(serviceType) : null;
}
public IEnumerable<object> GetServices(Type serviceType)
{
return container.Kernel.HasComponent(serviceType) ?
container.ResolveAll(serviceType).Cast<object>() : new object[] { };
}
}
服务器 (框架的一部分)
基本要求是如何监听 HTTP 请求。我进行了一些谷歌搜索,并找到了 HttpListener
类,该类允许用户代码监听 HTTP 请求。
使用 HttpListener
类的一个缺点是您将需要创建一个 URL ACL,但出于学习目的,这对我来说没问题。
如果您不喜欢这样做,您可能想看看这个项目,虽然它不是生产就绪的,但看起来很有趣,并且利用了 OWIN 堆栈,并且没有任何特定的 ACL 要求。
https://github.com/Bobris/Nowin
我在本文中介绍的简单 REST 框架的服务器部分出奇地简单,我所做的只是监听传入的 HTTP 请求,并将实际工作委托给所谓的路由动作器。路由动作器使用责任链设计模式逐一进行检查。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using RESTServer.Handlers;
using RESTServer.IOC;
using RESTServer.Routing;
namespace RESTServer
{
public class HttpServer
{
private readonly HttpListener listener = new HttpListener();
private readonly int accepts = 4;
private readonly IDependencyResolver dependencyResolver;
private Semaphore sem;
public HttpServer(IDependencyResolver dependencyResolver, int accepts = 4)
{
listener.IgnoreWriteExceptions = true;
// Multiply by number of cores:
this.accepts = accepts * Environment.ProcessorCount;
this.dependencyResolver = dependencyResolver;
}
public async void Run(params string[] uriPrefixes)
{
// Add the server bindings:
foreach (var prefix in uriPrefixes)
listener.Prefixes.Add(prefix);
listener.Start();
//Accept connections:
//1. Higher values mean more connections can be maintained yet at a
// much slower average response time; fewer connections will be rejected.
//2. Lower values mean less connections can be maintained yet at a
// much faster average response time; more connections will be rejected.
sem = new Semaphore(accepts, accepts);
await RunServer();
}
private async Task RunServer()
{
while (true)
{
// Fall through until we've initialized all our connection listeners.
// When the semaphore blocks (its count reaches 0) we wait until a connection occurs,
// upon which the semaphore is released and we create another connection "awaiter."
sem.WaitOne();
await StartConnectionListener();
}
}
private async Task StartConnectionListener()
{
// Wait for a connection
HttpListenerContext context = await listener.GetContextAsync();
// Allow a new connection listener to be set up.
sem.Release();
//process the current request
await ProcessRequest(context);
}
private async Task<bool> ProcessRequest(HttpListenerContext context)
{
// Setup Chain of Responsibility for route processing
VerbRouteActioner verbRouteActioner = new VerbRouteActioner();
DynamicRouteActioner dynamicRouteActioner = new DynamicRouteActioner();
BadRouteActioner badRouteActioner = new BadRouteActioner();
verbRouteActioner.Successor = dynamicRouteActioner;
dynamicRouteActioner.Successor = badRouteActioner;
var handlers = dependencyResolver.GetServices(typeof(IHandler)).Cast<IHandler>().ToList();
await verbRouteActioner.ActionRequest(context, handlers);
return true;
}
}
}
正如您所见,我没有说谎,基本循环非常简单:等待请求,当看到新请求时,调用路由动作器类。正如我所说的,路由动作器是使用责任链设计模式排列的。
使用的顺序如下:
VerbRouteActioner
:此项尝试将路由与用户基于的IVerbHandler<T,TKey>
进行匹配。如果找到,则使用它,否则会将请求交给后继者 (DynamicRouteHandler
) 来处理。DynamicRouteHandler
:此项尝试将路由与用户基于的IDynamicRouteHandler<T,TKey>
进行匹配。如果找到,则使用它,否则会将请求交给后继者 (BadRouteHandler
) 来处理。BadRouteHandler :
这是链中的最后一个环节,它仅返回 HTTP 状态码 404,未找到。
所以我们刚刚提到了一些新东西,xxxRouteActioner 到底是什么?简单来说,它是这个简单 REST 框架内部的一个类,我在这里介绍它,它知道如何处理 HTTP 请求并调用相关的派生 IHandler
对象,其中调用的确切方法不仅取决于请求 URL,还取决于 HTTP 动词 GET/PUT/POST/DELETE。
我遵循标准的 REST 实践,即每个处理 REST 请求的方法都有特定的输入参数和返回值。下表显示了此 REST 框架遵循的规则。
如果您还不完全理解 IHandler
代码的作用,请不要担心,我们稍后将对此进行更详细的介绍。
序列化
我允许了两种不同的序列化方式,即 Json/Xml,支持此的序列化器如下所示。
Json
public class JsonPipelineSerializer : ISerializer
{
public async Task<T> Deserialize<T>(string rawBodyData)
{
return await Task.Run(() => JsonConvert.DeserializeObject<T>(rawBodyData));
}
public async Task<Byte[]> SerializeAsBytes<T>(T item)
{
return await Task.Run(() => Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(item)));
}
public async Task<string> Serialize<T>(T item)
{
return await Task.Run(() => JsonConvert.SerializeObject(item));
}
}
XML
public class XmlPipelineSerializer : ISerializer { private XmlSerializer xmlSerializer; public async Task<T> Deserialize<T>(string rawBodyData) { xmlSerializer = new XmlSerializer(typeof(T)); return await Task.Run(async () => { T temp = default(T); using (Stream ms = await GenerateStreamFromString(rawBodyData)) { temp = (T)xmlSerializer.Deserialize(ms); } return temp; }); } public async Task<Byte[]> SerializeAsBytes<T>(T item) { xmlSerializer = new XmlSerializer(typeof(T)); return await Task.Run(() => { using (MemoryStream ms = new MemoryStream()) { xmlSerializer.Serialize(ms, item); ms.Position = 0; return ms.ToArray(); } }); } public async Task<string> Serialize<T>(T item) { xmlSerializer = new XmlSerializer(typeof(T)); return await Task.Run(() => { using (MemoryStream ms = new MemoryStream()) { xmlSerializer.Serialize(ms, item); ms.Position = 0; using (StreamReader sr = new StreamReader(ms)) { return sr.ReadToEnd(); } } }); } private async Task<Stream> GenerateStreamFromString(string s) { MemoryStream stream = new MemoryStream(); await Task.Run(() => { StreamWriter writer = new StreamWriter(stream, Encoding.UTF8); writer.Write(s); writer.Flush(); stream.Position = 0; }); return stream; } }
路由/处理程序
我们已经提到,简单 REST 框架有一个概念,即使用一些代码来处理特定请求,如果无法处理,则使用 HttpServer
代码中设置的责任链设计模式调用其后继者。
那么,xxxRouteActioner 类到底是什么样子的?以下是任何 xxxRouteActioner 继承的抽象类。
public abstract class RouteActioner
{
public abstract Task<bool> ActionRequest(
HttpListenerContext context,
IList<IHandler> handlers);
public RouteActioner Successor { set; protected get; }
}
我实际上在 REST 框架中提供了三个基于 RouteActioner 的类,本文档介绍的 REST 框架。下表描述了它们。
xxxRouteHandler 代码处理以下类型的事务:
- HTTP 请求是否属于此类型的 xxxRouteActioner/
IHandler
? - 是否存在与所请求路由匹配的正确类型的
IHandler
? - 请求是否包含与 xxxRouteActioner/
IHandler
配合使用的正确参数?
我们将在下面查看主要的两个 xxxRouteHandlers 及其最终用户代码对应的部分。
VerbRouteActioner / IVerbHandler<T,TKey>
我们现在知道 xxxRouteActioners 和它们对应的用户代码之间存在一对一的映射。本节讨论 VerbRouteActioner / IVerbHandler<T,TKey>
。
IVerbHandler<T,TKey> (框架的一部分)
由于存在 IVerbHandler<T,TKey>
接口,如下所示,因此还有一个 VerbRouteActioner
与用户代码提供的 IVerbHandler<T,TKey>
实现一起使用。
这是 IVerbHandler<T,TKey>
接口,可以看到我们已经有了一组用户代码必须实现的方法。这些方法在我们上面看到的表中进行了概述,其中我们期望用户代码实现这些方法。
/// <summary>
/// A standard REST interface
/// </summary>
/// <typeparam name="T">An intention of the type of
/// REST resource</typeparam>
/// <typeparam name="TKey">An intention of the type of
/// the Id field of the REST resource</typeparam>
public interface IVerbHandler<T, TKey> : IHandler
{
/// <summary>
/// Gets a REST resource by its Id
/// </summary>
Task<T> Get(TKey id);
/// <summary>
/// Gets all instances of REST resource
/// </summary>
Task<IEnumerable<T>> Get();
/// <summary>
/// Add a new REST resource. Where the newly added resource is returned
/// </summary>
Task<T> Post(T item);
/// <summary>
/// Updates the REST resource identified by its Id, with the new REST resource
/// </summary>
Task<bool> Put(TKey id, T item);
/// <summary>
/// Deletes a new REST resource by its Id
/// </summary>
Task<bool> Delete(TKey id);
}
可以看到,由于所有需要实现的方法都是接口的一部分,我决定将返回类型设置为 Task<T>
。这允许 REST 框架使用 async/await 来 await
用户提供的 IVerbHandler<T,TKey>
实现的返回值。
IVerbHandler<T,TKey> 实现 (最终用户代码)
所以我们已经看到了接口,那么用户提供的 IVerbHandler<T,TKey>
实现到底是什么样子的?
它看起来像这样:
[RouteBase("/people", SerializationToUse.Xml)]
public class PersonHandler : IVerbHandler<Person, int>
{
private readonly IRepository<Person,int> personRepository;
public PersonHandler(IRepository<Person,int> personRepository)
{
this.personRepository = personRepository;
}
#region IVerbHandler<Person,int> Members
public async Task<Person> Get(int id)
{
return await Task.Run(() => personRepository.Get(id));
}
public async Task<IEnumerable<Person>> Get()
{
return await Task.Run(() => personRepository.GetAll());
}
public async Task<Person> Post(Person item)
{
return await Task.Run(() => personRepository.Add(item));
}
public async Task<bool> Put(int id, Person item)
{
return await Task.Run(() =>
{
item.Id = id;
return personRepository.Update(item);
});
}
public async Task<bool> Delete(int id)
{
return await Task.Run(() => personRepository.Delete(id));
}
#endregion
}
这里有几件事要讨论。
- 我们实现了
IVerbHandler<T,TKey>
,其中T
是资源类型,TKey
是T
类型使用的 ID 字段的类型。然后,我们必须编写IVerbHandler<T,TKey>
中的所有方法。 - 我们可以接受其他服务的依赖项。这里显示的是,我们接受了
IRepository<Person>
的依赖项,这是一个内存中的存储库。 - 我们使用一个特殊属性来提供一些路由元数据。此属性称为
RouteBaseAttribute
,它允许指定两项内容:- 请求路由的基础部分。这类似于您在使用约定式的东西(如 ASP MVC)时得到的内容,我们将把 HTTP 请求解析到一个特定的控制器来处理请求。我为了简单起见选择了属性。我们将继续查看
VerbRouteActioner
时了解其用法。 - 我们可以指定要使用的序列化,即 Xml/Json。
- 请求路由的基础部分。这类似于您在使用约定式的东西(如 ASP MVC)时得到的内容,我们将把 HTTP 请求解析到一个特定的控制器来处理请求。我为了简单起见选择了属性。我们将继续查看
VerbRouteActioner (框架的一部分)
VerbRouteActioner
是此简单 REST 框架中使用的责任链设计模式的一部分。VerbRouteActioner
内部的操作大致如下列要点所示:
- 我们尝试从我们拥有的
IHandler
列表中找到一个IVerbHandler<T,TKey>
,该列表与给定的请求路由匹配。这是通过使用RouteBaseAttribute
来完成的,该属性是用户实现的IVerbHandler<T,TKey>
的一部分。 - 然后,我们检查 HTTP 请求以查看使用的方法,这使我们能够识别正在请求的 REST 调用。
- 然后,我们提取
IVerbHandler<T,TKey>
的 ID(TKey
类型)和内容(T
类型),然后调用用户实现的IVerbHandler<T,TKey>
中的相关代码。
这是 VerbRouteActioner
的完整代码。它确实使用了另一个辅助类,我没有在此包含它,因为我认为即使不深入了解该辅助类的内部工作原理,代码也足够易读。如果您感兴趣,该类名为 RestMethodActioner
。
public class VerbRouteActioner : RouteActioner
{
RestMethodActioner restMethodActioner = new RestMethodActioner();
public override async Task<bool> ActionRequest(
HttpListenerContext context,
IList<IHandler> handlers)
{
return await Task.Run(async () =>
{
object matchingHandler = null;
//1. try and find the handler who has same base address as the request url
// if we find a handler, go to step 2, otherwise try successor
//2. find out what verb is being used
var httpMethod = context.Request.HttpMethod;
var url = context.Request.RawUrl;
bool result = false;
var routeResult = await restMethodActioner.FindHandler(
typeof (IVerbHandler<,>), context, handlers, false);
if (routeResult.Handler != null)
{
//handler is using RouteBase, so fair chance it is a VerbHandler
var genericArgs = GetVerbHandlerGenericArgs(routeResult.Handler.GetType());
MethodInfo method = typeof (VerbRouteActioner).GetMethod("DispatchToHandler",
BindingFlags.NonPublic | BindingFlags.Instance);
MethodInfo generic = method.MakeGenericMethod(genericArgs[0], genericArgs[1]);
result = await (Task<bool>) generic.Invoke(this, new object[]
{
context, routeResult.Handler, httpMethod, routeResult.SerializationToUse
});
return result;
}
result = await this.Successor.ActionRequest(context, handlers);
return result;
});
}
private async Task<bool> DispatchToHandler<T, TKey>(
HttpListenerContext context, object handler,
string httpMethod, SerializationToUse serializationToUse)
{
return await Task.Run(async () =>
{
MethodInfo method = typeof (VerbRouteActioner).GetMethod("CreateVerbHandler",
BindingFlags.NonPublic | BindingFlags.Instance);
MethodInfo generic = method.MakeGenericMethod(new[] {typeof (T), typeof (TKey)});
IVerbHandler<T, TKey> actualHandler =
(IVerbHandler<T, TKey>) generic.Invoke(this, new[] {handler});
var result = false;
switch (httpMethod)
{
case "GET":
result = await HandleGet<T, TKey>(
actualHandler, context, serializationToUse);
break;
case "PUT":
result = await HandlePut<T, TKey>(
actualHandler, context, serializationToUse);
break;
case "POST":
result = await HandlePost<T, TKey>(
actualHandler, context, serializationToUse);
break;
case "DELETE":
result = await HandleDelete<T, TKey>(
actualHandler, context, serializationToUse);
break;
}
return result;
});
}
private async Task<bool> HandleGet<T, TKey>(
IVerbHandler<T, TKey> actualHandler,
HttpListenerContext context,
SerializationToUse serializationToUse)
{
return await Task.Run(async () =>
{
var result = false;
if (restMethodActioner.IsGetAll(context.Request.RawUrl))
{
var items = await actualHandler.Get();
result = await restMethodActioner.SetResponse<List<T>>(
context, items.ToList(),
serializationToUse);
}
else
{
TKey id = await restMethodActioner.ExtractId<TKey>(
context.Request);
var item = await actualHandler.Get(id);
result = await restMethodActioner.SetResponse<T>(
context, item, serializationToUse);
}
return result;
});
}
private async Task<bool> HandlePost<T, TKey>(
IVerbHandler<T, TKey> actualHandler,
HttpListenerContext context,
SerializationToUse serializationToUse)
{
return await Task.Run(async () =>
{
T item = await restMethodActioner.ExtractContent<T>(
context.Request, serializationToUse);
T itemAdded = await actualHandler.Post(item);
bool result = await restMethodActioner.SetResponse<T>(
context, itemAdded, serializationToUse);
return result;
});
}
private async Task<bool> HandleDelete<T, TKey>(
IVerbHandler<T, TKey> actualHandler,
HttpListenerContext context,
SerializationToUse serializationToUse)
{
return await Task.Run(async () =>
{
TKey id = await restMethodActioner.ExtractId<TKey>(context.Request);
bool updatedOk = await actualHandler.Delete(id);
updatedOk &= await restMethodActioner.SetOkResponse(context);
return updatedOk;
});
}
private async Task<bool> HandlePut<T, TKey>(
IVerbHandler<T, TKey> actualHandler,
HttpListenerContext context,
SerializationToUse serializationToUse)
{
return await Task.Run(async () =>
{
TKey id = await restMethodActioner.ExtractId<TKey>(context.Request);
T item = await restMethodActioner.ExtractContent<T>(context.Request,
serializationToUse);
bool updatedOk = await actualHandler.Put(id, item);
updatedOk &= await restMethodActioner.SetOkResponse(context);
return updatedOk;
});
}
/// <summary>
/// Called via Reflection
/// </summary>
private IVerbHandler<T, TKey> CreateVerbHandler<T, TKey>(object item)
{
Expression convertExpr = Expression.Convert(
Expression.Constant(item),
typeof(IVerbHandler<T, TKey>)
);
var x = Expression.Lambda<
Func<IVerbHandler<T, TKey>>>(convertExpr).Compile()();
return x;
}
private Type[] GetVerbHandlerGenericArgs(Type item)
{
var ints = item.GetInterfaces();
var verbInterface = item.GetInterfaces().Single(
x => x.FullName.Contains("IVerbHandler"));
return verbInterface.GenericTypeArguments;
}
}
DynamicRouteActioner / IDynamicRouteHandler<T,TKey>
我们现在知道 xxxRouteActioners 和它们对应的用户代码之间存在一对一的映射。本节讨论 DynamicRouteActioner / IDynamicRouteHandler<T,TKey>
。
IDynamicRouteHandler<T,TKey> (框架的一部分)
存在一个 IDynamicRouteHandler<T,TKey>
接口,如下所示,还有一个 DynamicRouteActioner
与用户代码提供的 IDynamicRouteHandler<T,TKey>
实现一起使用。
这是 IDynamicRouteHandler<T,TKey>
接口,可以看到这次没有任何方法。这意味着用户可以自由指定任何方法名称/签名。这显然意味着最终用户代码可能是错误的,并且无法解析为标准的 REST 方法,因此我们需要对这种类型的最终用户处理程序进行更多检查,我们将在稍后看到。
/// <summary>
/// Used as a marker interface when you want to supply your own custom routes
/// by using the
/// </summary>
/// <typeparam name="T">An intention of the type of
/// REST resource</typeparam>
/// <typeparam name="TKey">An intention of the type of the Id
/// field of the REST resource</typeparam>
public interface IDynamicRouteHandler<T,TKey> : IHandler
{
}
IDynamicRouteHandler<T,TKey> 实现 (最终用户代码)
所以我们已经看到了接口,那么用户提供的 IDynamicRouteHandler<T,TKey>
实现到底是什么样子的?它看起来像这样:
[RouteBase("/users", SerializationToUse.Json)]
public class UserHandler : IDynamicRouteHandler<User, int>
{
private readonly IRepository<User,int> userRepository;
public UserHandler(IRepository<User,int> userRepository)
{
this.userRepository = userRepository;
}
#region IDynamicRouteHandler<User,int> Members
[Route("/GetUserByTheirId/{0}", HttpMethod.Get)]
public async Task<User> GetUserByTheirId(int id)
{
return await Task.Run(() => userRepository.Get(id));
}
[Route("/GetAllUsers", HttpMethod.Get)]
public async Task<IEnumerable<User>> GetAllUsers()
{
return await Task.Run(() => userRepository.GetAll());
}
[Route("/AddASingleUser", HttpMethod.Post)]
public async Task<User> AddASingleUser(User item)
{
return await Task.Run(() => userRepository.Add(item));
}
[Route("/UpdateAUserUsingId/{0}", HttpMethod.Put)]
public async Task<bool> UpdateTheUserWithId(int id, User item)
{
return await Task.Run(() =>
{
item.Id = id;
return userRepository.Update(item);
});
}
[Route("/DeleteUserByTheirId/{0}", HttpMethod.Delete)]
public async Task<bool> DeleteAUser(int id)
{
return await Task.Run(() => userRepository.Delete(id));
}
#endregion
}
这里有几件事要讨论。
- 我们实现了
IDynamicRouteHandler<T,TKey>
,其中T
是资源类型,TKey
是T
类型使用的 ID 字段的类型。然后,我们必须编写IDynamicRouteHandler<T,TKey>
中的所有方法。 - 我们可以接受其他服务的依赖项。这里显示的是,我们接受了
IRepository<User>
的依赖项,这是一个内存中的存储库。 - 我们使用一个特殊属性来提供一些路由元数据。此属性称为
RouteBaseAttribute
,它允许指定两项内容:- 请求路由的基础部分。这类似于您在使用约定式的东西(如 ASP MVC)时得到的内容,我们将把 HTTP 请求解析到一个特定的控制器来处理请求。我为了简单起见选择了属性。我们将继续查看
VerbRouteActioner
时了解其用法。 - 我们可以指定要使用的序列化,即 Xml/Json。
- 请求路由的基础部分。这类似于您在使用约定式的东西(如 ASP MVC)时得到的内容,我们将把 HTTP 请求解析到一个特定的控制器来处理请求。我为了简单起见选择了属性。我们将继续查看
- 每个方法都使用另一个特殊属性来提供更多的路由元数据。此属性称为
RouteAttribute
,它允许指定两项内容:- 请求的路由的操作部分。这类似于您在使用约定式的东西(如 ASP MVC)时得到的内容,我们将把 HTTP 请求解析到一个特定的操作来处理请求。我为了简单起见选择了属性。我们将继续查看
DynamicRouteActioner
时了解其用法。 - 我们可以指定要使用的
HttpMethod
,即 GET/PUT/POST/DELETE。
- 请求的路由的操作部分。这类似于您在使用约定式的东西(如 ASP MVC)时得到的内容,我们将把 HTTP 请求解析到一个特定的操作来处理请求。我为了简单起见选择了属性。我们将继续查看
DynamicRouteActioner (框架的一部分)
DynamicRouteActioner
是此简单 REST 框架中使用的责任链设计模式的一部分。DynamicRouteActioner
内部的操作大致如下列要点所示:
- 我们尝试从我们拥有的
IHandler
列表中找到一个IDynamicHandler<T,TKey>
,该列表与给定的请求路由匹配。这是通过使用RouteBaseAttribute
来完成的,该属性是用户实现的IVerbHandler<T,TKey>
的一部分。 - 然后,我们检查 HTTP 请求以查看使用的方法,这使我们能够识别正在请求的 REST 调用。因此,我们从请求中获取一个 HTTP 方法,但我们仍然需要识别
IDynamicHandler<T,TKey>
中要调用的正确方法,其中IDynamicHandler<T,TKey>
实际上只是一个标记接口,最终用户代码可以提供任何方法签名,这些签名可能是有效的,也可能无效。那么我们如何处理这个问题?我们如何知道要调用哪个方法?这是另一个自定义属性(即RouteAttribute
)的工作,我们可以像这样使用它:[Route("/GetUserByTheirId/{0}", HttpMethod.Get)]
放在最终用户IDynamicHandler<T,TKey>
实现中的任何方法上。通过使用RouteAttribute
,我们可以指定一个 URL 部分用于与传入的 HTTP 请求进行匹配(在约定式系统中,如 ASP MVC,可以认为此属性能够定位一个特定的操作),还可以指定IDynamicHandler<T,TKey>
打算处理的正确 HTTP 方法。那么,我们如何使用RouteAttribute
来帮助我们识别正确的IDynamicHandler<T,TKey>
方法呢?答案在于非常彻底的参数/返回值检查。只有当以下所有条件都为真时,我们才假设提供的IDynamicHandler<T,TKey>
方法之一是正确的,并且可以调用:RouteBaseAttribute
+RouteAttribute
部分代表整个请求 URL。RouteAttribute HttpMethod
值与传入的 HTTP 请求方法匹配。RouteAttribute
属性化方法的签名参数/返回值(可能是Task<T>
也可能不是)匹配。
- 然后,我们提取
IDynamicRouteHandler<T,TKey>
的 ID(TKey
类型)和内容(T
类型),然后调用用户实现的IDynamicRouteHandler<T,TKey>
中的相关代码。 - 如果当前找到的
IDynamicRouteHandler<T,TKey>
方法是Task<T>
类型,则使用 async/await 来await
方法调用的结果,并将其写入 HTTP 响应流。但是,如果它只是一个标准的返回值T
,则无需等待,返回值仅用于写入 HTTP 响应流。
这是 DynamicRouteHandler
的完整代码。它确实使用了另一个辅助类,我没有在此包含它,因为我认为即使不深入了解该辅助类的内部工作原理,代码也足够易读。如果您感兴趣,该类名为 RestMethodActioner
。
public class DynamicRouteActioner : RouteActioner
{
RestMethodActioner restMethodActioner = new RestMethodActioner();
public override async Task<bool> ActionRequest(
HttpListenerContext context, IList<IHandler> handlers)
{
return await Task.Run(async () =>
{
object matchingHandler = null;
//1. try and find the handler who has same base address as the request url
// if we find a handler, go to step 2, otherwise try successor
//2. find out what verb is being used
var httpMethod = context.Request.HttpMethod;
var url = context.Request.RawUrl;
bool result = false;
var routeResult = await restMethodActioner.FindHandler(
typeof (IDynamicRouteHandler<,>), context, handlers, true);
if (routeResult.Handler != null)
{
//handler is using RouteBase, so fair chance it is a VerbHandler
var genericArgs = GetDynamicRouteHandlerGenericArgs(
routeResult.Handler.GetType());
MethodInfo method = typeof (DynamicRouteActioner).GetMethod("DispatchToHandler",
BindingFlags.NonPublic | BindingFlags.Instance);
MethodInfo generic = method.MakeGenericMethod(genericArgs[0], genericArgs[1]);
result = await (Task<bool>) generic.Invoke(this, new object[]
{
context, routeResult.Handler, httpMethod, url,
routeResult.SerializationToUse
});
return result;
}
result = await this.Successor.ActionRequest(context, handlers);
return result;
});
}
private async Task<bool> DispatchToHandler<T, TKey>(
HttpListenerContext context, object handler,
string httpMethod, string url, SerializationToUse serializationToUse)
{
return await Task.Run(async () =>
{
var result = false;
DynamicMethodInfo method = null;
switch (httpMethod)
{
case "GET":
method = await ObtainGetMethod<T, TKey>(handler, url);
result = await HandleGet<T, TKey>(method, handler,
context, serializationToUse);
break;
case "PUT":
method = await ObtainPutMethod<T, TKey>(handler, url);
result = await HandlePut<T, TKey>(method, handler,
context, serializationToUse);
break;
case "POST":
method = await ObtainPostMethod<T, TKey>(handler, url);
result = await HandlePost<T, TKey>(method, handler,
context, serializationToUse);
break;
case "DELETE":
method = await ObtainDeleteMethod<T, TKey>(handler, url);
result = await HandleDelete<T, TKey>(method, handler,
context, serializationToUse);
break;
}
return result;
});
}
private async Task<DynamicMethodInfo> ObtainGetMethod<T, TKey>(
object handler, string url)
{
return await Task.Run(async () =>
{
var possibleGetMethods = ObtainPossibleMethodMatches(handler, HttpMethod.Get);
if (restMethodActioner.IsGetAll(url))
{
string attributeUrl = url.Substring(url.LastIndexOf("/"));
var method = possibleGetMethods.Where(x => x.Route.Route == attributeUrl)
.Select(x => x.Method).FirstOrDefault();
if (MethodResultIsCorrectType<Task<IEnumerable<T>>>(method))
{
return new DynamicMethodInfo(method, true);
}
if (MethodResultIsCorrectType<IEnumerable<T>>(method))
{
return new DynamicMethodInfo(method, false);
}
throw new HttpResponseException(string.Format(
"Incorrect return type/parameters for route '{0}'", url));
}
else
{
var method = GetIdMethodMatch(possibleGetMethods, url);
if (MethodResultIsCorrectType<Task<T>>(method)
&& MethodHasSingleCorrectParameterOfType<TKey>(method))
{
return new DynamicMethodInfo(method, true);
}
if (MethodResultIsCorrectType<T>(method)
&& MethodHasSingleCorrectParameterOfType<TKey>(method))
{
return new DynamicMethodInfo(method, false);
}
throw new HttpResponseException(string.Format(
"Incorrect return type/parameters for route '{0}'", url));
}
});
}
private async Task<DynamicMethodInfo> ObtainPutMethod<T, TKey>(
object handler, string url)
{
return await Task.Run(async () =>
{
var possiblePutMethods = ObtainPossibleMethodMatches(handler, HttpMethod.Put);
var method = GetIdMethodMatch(possiblePutMethods, url);
if (MethodResultIsCorrectType<Task<bool>>(method)
&& MethodHasCorrectPutParameters<T, TKey>(method))
{
return new DynamicMethodInfo(method, true);
}
if (MethodResultIsCorrectType<bool>(method)
&& MethodHasCorrectPutParameters<T, TKey>(method))
{
return new DynamicMethodInfo(method, false);
}
throw new HttpResponseException(string.Format(
"Incorrect return type/parameters for route '{0}'", url));
});
}
private async Task<DynamicMethodInfo> ObtainPostMethod<T, TKey>(
object handler, string url)
{
return await Task.Run(async () =>
{
var possiblePostMethods = ObtainPossibleMethodMatches(handler, HttpMethod.Post);
string attributeUrl = url.Substring(url.LastIndexOf("/"));
var method = possiblePostMethods.Where(x => x.Route.Route == attributeUrl)
.Select(x => x.Method).FirstOrDefault();
if (MethodResultIsCorrectType<Task<T>>(method)
&& MethodHasSingleCorrectParameterOfType<T>(method))
{
return new DynamicMethodInfo(method, true);
}
if (MethodResultIsCorrectType<T>(method)
&& MethodHasSingleCorrectParameterOfType<T>(method))
{
return new DynamicMethodInfo(method, false);
}
throw new HttpResponseException(string.Format(
"Incorrect return type/parameters for route '{0}'", url));
});
}
private async Task<DynamicMethodInfo> ObtainDeleteMethod<T, TKey>(
object handler, string url)
{
return await Task.Run(async () =>
{
var possibleDeleteMethods = ObtainPossibleMethodMatches(handler, HttpMethod.Delete);
var method = GetIdMethodMatch(possibleDeleteMethods, url);
if (MethodResultIsCorrectType<Task<bool>>(method)
&& MethodHasSingleCorrectParameterOfType<TKey>(method))
{
return new DynamicMethodInfo(method, true);
}
if (MethodResultIsCorrectType<bool>(method)
&& MethodHasSingleCorrectParameterOfType<TKey>(method))
{
return new DynamicMethodInfo(method, false);
}
throw new HttpResponseException(string.Format(
"Incorrect return type/parameters for route '{0}'", url));
});
}
private async Task<bool> HandleGet<T, TKey>(
DynamicMethodInfo methodInfo, object handler,
HttpListenerContext context,
SerializationToUse serializationToUse)
{
return await Task.Run(async () =>
{
var result = false;
if (restMethodActioner.IsGetAll(context.Request.RawUrl))
{
if (methodInfo.IsTask)
{
var items = await (Task<IEnumerable<T>>) methodInfo.Method.Invoke(handler, null);
result = await restMethodActioner.SetResponse<List<T>>(context,
items.ToList(), serializationToUse);
}
else
{
var items = (IEnumerable<T>) methodInfo.Method.Invoke(handler, null);
result = await restMethodActioner.SetResponse<List<T>>(context,
items.ToList(), serializationToUse);
}
}
else
{
TKey id = await restMethodActioner.ExtractId<TKey>(context.Request);
if (methodInfo.IsTask)
{
var item = await (Task<T>) methodInfo.Method.Invoke(
handler, new object[] {id});
result = await restMethodActioner.SetResponse<T>(context,
item, serializationToUse);
}
else
{
var item = (T) methodInfo.Method.Invoke(handler, new object[] {id});
result = await restMethodActioner.SetResponse<T>(context,
item, serializationToUse);
}
}
return result;
});
}
private async Task<bool> HandlePut<T, TKey>(
DynamicMethodInfo methodInfo, object handler,
HttpListenerContext context,
SerializationToUse serializationToUse)
{
return await Task.Run(async () =>
{
T item = await restMethodActioner.ExtractContent<T>(
context.Request, serializationToUse);
TKey id = await restMethodActioner.ExtractId<TKey>(
context.Request);
var updatedOk = false;
if (methodInfo.IsTask)
{
updatedOk = await (Task<bool>) methodInfo.Method.Invoke(handler,
new object[] {id, item});
}
else
{
updatedOk = (bool) methodInfo.Method.Invoke(handler, null);
}
updatedOk &= await restMethodActioner.SetOkResponse(context);
return updatedOk;
});
}
private async Task<bool> HandlePost<T, TKey>(
DynamicMethodInfo methodInfo, object handler,
HttpListenerContext context,
SerializationToUse serializationToUse)
{
return await Task.Run(async () =>
{
T itemAdded = default(T);
T item = await restMethodActioner.ExtractContent<T>(
context.Request, serializationToUse);
if (methodInfo.IsTask)
{
itemAdded = await (Task<T>) methodInfo.Method.Invoke(
handler, new object[] {item});
}
else
{
itemAdded = (T) methodInfo.Method.Invoke(handler,
new object[] {item});
}
bool result = await restMethodActioner.SetResponse<T>(
context, itemAdded, serializationToUse);
return result;
});
}
private async Task<bool> HandleDelete<T, TKey>(
DynamicMethodInfo methodInfo, object handler,
HttpListenerContext context,
SerializationToUse serializationToUse)
{
return await Task.Run(async () =>
{
var updatedOk = false;
TKey id = await restMethodActioner.ExtractId<TKey>(context.Request);
if (methodInfo.IsTask)
{
updatedOk = await (Task<bool>) methodInfo.Method.Invoke(
handler, new object[] {id});
}
else
{
updatedOk = (bool) methodInfo.Method.Invoke(handler, new object[] {id});
}
updatedOk &= await restMethodActioner.SetOkResponse(context);
return updatedOk;
});
}
private bool MethodHasSingleCorrectParameterOfType<TKey>(MethodInfo method)
{
var parameters = method.GetParameters();
return parameters.Count() == 1 && parameters[0].ParameterType == typeof (TKey);
}
private bool MethodHasCorrectPutParameters<T,TKey>(MethodInfo method)
{
var parameters = method.GetParameters();
return parameters.Count() == 2 &&
parameters[0].ParameterType == typeof (TKey) &&
parameters[1].ParameterType == typeof (T);
}
private bool MethodResultIsCorrectType<T>(MethodInfo method)
{
return method.ReturnType.IsAssignableFrom(typeof (T));
}
private IEnumerable<MethodMatch> ObtainPossibleMethodMatches(
object handler, HttpMethod httpMethod)
{
return from x in handler.GetType().GetMethods()
let attrib = x.GetCustomAttributes(typeof(RouteAttribute), false)
where attrib.Length > 0 && ((RouteAttribute)attrib[0]).HttpVerb == httpMethod
select new MethodMatch(x, (RouteAttribute)attrib[0]);
}
private MethodInfo GetIdMethodMatch(IEnumerable<MethodMatch> possibleMatches, string url)
{
string attributeUrl = url.Substring(0, url.LastIndexOf("/"));
attributeUrl = attributeUrl.Substring(attributeUrl.LastIndexOf("/"));
attributeUrl = attributeUrl + "/{0}";
return possibleMatches.Where(x => x.Route.Route == attributeUrl)
.Select(x => x.Method).FirstOrDefault();
}
private Type[] GetDynamicRouteHandlerGenericArgs(Type item)
{
var ints = item.GetInterfaces();
var verbInterface = item.GetInterfaces().Single(
x => x.FullName.Contains("IDynamicRouteHandler"));
return verbInterface.GenericTypeArguments;
}
}
IBadRouteActioner
正如我前面所说,这个简单 REST 框架使用的责任链设计模式中的最后一个链是 BadRouteActioner
,它仅用于返回 HTTP 状态码 404,未找到。
这是它的代码:
public class BadRouteActioner : RouteActioner
{
public override async Task<bool> ActionRequest(
HttpListenerContext context,
IList<IHandler> handlers)
{
return await Task.Run(async () =>
{
HttpListenerResponse response = context.Response;
using (System.IO.Stream output = response.OutputStream)
{
var buffer = Encoding.UTF8.GetBytes("Not Found");
output.Write(buffer, 0, buffer.Length);
response.StatusCode = 404;
response.StatusDescription = Enum.GetName(typeof (HttpStatusCode),
HttpStatusCode.NotFound);
}
return true;
});
}
}
.NET 客户端
我想创建一些辅助代码,使使用 .NET 客户端尽可能简单。为此,我开发了以下辅助代码,它使得编写 REST 方法非常容易。
public class RESTWebClient : WebClient
{
private WebRequest request = null;
private XmlPipelineSerializer xmlPipelineSerializer = new XmlPipelineSerializer();
private JsonPipelineSerializer jsonPipelineSerializer = new JsonPipelineSerializer();
protected override WebRequest GetWebRequest(Uri address)
{
this.request = base.GetWebRequest(address);
if (this.request is HttpWebRequest)
{
((HttpWebRequest)this.request).AllowAutoRedirect = false;
}
return this.request;
}
public async Task<RESTResponse<T>> Get<T>(
string url, SerializationToUse serializationToUse)
{
return await Task.Run(async () =>
{
string response = await Task.Run(() => DownloadString(url));
return await CreateResponse<T>(response, serializationToUse);
});
}
public async Task<RESTResponse<T>> Post<T>(
string url, T item, SerializationToUse serializationToUse)
{
return await Task.Run(async () =>
{
byte[] responsebytes = await UploadDataForMethod(url, "POST", item, serializationToUse);
string responsebody = string.Empty;
if (serializationToUse == SerializationToUse.Xml)
{
responsebody = Encoding.UTF8.GetString(responsebytes);
}
if (serializationToUse == SerializationToUse.Json)
{
responsebody = Encoding.UTF8.GetString(responsebytes);
}
return await CreateResponse<T>(responsebody, serializationToUse);
});
}
public async Task<HttpStatusCode> Delete(string url)
{
return await Task.Run(async () =>
{
var request = WebRequest.Create(url);
request.Method = "DELETE";
var response = await request.GetResponseAsync();
return ((HttpWebResponse) response).StatusCode;
});
}
public async Task<HttpStatusCode> Put<T>(
string url, T item, SerializationToUse serializationToUse)
{
return await Task.Run(async () =>
{
await UploadDataForMethod(url, "PUT", item, serializationToUse);
return await StatusCode();
});
}
private async Task<byte[]> UploadDataForMethod<T>(
string url, string httpMethod, T item, SerializationToUse serializationToUse)
{
return await Task.Run(async () =>
{
if (serializationToUse == SerializationToUse.Xml)
{
Headers.Add("Content-Type", "application/xml");
var serialized = await xmlPipelineSerializer.SerializeAsBytes(item);
return await Task.Run(() => UploadData(url, httpMethod, serialized));
}
if (serializationToUse == SerializationToUse.Json)
{
Headers.Add("Content-Type", "application/json");
var serialized = await jsonPipelineSerializer.SerializeAsBytes(item);
return await Task.Run(() => UploadData(url, httpMethod, serialized));
}
throw new InvalidOperationException("You need to specify either Xml or Json serialization");
});
}
private async Task<HttpStatusCode> StatusCode()
{
return await Task.Run(() =>
{
if (this.request == null)
{
throw (new InvalidOperationException(
"Unable to retrieve the status code, maybe you haven't made a request yet."));
}
HttpWebResponse response = base.GetWebResponse(this.request) as HttpWebResponse;
if (response != null)
{
return response.StatusCode;
}
throw (new InvalidOperationException(
"Unable to retrieve the status code, maybe you haven't made a request yet."));
});
}
private async Task<RESTResponse<T>> CreateResponse<T>(
string response, SerializationToUse serializationToUse)
{
return await Task.Run(async () =>
{
if (serializationToUse == SerializationToUse.Xml)
{
return new RESTResponse<T>()
{
Content = await xmlPipelineSerializer.Deserialize<T>(response),
StatusCode = await StatusCode()
};
}
if (serializationToUse == SerializationToUse.Json)
{
return new RESTResponse<T>()
{
Content = await jsonPipelineSerializer.Deserialize<T>(response),
StatusCode = await StatusCode()
};
}
throw new InvalidOperationException(
"You need to specify either Xml or Json serialization");
});
}
}
使用此代码,执行此简单 REST 框架所需的各种 REST 调用归结为如下代码:
GET
一个简单的 GET 请求如下所示:
public async Task GetPerson(int id)
{
await Task.Run(async () =>
{
using (RESTWebClient client = new RESTWebClient())
{
string getUrl = string.Format("https://:8001/people/{0}", id);
var response = await client.Get<Person>(getUrl, SerializationToUse.Xml);
}
});
}
PUT
一个简单的 PUT 请求如下所示:
public async Task PutPerson()
{
await Task.Run(async () =>
{
using (RESTWebClient client = new RESTWebClient())
{
string getUrl = string.Format("https://:8001/people/{0}", 1);
var response = await client.Get<Person>(getUrl, SerializationToUse.Xml);
var person = response.Content;
string newLastName = string.Format("{0}_Modified_{1}", person.LastName, DateTime.Now.Ticks);
person.LastName = newLastName;
string putUrl = string.Format("https://:8001/people/{0}", 1);
var statusCode = await client.Put(putUrl, person, SerializationToUse.Xml);
}
});
}
POST
一个简单的 POST 请求如下所示:
public async Task PostPerson()
{
await Task.Run(async () =>
{
using (RESTWebClient client = new RESTWebClient())
{
string postUrl = "https://:8001/people";
Person newPerson = new Person();
newPerson.FirstName = string.Format("FirstName_{0}", DateTime.Now.Ticks);
newPerson.LastName = string.Format("LastName_{0}", DateTime.Now.Ticks);
var response = await client.Post<Person>(postUrl, newPerson, SerializationToUse.Xml);
}
});
}
删除
一个简单的 DELETE 请求如下所示:
public async Task DeletePerson()
{
await Task.Run(async () =>
{
using (RESTWebClient client = new RESTWebClient())
{
string deleteUrl = string.Format("https://:8001/people/{0}", 1);
var statusCode = await client.Delete(deleteUrl);
}
});
}
其他客户端
要使用其他客户端,如 JavaScript 客户端,有许多有用的工具,如 JQuery/Angular。所以,如果您计划使用 JavaScript 客户端进行实验,互联网/StackOverflow 是您的朋友。
就这些
总之,这就是我想说的全部。我意识到这对于人们来说可能没有什么实际用处,除了作为一个学习练习,但如果您愿意写评论或投票,我将非常欢迎。