当前位置: 首页 > 知识库问答 >
问题:

如何在文件仍在写入时在ASP.NET核心中提供服务

赵俊侠
2023-03-14

我有一个由后台服务持续写入的日志文件。到目前为止,用户需要能够下载该文件。当我返回 MVC FileResult 时,由于内容长度不匹配,我得到一个 InvalidOperationException,大概是因为在提供文件时某些内容已写入文件。有一个文件已提供,它基本上没问题,但它通常有一个不完整的最后一行。

后台服务本质上是这样做的:

var stream = new FileStream(evidenceFilePath, FileMode.Append, FileAccess.Write, FileShare.Read);
while (true) // obviously it isn't actually this, but it does happen a lot!
{
    var content = "log content\r\n";
    stream.Write(Encoding.UTF8.GetBytes(content);
}

以下是控制器动作的一些变化(都有相同的结果):

public IActionResult DownloadLog1()
{
    return PhysicalFile("C:\\path\\to\\the\\file.txt", "text/plain", enableRangeProcessing: false); // also tried using true
}

public IActionResult DownloadLog2()
{
    var stream = new FileStream("C:\\path\\to\\the\\file.txt", FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
    return File(stream, "text/plain", enableRangeProcessing: false); // also tried true
}

这是我尝试上述任一方法时遇到的异常

System.InvalidOperationException: Response Content-Length mismatch: too many bytes written (216072192 of 216059904).
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ThrowTooManyBytesWritten(Int32 count)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.VerifyAndUpdateWrite(Int32 count)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.WriteAsync(ReadOnlyMemory`1 data, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpResponseStream.WriteAsync(Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(Stream source, Stream destination, Nullable`1 count, Int32 bufferSize, CancellationToken cancel)
   at Microsoft.AspNetCore.Mvc.Infrastructure.FileResultExecutorBase.WriteFileAsync(HttpContext context, Stream fileStream, RangeItemHeaderValue range, Int64 rangeLength)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeResultAsync(IActionResult result)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResultFilterAsync[TFilter,TFilterAsync]()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResultExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeResultFilters()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)

我不太介意例外,但我更希望它不会发生。不过,我确实需要解决最后一行不完整的问题。对我来说,最显而易见的解决方案是跟踪已经明确写入文件的字节数,并且只处理前n个字节。我看不出有什么简单的方法可以用< code>FileResult和各种构建它的helper方法来实现。文件可能会变得非常大(高达500MB左右),因此在内存中进行缓冲似乎不太实际。

共有3个答案

程彭祖
2023-03-14

一般来说,您可能会遇到文件锁定的问题,因此您需要对此进行计划和补偿。然而,您眼前的问题更容易解决。问题归结为返回一个流。该流在返回响应时被写入,因此在创建响应正文时计算的内容长度是错误的。

您需要做的是在某个时间点捕获日志,即通过将其读取为byte[]。然后,您可以返回它,而不是流,并且内容长度将被正确计算,因为byte[]在读取后不会更改。

using (var stream = new FileStream("C:\\path\\to\\the\\file.txt", FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var ms = new MemoryStream())
{
    await stream.CopyToAsync(ms);
    return File(ms.ToArray(), "text/plain");
}
范承望
2023-03-14

文件是非托管资源。

所以当你访问一个非托管资源,比如一个文件,它是通过一个句柄打开的。在文件的情况下,它是open_file_handle(从内存中重新收集)。

因此,我可以建议(非常通用)编写日志条目的最佳方法:

打开文件,

写文件,

关闭文件,

如适用则处置

简而言之,不要让水流开着。

其次,对于控制器,你可以看看MSDN通过控制器提供文件的例子。

方苗宣
2023-03-14

我最终编写了一个自定义的ActionResult和IActionResultExecutor来匹配,它们主要基于MVC FileStreamResult和FileStreamResultExecutor:

public class PartialFileStreamResult : FileResult
{
    Stream stream;
    long bytes;

    /// <summary>
    /// Creates a new <see cref="PartialFileStreamResult"/> instance with
    /// the provided <paramref name="fileStream"/> and the
    /// provided <paramref name="contentType"/>, which will download the first <paramref name="bytes"/>.
    /// </summary>
    /// <param name="stream">The stream representing the file</param>
    /// <param name="contentType">The Content-Type header for the response</param>
    /// <param name="bytes">The number of bytes to send from the start of the file</param>
    public PartialFileStreamResult(Stream stream, string contentType, long bytes)
        : base(contentType)
    {
        this.stream = stream ?? throw new ArgumentNullException(nameof(stream));
        if (bytes == 0)
        {
            throw new ArgumentOutOfRangeException(nameof(bytes), "Invalid file length");
        }
        this.bytes = bytes;
    }

    /// <summary>
    /// Gets or sets the stream representing the file to download.
    /// </summary>
    public Stream Stream
    {
        get => stream;
        set => stream = value ?? throw new ArgumentNullException(nameof(stream));
    }

    /// <summary>
    /// Gets or sets the number of bytes to send from the start of the file.
    /// </summary>
    public long Bytes
    {
        get => bytes;
        set
        {
            if (value == 0)
            {
                throw new ArgumentOutOfRangeException(nameof(bytes), "Invalid file length");
            }
            bytes = value;
        }
    }

    /// <inheritdoc />
    public override Task ExecuteResultAsync(ActionContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }
        var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<PartialFileStreamResult>>();
        return executor.ExecuteAsync(context, this);
    }
}

public class PartialFileStreamResultExecutor : FileResultExecutorBase, IActionResultExecutor<PartialFileStreamResult>
{
    public PartialFileStreamResultExecutor(ILoggerFactory loggerFactory)
        : base(CreateLogger<PartialFileStreamResultExecutor>(loggerFactory))
    {
    }

    public async Task ExecuteAsync(ActionContext context, PartialFileStreamResult result)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (result == null)
        {
            throw new ArgumentNullException(nameof(result));
        }

        using (result.Stream)
        {
            long length = result.Bytes;
            var (range, rangeLength, serveBody) = SetHeadersAndLog(context, result, length, result.EnableRangeProcessing);
            if (!serveBody) return;

            try
            {
                var outputStream = context.HttpContext.Response.Body;
                if (range == null)
                {
                    await StreamCopyOperation.CopyToAsync(result.Stream, outputStream, length, bufferSize: BufferSize, cancel: context.HttpContext.RequestAborted);
                }
                else
                {
                    result.Stream.Seek(range.From.Value, SeekOrigin.Begin);
                    await StreamCopyOperation.CopyToAsync(result.Stream, outputStream, rangeLength, BufferSize, context.HttpContext.RequestAborted);
                }
            }
            catch (OperationCanceledException)
            {
                // Don't throw this exception, it's most likely caused by the client disconnecting.
                // However, if it was cancelled for any other reason we need to prevent empty responses.
                context.HttpContext.Abort();
            }
        }
    }
}

我本可以做更多的工作来添加额外的构造函数重载来设置一些可选参数(例如下载文件名等),但这足以满足我的需要。

您需要在启动时添加IActionResultExecutor。配置服务:

services.AddTransient<IActionResultExecutor<PartialFileStreamResult>, PartialFileStreamResultExecutor>();

我的控制器动作因此变成了:

[HttpGet]
public IActionResult DownloadLog()
{
    var (path, bytes) = GetThePathAndTheNumberOfBytesIKnowHaveBeenFlushed();

    var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); // this ensures that the file can be read while it's still being written
    return new PartialFileStreamResult(stream, "text/plain", bytes);
}
 类似资料:
  • 我想在ASP.NET Web API控制器中返回一个文件,但我的所有方法都将作为JSON返回。 当我在浏览器中调用这个endpoint时,Web API将返回为JSON,HTTP内容头设置为。

  • 我正在尝试将ASP.NET MVC webform迁移到ASP.NET核心MVC。当前,类遇到问题。 原来的行是: 但是,对于ASP.NET核心,UrlReferrer不可用。我发现了以下内容: 它返回StringValues而不是String。我不确定我是否应该尝试使用这一个,或者是否有任何其他解决办法来解决这种情况。也不可用,或者我没有该命名空间。我的命名空间如下: 如果有人能指引我正确的方向

  • 问题内容: 我在一个免费的支持PHP的服务器上安装了此脚本: 它创建文件,但为空。 如何创建文件并向其中写入内容,例如“猫追老鼠”行? 问题答案: 您可以使用更高级别的函数,例如,与调用,相同,然后依次将数据写入文件。

  • 问题内容: 所以这很尴尬。我有一个应用程序,该应用程序已集成在一起,现在它只提供一个静态HTML页面,其中包含指向CSS和JS的链接。而且我找不到文档中描述返回静态文件的位置。是的,我可以使用,但是我知道数据没有模板化。我还以为或者是正确的事情,但我不能让这些工作。同时,我正在打开文件,阅读内容,并装配Response具有适当mimetype的: 有人要为此提供代码示例或网址吗?我知道这将变得简单

  • 问题内容: 这个问题已经被回答了好几次了,我已经看到了几乎所有相关的帖子,但是无法加载CSS文件。我在我的项目中有以下结构:中,我有以下代码: 在urls.py中,我有以下代码: 我在模板中使用它 我在做什么错?我应该做什么?例如应该在中添加任何内容?或在的设置中?请假设我是一个绝对的初学者。我真的需要一个答案。谢谢。 问题答案: 什么都不做的装饰器类如下所示: 然后您可以正常应用它: