是什么导致 C# 中的结构重置其值?
What causes a struct in C# to reset its values?
我有一个用 C# 编写的基于 ASP.NET Core 3.1 的项目。我有以下 struct
对象,我正尝试使用中间件调用它。
internal struct StaticAsyncFileContext
{
private const int StreamCopyBufferSize = 64 * 1024;
private readonly HttpContext _context;
private readonly StaticAsyncFileOptions _options;
private readonly HttpRequest _request;
private readonly HttpResponse _response;
private readonly ILogger _logger;
private readonly IAsyncFileProvider _fileProvider;
private readonly IContentTypeProvider _contentTypeProvider;
private string _method;
private bool _isGet;
private PathString _subPath;
private string _contentType;
private IFileInfo _fileInfo;
private long _length;
private DateTimeOffset _lastModified;
private EntityTagHeaderValue _etag;
private RequestHeaders _requestHeaders;
private ResponseHeaders _responseHeaders;
private PreconditionState _ifMatchState;
private PreconditionState _ifNoneMatchState;
private PreconditionState _ifModifiedSinceState;
private PreconditionState _ifUnmodifiedSinceState;
private RangeItemHeaderValue _range;
public IFileInfo GetFileInfo()
{
return _fileInfo;
}
public StaticAsyncFileContext(HttpContext context, StaticAsyncFileOptions options, PathString matchUrl, IAsyncFileProvider fileProvider, IContentTypeProvider contentTypeProvider)
{
_context = context;
_options = options;
_request = context.Request;
_response = context.Response;
_requestHeaders = _request.GetTypedHeaders();
_responseHeaders = _response.GetTypedHeaders();
_fileProvider = fileProvider ?? throw new ArgumentNullException($"{nameof(fileProvider)} cannot be null.");
_contentTypeProvider = contentTypeProvider ?? throw new ArgumentNullException($"{nameof(contentTypeProvider)} cannot be null.");
_subPath = PathString.Empty;
_method = null;
_contentType = null;
_fileInfo = null;
_length = 0;
_etag = null;
_range = null;
_isGet = false;
IsHeadMethod = false;
IsRangeRequest = false;
_lastModified = new DateTimeOffset();
_ifMatchState = PreconditionState.Unspecified;
_ifNoneMatchState = PreconditionState.Unspecified;
_ifModifiedSinceState = PreconditionState.Unspecified;
_ifUnmodifiedSinceState = PreconditionState.Unspecified;
}
internal enum PreconditionState
{
Unspecified,
NotModified,
ShouldProcess,
PreconditionFailed
}
public bool IsHeadMethod { get; private set; }
public bool IsRangeRequest { get; private set; }
public string SubPath
{
get { return _subPath.Value; }
}
public async Task<bool> LookupFileInfoAsync()
{
_fileInfo = await _fileProvider.GetFileInfoAsync(_subPath.Value);
if (_fileInfo.Exists)
{
_length = _fileInfo.Length;
DateTimeOffset last = _fileInfo.LastModified;
// Truncate to the second.
_lastModified = new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Offset).ToUniversalTime();
long etagHash = _lastModified.ToFileTime() ^ _length;
_etag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
}
return _fileInfo.Exists;
}
public void ApplyResponseHeaders(int statusCode)
{
// Here _fileInfo should not be null since it was set by LookupFileInfoAsync()
_response.StatusCode = statusCode;
if (statusCode < 400)
{
// these headers are returned for 200, 206, and 304
// they are not returned for 412 and 416
if (!string.IsNullOrEmpty(_contentType))
{
_response.ContentType = _contentType;
}
_responseHeaders.LastModified = _lastModified;
_responseHeaders.ETag = _etag;
_responseHeaders.Headers[HeaderNames.AcceptRanges] = "bytes";
}
if (statusCode == Constants.Status200Ok)
{
// this header is only returned here for 200
// it already set to the returned range for 206
// it is not returned for 304, 412, and 416
_response.ContentLength = _length;
}
_options.OnPrepareResponse(new StaticFileResponseContext(_context, _fileInfo));
}
public Task SendStatusAsync(int statusCode)
{
ApplyResponseHeaders(statusCode);
_logger.LogHandled(statusCode, SubPath);
return Task.CompletedTask;
}
public PreconditionState GetPreconditionState()
{
return GetMaxPreconditionState(_ifMatchState, _ifNoneMatchState, _ifModifiedSinceState, _ifUnmodifiedSinceState);
}
public async Task SendAsync()
{
ApplyResponseHeaders(Constants.Status200Ok);
string physicalPath = _fileInfo.PhysicalPath;
var sendFile = _context.Features.Get<IHttpResponseBodyFeature>();
if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
{
// We don't need to directly cancel this, if the client disconnects it will fail silently.
await sendFile.SendFileAsync(physicalPath, 0, _length, CancellationToken.None);
return;
}
try
{
using (var readStream = _fileInfo.CreateReadStream())
{
// Larger StreamCopyBufferSize is required because in case of FileStream readStream isn't going to be buffering
await StreamCopyOperation.CopyToAsync(readStream, _response.Body, _length, StreamCopyBufferSize, _context.RequestAborted);
}
}
catch (OperationCanceledException ex)
{
_logger.LogWriteCancelled(ex);
// 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.Abort();
}
}
}
在上面的struct
中,当方法LookupFileInfoAsync()
被调用时,假设设置了_fileInfo
、_length
和_lastModified
属性的值。如果在 LookupFileInfoAsync()
之后调用方法 GetPreconditionState()
或 GetFileInfo()
,如果 _fileInfo.Exists
return 为真,_fileInfo
的值不应为 null。
下面是我的中间件如何调用这个 struct
public async Task Invoke(HttpContext context)
{
var fileContext = new StaticAsyncFileContext(context, _options, _matchUrl, _logger, _fileProvider, _contentTypeProvider);
if (!await fileContext.LookupFileInfoAsync())
{
_logger.LogFileNotFound(fileContext.SubPath);
} else {
// This is an unesassary line, it is only added to ensure that FileInfo was set
IFileInfo fileInfo = fileContext.GetFileInfo();
switch (fileContext.GetPreconditionState())
{
case StaticAsyncFileContext.PreconditionState.Unspecified:
case StaticAsyncFileContext.PreconditionState.ShouldProcess:
if (fileContext.IsHeadMethod)
{
await fileContext.SendStatusAsync(Constants.Status200Ok);
return;
}
try
{
await fileContext.SendAsync();
_logger.LogFileServed(fileContext.SubPath, fileContext.PhysicalPath);
return;
}
catch (FileNotFoundException)
{
context.Response.Clear();
}
break;
//... stripped out for simplicity
default:
var exception = new NotImplementedException(fileContext.GetPreconditionState().ToString());
Debug.Fail(exception.ToString());
throw exception;
}
}
await _next(context);
}
如您所见,首先调用 LookupFileInfoAsync()
并确保 _infoFile
属性 不为空,因为它 return 为真。但就我而言,在调用 LookupFileInfoAsync()
return null 之后调用 fileContext.GetFileInfo()
。在单步执行我的代码时,我可以不同地看到 _fileInfo
设置正确。但是在下一个方法调用中,_fileInfo
的值将以某种方式重置为 null。如果您查看 Microsoft 的 StaticFileContext.cs 文件,您会注意到代码非常相似,只是在我的例子中 LookupFileInfo()
被异步调用。
问题
什么可能导致 _fileInfo
属性 在调用 LookupFileInfoAsync()
后重置为 null?
首先,如果结构不可变是理想的。这条规则很可能会解决您的问题。
其次structs
是值类型,这意味着你很多时候都在复制它们(不是引用给他们)。
就像任何其他值类型一样,如果您复制它们,更改将独立于其他副本
Structs (C# Programming Guide)
Structs are copied on assignment. When a struct is assigned to a new
variable, all the data is copied, and any modification to the new copy
does not change the data for the original copy. This is important to
remember when working with collections of value types such as
Dictionary<string, myStruct>
.
我有一个用 C# 编写的基于 ASP.NET Core 3.1 的项目。我有以下 struct
对象,我正尝试使用中间件调用它。
internal struct StaticAsyncFileContext
{
private const int StreamCopyBufferSize = 64 * 1024;
private readonly HttpContext _context;
private readonly StaticAsyncFileOptions _options;
private readonly HttpRequest _request;
private readonly HttpResponse _response;
private readonly ILogger _logger;
private readonly IAsyncFileProvider _fileProvider;
private readonly IContentTypeProvider _contentTypeProvider;
private string _method;
private bool _isGet;
private PathString _subPath;
private string _contentType;
private IFileInfo _fileInfo;
private long _length;
private DateTimeOffset _lastModified;
private EntityTagHeaderValue _etag;
private RequestHeaders _requestHeaders;
private ResponseHeaders _responseHeaders;
private PreconditionState _ifMatchState;
private PreconditionState _ifNoneMatchState;
private PreconditionState _ifModifiedSinceState;
private PreconditionState _ifUnmodifiedSinceState;
private RangeItemHeaderValue _range;
public IFileInfo GetFileInfo()
{
return _fileInfo;
}
public StaticAsyncFileContext(HttpContext context, StaticAsyncFileOptions options, PathString matchUrl, IAsyncFileProvider fileProvider, IContentTypeProvider contentTypeProvider)
{
_context = context;
_options = options;
_request = context.Request;
_response = context.Response;
_requestHeaders = _request.GetTypedHeaders();
_responseHeaders = _response.GetTypedHeaders();
_fileProvider = fileProvider ?? throw new ArgumentNullException($"{nameof(fileProvider)} cannot be null.");
_contentTypeProvider = contentTypeProvider ?? throw new ArgumentNullException($"{nameof(contentTypeProvider)} cannot be null.");
_subPath = PathString.Empty;
_method = null;
_contentType = null;
_fileInfo = null;
_length = 0;
_etag = null;
_range = null;
_isGet = false;
IsHeadMethod = false;
IsRangeRequest = false;
_lastModified = new DateTimeOffset();
_ifMatchState = PreconditionState.Unspecified;
_ifNoneMatchState = PreconditionState.Unspecified;
_ifModifiedSinceState = PreconditionState.Unspecified;
_ifUnmodifiedSinceState = PreconditionState.Unspecified;
}
internal enum PreconditionState
{
Unspecified,
NotModified,
ShouldProcess,
PreconditionFailed
}
public bool IsHeadMethod { get; private set; }
public bool IsRangeRequest { get; private set; }
public string SubPath
{
get { return _subPath.Value; }
}
public async Task<bool> LookupFileInfoAsync()
{
_fileInfo = await _fileProvider.GetFileInfoAsync(_subPath.Value);
if (_fileInfo.Exists)
{
_length = _fileInfo.Length;
DateTimeOffset last = _fileInfo.LastModified;
// Truncate to the second.
_lastModified = new DateTimeOffset(last.Year, last.Month, last.Day, last.Hour, last.Minute, last.Second, last.Offset).ToUniversalTime();
long etagHash = _lastModified.ToFileTime() ^ _length;
_etag = new EntityTagHeaderValue('\"' + Convert.ToString(etagHash, 16) + '\"');
}
return _fileInfo.Exists;
}
public void ApplyResponseHeaders(int statusCode)
{
// Here _fileInfo should not be null since it was set by LookupFileInfoAsync()
_response.StatusCode = statusCode;
if (statusCode < 400)
{
// these headers are returned for 200, 206, and 304
// they are not returned for 412 and 416
if (!string.IsNullOrEmpty(_contentType))
{
_response.ContentType = _contentType;
}
_responseHeaders.LastModified = _lastModified;
_responseHeaders.ETag = _etag;
_responseHeaders.Headers[HeaderNames.AcceptRanges] = "bytes";
}
if (statusCode == Constants.Status200Ok)
{
// this header is only returned here for 200
// it already set to the returned range for 206
// it is not returned for 304, 412, and 416
_response.ContentLength = _length;
}
_options.OnPrepareResponse(new StaticFileResponseContext(_context, _fileInfo));
}
public Task SendStatusAsync(int statusCode)
{
ApplyResponseHeaders(statusCode);
_logger.LogHandled(statusCode, SubPath);
return Task.CompletedTask;
}
public PreconditionState GetPreconditionState()
{
return GetMaxPreconditionState(_ifMatchState, _ifNoneMatchState, _ifModifiedSinceState, _ifUnmodifiedSinceState);
}
public async Task SendAsync()
{
ApplyResponseHeaders(Constants.Status200Ok);
string physicalPath = _fileInfo.PhysicalPath;
var sendFile = _context.Features.Get<IHttpResponseBodyFeature>();
if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
{
// We don't need to directly cancel this, if the client disconnects it will fail silently.
await sendFile.SendFileAsync(physicalPath, 0, _length, CancellationToken.None);
return;
}
try
{
using (var readStream = _fileInfo.CreateReadStream())
{
// Larger StreamCopyBufferSize is required because in case of FileStream readStream isn't going to be buffering
await StreamCopyOperation.CopyToAsync(readStream, _response.Body, _length, StreamCopyBufferSize, _context.RequestAborted);
}
}
catch (OperationCanceledException ex)
{
_logger.LogWriteCancelled(ex);
// 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.Abort();
}
}
}
在上面的struct
中,当方法LookupFileInfoAsync()
被调用时,假设设置了_fileInfo
、_length
和_lastModified
属性的值。如果在 LookupFileInfoAsync()
之后调用方法 GetPreconditionState()
或 GetFileInfo()
,如果 _fileInfo.Exists
return 为真,_fileInfo
的值不应为 null。
下面是我的中间件如何调用这个 struct
public async Task Invoke(HttpContext context)
{
var fileContext = new StaticAsyncFileContext(context, _options, _matchUrl, _logger, _fileProvider, _contentTypeProvider);
if (!await fileContext.LookupFileInfoAsync())
{
_logger.LogFileNotFound(fileContext.SubPath);
} else {
// This is an unesassary line, it is only added to ensure that FileInfo was set
IFileInfo fileInfo = fileContext.GetFileInfo();
switch (fileContext.GetPreconditionState())
{
case StaticAsyncFileContext.PreconditionState.Unspecified:
case StaticAsyncFileContext.PreconditionState.ShouldProcess:
if (fileContext.IsHeadMethod)
{
await fileContext.SendStatusAsync(Constants.Status200Ok);
return;
}
try
{
await fileContext.SendAsync();
_logger.LogFileServed(fileContext.SubPath, fileContext.PhysicalPath);
return;
}
catch (FileNotFoundException)
{
context.Response.Clear();
}
break;
//... stripped out for simplicity
default:
var exception = new NotImplementedException(fileContext.GetPreconditionState().ToString());
Debug.Fail(exception.ToString());
throw exception;
}
}
await _next(context);
}
如您所见,首先调用 LookupFileInfoAsync()
并确保 _infoFile
属性 不为空,因为它 return 为真。但就我而言,在调用 LookupFileInfoAsync()
return null 之后调用 fileContext.GetFileInfo()
。在单步执行我的代码时,我可以不同地看到 _fileInfo
设置正确。但是在下一个方法调用中,_fileInfo
的值将以某种方式重置为 null。如果您查看 Microsoft 的 StaticFileContext.cs 文件,您会注意到代码非常相似,只是在我的例子中 LookupFileInfo()
被异步调用。
问题
什么可能导致 _fileInfo
属性 在调用 LookupFileInfoAsync()
后重置为 null?
首先,如果结构不可变是理想的。这条规则很可能会解决您的问题。
其次structs
是值类型,这意味着你很多时候都在复制它们(不是引用给他们)。
就像任何其他值类型一样,如果您复制它们,更改将独立于其他副本
Structs (C# Programming Guide)
Structs are copied on assignment. When a struct is assigned to a new variable, all the data is copied, and any modification to the new copy does not change the data for the original copy. This is important to remember when working with collections of value types such as
Dictionary<string, myStruct>
.