The realization of vernacular series simple mvc web API framework

Posted by tvance929 on Tue, 22 Oct 2019 13:46:16 +0200

This article is a very simple mvc api framework, which is only used as the parsing method of the entry api, and this is not mvc framework, because there is no view layer, after all, most of them are front-end and back-end separation, of course, the view layer can also be provided, because only view is returned as text.

GitHub address: https://github.com/BestHYC/WebAPISolution.git

Example:

 

 

 

Objectives:

1. Analyze Home/default. 2. Provide simple httpcontext processing. 3. Have the simplest understanding of mvc framework. 4. Build your own framework style step by step through good framework style.

The key point is that many things are simpler than you think. It's hard to find scalable and maintainable implementation methods and comprehensive security verification inside. However, we can learn from the experience of predecessors who have planted trees for future generations.

One: create controller object

Target: through the factory mode, get all controller s and create their objects

1.1. Define the interface and the base class ApiBaseController inherited by all controllers

    public interface IApiController
    {
    }
    public abstract class ApiBaseController : IApiController
    {
    }

Simulate to create a Controller, which is our own defined Controller, in fact, the Controller you implement yourself

    public class HomeController : ApiBaseController
    {
    }

1.2. In the factory mode, call the corresponding control object by name, because the controller will not change basically, and then find the corresponding controller by 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, so you can take a simple controller factory mode.

2. Since the controller has been created, the method in the same case will be called. At present, home/default will be directly resolved to default method.

1. Simply implement the calling method first

 

    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);
        }
    }

 

This is very simple. Just call the corresponding method name directly. This is the simplest way for webapi to resolve routes.

In fact, the theory is so simple that it is not as difficult as others think. Next, we will start to modify it and build a simple but slightly sound api step by step.

Third: optimize the code and cache the controller and methods

/// <summary>
    /// Global ownership IApiController Operations of type are cached here
    /// Other places only do type processing,such as A/B,So it corresponds to AController still A,It's all handled in other places.
    /// Notice here,Cache only for types and methods,Do not do anything to return the result of execution and transfer the object,Keep the function single
    /// Keep the path single,Namely A/B in A There is only one controller,B There is only one method,Even if there are overloads,It must also be distinguished by the routing name
    /// </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}Same route name exists,Please keep the route unique");
                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("There is no corresponding type for this route");
            return s_controllerTypes[controllername];
        }
        /// <summary>
        /// Get the corresponding delegation method through the route value
        /// </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("There is no corresponding type for this route");
            ActionCache cache = s_actionCache[controller];
            if (!cache.ContainsKey(actionname)) throw new Exception("There is no method for this route");
            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>
        /// Delay loading can be considered
        /// </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}in{name}repeat,Please keep the path unique");
                m_methodinfo.Add(name, info);
            }
        }
        /// <summary>
        /// Get the corresponding delegation by name
        /// </summary>
        /// <param name="methodInfo"></param>
        /// <returns>IApiController:Execution method passed, Object[]:Method parameter, Object Return value,void Empty</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();
        }
    }

Note: the global controller is cached once here, which is limited to the method that will not be determined by the parameters passed. Only a single interface is allowed, that is,

1. If you have the same type name in different spaces, you must have different preroute attribute qualifications.

2. If a class method is overloaded, the route attribute must be used to restrict the unique identifier.

3. The expression tree is to create a delegate and pass the current controller object to call the corresponding method.

The above is not a framework, only a single call method function implementation and optimization, and then an API framework implementation.

4. API implementation first needs to determine the transmission value and protocol standard.

4.1. Determine the necessary information in the transmission. After removing all other additional information, there are the following points. For simplicity, first save all information in the form of string:

Where to come from (URlReferrer), where to go URL (request interface),

Parameter of URL request (query value resolution in a/b?query=1), value saved in body (frombody), including request headers

Value information returned after all requests are processed

 

public class HttpRequest
    {
        /// <summary>
        /// Where did you come from
        /// </summary>
        public String UrlReferrer { get; }
        /// <summary>
        /// Where to go?
        /// </summary>
        public String Uri { get; set; }
        /// <summary>
        /// Uri Request parameter processing
        /// </summary>
        public String QueryParams { get; set; }
        /// <summary>
        /// Requested content
        /// </summary>
        public String RequestContent { get; set; }
        /// <summary>
        /// Request header parameters
        /// </summary>
        public String Headers { get; set; }
    }
    public class HttpResponse
    {
        /// <summary>
        /// Returned content
        /// </summary>
        public String ResponseContent { get; set; }
    }
    public class HttpContext
    {
        public HttpRequest Request { get; }
        public HttpResponse Response { get; }
    }

 

This is just for demonstration use. When writing an MQ Middleware in the later stage, we are extending it. Only HttpContext is shown.

4.2. Make a unified interface for Http requests

    public class UrlRoutingModule : IRoutingModule
    {
        public void Init(HttpBaseContext context)
        {
            
        }
    }

V. collection through routing template

5.1. When writing the API, a default API matching route will be added. Note: This is implemented by MVC resolution rules.

    
 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. Consider the function and implementation of this template

1. Because there may be multiple default matches and sequential loading, a RouteTable static class collects global template routes.

2. The route has corresponding default route and route data and Collection

3.Route implements route resolution, provides RouteData value, and provides implementation.

4. When parsing Route, the generated Route handle DefaultRouterHandler is used to go to RouteData for subsequent processing.

5. Analyze the route through DefaultApiHandler to generate the corresponding controller

6. Through the controller, analyze the current action and bind the route

7. After calling the currently executed method, obtain the returned value object and process the returned value

The general model calling process is shown in the figure

 

6: implementation code: please check the corresponding github address

https://github.com/BestHYC/WebAPISolution.git

7. Example: (the corresponding json serialization and parsing are ready, just paste the assignment, as shown in the figure above)

Topics: C# github git Attribute Lambda