背景

我们有一些对外公共接口,为很多不同的调用方提供服务,经常会有调用方因为使用了错误的协议、数据提交方式而导致提交失败,所以我们决定整合一下这个接口,使接口同时支持GETPOST请求,并且数据提交方式支持application/jsonapplication/x-www-form-urlencodedURL参数

要解决的问题

  • 同时支持GETPOST
  • 同时支持application/jsonapplication/x-www-form-urlencodedURL参数
  • 因为接口比较多,所以绑定的方法必须能够复用

具体实现

本文基于.NET5实现

  1. 要实现同时支持GETPOST很简单,直接为接口标识需要的协议即可
1
2
3
4
5
        [HttpGet]
        [HttpPost]
        public JsonResult Test()
        {
        }
  1. 要支持不同形式的参数格式,那么.NET自带的[FromQuery][FromForm]特性就不能满足要求了。这时需要使用ModelBinder来实现一个自定义绑定数据的方法

ModelBinderAttribute可以指定要用于绑定的模型名称或类型的属性 IModelBinder

基础定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
        [ModelBinder(typeof(MyBinder))]
        public class MyVO
        {
            public string Name { get; set; }
            public int Age { get; set; }
        }

        //实现IModelBinder接口以完成数据绑定
        public class MyBinder : IModelBinder
        {
            public Task BindModelAsync(ModelBindingContext bindingContext)
            {
                return Task.Run(() =>
                {
                    //具体的解析数据
                    var vo = new MyVO { Name = "张三", Age = 23 };
                    bindingContext.Result = ModelBindingResult.Success(vo);
                    //解析失败
                    //bindingContext.Result = ModelBindingResult.Failed();
                });
            }
        } 

解析application/json示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
            public Task BindModelAsync(ModelBindingContext bindingContext)
            {
                return Task.Run(() =>
                {
                    var request = bindingContext.HttpContext.Request;
                    Int64? len = request.ContentLength;
                    if (!len.HasValue)
                    {
                        bindingContext.Result = ModelBindingResult.Failed();
                        return;
                    }
                    byte[] buffer = new byte[len.Value];
                    //.NET5默认必须使用异步的方式读取流
                    int readLength = request.Body.ReadAsync(buffer, 0, (int)len.Value).Result;

                    string json = Encoding.UTF8.GetString(buffer);
                    var obj = System.Text.Json.JsonSerializer.Deserialize<MyVO>(json);

                    bindingContext.Result = ModelBindingResult.Success(obj);
                });
            }

解析application/x-www-form-urlencodedURL参数示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
            public Task BindModelAsync(ModelBindingContext bindingContext)
            {
                return Task.Run(() =>
                {
                    var vo = new MyVO();
                    //application/x-www-form-urlencoded格式
                    vo.Name = bindingContext.HttpContext.Request.Form["Name"].ToString();
                    vo.Age = Convert.ToInt32(bindingContext.HttpContext.Request.Form["Age"].ToString());

                    //URL参数格式
                    //vo.Name = bindingContext.HttpContext.Request.Query["Name"].ToString();
                    //vo.Age = Convert.ToInt32(bindingContext.HttpContext.Request.Query["Age"].ToString());

                    bindingContext.Result = ModelBindingResult.Success(vo);
                });
            }
  1. 要支持解析不同的模型,这里就比较简单了,直接使用泛型即可。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
        [ModelBinder(typeof(MyBinder<MyVO>))]
        public class MyVO
        {
            public string Name { get; set; }
            public int Age { get; set; }
        }

        //实现IModelBinder接口以完成数据绑定
        public class MyBinder<T> : IModelBinder where T : new()
        {
            public Task BindModelAsync(ModelBindingContext bindingContext)
            {
                return Task.Run(() =>
                {
                    var t = new T();//因为是为模型赋值,所以必须实例化对象,泛型需要实例化约束
                    var request = bindingContext.HttpContext.Request;
                    if (request.ContentType == "application/json")
                    {
                        Int64? len = request.ContentLength;
                        if (!len.HasValue)
                        {
                            bindingContext.Result = ModelBindingResult.Failed();
                            return;
                        }
                        byte[] buffer = new byte[len.Value];
                        string data = "";
                        var l = request.Body.ReadAsync(buffer, 0, (int)len.Value).Result;
                        string json = Encoding.UTF8.GetString(buffer);

                        //Json格式的解析比较简单,直接传递一个泛型T就完事了
                        t = System.Text.Json.JsonSerializer.Deserialize<T>(json);
                        bindingContext.Result = ModelBindingResult.Success(t);
                        return;
                    }

                    //form和url参数需要使用反射来获取不同的字段,然后再获取对应的参数值
                    foreach (PropertyInfo p in t.GetType().GetProperties())
                    {
                        string fieldName = p.Name;
                        object tmpValue = "";

                        switch (request.ContentType)
                        {
                            case "application/x-www-form-urlencoded":
                                tmpValue = request.Form[fieldName].ToString();
                                break;
                            default:
                                tmpValue = request.Query[fieldName].ToString();
                                break;
                        }

                        var property = t.GetType().GetProperty(fieldName);
                        property?.SetValue(t, Convert.ChangeType(tmpValue, property.PropertyType));
                    }

                    bindingContext.Result = ModelBindingResult.Success(t);
                });
            }
        }

因为指定了自定义绑定方式,所以在具体的接口方法定义时就不需要再指定绑定方式了

1
2
3
4
5
        [HttpGet]
        [HttpPost]
        public JsonResult Test(MyVO myVo)
        {
        }

好了,大功告成,收工~~👊👊 ModelBinder.jpg