ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

【ASP.NET Core】MVC模型绑定:自定义InputFormatter读取CSV内容

2022-03-28 19:36:23  阅读:143  来源: 互联网

标签:Core ASP return 自定义 int IEnumerable var Type public


在上一篇文章中,老周介绍了用自定义 ModelBinder 的方式实现一个 API(或MVC操作方法)可以同时支持 JSON 格式和 Form-data 格式的数据正文。今天该轮到 InputFormatter 了——接下来老周会演示如何实现自定义的 InputFormatter,使其可以读取 CSV 格式的正文。

CSV 的格式比较简单,一般是一行文本一条数据记录,每条记录的字段值用逗号隔开(英文逗号)。

CSV 的妙处就是格式简单,一次性提交多条记录时体积较小。比如,我要提交一批员工信息。这个在客户端必须先知道员工对象中各属性的顺序,因为 CSV 是逗号分隔的文本,顺序不要打乱。

有时候我们可以这样规范一下:CSV 的第一行写字段标题,从第二行开始才是数据记录。就像这样的员工信息:

emp_id, emp_name, emp_age, emp_part
050025, 小张, 31, 开发部
050130, 小谢, 29, 市场部
038012, 小李, 37, 财务部
045211, 小刘, 36, 讨债部

其实,就算第一行写上字段名,这种规范也是没什么用处的,客户端可以瞎传,比如,它可以传成这样:

emp_id, emp_name, emp_age, emp_part
土坑部, 523014, 34, 小明
酸菜部, 301027, 28, 小何
牛肉部, 621143, 32, 老高

你瞧,这不全乱套了吗?所以,提不提供字段名一行其实不关键,关键是客户端在提交 CSV 数据时,你得按规矩来,不然这戏就演不下去了。

 

OK,现在咱们开始今天的表演吧。

首先,我们定义两个模型类。

public sealed class Album
{
    public string? Title { get; set; } = string.Empty;
    public int Year { get; set; }
    public string? Artist { get; set; }
}

public sealed class Book
{
    public string? Name { get; set; } = string.Empty;
    public string? Author { get; set; }
    public int? Year { get; set; }
    public string? Publisher { get; set; }
}

 之所以定义了两个类,是为了稍验证一下自定义的 InputFormatter 是否能通用。Album 类表示一张音乐专辑,有标题、发行年份、艺术家三个属性;Book 表示一本书的信息,有书名、作者、出版年份、出版社四个属性。注意 Book 类的 Year 属性的类型,我故意弄成了 int?,即可以 null 的整数值。稍后用于验证自定义的 InputFormatter 是否能处理这样的值类型。

 

接着,写一个控制器类和两个操作方法。

public class TestController : ControllerBase
{
    [HttpPost, ActionName("buyalbums")]
    public IActionResult NewAlbums([FromBody]IEnumerable<Album> albums)
    {
        return Ok(albums);
    }

    [HttpPost, ActionName("buybooks")]
    public IActionResult NewBooks([FromBody] Book[] books)
    {
        return Ok(books);
    }
}

这个控制器类没有应用 [ApiController] 特性,所以,要让其能从 body 读取数据,参数上要应用 [FromBody] 特性。这时候,两个操作方法的参数并不是单个模型对象,而是集合。NewAlbums 方法声明为 IEnumerable<T> 接口类型,所以在模型绑定时,你为它分配的对象实例只要实现了 IEnumerable<T> 接口就 OK,如 List<T> 实例。NewBooks 方法的参数是一个 Book 数组。

这也说明,咱们待会儿要编写的 Formatter 要考虑参数是 IEnumerable<> 泛型对象还是数组。

另外,操作方法上应用了 [ActionName] 特性,表示给操作方法分配一个别名,调用时 Url 上要写 buyalbums 和 buybooks。

 

好,重头戏上面,下面咱们来自定义 Formatter。

    public class CSVInputFormatter : InputFormatter
    {
        public CSVInputFormatter()
        {
            SupportedMediaTypes.Add("text/csv");
        }

        // 只有数组或实现 IEnumerable<> 的类型可用
        protected override bool CanReadType(Type type)
        {
            if (type.IsArray)
                return true;
            if(type.IsGenericType)
            {
                Type genparmtype = type.GenericTypeArguments[0];
                Type enumerabletype = typeof(IEnumerable<>).MakeGenericType(genparmtype);
                return type == enumerabletype;
            }
            return false;
        }

        public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
        {
            // 看看使用什么编码方式
            var request = context.HttpContext.Request;
            var contentType = request.ContentType;
            MediaType mdtype = new(contentType!);
            Encoding contentEncoding = mdtype.Encoding ?? Encoding.UTF8;
            // 创建 reader
            var reader = context.ReaderFactory(request.Body, contentEncoding);
            // 模型的元数据
            var metadata = context.Metadata;
            // 模型里面单个元素的元数据
            var itemMeta = metadata.ElementMetadata;

            // 先临时弄个List来存放元素
            Type listtype = typeof(List<>).MakeGenericType(itemMeta!.ModelType);
            IList itemList = (IList)Activator.CreateInstance(listtype)!;

            // 一行一行地读
            var line = await reader.ReadLineAsync();
            for (; line != null; line = await reader.ReadLineAsync())
            {
                // 创建子元素对象实例
                object? itemObject = Activator.CreateInstance(itemMeta.ModelType);
                // CSV 一行用逗号分隔各个值
                string[] parts = line.Split(",");
                
                // 每一行分割出来的段数应该与对象的
                // 属性个数一致,不然没法准确还原对象数据
                if (itemMeta!.Properties.Count != parts.Length)
                    continue;
                // 为属性赋值
                for (int i = 0; i < parts.Length; i++)
                {
                    var property = itemMeta.Properties[i];
                    // 看能不能赋值
                    if (property.IsReadOnly || property.PropertySetter is null)
                    {
                        continue;   //不能赋值,有请下一个
                    }
                    // 属性值的类型
                    Type propertyType = property.ModelType;
                    object? propertyValue = null;
                    if(propertyType == typeof(string))
                    {
                        // 如果是字符串,直接赋值
                        propertyValue = parts[i];
                    }
                    else
                    {
                        // 不是字符串要进行类型转换
                        if(property.IsNullableValueType)
                        {
                            // 如果是 Nullable 的值类型
                            // int?、long? 等类型直接赋值会报错
                            // UnderlyingOrModelType获取到里面的真实类型
                            // 例如,int? 它能获取到 int
                            propertyType = property.UnderlyingOrModelType;
                        }
                        try
                        {
                            propertyValue = Convert.ChangeType(parts[i], propertyType);
                        }
                        catch
                        {
                            // 忽略
                        }
                    }
                    if (propertyValue != null)
                    {
                        // 如果值有效,赋值
                        property.PropertySetter(itemObject!, propertyValue);
                    }
                }
                // 把对象实例加入到列表对象中
                itemList.Add(itemObject);
            }

            // 创建最终的模型对象
            if (metadata.ModelType.IsArray)
            {
                // 它是数组类型
                var arr = Array.CreateInstance(itemMeta.ModelType, itemList.Count);
                // 为元素赋值
                for (int i = 0; i < arr.Length; i++)
                {
                    arr.SetValue(itemList[i], i);
                }
                return InputFormatterResult.Success(arr);
            }
            // 如果不是直接把列表对象返回即可
            return InputFormatterResult.Success(itemList);
        }
    }

代码又长又坑爹,下面老周解释一下。

1、这里我不实现 IInputFormatter 接口,而是实现 InputFormatter 抽象类。因为在抽象类中已经为我们做了一些前提工作,比如判断客户端的 HTTP 请求有没有 body。我们可以省了不少功夫。当然,直接实现 TextInputFormatter  抽象类也不错的,它为我们做了一些根文本编码有关的处理,比如选择 Encoding。不过,此处老周觉得实现 InputFormatter 抽象类就足够了。

2、在这个类的构造函数中,向 SupportedMediaTypes 列表添加受支持的 MIME Type。比如本例,咱们处理 CSV 文本,可以让它只支持 text/csv 格式,这样运行时在处理时会自动选择咱们自定义的这个 Formatter 来读数据。

3、重写 CanReadType 方法。它有个 Type 类型的参数,表示模型绑定的目标类型相关的信息。在本例中,可能是 Book 类或 Album 类的 Type。老周是这样判断的:

        protected override bool CanReadType(Type type)
        {
            if (type.IsArray)
                return true;
            if(type.IsGenericType)
            {
                Type genparmtype = type.GenericTypeArguments[0];
                Type enumerabletype = typeof(IEnumerable<>).MakeGenericType(genparmtype);
                return type == enumerabletype;
            }
            return false;
        }

  a、如果是数组类型,Pass。好像不太严谨,但此处我们不需要太复杂的验证。

  b、如果是泛型类(就是针对 IEnumerable<T>的),获取类型参数T的 Type,然后用 MakeGenericType 方法创建一个 Type 对象。为什么要这样做呢?因为只有这样创建的 Type 才表示 IEnumerable<T>,直接用 typeof(IEnumerable<>) 是不行的。

3、实现 ReadRequestBodyAsync 抽象方法。这是核心,咱们在这个方法中读取数据并还原模型对象。这个方法的处理中,我们不需要去验证 HttpRequest 有没有 Body,因为 InputFormatter 基类已经帮我们做了,能调用 ReadRequestBodyAsync 方法说明是有 Body 的。

4、根据请求的 Content-Type 头,获取文本编码,如果获取不到,默认 UTF-8。

            MediaType mdtype = new(contentType!);
            Encoding contentEncoding = mdtype.Encoding ?? Encoding.UTF8;

5、读 body 用的 TextReader 我们不用自己找,调用 context 参数(InputFormatterContext)的 ReaderFactory 委托就能获取到,它会帮我们自动创建。

6、Metadata 属性表示的是顶层的对象,比如我们这里是 IEnumerable<X> 或 X[]。而 ElementMetadata 属性表示的是集合中元素的模型元数据,比如 Book 的,Album 的。

7、因为控制器类中的方法参数可能是 IEnumerable<T> 类型的,也可能是 T[] 类型的。我们暂时不管它。我们临时创建一个 List<T> 实例,用来存储从 body 中读到的对象。

            Type listtype = typeof(List<>).MakeGenericType(itemMeta!.ModelType);
            IList itemList = (IList)Activator.CreateInstance(listtype)!;

itemList 变量声明为 IList 类型,这样我们可以调用它的 Add 方法,动态添加对象。

8、接下来是一个循环,一行一行地读入。每一行就是一个元素对象(Book 或 Album 或其他)。

9、读到一行后,以逗号为分隔符拆解字符串,然后循环访问元素类型的属性列表(ModelMetadata的 Properties 集合)。

                for (int i = 0; i < parts.Length; i++)
                {
                    var property = itemMeta.Properties[i];
    
                    ……
                }

10、在获取到属性值的类型后,我们要做几个判断:

  a、字符串,好办,直接赋值;

  b、非字符串。看看是不是 Nullable<T>,如果是,取出 T 的 Type 再用。 Convert.ChangeType 方法遇到 int?、byte? 等类型是无法转换的,会发生异常;

  c、类型转换。

11、得到属性值后,用 PropertySetter 委托来设置属性。

        if (propertyValue != null)
        {
              // 如果值有效,赋值
              property.PropertySetter(itemObject!, propertyValue);
        }

12、此时,一个元素对象还原完毕,添加到刚才声明的那个 IList 类型变量中。

   itemList.Add(itemObject);

13、下一轮循环,过程一样,直到读完整 body。

14、等所有元素都搞定后,就剩下容器类了。这里要分情况:

  a、如果是数组,先创建数组实例,再按索引把 IList 类型变量中的元素引用传过去;

  b、如果是 IEnumerable<T>,可以直接使用 IList 类型的变量,因为它的实例类型是 List<T>,已经实现了 IEnumerable<T> 接口,直接赋值是兼容的。

            if (metadata.ModelType.IsArray)
            {
                // 它是数组类型
                var arr = Array.CreateInstance(itemMeta.ModelType, itemList.Count);
// 为元素赋值
                for (int i = 0; i < arr.Length; i++)
                {
                    arr.SetValue(itemList[i], i);
                }
                return InputFormatterResult.Success(arr);
            }
            // 如果不是直接把列表对象返回即可
            return InputFormatterResult.Success(itemList);

 

自定义 Formatter 完工后,要在 MvcOptions 中配置一下。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(opt =>
{
    opt.InputFormatters.Insert(0, new CSVInputFormatter());
});
var app = builder.Build();

app.MapControllerRoute("main", "{controller}/{action}");

app.Run();

 

来来来,测试一下。

POST /test/buyalbums HTTP/1.1
Content-Type: text/csv
User-Agent: PostmanRuntime/7.29.0
Accept: */*
Host: localhost:5031
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 92
 
高大上,1999,老杜
疯子村,2002,小馒头
风雨同路,,老周
放大招合集,2017,
 
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 28 Mar 2022 11:06:51 GMT
Server: Kestrel
Transfer-Encoding: chunked
 
[{"title":"高大上","year":1999,"artist":"老杜"},{"title":"疯子村","year":2002,"artist":"小馒头"},{"title":"风雨同路","year":0,"artist":"老周"},{"title":"放大招合集","year":2017,"artist":""}]

注意提交的最后一行,老周故意搞了个鬼,缺少了 Artist 属性的值(所以最后一行是逗号结尾)。

放大招合集,2017,(缺)

于是,产生的对象列表中,最后一个 Album 对象的 Artist 属性就是空字符串。

{
        "title": "放大招合集",
        "year": 2017,
        "artist": ""
}

 

再测试一个。

POST /test/buybooks HTTP/1.1
Content-Type: text/csv
User-Agent: PostmanRuntime/7.29.0
Accept: */*
Host: localhost:5031
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 224
 
VB从入门到跳河,张智慧,2021,半个脑袋出版社
PHP钓鱼网站开发,王小三,2023,天国白日梦传媒
和尚与女魔头,二麻子,2009,一刀切综合出版社
离离原上谱,老甘,,一键三联出版社
 
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 28 Mar 2022 11:13:41 GMT
Server: Kestrel
Transfer-Encoding: chunked
 
[{"name":"VB从入门到跳河","author":"张智慧","year":2021,"publisher":"半个脑袋出版社"},{"name":"PHP钓鱼网站开发","author":"王小三","year":2023,"publisher":"天国白日梦传媒"},{"name":"和尚与女魔头","author":"二麻子","year":2009,"publisher":"一刀切综合出版社"},{"name":"离离原上谱","author":"老甘","year":null,"publisher":"一键三联出版社"}]

最后一行,缺了 Year 属性的值。

离离原上谱,老甘,(缺),一键三联出版社

所以得到的对象中 Year 属性为 null,因为它是 int?,可为 null,不会分配默认的 0 。

 {
        "name": "离离原上谱",
        "author": "老甘",
        "year": null,
"publisher": "一键三联出版社"
 }

 

好了,今天老周的文章就水到这里了,改天再聊。

 

标签:Core,ASP,return,自定义,int,IEnumerable,var,Type,public
来源: https://www.cnblogs.com/tcjiaan/p/16067851.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有