• .Net MinimalApis响应返回值


    前言

    文本主要讲 MinimalApis 中的使用自定义IResultModel和系统自带IResult做响应返回值。
    MinimalApis支持以下类型的返回值:

    • string - 这包括 TaskValueTask

    • T(任何其他类型)- 这包括 TaskValueTask

    • 基于 IResult - 这包括 TaskValueTask

      本文的完整源代码在文末

    string 返回值

    行为 Content-Type
    框架将字符串直接写入响应。 text/plain

    200 状态代码与 text/plain Content-Type 标头和以下内容一起返回

    Hello World
    

    T(任何其他类型)返回值

    我们上面说的自定义 IResultModel就是用这种模式处理的

    行为 Content-Type
    框架 JSON 序列化响应。 application/json

    MinimalApis 框架 Json 序列化全局配置如下

        //通过调用 ConfigureHttpJsonOptions 来全局配置应用的选项
        builder.Services.ConfigureHttpJsonOptions(options =>
        {
            options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;//忽略循环引用
            options.SerializerOptions.WriteIndented = true;
            options.SerializerOptions.IncludeFields = true;
            options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
        });
    

    返回 T

    
    app.MapGet("/TestT", User () => new User() { Name = "Ruipeng", Email = "xxx@163.com", Age = 18 })
       .WithSummary("测试类")
       .Produces();
    

    返回值

    {
      "name": "Ruipeng",
      "email": "xxx@163.com",
      "age": 18
    }
    

    200 状态代码与 application/json Content-Type 标头和以下内容一起返回

    这个 HttpCode状态码只能返回 200,且不支持多种返回形式,比较局限


    统一响应格式代码

    
    public interface IResultModel
    {
        /// 
        ///     是否成功
        /// 
        bool? IsSuccess { get; }
    
        /// 
        ///     错误信息
        /// 
        string? Message { get; }
    
        /// 
        ///     业务码,用于业务中自定义
        /// 
        int? StatusCode { get; set; }
    
        /// 
        ///     时间戳
        /// 
        long? Timestamp { get; }
    }
    
    /// 
    ///     返回结果模型泛型接口
    /// 
    /// 
    public interface IResultModel : IResultModel
    {
        /// 
        ///     返回数据
        /// 
    
        T? Data { get; }
    }
    

    实现

    public class ResultModel<T> : IResultModel
    {
        public ResultModel()
        {
            Timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
        }
    
        /// 
        ///     处理是否成功
        /// 
        public bool? IsSuccess { get; set; }
    
        /// 
        ///     错误信息
        /// 
        public string? Message { get; set; }
    
        /// 
        ///     业务码
        /// 
        public int? StatusCode { get; set; }
    
        /// 
        ///     时间戳
        /// 
        public long? Timestamp { get; }
    
        /// 
        ///     返回数据
        /// 
        public T? Data { get; set; }
    
    
        /// 
        ///     成功
        /// 
        /// 
        public ResultModel Success(T? data = default)
        {
            this.IsSuccess = true;
            StatusCode = 200;
            Data = data;
            return this;
        }
    
        /// 
        ///     失败
        /// 
        /// 说明
        /// 
        public ResultModel Failed(string? msg = "failed", int? code = 500)
        {
            IsSuccess = false;
            Message = msg;
            StatusCode = code;
            return this;
        }
    }
    
    /// 
    ///     返回结果
    /// 
    public static class ResultModel
    {
        /// 
        ///     数据已存在
        /// 
        /// 
        public static IResultModel<string> HasExists => Failed("data already exists");
    
        /// 
        ///     数据不存在
        /// 
        public static IResultModel<string> NotExists => Failed("data doesn't exist");
    
        /// 
        ///     成功
        /// 
        /// 返回数据
        /// 
        public static IResultModel Success(T? data = default)
        {
            return new ResultModel().Success(data);
        }
    
        /// 
        ///     成功
        /// 
        /// 任务
        /// 
        public static async Task> SuccessAsync(Task? task = default)
        {
            return task is not null && task != default ? new ResultModel().Success(await task) : new ResultModel();
        }
    
        /// 
        ///     成功
        /// 
        /// 
        public static IResultModel<string> Success()
        {
            return Success<string>();
        }
    
    
        /// 
        ///     失败
        /// 
        /// 错误信息
        /// 
        public static IResultModel Failed(string? error = null)
        {
            return new ResultModel().Failed(error ?? "failed");
        }
    
        /// 
        ///     失败
        /// 
        /// 
        public static IResultModel<string> Failed(string? error = null)
        {
            return Failed<string>(error);
        }
    
        /// 
        ///     根据布尔值返回结果
        /// 
        /// 
        /// 
        public static IResultModel Result(bool success)
        {
            return success ? Success() : Failed();
        }
    
        /// 
        ///     根据布尔值返回结果
        /// 
        /// 
        /// 
        public static async Task Result(Task<bool> success)
        {
            return await success ? Success() : Failed();
        }
    
        /// 
        ///     根据布尔值返回结果
        /// 
        /// 
        /// 
        public static IResultModel<string> Result(bool success)
        {
            return success ? Success() : Failed();
        }
    
        /// 
        /// 时间戳起始日期
        /// 
        public static readonly DateTime TimestampStart = new(1970, 1, 1, 0, 0, 0, 0);
    
    
    }
    

    定义接口

    app.MapGet("/TestResultModel", IResultModel (int age) =>
    {
        List users = [new User() { Name = "Ruipeng", Email = "xxx@163.com", Age = 18 }];
        return users.FirstOrDefault(_ => _.Age > age) is User user ? ResultModel.Success(user) : ResultModel.Failed();
    })
       .WithSummary("测试自定义IResultModel")
       .Produces>();
    

    封装了一个静态类来简化自定义类的创建,支持多个返回类型

    返回值

    {
      "isSuccess": true,
      "statusCode": 200,
      "timestamp": 1711001093,
      "data": {
        "name": "Ruipeng",
        "email": "xxx@163.com",
        "age": 18
      }
    

    自定义类的自动包装实现

    创建一个Attribute

    [AttributeUsage(AttributeTargets.Method)]
    public class EnableResponseWrapperAttribute : Attribute { }
    

    创建中间件自动包装

    public class ResponseWrapperMiddleware(RequestDelegate next)
    {
        public async Task InvokeAsync(HttpContext context)
        {
    
            if (context.GetEndpoint()?.Metadata.GetMetadata() is not null)
            {
                // 存储原始响应体流
                var originalResponseBodyStream = context.Response.Body;
                try
                {
                    // 创建内存流以捕获响应
                    using var memoryStream = new MemoryStream();
                    context.Response.Body = memoryStream;
    
                    // 调用管道中的下一个中间件
                    await next(context);
    
                    // 恢复原始响应体流并写入格式化结果
                    context.Response.Body = originalResponseBodyStream;
    
                    // 重置内存流位置并读取响应内容
                    memoryStream.Seek(0, SeekOrigin.Begin);
                    var readToEnd = await new StreamReader(memoryStream).ReadToEndAsync();
                    var objResult = JsonSerializer.Deserialize(readToEnd);
                    var result = new ResultModel
                    {
                        Data = objResult,
                        IsSuccess = true,
                        StatusCode = context.Response.StatusCode
                    };
                    await context.Response.WriteAsJsonAsync(result as object);
    
                }
                finally
                {
                    // 确保在出现异常时恢复原始响应体流
                    context.Response.Body = originalResponseBodyStream;
                }
            }
            else
            {
                await next(context);
            }
        }
    }
    
    

    应用中间件

    app.UseMiddleware();
    

    创建测试接口

    app.MapGet("/TestTestAutoWarpper", [EnableResponseWrapper] User () => new User() { Name = "Ruipeng", Email = "xxx@163.com", Age = 18 }).WithSummary("测试类")
       .Produces();
    

    返回值

    {
      "isSuccess": true,
      "statusCode": 200,
      "timestamp": 1711005201,
      "data": {
        "name": "Ruipeng",
        "email": "xxx@163.com",
        "age": 18
      }
    }
    

    为了方便测试在MinimalApis 的接口上如果添加了EnableResponseWrapperAttribute则通过中间件自动包装返回值

    IResult 返回值

    行为 Content-Type
    框架调用 IResult.ExecuteAsync 由 IResult 实现决定

    dotNet7 之后多了一个TypedResults类来替代 Results
    IResult 接口定义一个表示 HTTP 终结点结果的协定。 静态 Results 类和静态 TypedResults 用于创建表示不同类型的响应的各种 IResult 对象。

    返回 TypedResults(而不是 Results)有以下优点:

    • TypedResults 帮助程序返回强类型对象,这可以提高代码可读性、改进单元测试并减少运行时错误的可能性。
    • 实现类型会自动为 OpenAPI 提供响应类型元数据来描述终结点。
      实现在Microsoft.AspNetCore.Http.HttpResults
    //Return IResult
    app.MapGet("/IResult/TestResult", IResult () => Results.Ok(new User() { Name = "Ruipeng", Email = "xxx@163.com", Age = 18 }));
    

    没有调用扩展方法 Produces

    app.MapGet("/IResult/TestTypedResult", IResult () => TypedResults.Ok(new User() { Name = "Ruipeng", Email = "xxx@163.com", Age = 18 }));
    

    可以看到 TypedResults 默认就会添加路由终结点的元数据描述

    返回多个 IResult 实现类型

    app.MapGet("/IResult/ReturnMultipleTypes", Results, NotFound> (int age) =>
    {
        List users = [new User() { Name = "Ruipeng", Email = "xxx@163.com", Age = 18 }];
        return users.FirstOrDefault(_ => _.Age > age) is User user ? TypedResults.Ok(user) : TypedResults.NotFound();
    });
    

    图简单可以直接用 IResult 返回类型 但是,由于 TypedResults 帮助程序自动包含终结点的元数据,因此可以改为返回 Results, NotFound> 联合类型

    IResult 自定义响应

    添加 Html 扩展

    
    public static class ResultsExtensions
    {
        public static IResult Html(this IResultExtensions resultExtensions, string html)
        {
            ArgumentNullException.ThrowIfNull(resultExtensions);
    
            return new HtmlResult(html);
        }
    }
    
    
    class HtmlResult(string html) : IResult
    {
        private readonly string _html = html;
    
        public Task ExecuteAsync(HttpContext httpContext)
        {
            httpContext.Response.ContentType = MediaTypeNames.Text.Html;
            httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
            return httpContext.Response.WriteAsync(_html);
        }
    }
    
    app.MapGet("/IResult/Html", () => Results.Extensions.Html(@$"
    
        miniHTML
        
            

    Hello World

    The time on the server is {DateTime.Now:O}

    "
    ));

    返回结果

    html>
    <html>
      <head>
        <title>miniHTMLtitle>
      head>
      <body>
        <h1>Hello Worldh1>
        <p>The time on the server is 2024-03-21T17:31:36.2931959+08:00p>
      body>
    html>
    

    自定义 Json 格式

    上面写了ConfigureHttpJsonOptions方法来配置全局请求的 Json 格式,下面则是针对单个路由终结点请求,方便一些个性化接口的处理

    var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
    { WriteIndented = true };
    
    app.MapGet("/IResult/CustomJsonConfig", () =>
        TypedResults.Json(new User() { Name = "Ruipeng", Email = "xxx@163.com", Age = 18 }, options));
    

    返回 ProblemDetail

    app.MapGet("/IResult/ProblemDetail", () =>
    {
        var problemDetail = new ProblemDetails()
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = "内部错误"
        };
        return TypedResults.Problem(problemDetail);
    });
    
    

    返回值

    {
      "type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
      "title": "内部错误",
      "status": 500
    }
    

    Microsoft.AspNetCore.Http.Results的扩展下,TypedResults 有非常多扩展的方法,比如处理文件,回调,流以及登录认证等,大家可以根据需求使用.

    最后

    用那种方式还是取决于项目的实际情况,如果你的系统是业务码httpStateCode要求分离的形式那建议用上面自定义统一响应的形式,要是没这方面的需求那dotNet自带的TypedResults使用起来就更合适。

    官网文档 如何在最小 API 应用中创建响应

    以下是本文的完整 源代码

    希望本文对你有帮助!

  • 相关阅读:
    豆瓣评分9.8,阿里内部的分布式架构手册让多少人突破了瓶颈?
    【Unity-Cinemachine相机】相机跟随之Transposer属性
    【数据结构与算法】环形队列的介绍、用数组实现环形数组
    OS复习笔记ch7-2
    Ubuntu20.04 安装微信 【优麒麟的镜像源方式安装】
    无线传感器网络的Z-SEP路由协议及对比(Matlab代码实现)
    想通过python生成随机数据集,并导出为xlsx文件,但是一直提示路径不对,但是可以确认路径是好的
    js实现广告条+缓冲效果/键盘事件实现小人跑步
    常用xshell中使用的linux命令
    java计算机毕业设计高校医务管理系统源程序+mysql+系统+lw文档+远程调试
  • 原文地址:https://www.cnblogs.com/ruipeng/p/18088161