白话系列之实现自己简单的mvc式webapi框架

2022-10-14,,,,

前言:此文为极简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序列化解析,赋值粘贴即可,看上图)

 

《白话系列之实现自己简单的mvc式webapi框架.doc》

下载本文的Word格式文档,以方便收藏与打印。