由ASP.NET Core读取Response.Body引发的思考

这篇具有很好参考价值的文章主要介绍了由ASP.NET Core读取Response.Body引发的思考。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

    前几天有群友在群里问如何在我之前的文章《ASP.NET Core WebApi返回结果统一包装实践》的时候有点疑问,主要的疑问点就是关于Respouse的读取的问题。在之前的文章《深入探究ASP.NET Core读取Request.Body的正确方式》曾分析过关于Request的读取问题,需要读取Response的场景同样经常遇到,比如读取输出信息或者包装一下输出结果等。无独有偶Response的读取同样存在类似的问题,本文我们便来分析一下如何进行Response的Body读取。

使用方式

我们在日常的使用中是如何读取流呢?很简单,直接使用StreamReader去读取,方式如下

public override void OnResultExecuted(ResultExecutedContext context)
{
    //操作流之前恢复一下操作位
    context.HttpContext.Response.Body.Position = 0;

    StreamReader stream = new StreamReader(context.HttpContext.Response.Body);
    string body = stream.ReadToEnd();
    _logger.LogInformation("body content:" + body);

    context.HttpContext.Response.Body.Position = 0;
    base.OnResultExecuted(context);
}

代码很简单,直接读取即可,可是这样读取是有问题的会抛出异常System.ArgumentException:“Stream was not readable.”异常信息就是的意思是当前Stream不可读,也就是Respouse的Body是不可以被读取的。关于StreamReader到底和Stream有啥关联,我们在之前的文章深入探究ASP.NET Core读取Request.Body的正确方式一文中有过源码分析,这里就不在赘述了,有兴趣的同学可以自行翻阅,强烈建议在阅读本文之前可以看一下那篇文章,方便更容易了解。
如何解决上面的问题呢?方式也很简单,比如你想在你的程序中保证Response的Body都是可读的,你可以定义一个中间件解决这个问题。

public static IApplicationBuilder UseResponseBodyRead(this IApplicationBuilder app)
{
    return app.Use(async (context, next) =>
    {
        //获取原始的Response Body
        var originalResponseBody = context.Response.Body;
        try
        {
            //声明一个MemoryStream替换Response Body
            using var swapStream = new MemoryStream();
            context.Response.Body = swapStream;
            await next(context);
            //重置标识位
            context.Response.Body.Seek(0, SeekOrigin.Begin);
            //把替换后的Response Body复制到原始的Response Body
            await swapStream.CopyToAsync(originalResponseBody);
        }
        finally
        {
            //无论异常与否都要把原始的Body给切换回来
            context.Response.Body = originalResponseBody;
        }
    });
}

本质就是先用一个可操作的Stream比如咱们这里的MemoryStream替换默认的ResponseBody,让后续对ResponseBody的操作都是针对新的ResponseBody进行操作,完成之后把替换后的ResponseBody复制到原始的ResponseBody。最终无论异常与否都要把原始的Body给切换回来。需要注意的是,这个中间件的位置尽量要放在比较靠前的位置注册,至少也要保证在你所有要操作ResponseBody之前的位置注册。如下所示

var app = builder.Build();
app.UseResponseBodyRead();

源码探究

通过上面我们了解到了ResponseBody是不可以被读取的,至于为什么呢,这个我们需要通过相关源码了解一下。通过HttpContext类的源码我们可以看到相关定义

public abstract class HttpContext
{
    public abstract HttpResponse Response { get; }
}

这里看到HttpContext本身是个抽象类,看一下它的属性HttpResponse类的定义也是一个抽象类

public abstract class HttpResponse
{
}

由上面可知Response属性是抽象的,所以抽象类HttpResponse必然包含一个子类去实现它,否则没办法直接操作相关方法。这里我们介绍一个网站https://source.dot.net用它可以更轻松的阅读微软类库的源码,比如CLR、ASP.NET Core、EF Core等等,双击一个类或者属性方法可以查找引用和定义它们的地方,非常方便,它的源码都是最新版本的,来源就是GitHub上的相关仓库。找到实例化HttpResponse的为位置在HttpContext的子类DefaultHttpContext类中[点击查看源码👈]

public sealed class DefaultHttpContext : HttpContext
{
    private readonly DefaultHttpRequest _request;
    private readonly DefaultHttpResponse _response;

    public DefaultHttpContext(IFeatureCollection features)
    {
        _features.Initalize(features);
        _request = new DefaultHttpRequest(this);
        _response = new DefaultHttpResponse(this);
    }

    public override HttpRequest Request => _request;
    public override HttpResponse Response => _response;
}

防止大家比较绕解释一下,因为HttpContext是抽象类,它包含了抽象属性HttpResponse类型的属性Response,所以HttpContext必然有子类去集成它,由于HttpResponse也是抽象类,所以也必须包含了子类去继承它。

寻找HttpResponse Body定义

通过上面的代码我们可以看到HttpResponse的子类为DefaultHttpResponse类。找到类中Body属性定义的地方[点击查看源码👈]看一下具体实现

internal sealed class DefaultHttpResponse : HttpResponse
{
    private static readonly Func<IFeatureCollection, IHttpResponseBodyFeature?> _nullResponseBodyFeature = f => null;

    private readonly DefaultHttpContext _context;
    private FeatureReferences<FeatureInterfaces> _features;

    public DefaultHttpResponse(DefaultHttpContext context)
    {
        _context = context;
        _features.Initalize(context.Features);
    }

    //在FeatureReferences<FeatureInterfaces>中取出ResponseBody的交互操作IHttpResponseBodyFeature
    private IHttpResponseBodyFeature HttpResponseBodyFeature => _features.Fetch(ref _features.Cache.ResponseBody, _nullResponseBodyFeature)!;

    //Body本身是Stream它是抽象类
    public override Stream Body
    {
        //在IHttpResponseBodyFeature实例中查找Stream
        get { return HttpResponseBodyFeature.Stream; }
        set
        {
            var otherFeature = _features.Collection.GetRequiredFeature<IHttpResponseBodyFeature>();

            if (otherFeature is StreamResponseBodyFeature streamFeature
                && streamFeature.PriorFeature != null
                && object.ReferenceEquals(value, streamFeature.PriorFeature.Stream))
            {
                _features.Collection.Set(streamFeature.PriorFeature);
                return;
            }

            _features.Collection.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(value, otherFeature));
        }
    }
}

Body本身是Stream但是Stream是抽象类,但是这里并没有对Stream的子类直接进行定义,而是引入了IHttpResponseBodyFeature去和Stream交互,主要原因还是因为ResponseBody涉及到一个交互体系,比如包含PipeWriter、SendFile等操作。所以这里我们只能顺着IHttpResponseBodyFeature的操作找到相关的实现类,通过查找引用关系我找到了实现类HttpProtocol[点击查看源码👈]我们看一下它的定义

 internal partial class HttpProtocol : IFeatureCollection,
                                          IHttpRequestFeature,
                                          IHttpResponseFeature,
                                          IHttpResponseBodyFeature,
                                          IRouteValuesFeature,
                                          IEndpointFeature,
                                          IHttpRequestIdentifierFeature,
                                          IHttpRequestTrailersFeature,
                                          IHttpExtendedConnectFeature,
                                          IHttpUpgradeFeature,
                                          IRequestBodyPipeFeature,
                                          IHttpConnectionFeature,
                                          IHttpRequestLifetimeFeature,
                                          IHttpBodyControlFeature,
                                          IHttpMaxRequestBodySizeFeature,
                                          IHttpRequestBodyDetectionFeature,
                                          IHttpWebTransportFeature,
                                          IBadRequestExceptionFeature
{
    internal protected IHttpResponseBodyFeature? _currentIHttpResponseBodyFeature;
    private void FastReset()
    {
        //省略一部分代码
        _currentIHttpResponseBodyFeature = this;
        //省略一部分代码
    }
}

它实现了很多接口,其中包含了IHttpResponseBodyFeature接口和IFeatureCollection接口,这两个接口在DefaultHttpResponse类中都有涉猎,是Response输出的交互类,可以理解为Response类是门面,实际的操作都是调用的具体类。我们可以分析一下包含获取具体类型实例的操作,第一个便是它的索引器操作

internal protected IHttpResponseBodyFeature? _currentIHttpResponseBodyFeature;
object? IFeatureCollection.this[Type key]
{
    get
    {
        object? feature = null;
        //省略一部分代码
        if (key == typeof(IHttpResponseBodyFeature))
        {
            feature = _currentIHttpResponseBodyFeature;
        }
        //省略一部分代码
        return feature ?? ConnectionFeatures?[key];
    }
    set
    {
        _featureRevision++;
        //省略一部分代码
        if (key == typeof(IHttpResponseBodyFeature))
        {
            _currentIHttpResponseBodyFeature = (IHttpResponseBodyFeature?)value;
        }
        //省略一部分代码
    }
}

它本身也提供Get和Set相关的类来操作和获取具体的相关的类型

TFeature? IFeatureCollection.Get<TFeature>() where TFeature : default
{
    TFeature? feature = default;
    if (typeof(TFeature) == typeof(IHttpResponseBodyFeature))
    {
        feature = Unsafe.As<IHttpResponseBodyFeature?, TFeature?>(ref _currentIHttpResponseBodyFeature);
    }
    return feature;
}

void IFeatureCollection.Set<TFeature>(TFeature? feature) where TFeature : default
{
    _featureRevision++;
    if (typeof(TFeature) == typeof(IHttpResponseBodyFeature))
    {
        _currentIHttpResponseBodyFeature = Unsafe.As<TFeature?, IHttpResponseBodyFeature?>(ref feature);
    }
}

为什么会这样的,相信大家已经猜到了HttpProtocol实现了很多的接口,意味着它有很多接口的能力。提供的这几个方法可以根据类型快速的获取想得到的实例。因为在HttpProtocol定义了许多变量承载它实现的接口的变量来承载当前实例,所以在DefaultHttpResponse看到了类似缓存的效果获取具体接口的对应实例。我们知道了HttpProtocol实现了IHttpResponseBodyFeature接口,所以我们在HttpProtocol类中查找给IHttpResponseBodyFeature的Stream属性赋值的地方即可,通过上面HttpProtocol类的定义方式我们可以看到它是partial也就是部分类,在另一个部分类中找到了赋值的地方[点击查看源码👈]

Stream IHttpResponseBodyFeature.Stream => ResponseBody;
PipeWriter IHttpResponseBodyFeature.Writer => ResponseBodyPipeWriter;

Stream IHttpResponseFeature.Body
{
    get => ResponseBody;
    set => ResponseBody = value;
}

通过这个代码我们可以看到IHttpResponseBodyFeature.Stream来自ResponseBody属性,找到给HttpProtocol属性ResponseBody赋值的地方[点击查看源码👈]

protected BodyControl? _bodyControl;
public Stream ResponseBody { get; set; } = default!;
public PipeWriter ResponseBodyPipeWriter { get; set; } = default!;
public void InitializeBodyControl(MessageBody messageBody)
{
    if (_bodyControl == null)
    {
        _bodyControl = new BodyControl(bodyControl: this, this);
    }

    (RequestBody, ResponseBody, RequestBodyPipeReader, ResponseBodyPipeWriter) = _bodyControl.Start(messageBody);
}

上面的代码我们可以看到ResponseBody定义和赋值的地方,我们可以看到给ResponseBody赋值来自BodyControl实例的Start方法里这个方法传递的是当前HttpProtocol实例,所以直接找到BodyControl.Start方法定义的地方[点击查看源码👈]查看实现

internal sealed class BodyControl
{
    //HttpResponseStream
    private readonly HttpResponseStream _response;
    private readonly HttpResponsePipeWriter _responseWriter;

    private readonly HttpRequestPipeReader _requestReader;
    private readonly HttpRequestStream _request;

    public BodyControl(IHttpBodyControlFeature bodyControl, IHttpResponseControl responseControl)
    {
        _requestReader = new HttpRequestPipeReader();
        _request = new HttpRequestStream(bodyControl, _requestReader);

        _responseWriter = new HttpResponsePipeWriter(responseControl);
        //实例化HttpResponseStream的地方
        _response = new HttpResponseStream(bodyControl, _responseWriter);
    }

    public (Stream request, Stream response, PipeReader reader, PipeWriter writer) Start(MessageBody body)
    {
        //省略代码
        if (body.RequestUpgrade)
        {
        //默认走不到暂时忽略
        }
        else if (body.ExtendedConnect)
        {
        //默认走不到暂时忽略
        }
        else
        {
            //默认走到这里
            return (_request, _response, _requestReader, _responseWriter);
        }
    }
}

好了,饶了这么多的弯,我们水落石出了找到了HttpResponse.Body的最终来源来自HttpResponseStream类的实例。所以结论就是HttpResponse的Body是HttpResponseStream实例。总结一下

  • HttpResponse的Body是Stream类型的,在DefaultHttpResponse中并未给Body直接赋值,而是在IHttpResponseBodyFeature实例中获取Stream属性,这个类负责是ResponseBody相关的交互。
  • IHttpResponseBodyFeature的实现类是HttpProtocol,这是一个部分类。在这里IHttpResponseBodyFeature.Stream属性来自HttpProtocol类ResponseBody属性。
  • HttpProtocol类ResponseBody属性赋值来自BodyControl的Start方法,它返回的是BodyControl类的_response属性,这个属性的是HttpResponseStream类型的。
  • 所以得到结论HttpResponse.Body也就是Stream类型的,来自HttpResponseStream类的实例。

HttpResponseStream类定义

上面饶了这么大的圈找到了HttpResponse.Body实例的类型HttpResponseStream类,找到类定义的地方看一下里面的实现[点击查看源码👈]

internal sealed partial class HttpResponseStream : Stream
{
    //说明不支持读,如果想知道流是否可读可以使用这个属性先判断
    public override bool CanRead => false;
    //流不可查找
    public override bool CanSeek => false;
    //支持写
    public override bool CanWrite => true;
    //不能获取流的长度否则抛出异常
    public override long Length => throw new NotSupportedException(SR.net_noseek);

    //不可读取和设置位置否则抛出异常
    public override long Position
    {
        get => throw new NotSupportedException(SR.net_noseek);
        set => throw new NotSupportedException(SR.net_noseek);
    }
    //不支持设置Seek否则抛出异常
    public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(SR.net_noseek);
    //不支持Length否则抛出异常
    public override void SetLength(long value) => throw new NotSupportedException(SR.net_noseek);
    //不支持读取操作否则抛出异常
    public override int Read(byte[] buffer, int offset, int size) => throw new InvalidOperationException(SR.net_writeonlystream);
    //不支持读读相关的操作
    public override IAsyncResult BeginRead(byte[] buffer, int offset, int size, AsyncCallback? callback, object? state)
    {
        throw new InvalidOperationException(SR.net_writeonlystream);
    }
    public override int EndRead(IAsyncResult asyncResult) => throw new InvalidOperationException(SR.net_writeonlystream);

    //省略写相关方法和释放相关的方法,只看设计到读相关的地方
}

通过HttpResponseStream类的定义我们可以看到,HttpResponseStream本身是Stream抽象类的子类。涉及到读相关的方法是直接抛出异常,也就是最开始我们直接读取HttpResponse.Body读取直接抛出异常的原因。不仅仅是读取的方法不可用Postion、Length、Seek相关的方法都是不可操作的,操作了都会抛出异常。

UseHttpLogging的解决方式

从ASP.NET Core6.0之后开始,推出了HTTP日志记录功能,使用方式如下

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.ResponseBody;
    logging.RequestBodyLogLimit = 4096;
});

var app = builder.Build();
app.UseHttpLogging();

不过我们通过上面看到了HttpResponse.Body默认情况下是不可以读取的,但是输出Http日志时候是可以读取ResponseBody的,所以我们可以看一下里面的相关实现,在HttpLoggingMiddleware中间件里,因为这个中间件里涉及到Http日志记录的相关逻辑实现,而ResponseBody只是其中的一个选项,所以咱们只关注这一部分的实现[点击查看源码👈]

ResponseBufferingStream? responseBufferingStream = null;
IHttpResponseBodyFeature? originalBodyFeature = null;
try
{
    //获取原始的response
    var response = context.Response;

    if (options.LoggingFields.HasFlag(HttpLoggingFields.ResponseBody))
    {
        //保存原始的IHttpResponseBodyFeature也就是上面提到的ResponseBody交互类
        originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>()!;
        //实例化ResponseBufferingStream
        responseBufferingStream = new ResponseBufferingStream(originalBodyFeature,
            options.ResponseBodyLogLimit,
            _logger,
            context,
            options.MediaTypeOptions.MediaTypeStates,
            options);
        //用ResponseBufferingStream实例替换原始ResponseBody
        response.Body = responseBufferingStream;
        //将responseBufferingStream设置到当前的IHttpResponseBodyFeature
        context.Features.Set<IHttpResponseBodyFeature>(responseBufferingStream);
    }

    await _next(context);

    //输出日志
    if (requestBufferingStream?.HasLogged == false)
    {
        requestBufferingStream.LogRequestBody();
    }
    if (responseBufferingStream != null)
    {
        var responseBody = responseBufferingStream.GetString(responseBufferingStream.Encoding);
        if (!string.IsNullOrEmpty(responseBody))
        {
            _logger.ResponseBody(responseBody);
        }
    }
}
finally
{
    responseBufferingStream?.Dispose();
    if (originalBodyFeature != null)
    {
        //还原原始的IHttpResponseBodyFeature
        context.Features.Set(originalBodyFeature);
    }
}

通过上面的代码我们可以看到,其实也是实现了类似的操作,用ResponseBufferingStream替换掉原始的HttpResponseStream类型,替换的逻辑要在中间件执行next()之前,操作完成之后也就是执行了next()之后再把原始的IHttpResponseBodyFeature替换回来,有关具体的ResponseBufferingStream实现方式咱们这里不做详细描述了,不是本文重点。

ResponseBufferingStream的实现并不是使用MemoryStream这种可读取的流替换掉默认的HttpResponseStream,ResponseBufferingStreamLogRequestBody()方法使用ILogger输出日志并没有直接去读取Stream,而是反其道重写了Stream的Write()方法,因为对HttpResponseBody实例
HttpResponseStream的输出写操作本质是调用Stream的Write()方法,重写了Write()方法之后会把写入的内容记录到Buffer中,LogRequestBody()方法通过读取Buffer中的内容得到字符串,使用ILogger输出日志。

答疑解惑

在之前的讨论中有许多小伙伴对用MemoryStream替换ResponseBody存在一个疑惑,就是既然已经替换掉了,一直用MemoryStream不就好了嘛,为啥还要把ResponseBody原始值记录下来,结束后再替换回来。这个疑问咋一听确实也没毛病,但是等大致了解了它的使用过程之后才恍然大悟,原来是这么回事,在这里咱们就看一下为啥会是这样。
首先说一下结论,如果把ResponseBody替换为MemoryStream之后,不对原始的ResponseBody进行操作的话,在这个中间件(类似上面说的到的UseResponseBodyRead中间件)之后的操作,可能是后续的其它中间件或者是各种终结点比如Controller的Action亦或者是MinimalApi的Map方法等,是可以读取和写入值的,也就是在替换中间件的范围内,也就是大家经常说的套娃模式,被它套进去的是一直生效的,没任何问题,终结点本身也是中间件。下面这张图相信大家经常看到打个比方如果我的UseResponseBodyRead中间件是图里的Middleware1把ResponseBody替换为MemoryStream,那么后续的操作比如Middleware2和Middleware3还有后续的终结点之类的读取ResponseBody是完全没有问题的。但是最终Http的输出结果肯定是不符合预期的,这主要涉及到HttpResponseStream.Write()的问题,我们知道最终我们输出的结果会体现在Write()方法上[点击查看源码👈],核心代码如下所示

internal sealed class HttpResponseStream : Stream
{
    private readonly HttpResponsePipeWriter _pipeWriter;
    private readonly IHttpBodyControlFeature _bodyControl;

    public HttpResponseStream(IHttpBodyControlFeature bodyControl, HttpResponsePipeWriter pipeWriter)
    {
        _bodyControl = bodyControl;
        _pipeWriter = pipeWriter;
    }

    //重写Stream的Write操作
    public override void Write(byte[] buffer, int offset, int count)
    {
        if (!_bodyControl.AllowSynchronousIO)
        {
            throw new InvalidOperationException(CoreStrings.SynchronousWritesDisallowed);
        }
        //调用WriteAsync方法
        WriteAsync(buffer, offset, count, default).GetAwaiter().GetResult();
    }

    public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
    {
        //本质调用了HttpResponsePipeWriter的写方法
        return _pipeWriter.WriteAsync(new ReadOnlyMemory<byte>(buffer, offset, count), cancellationToken).GetAsTask();
    }
)

通过上面我们可以看到HttpResponseStreamWrite()方法本质是调用了HttpResponsePipeWriterWriteAsync()方法,HttpResponseStream本身不存储写入的数据。而HttpResponsePipeWriter实例的构建是在BodyControl类中上面咱们已经粘贴过实例化的源码了,可自行翻阅上去看看HttpResponsePipeWriter类的定义相关。所以上面把ResponseBody替换为MemoryStream,最终的结果要体现在HttpResponseStream实例中,否则的话没有办法正常输出。可以用一个伪代码例子演示一下这个原理

Order order1 = new Order 
{
    Address = "北京市海淀区"
};

SetOrder(order1);

Console.WriteLine($"最后地址:{order1.Address}");

public void SetOrder(Order order2)
{
    order2 = new Order
    {
        Address = "上海市闵行区"
    };
    Console.WriteLine($"设置地址:{order2.Address}");
}

这个示例中即使SetOrder方法中设置了新的Address,但是脱离了SetOrder方法作用域后,外面的最后地址依然是北京市海淀区。在调用SetOrder进入方法的时候order1和方法形参order2都指向的是Address = "北京市海淀区",在SetOrder方法内部完成实例化之后order2指向的是Address = "上海市闵行区",但是order1依然指向的是Address = "北京市海淀区",因为引用传递形参本身只是存储的引用地址,更换了引用地址就和原来的地址脱钩了,如果想让内外行为一直必须要体现到原始值上面去。我们替换ResponseBody的时候也是同理,最终Write本质还是要依赖HttpResponseStream里的HttpResponsePipeWriter属性,但是MemoryStream可没有HttpResponsePipeWriter。你可能会有疑问,我上面也没把MemoryStream结果Write()HttpResponseStream里去啊?但是上面使用了CopyToAsync方法与原始的的ResponseBody类型HttpResponseStream交互,CopyToAsync方法本质就是在调用WriteAsync()方法,口说无凭直接上代码[点击查看源码👈],核心代码如下所示

public virtual Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
{
    //省略一部分代码

    return Core(this, destination, bufferSize, cancellationToken);

    static async Task Core(Stream source, Stream destination, int bufferSize, CancellationToken cancellationToken)
    {
        //使用了对象池复用空间
        byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
        try
        {
            int bytesRead;
            while ((bytesRead = await source.ReadAsync(new Memory<byte>(buffer), cancellationToken).ConfigureAwait(false)) != 0)
            {
                //最终也是调用的目标流的WriteAsync方法
                await destination.WriteAsync(new ReadOnlyMemory<byte>(buffer, 0, bytesRead), cancellationToken).ConfigureAwait(false);
            }
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(buffer);
        }
    }
}

总结

    本文主要讲解了如何读取ResponseBody,默认情况下是不可以读取的,需要我们使用了中间件结合MemoryStream自行处理一下,同时我们结合和Http日志记录中间件里的处理方式对比了一下,最终答疑了为了要把替换的结果还得继续体现在原始的ResponseBody上面去,整体来说这方面还是相对容易理解的,只是找起来可能比较麻烦。大致总结一下

  • ResponseBody默认不可读取,因为它的实例是HttpResponseStream这个类重写了Stream的Read相关的方法,但是实现是抛出异常的,所以我们需要可读的类来替换默认的操作,MemoryStream可以辅助实现。
  • UseHttpLogging中间件也可以读取ResponseBody里的结果,但是它是使用的重写Stream的Write相关的方法,在Write方法里使用Buffer记录了写过的数据,然后通过GetString()方法读取Buffer里的内容实现记录要输出的值。
  • MemoryStream解决的是我们在写代码过程中对ResponseBody的读取或写入操作,但是程序处理完之后要把MemoryStream的结果在体现到HttpResponseStream中去,否则虽然程序中读取写入Body没问题,但是输出的结果会出问题。

    说句题外话,ChatGTP的发布对人们心里的冲击还是挺大的,因为它表现出来的强大效果让人眼前一亮,很多博主和企业也借此风口寻找新的出路,甚至有人会担心会不会被替代失业。个人以为新的技术大行其道必然会带来新的产业,新的产业的新的岗位同时也是需要更多的人参与进来。所以保持对新事物的好奇心多多参与。工具不会替代人,能替代人的是会使用工具的人。

文章来源地址https://www.toymoban.com/news/detail-409348.html

👇欢迎扫码关注我的公众号👇 由ASP.NET Core读取Response.Body引发的思考

到了这里,关于由ASP.NET Core读取Response.Body引发的思考的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • ASP.NET和ASP.NET Core的区别

    ASP.NET和ASP.NET Core是两个不同的Web应用程序框架,它们都是由Microsoft开发的。ASP.NET是Microsoft推出的第一个Web应用程序框架,而ASP.NET Core是其最新版本。本文将介绍ASP.NET和ASP.NET Core的简介和区别。 ASP.NET的简介 ASP.NET是一个基于.NET框架的Web应用程序框架,它是Microsoft推出的第一

    2024年02月16日
    浏览(76)
  • Asp.Net VS ASP.NET Core 请求管道

    参考链接 ASP.NET CORE 启动过程及源码解读 请求进入Asp.Net工作进程后,由进程创建HttpWorkRequest对象,封装此次请求有关的所有信息,然后进入HttpRuntime类进行进一步处理。HttpRuntime通过请求信息创建HttpContext上下文对象,此对象将贯穿整个管道,直到响应结束。同时创建或从应用

    2024年02月04日
    浏览(81)
  • 【ASP.NET Core 基础知识】--最佳实践和进阶主题--设计模式在ASP.NET Core中的应用

    一、设计模式概述 1.1 什么是设计模式 设计模式是在软件设计过程中反复出现的、经过验证的、可重用的解决问题的方法。它们是针对特定问题的通用解决方案,提供了一种在软件开发中可靠的指导和标准化方法。设计模式通常描述了一种在特定情景下的解决方案,包括了问

    2024年02月21日
    浏览(160)
  • ASP.NET Core SingleR Core:WebApi + .net 客户端开发

    我之前稍微研究了一下SignalR Core。用起来还行。简单来说SignalR就是用来解决实时通讯的问题的。 ASP.NET Core SingleR:初次体验和简单项目搭建 SignalR支持三种客户端,C#,Java,JavaScirpt。基本够用了。本身就是微软开发的,肯定支持自己的语言。因为是Websocket的上层封装,所以也要支

    2024年01月20日
    浏览(134)
  • ASP.NET Core —配置系统

    一个应用要运行起来,往往需要读取很多的预设好的配置信息,根据约定好的信息或方式执行一定的行为。 配置的本质就是软件运行的参数,在一个软件实现中需要的参数非常多,如果我们以 Hard Code (硬编码)的方式写在应用代码中,这样配置就会很乱,而且后续也不容易修

    2024年02月08日
    浏览(53)
  • ASP.NET Core 8 基础

    2023年11月将发布发布.NET 8,基于.NET 8 的 ASP.NET Core 8.0也会一并发布,这是继ASP.NET Core 6.0之后,又一个重要版本,因为引入了nativeAOT,在性能上有很大提升,所以系统地学习一下这项技术。 ASP.NET Core 的几个主要优势: 跨平台,支持 Windows, macOS, Linux,Docker,Azure和AWS等云服务自

    2024年02月11日
    浏览(49)
  • Asp.Net Core 6 - 概述

    Q: 什么是 .NET? A:.NET 是一个开发人员平台,由工具、编程语言、库组成,用于构建许多不同类型的应用程序。使用 .NET,可以使用多种语言、编辑器和库来构建 Web、移动、桌面、游戏和 IoT 等,可以使用 C#、F# 或 Visual Basic 编写 .NET 应用。 .NET 发展至今,出现了两种实现 n

    2024年02月06日
    浏览(54)
  • .NET Core 引发的异常: “SqlSugar.SqlSugarException“ 位于 System.Private.CoreLib.dll 中

    在使用.NET Core开发应用程序时,有时候会遇到各种不同的异常情况。其中一种常见的异常是 “SqlSugar.SqlSugarException”。这个异常通常在与数据库交互的过程中出现,特别是在使用SqlSugar库时。 SqlSugar是一个流行的ORM(对象关系映射)框架,它简化了与数据库的交互操作。它提

    2024年02月04日
    浏览(43)
  • ASP.NET Core IOC容器

      ServiceCollection 抽象和具体之间多种注册方式

    2024年02月09日
    浏览(42)
  • ASP.NET Core 的日志系统

    ASP.NET Core 提供了丰富日志系统。 可以通过多种途径输出日志,以满足不同的场景,内置的几个日志系统包括: Console,输出到控制台,用于调试,在产品环境可能会影响性能。 Debug,输出到 System.Diagnostics.Debug.WriteLine EventSource,输出到对应操作系统的日志系统中,在Windows上是

    2024年02月10日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包