白话系列之实现自己简单的mvc式webapi框架
前言:此文为极简mvc式的api框架,只当做入门api的解析方式,并且这里也不算是mvc框架,因为没有view层,毕竟现在大部分都属于前后端分离,当然也可以提供view层,因为只是将view当做文本返回.
github地址:https://github.com/besthyc/webapisolution.git
演示例子:
目标:
1.针对home/default进行解析.2.提供简单的httpcontext处理.3.对mvc的框架有个最简单的了解.4.一步步通过好的框架风格,建立自己的框架风格
关键还有一点就是很多东西比你想的还简单,难得是里面可扩展可维护的实现方式以及面面俱到的安全验证.但是前人栽树后人乘凉,我们借鉴就好.
一:创建controller对象
目标:通过工厂模式,获取所有controller,并创建其对象
1.1.定义接口,并定义所有controller继承的基类apibasecontroller
public interface iapicontroller
{
}
public abstract class apibasecontroller : iapicontroller
{
}
模拟创建一个controller,这就是我们自己定义的控制器,其实就是你自己实现的控制器
public class homecontroller : apibasecontroller
{
}
1.2.通过工厂模式,通过名称调用对应的控制对象,因为控制器基本上不会改变,然后通过name找到对应的控制器
public interface icontrollerfactory
{
iapicontroller createcontroller(string name);
}
public class defaultcontrollerfactory : icontrollerfactory
{
private static list<type> controllertypes = new list<type>();
static defaultcontrollerfactory()
{
assembly assembly = assembly.getexecutingassembly();
foreach (type type in assembly.gettypes().where(type => typeof(iapicontroller).isassignablefrom(type)))
{
controllertypes.add(type);
}
}
public iapicontroller createcontroller(string name)
{
if (name.indexof("controller") == -1) name += "controller";
type controllertype = controllertypes.firstordefault(c => string.compare(name, c.name, true) == 0);
if (controllertype == null)
{
return null;
}
return (iapicontroller)activator.createinstance(controllertype);
}
}
ok,这样就可以取个简单的控制器工厂模式.
二:既然控制器已经创建,那么同样的情况调用里面的方法,目前home/default,会直接解析成default方法
1.先简单的实现出调用方法
public interface actioninvoker
{
void invokeaction(object type, string actionname);
}
public class defaultactioninvoker : actioninvoker
{
public void invokeaction(object controller, string actionname)
{
methodinfo methodinfo = controller.gettype().getmethods().first(m => string.compare(actionname, m.name, true) == 0);
methodinfo.invoke(controller, null);
}
}
此处非常简单,就直接调用对应的方法名即可,这就是webapi在解析路由中出现的最简单的实现方式,
其实理论就是如此简单,没其他人想的那么困难,接下来开始会做修饰,一步步来构建一个也是简单,但是稍微有点健全的api
三:优化代码,针对控制器及方法做缓存
/// <summary> /// 全局所有iapicontroller类型的操作都是由此处进行缓存 /// 其他地方只做类型处理,比如 a/b,那么是对应的是acontroller 还是a,都是其他地方做处理 /// 注意此处,只当做类型及方法的缓存,不做任何对执行返回结果及传递对象的处理,保持功能单一 /// 保持路径单一,即a/b中a控制器只有1个,b方法也只有1个,即使有重载,也必须得通过 路由名 进行区分 /// </summary> internal static class apicontrolleractioncache { private static dictionary<string, type> s_controllertypes = new dictionary<string, type>(); private static dictionary<type, actioncache> s_actioncache = new dictionary<type, actioncache>(); static apicontrolleractioncache() { assembly assembly = assembly.getexecutingassembly(); foreach (type type in assembly.gettypes().where(type => typeof(iapicontroller).isassignablefrom(type))) { string name = type.name; if(type.getcustomattribute<prerouteattribute>() != null) { name = type.getcustomattribute<prerouteattribute>().preroutename; } if (s_controllertypes.containskey(name)) throw new exception($"{name}存在相同的路由名,请保持路由唯一"); s_controllertypes.add(name, type); s_actioncache.add(type, new actioncache(type)); } } public static type getcontroller(string controllername) { if (!s_controllertypes.containskey(controllername)) throw new exception("没有此路由对应的类型"); return s_controllertypes[controllername]; } /// <summary> /// 通过路由值获取对应的委托方法 /// </summary> /// <param name="controllername"></param> /// <param name="actionname"></param> /// <returns></returns> public static func<iapicontroller, object[], object> getmethod(type controller, string actionname) { if(!s_actioncache.containskey(controller)) throw new exception("没有此路由对应的类型"); actioncache cache = s_actioncache[controller]; if (!cache.containskey(actionname)) throw new exception("没有此路由对应的方法"); return cache.getmethodinfo(actionname); } } public class actioncache { private dictionary<string, methodinfo> m_methodinfo; private dictionary<methodinfo, func<iapicontroller, object[], object>> m_funccache ; public methodinfo this[string name] { get { return m_methodinfo[name]; } } public boolean containskey(string name) { return m_methodinfo.containskey(name); } private object m_lock = new object(); /// <summary> /// 可以考虑延迟加载 /// </summary> /// <param name="type"></param> public actioncache(type type) { m_methodinfo = new dictionary<string, methodinfo>(); m_funccache = new dictionary<methodinfo, func<iapicontroller, object[], object>>(); foreach(methodinfo info in type.getmethods()) { string name = info.name; if(info.getcustomattribute<routeattribute>() != null) { name = info.getcustomattribute<routeattribute>().routename; } if (m_methodinfo.containskey(name)) throw new exception($"{type.name}中{name}重复,请保持路径唯一"); m_methodinfo.add(name, info); } } /// <summary> /// 通过名称获取对应的委托 /// </summary> /// <param name="methodinfo"></param> /// <returns>iapicontroller:传递的执行方法, object[]:方法参数, object 返回值,void为空</returns> public func<iapicontroller, object[], object> getmethodinfo(string methodname) { methodinfo methodinfo = m_methodinfo[methodname]; if (!m_funccache.containskey(methodinfo)) { lock (m_lock) { if (!m_funccache.containskey(methodinfo)) { m_funccache.add(methodinfo, createexecutor(methodinfo)); } } } return m_funccache[methodinfo]; } private func<object, object[], object> createexecutor(methodinfo methodinfo) { parameterexpression target = expression.parameter(typeof(object), "target"); parameterexpression arguments = expression.parameter(typeof(object[]), "arguments"); list<expression> parameters = new list<expression>(); parameterinfo[] paraminfos = methodinfo.getparameters(); for (int32 i = 0; i < paraminfos.length; i++) { parameterinfo paraminfo = paraminfos[i]; binaryexpression getelementbyindex = expression.arrayindex(arguments, expression.constant(i)); unaryexpression convertoparamtertype = expression.convert(getelementbyindex, paraminfo.parametertype); parameters.add(convertoparamtertype); } unaryexpression instancecast = expression.convert(target, methodinfo.reflectedtype); methodcallexpression methodcall = expression.call(instancecast, methodinfo, parameters); unaryexpression convertoobjecttype = expression.convert(methodcall, typeof(object)); return expression.lambda<func<object, object[], object>>(convertoobjecttype, target, arguments).compile(); } }
注意:此处对全局controller做了一次缓存,限制为不会通过传的参数进行判定使用哪个方法,只允许单个接口存在,即
1.如果在不同空间下具有相同类型名的,必须具有不同的preroute特性限定,
2.如果一个类方法重载,得通过route特性限定唯一标识
3.表达式树是通过创建一个委托,传递当前的controller对象调用对应的方法
以上并不算框架,只属于单一调用方法功能实现,并做优化,接下来属于api框架实现
四:api实现首先得确定传输的值及协议标准,
4.1.确定传输中的必须信息,去掉其他所有的额外信息后有如下几点,为求简单,先全部保存成字符串形式:
从哪里来(urlreferrer),到哪里去 url(请求的接口),
url请求的参数(a/b?query=1中的query值解析),body中保存的值(frombody),包含的请求头参数(headers)
所有请求处理完成后,返回的值信息
public class httprequest { /// <summary> /// 从哪里来的 /// </summary> public string urlreferrer { get; } /// <summary> /// 到哪里去 /// </summary> public string uri { get; set; } /// <summary> /// uri请求参数处理 /// </summary> public string queryparams { get; set; } /// <summary> /// 请求的内容 /// </summary> public string requestcontent { get; set; } /// <summary> /// 请求头参数 /// </summary> public string headers { get; set; } } public class httpresponse { /// <summary> /// 返回的内容 /// </summary> public string responsecontent { get; set; } } public class httpcontext { public httprequest request { get; } public httpresponse response { get; } }
此处只是单纯做展示使用,在后期会写一个mq中间件时候,在做扩展.只展示httpcontext
4.2.对http请求做一个统一接口处理
public class urlroutingmodule : iroutingmodule { public void init(httpbasecontext context) { } }
五:通过路由模板收集
5.1.在写api的时候,会添加一个defaultapi匹配路由,注意:这里以mvc的解析规则实现
routeconfig.registerroutes(routetable.routes);
public static class routeconfig { public static void registerroutes(routecollection routes) { routes.ignoreroute("{resource}.axd/{*pathinfo}"); routes.maproute( name: "defaultapi", routetemplate: "api/{controller}/{action}/{id}", defaults: new { controller = "home", action = "index", id = urlparameter.optional } ); } }
5.2.考虑此模板的功能及实现方式
1.因为可能有多个默认匹配,并且执行的是顺序加载过程,所以有个routetable静态类收集全局模板路由
2.路由有对应的默认路由及路由值(route routedata)及收集collection
3.route实现路由解析,并提供routedata值,并提供实现
4.在解析route的时候,通过生成的路由句柄defaultrouterhandler去routedata进行后续处理
5.通过defaultapihandler对路由进行解析,生成对应的控制器
6.通过controller,对当前的action进行解析,并绑定路由
7.调取当前执行的方法后,获取返回的值对象,并对返回值进行处理
大体的模型调用过程如图
6:实现代码:请查看对应的github地址
https://github.com/besthyc/webapisolution.git
7.例子:(已经准备好对应的json序列化解析,赋值粘贴即可,看上图)