REST:一个简单的 REST 框架






4.93/5 (73投票s)
一个简单的 REST 框架,完全使用 .NET 基础类库从头开始编写
- 引言
- 代码在哪里?
- 它是如何工作的
- 就这些
引言
在我工作的地方,我很幸运地接触到各种各样的技术,包括 Web 工作、桌面工作、消息传递、线程等等。但我一直对人们如何创建完整的 REST 堆栈很感兴趣,并想知道仅凭自己的智慧和 .NET BCL 来完成这项工作需要什么。
所以我想了想,并提出了以下要求/问题:
- 如何监听 Http 请求?
- 如何让用户代码尽可能简单?
- 如何让用户代码通过 IOC / 依赖注入来使用其他依赖项?
- 能否支持 Json/Xml 以及其他可能的序列化偏好?
因此,考虑到这些要点,我开始从头开始创建一个 REST 框架。
对于那些不知道 REST 是什么/代表什么的人,以下是维基百科的说法:
表示状态转移(REST)是对万维网体系结构的抽象;更准确地说,REST 是一种架构风格,由一组协调一致的架构约束组成,应用于分布式超媒体系统中的组件、连接器和数据元素。REST 忽略了组件实现和协议语法的细节,以便专注于组件的角色、它们与其他组件交互的约束以及它们对重要数据元素的解释。
“表示状态转移”一词由 Roy Fielding 在其 2000 年的加州大学欧文分校博士论文中首次提出并定义。REST 已被用于描述理想的 Web 架构、识别现有问题、比较替代解决方案以及确保协议扩展不会违反使 Web 取得成功的核心约束。Fielding 在与同事合作期间开发了 REST,当时他还在研究 HTTP 1.1 和统一资源标识符(URI)。
REST 架构风格也应用于 Web 服务的开发,作为 SOAP 等其他分布式计算规范的替代方案。如果 Web 服务符合架构约束部分中描述的约束,则可以将其称为“RESTful”。如果您只对 REST 在 Web API 中的应用感兴趣,请参阅 Web 服务应用部分。
http://en.wikipedia.org/wiki/Representational_state_transfer 更新于 2014 年 10 月 3 日
代码在哪里?
代码可在我的 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(s)
。
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 请求,并将实际工作委托给所谓的路由 Actioner。路由 Actioner 逐一检查,使用了 责任链设计模式。
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;
}
}
}
正如您所见,我没有撒谎,基本循环非常简单:等待请求,当看到新请求时,调用路由 Actioner 类。正如我所提到的,路由 Actioner 是使用 责任链设计模式 arranged 的。
使用的顺序如下:
VerbRouteActioner
:此项尝试将路由与用户定义的IVerbHandler<T,TKey>
匹配。如果找到一个,则使用它;否则,将请求交给后继者(DynamicRouteHandler
)处理。DynamicRouteHandler
:此项尝试将路由与用户定义的IDynamicRouteHandler<T,TKey>
匹配。如果找到一个,则使用它;否则,将请求交给后继者(BadRouteHandler
)处理。BadRouteHandler
:这是链中的最后一个环节,它简单地返回 HTTP 状态码 404,Not Found。
所以我们刚才提到了几个新东西,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 框架中实际上有 3 个基于 RouteActioner 的类。下表描述了它们。
xxxRouteHandler 代码处理以下类型的关注点:
- HTTP 请求是否属于此类型的 xxxRouteActioner/
IHandler
? - 是否有匹配所请求路由的正确类型的
IHandler
? - 请求是否包含正确的参数以与 xxxRouteActioner/
IHandler
配合使用?
我们将在下面详细介绍主要的 2 个 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 调用。
- 然后,我们提取 Id(
IVerbHandler<T,TKey>
的TKey
类型)和 Content(IVerbHandler<T,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
,允许指定两件事:- 请求的路由的 Action 部分。这类似于您在使用某些基于约定的事物(如 ASP MVC)时会得到的内容,在那里我们将本质上把 HTTP 请求解析到一个特定的 Action 来处理请求。我选择了一个属性以简化。我们将继续查看
DynaicRouteActioner
时看到它是如何使用的。 - 我们能够指定要使用的
HttpMethod
,即 GET/PUT/POST/DELETE。
- 请求的路由的 Action 部分。这类似于您在使用某些基于约定的事物(如 ASP MVC)时会得到的内容,在那里我们将本质上把 HTTP 请求解析到一个特定的 Action 来处理请求。我选择了一个属性以简化。我们将继续查看
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
,我们可以为匹配传入的 HTTP 请求指定一个 URL 部分(在像 ASP MVC 这样的约定式系统中,可以将此属性视为定位特定 Action 的能力),并指定IDynamicHandler<T,TKey>
打算处理的正确 HTTP 方法。那么我们如何使用RouteAttribute
来帮助我们识别正确的IDynamicHandler<T,TKey>
方法呢?问题的答案在于非常彻底的参数/返回值检查。我们只假设提供的IDynamicHandler<T,TKey>
方法之一是正确的,并且可以调用,前提是以下所有条件都成立:RouteBaseAttribute
+RouteAttribute
部分代表了整个请求 URL。RouteAttribute HttpMethod
值匹配传入的 HTTP 请求方法。RouteAttribute
属性化方法签名参数/返回值(可能是Task<T>
,也可能不是)匹配。
- 然后,我们提取 Id(
IDynamicRouteHandler<T,TKey>
的TKey
类型)和 Content(IDynamicRouteHandler<T,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,Not Found。
这是它的代码:
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 是您的朋友。
就这些
总之,这就是我想说的全部。我意识到这对于人们来说并没有多大用处,除了作为一项学习练习,但如果您愿意写评论或投票,我将非常欢迎。