зеркало из https://github.com/dotnet/razor.git
Revisit Razor logging (#10641)
I recommend reviewing this commit-by-commit. I took a look at logging to address an integration test issue where we should ignore any exceptions that occur during logging because xUnit's `ITestOutputHelper` is no longer available. Previously, we had swallowed these exceptions across all unit tests as well. However, in unit tests, these exceptions can be an early warning sign of other problems. While addressing integration tests, I ended up taking a more thorough look at logging and ended up re-implementing the log message formatting to reduce string allocations. In addition, I cleaned up a fair number of redundant "no-op" loggers and did an audit of all ILoggerProviders to determine which ones needed to implement IDisposable.
This commit is contained in:
Коммит
abec8947c6
|
@ -8,6 +8,7 @@ using System.Diagnostics;
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.AspNetCore.Razor.PooledObjects;
|
||||
using static System.StringExtensions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration;
|
||||
|
||||
|
@ -100,10 +101,7 @@ public sealed partial class CodeWriter : IDisposable
|
|||
get => _indentSize;
|
||||
set
|
||||
{
|
||||
if (value < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
}
|
||||
ArgHelper.ThrowIfNegative(value);
|
||||
|
||||
if (_indentSize != value)
|
||||
{
|
||||
|
@ -126,10 +124,7 @@ public sealed partial class CodeWriter : IDisposable
|
|||
[MemberNotNull(nameof(_newLine))]
|
||||
private void SetNewLine(string value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
ArgHelper.ThrowIfNull(value);
|
||||
|
||||
if (value != "\r\n" && value != "\n")
|
||||
{
|
||||
|
@ -219,10 +214,7 @@ public sealed partial class CodeWriter : IDisposable
|
|||
|
||||
public CodeWriter Write(string value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
ArgHelper.ThrowIfNull(value);
|
||||
|
||||
return WriteCore(value.AsMemory());
|
||||
}
|
||||
|
@ -232,25 +224,10 @@ public sealed partial class CodeWriter : IDisposable
|
|||
|
||||
public CodeWriter Write(string value, int startIndex, int count)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
|
||||
if (startIndex < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex));
|
||||
}
|
||||
|
||||
if (count < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
}
|
||||
|
||||
if (startIndex > value.Length - count)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(startIndex));
|
||||
}
|
||||
ArgHelper.ThrowIfNull(value);
|
||||
ArgHelper.ThrowIfNegative(startIndex);
|
||||
ArgHelper.ThrowIfNegative(count);
|
||||
ArgHelper.ThrowIfGreaterThan(startIndex, value.Length - count);
|
||||
|
||||
return WriteCore(value.AsMemory(startIndex, count));
|
||||
}
|
||||
|
@ -259,7 +236,7 @@ public sealed partial class CodeWriter : IDisposable
|
|||
=> this;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private unsafe CodeWriter WriteCore(ReadOnlyMemory<char> value, bool allowIndent = true)
|
||||
private CodeWriter WriteCore(ReadOnlyMemory<char> value, bool allowIndent = true)
|
||||
{
|
||||
if (value.IsEmpty)
|
||||
{
|
||||
|
@ -323,10 +300,7 @@ public sealed partial class CodeWriter : IDisposable
|
|||
|
||||
public CodeWriter WriteLine(string value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
}
|
||||
ArgHelper.ThrowIfNull(value);
|
||||
|
||||
return WriteCore(value.AsMemory()).WriteLine();
|
||||
}
|
||||
|
@ -336,50 +310,29 @@ public sealed partial class CodeWriter : IDisposable
|
|||
|
||||
public string GenerateCode()
|
||||
{
|
||||
unsafe
|
||||
// Eventually, we need to remove this and not return a giant string, which can
|
||||
// easily be allocated on the LOH. The work to remove this is tracked by
|
||||
// https://github.com/dotnet/razor/issues/8076.
|
||||
return CreateString(Length, _pages, static (span, pages) =>
|
||||
{
|
||||
// This might look a bit scary, but it's pretty simple. We allocate our string
|
||||
// with the correct length up front and then use simple pointer math to copy
|
||||
// the pages of ReadOnlyMemory<char> directly into it.
|
||||
|
||||
// Eventually, we need to remove this and not return a giant string, which can
|
||||
// easily be allocated on the LOH. The work to remove this is tracked by
|
||||
// https://github.com/dotnet/razor/issues/8076.
|
||||
|
||||
var length = Length;
|
||||
var result = new string('\0', length);
|
||||
|
||||
fixed (char* stringPtr = result)
|
||||
foreach (var page in pages)
|
||||
{
|
||||
var destination = stringPtr;
|
||||
|
||||
// destinationSize and sourceSize track the number of bytes (not chars).
|
||||
var destinationSize = length * sizeof(char);
|
||||
|
||||
foreach (var page in _pages)
|
||||
foreach (var chars in page)
|
||||
{
|
||||
foreach (var chars in page)
|
||||
if (chars.IsEmpty)
|
||||
{
|
||||
var source = chars.Span;
|
||||
var sourceSize = source.Length * sizeof(char);
|
||||
|
||||
fixed (char* srcPtr = source)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destination, destinationSize, sourceSize);
|
||||
}
|
||||
|
||||
destination += source.Length;
|
||||
destinationSize -= sourceSize;
|
||||
|
||||
Debug.Assert(destinationSize >= 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Assert(destinationSize == 0, "We didn't exhaust our destination pointer!");
|
||||
chars.Span.CopyTo(span);
|
||||
span = span[chars.Length..];
|
||||
|
||||
Debug.Assert(span.Length >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
Debug.Assert(span.Length == 0, "We didn't fill the whole span!");
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Razor is a markup syntax for adding server-side logic to web pages. This package contains the Razor compiler.</Description>
|
||||
<TargetFrameworks>$(DefaultNetCoreTargetFramework);netstandard2.0</TargetFrameworks>
|
||||
<ExcludeFromSourceBuild>false</ExcludeFromSourceBuild>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
|
||||
<!--
|
||||
RS2008: Enable analyzer release tracking
|
||||
|
|
|
@ -85,7 +85,7 @@ public class RazorDiagnosticsBenchmark : RazorLanguageServerBenchmarkBase
|
|||
RazorRequestContext = new RazorRequestContext(documentContext.Object, null!, "lsp/method", uri: null);
|
||||
VersionedDocumentContext = documentContext.Object;
|
||||
|
||||
var loggerFactory = BuildLoggerFactory();
|
||||
var loggerFactory = EmptyLoggerFactory.Instance;
|
||||
var languageServerFeatureOptions = BuildFeatureOptions();
|
||||
var languageServer = new ClientNotifierService(Diagnostics!);
|
||||
var documentMappingService = BuildRazorDocumentMappingService();
|
||||
|
@ -147,11 +147,6 @@ public class RazorDiagnosticsBenchmark : RazorLanguageServerBenchmarkBase
|
|||
return razorDocumentMappingService.Object;
|
||||
}
|
||||
|
||||
private ILoggerFactory BuildLoggerFactory() => Mock.Of<ILoggerFactory>(
|
||||
r => r.GetOrCreateLogger(
|
||||
It.IsAny<string>()) == new NoopLogger(),
|
||||
MockBehavior.Strict);
|
||||
|
||||
private string GetFileContents()
|
||||
=> """
|
||||
<div></div>
|
||||
|
|
|
@ -28,8 +28,8 @@ public class RazorLanguageServerBenchmarkBase : ProjectSnapshotManagerBenchmarkB
|
|||
public RazorLanguageServerBenchmarkBase()
|
||||
{
|
||||
var (_, serverStream) = FullDuplexStream.CreatePair();
|
||||
Logger = new NoopLogger();
|
||||
var razorLoggerFactory = new NoopLoggerFactory();
|
||||
var razorLoggerFactory = EmptyLoggerFactory.Instance;
|
||||
Logger = razorLoggerFactory.GetOrCreateLogger(GetType());
|
||||
RazorLanguageServerHost = RazorLanguageServerHost.Create(
|
||||
serverStream,
|
||||
serverStream,
|
||||
|
@ -55,7 +55,7 @@ public class RazorLanguageServerBenchmarkBase : ProjectSnapshotManagerBenchmarkB
|
|||
|
||||
private protected RazorLanguageServerHost RazorLanguageServerHost { get; }
|
||||
|
||||
private protected NoopLogger Logger { get; }
|
||||
private protected ILogger Logger { get; }
|
||||
|
||||
internal async Task<IDocumentSnapshot> GetDocumentSnapshotAsync(string projectFilePath, string filePath, string targetPath)
|
||||
{
|
||||
|
@ -106,54 +106,4 @@ public class RazorLanguageServerBenchmarkBase : ProjectSnapshotManagerBenchmarkB
|
|||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
internal class NoopLoggerFactory() : AbstractLoggerFactory([new NoopLoggerProvider()]);
|
||||
|
||||
internal class NoopLoggerProvider : ILoggerProvider
|
||||
{
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
return new NoopLogger();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal class NoopLogger : ILogger, ILspLogger
|
||||
{
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Log(LogLevel logLevel, string message, Exception exception)
|
||||
{
|
||||
}
|
||||
|
||||
public void LogEndContext(string message, params object[] @params)
|
||||
{
|
||||
}
|
||||
|
||||
public void LogError(string message, params object[] @params)
|
||||
{
|
||||
}
|
||||
|
||||
public void LogException(Exception exception, string message = null, params object[] @params)
|
||||
{
|
||||
}
|
||||
|
||||
public void LogInformation(string message, params object[] @params)
|
||||
{
|
||||
}
|
||||
|
||||
public void LogStartContext(string message, params object[] @params)
|
||||
{
|
||||
}
|
||||
|
||||
public void LogWarning(string message, params object[] @params)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Logging;
|
||||
|
||||
internal sealed partial class EmptyLoggingFactory
|
||||
{
|
||||
private sealed class EmptyLogger : ILogger
|
||||
{
|
||||
public static readonly EmptyLogger Instance = new();
|
||||
|
||||
private EmptyLogger()
|
||||
{
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => false;
|
||||
|
||||
public void Log(LogLevel logLevel, string message, Exception? exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Logging;
|
||||
|
||||
internal sealed partial class EmptyLoggingFactory : ILoggerFactory
|
||||
{
|
||||
public static readonly EmptyLoggingFactory Instance = new();
|
||||
|
||||
private EmptyLoggingFactory()
|
||||
{
|
||||
}
|
||||
|
||||
public void AddLoggerProvider(ILoggerProvider provider)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public ILogger GetOrCreateLogger(string categoryName)
|
||||
=> EmptyLogger.Instance;
|
||||
}
|
|
@ -5,9 +5,9 @@ using System;
|
|||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Logging;
|
||||
using Microsoft.AspNetCore.Razor.PooledObjects;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
|
@ -62,6 +62,6 @@ public abstract partial class ProjectSnapshotManagerBenchmarkBase
|
|||
{
|
||||
return new ProjectSnapshotManager(
|
||||
projectEngineFactoryProvider: StaticProjectEngineFactoryProvider.Instance,
|
||||
loggerFactory: EmptyLoggingFactory.Instance);
|
||||
loggerFactory: EmptyLoggerFactory.Instance);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ using Microsoft.CodeAnalysis.Razor.Protocol;
|
|||
using Microsoft.CodeAnalysis.Razor.Workspaces;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.DocumentMapping;
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ using Microsoft.CodeAnalysis.Razor.Protocol;
|
|||
using Microsoft.CodeAnalysis.Razor.Workspaces;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.DocumentMapping;
|
||||
|
||||
|
|
|
@ -2,13 +2,14 @@
|
|||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.Workspaces;
|
||||
|
||||
internal static class LinePositionSpanExtensions
|
||||
{
|
||||
public static Range ToRange(this LinePositionSpan linePositionSpan)
|
||||
=> new Range
|
||||
=> new()
|
||||
{
|
||||
Start = linePositionSpan.Start.ToPosition(),
|
||||
End = linePositionSpan.End.ToPosition()
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.Workspaces;
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ using Microsoft.VisualStudio.LanguageServer.Protocol;
|
|||
namespace Microsoft.CodeAnalysis.Razor.Workspaces;
|
||||
|
||||
using Microsoft.AspNetCore.Razor.Language.Syntax;
|
||||
using Range = VisualStudio.LanguageServer.Protocol.Range;
|
||||
|
||||
internal static class RazorSyntaxNodeExtensions
|
||||
{
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.Workspaces;
|
||||
|
||||
|
|
|
@ -2,9 +2,8 @@
|
|||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Remote.Razor.Logging;
|
||||
namespace Microsoft.CodeAnalysis.Razor.Logging;
|
||||
|
||||
internal sealed class EmptyLoggerFactory : ILoggerFactory
|
||||
{
|
|
@ -0,0 +1,163 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Razor;
|
||||
using static System.StringExtensions;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.Logging;
|
||||
|
||||
internal static partial class LogMessageFormatter
|
||||
{
|
||||
private readonly struct FormattedMessageState
|
||||
{
|
||||
// The leading whitespace matches the time space length "[hh:mm:ss.fffffff] "
|
||||
private static readonly ReadOnlyMemory<char> s_leadingWhiteSpace = " ".AsMemory();
|
||||
private static readonly ReadOnlyMemory<char> s_newLine = Environment.NewLine.AsMemory();
|
||||
|
||||
private readonly ReadOnlyMemory<char> _message;
|
||||
private readonly ReadOnlyMemory<Range> _messageLineRanges;
|
||||
private readonly ReadOnlyMemory<char> _exceptionText;
|
||||
private readonly ReadOnlyMemory<Range> _exceptionLineRanges;
|
||||
private readonly ReadOnlyMemory<char> _categoryNamePart;
|
||||
private readonly ReadOnlyMemory<char> _timeStampPart;
|
||||
private readonly ReadOnlyMemory<char> _newLine;
|
||||
private readonly ReadOnlyMemory<char> _leadingWhiteSpace;
|
||||
|
||||
public ReadOnlySpan<char> MessageText => _message.Span;
|
||||
public ReadOnlySpan<Range> MessageLineRanges => _messageLineRanges.Span;
|
||||
public ReadOnlySpan<char> ExceptionText => _exceptionText.Span;
|
||||
public ReadOnlySpan<Range> ExceptionLineRanges => _exceptionLineRanges.Span;
|
||||
public ReadOnlySpan<char> CategoryNamePart => _categoryNamePart.Span;
|
||||
public ReadOnlySpan<char> TimeStampPart => _timeStampPart.Span;
|
||||
public ReadOnlySpan<char> NewLine => _newLine.Span;
|
||||
public ReadOnlySpan<char> LeadingWhiteSpace => _leadingWhiteSpace.Span;
|
||||
|
||||
public int Length { get; }
|
||||
|
||||
private FormattedMessageState(
|
||||
ReadOnlyMemory<char> messageText, ReadOnlyMemory<Range> messageLineRanges,
|
||||
ReadOnlyMemory<char> exceptionText, ReadOnlyMemory<Range> exceptionLineRanges,
|
||||
ReadOnlyMemory<char> categoryNamePart,
|
||||
ReadOnlyMemory<char> timeStampPart,
|
||||
ReadOnlyMemory<char> newLine,
|
||||
ReadOnlyMemory<char> leadingWhiteSpace)
|
||||
{
|
||||
_message = messageText;
|
||||
_messageLineRanges = messageLineRanges;
|
||||
_exceptionText = exceptionText;
|
||||
_exceptionLineRanges = exceptionLineRanges;
|
||||
_categoryNamePart = categoryNamePart;
|
||||
_timeStampPart = timeStampPart;
|
||||
_newLine = newLine;
|
||||
_leadingWhiteSpace = leadingWhiteSpace;
|
||||
|
||||
Length = ComputeLength();
|
||||
}
|
||||
|
||||
private int ComputeLength()
|
||||
{
|
||||
// Calculate the length of the final formatted string.
|
||||
var isFirst = true;
|
||||
var length = 0;
|
||||
|
||||
length += CategoryNamePart.Length;
|
||||
|
||||
foreach (var range in MessageLineRanges)
|
||||
{
|
||||
if (isFirst)
|
||||
{
|
||||
length += TimeStampPart.Length;
|
||||
isFirst = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
length += NewLine.Length;
|
||||
length += LeadingWhiteSpace.Length;
|
||||
}
|
||||
|
||||
var (_, lineLength) = range.GetOffsetAndLength(MessageText.Length);
|
||||
length += lineLength;
|
||||
}
|
||||
|
||||
foreach (var range in ExceptionLineRanges)
|
||||
{
|
||||
length += TimeStampPart.Length;
|
||||
|
||||
var (_, lineLength) = range.GetOffsetAndLength(ExceptionText.Length);
|
||||
length += lineLength;
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
public static FormattedMessageState Create(
|
||||
string message,
|
||||
string categoryName,
|
||||
Exception? exception,
|
||||
bool includeTimeStamp,
|
||||
ref MemoryBuilder<Range> messageLineRangeBuilder,
|
||||
ref MemoryBuilder<Range> exceptionLineRangeBuilder)
|
||||
{
|
||||
var messageText = message.AsMemory();
|
||||
var newLine = s_newLine;
|
||||
|
||||
var categoryNamePart = ('[' + categoryName + "] ").AsMemory();
|
||||
|
||||
ReadOnlyMemory<char> timeStampPart, leadingWhiteSpace;
|
||||
|
||||
if (includeTimeStamp)
|
||||
{
|
||||
timeStampPart = ('[' + DateTime.Now.TimeOfDay.ToString("hh\\:mm\\:ss\\.fffffff") + "] ").AsMemory();
|
||||
leadingWhiteSpace = s_leadingWhiteSpace;
|
||||
}
|
||||
else
|
||||
{
|
||||
timeStampPart = default;
|
||||
leadingWhiteSpace = default;
|
||||
}
|
||||
|
||||
// Collect the range of each line in the message text.
|
||||
CollectLineRanges(messageText.Span, newLine.Span, ref messageLineRangeBuilder);
|
||||
|
||||
var exceptionText = exception is not null
|
||||
? exception.ToString().AsMemory()
|
||||
: default;
|
||||
|
||||
// If specified, Collect the range of each line in the exception text.
|
||||
if (exceptionText.Length > 0)
|
||||
{
|
||||
CollectLineRanges(exceptionText.Span, newLine.Span, ref exceptionLineRangeBuilder);
|
||||
}
|
||||
|
||||
return new(
|
||||
messageText, messageLineRangeBuilder.AsMemory(),
|
||||
exceptionText, exceptionLineRangeBuilder.AsMemory(),
|
||||
categoryNamePart, timeStampPart, newLine, leadingWhiteSpace);
|
||||
}
|
||||
|
||||
private static void CollectLineRanges(ReadOnlySpan<char> source, ReadOnlySpan<char> newLine, ref MemoryBuilder<Range> builder)
|
||||
{
|
||||
var startIndex = 0;
|
||||
|
||||
while (startIndex < source.Length)
|
||||
{
|
||||
// Find the index of the next new line.
|
||||
var endIndex = source[startIndex..].IndexOf(newLine);
|
||||
|
||||
// If endIndex == -1, there isn't another new line.
|
||||
// So, add the remaining range and break.
|
||||
if (endIndex == -1)
|
||||
{
|
||||
builder.Append(startIndex..);
|
||||
break;
|
||||
}
|
||||
|
||||
var realEndIndex = startIndex + endIndex;
|
||||
|
||||
builder.Append(startIndex..realEndIndex);
|
||||
startIndex = realEndIndex + newLine.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.AspNetCore.Razor;
|
||||
using static System.StringExtensions;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.Logging;
|
||||
|
||||
internal static partial class LogMessageFormatter
|
||||
{
|
||||
public static string FormatMessage(string message, string categoryName, Exception? exception, bool includeTimeStamp = true)
|
||||
{
|
||||
// Note
|
||||
MemoryBuilder<Range> messageLineRangeBuilder = new(initialCapacity: 4);
|
||||
MemoryBuilder<Range> exceptionLineRangeBuilder = exception is not null ? new(initialCapacity: 64) : default;
|
||||
try
|
||||
{
|
||||
var state = FormattedMessageState.Create(
|
||||
message, categoryName, exception, includeTimeStamp,
|
||||
ref messageLineRangeBuilder, ref exceptionLineRangeBuilder);
|
||||
|
||||
// Create the final string.
|
||||
return CreateString(state.Length, state, static (span, state) =>
|
||||
{
|
||||
Write(state.CategoryNamePart, ref span);
|
||||
|
||||
var isFirst = true;
|
||||
|
||||
foreach (var range in state.MessageLineRanges)
|
||||
{
|
||||
if (isFirst)
|
||||
{
|
||||
// Write the time stamp if this is the first line.
|
||||
Write(state.TimeStampPart, ref span);
|
||||
isFirst = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, write a new line and the leading whitespace.
|
||||
Write(state.NewLine, ref span);
|
||||
Write(state.LeadingWhiteSpace, ref span);
|
||||
}
|
||||
|
||||
Write(state.MessageText[range], ref span);
|
||||
}
|
||||
|
||||
foreach (var range in state.ExceptionLineRanges)
|
||||
{
|
||||
Write(state.LeadingWhiteSpace, ref span);
|
||||
Write(state.ExceptionText[range], ref span);
|
||||
}
|
||||
|
||||
Debug.Assert(span.Length == 0, "We didn't fill the whole span!");
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static void Write(ReadOnlySpan<char> source, ref Span<char> destination)
|
||||
{
|
||||
if (source.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
source.CopyTo(destination);
|
||||
destination = destination[source.Length..];
|
||||
|
||||
Debug.Assert(destination.Length >= 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
messageLineRangeBuilder.Dispose();
|
||||
exceptionLineRangeBuilder.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,10 +14,6 @@
|
|||
<NoWarn>$(NoWarn);IDE0073</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Alias="Range" Include="Microsoft.VisualStudio.LanguageServer.Protocol.Range" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.ExternalAccess.Razor" />
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.Protocol.Debugging;
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
|
||||
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.Protocol.DocumentMapping;
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.Protocol.DocumentMapping;
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.Razor.Workspaces.Protocol.SemanticTokens;
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ using System.Diagnostics;
|
|||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||
using Microsoft.CodeAnalysis.Razor.Remote;
|
||||
using Microsoft.CodeAnalysis.Remote.Razor.Logging;
|
||||
using Microsoft.ServiceHub.Framework;
|
||||
|
|
|
@ -17,8 +17,4 @@ internal sealed class RazorLogHubLoggerProvider(RazorLogHubTraceProvider tracePr
|
|||
{
|
||||
return new RazorLogHubLogger(categoryName, _traceProvider);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,11 +18,8 @@ internal partial class MemoryLoggerProvider
|
|||
|
||||
public void Log(LogLevel logLevel, string message, Exception? exception)
|
||||
{
|
||||
_buffer.Append($"{DateTime.Now:h:mm:ss.fff} [{_categoryName}] {message}");
|
||||
if (exception is not null)
|
||||
{
|
||||
_buffer.Append(exception.ToString());
|
||||
}
|
||||
var formattedMessage = LogMessageFormatter.FormatMessage(message, _categoryName, exception);
|
||||
_buffer.Append(formattedMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,4 @@ internal partial class MemoryLoggerProvider : ILoggerProvider
|
|||
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
=> new Logger(_buffer, categoryName);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ internal class OutputWindowLoggerProvider(
|
|||
// or used anything that does logging, so make sure everything of ours is imported lazily
|
||||
Lazy<IClientSettingsManager> clientSettingsManager,
|
||||
JoinableTaskContext joinableTaskContext)
|
||||
: ILoggerProvider
|
||||
: ILoggerProvider, IDisposable
|
||||
{
|
||||
private readonly Lazy<IClientSettingsManager> _clientSettingsManager = clientSettingsManager;
|
||||
private readonly OutputPane _outputPane = new OutputPane(joinableTaskContext);
|
||||
|
@ -58,11 +58,8 @@ internal class OutputWindowLoggerProvider(
|
|||
{
|
||||
if (IsEnabled(logLevel))
|
||||
{
|
||||
_outputPane.WriteLine($"{DateTime.Now:h:mm:ss.fff} [{_categoryName}] {message}");
|
||||
if (exception is not null)
|
||||
{
|
||||
_outputPane.WriteLine(exception.ToString());
|
||||
}
|
||||
var formattedMessage = LogMessageFormatter.FormatMessage(message, _categoryName, exception);
|
||||
_outputPane.WriteLine(formattedMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,4 @@ internal class LoggerProvider(LogLevel logLevel, IClientConnection clientConnect
|
|||
{
|
||||
return new LspLogger(categoryName, _logLevel, _clientConnection);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,15 +42,12 @@ internal class LspLogger(string categoryName, LogLevel logLevel, IClientConnecti
|
|||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
if (exception is not null)
|
||||
{
|
||||
message += Environment.NewLine + exception.ToString();
|
||||
}
|
||||
var formattedMessage = LogMessageFormatter.FormatMessage(message, _categoryName, exception, includeTimeStamp: false);
|
||||
|
||||
var @params = new LogMessageParams
|
||||
{
|
||||
MessageType = messageType,
|
||||
Message = $"[{_categoryName}] {message}",
|
||||
Message = formattedMessage,
|
||||
};
|
||||
|
||||
_clientConnection.SendNotificationAsync(Methods.WindowLogMessageName, @params, CancellationToken.None).Forget();
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Test.Common.Logging;
|
||||
|
||||
internal sealed class NoOpDisposable : IDisposable
|
||||
{
|
||||
public static IDisposable Instance { get; } = new NoOpDisposable();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
|
@ -3,81 +3,31 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Test.Common.Logging;
|
||||
|
||||
internal partial class TestOutputLogger : ILogger
|
||||
internal partial class TestOutputLogger(TestOutputLoggerProvider provider, string categoryName, LogLevel logLevel = LogLevel.Trace) : ILogger
|
||||
{
|
||||
[ThreadStatic]
|
||||
private static StringBuilder? g_builder;
|
||||
|
||||
private readonly TestOutputLoggerProvider _provider;
|
||||
|
||||
public string? CategoryName { get; }
|
||||
public LogLevel LogLevel { get; }
|
||||
|
||||
public TestOutputLogger(
|
||||
TestOutputLoggerProvider provider,
|
||||
string? categoryName = null,
|
||||
LogLevel logLevel = LogLevel.Trace)
|
||||
{
|
||||
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
|
||||
CategoryName = categoryName;
|
||||
LogLevel = logLevel;
|
||||
}
|
||||
private readonly TestOutputLoggerProvider _provider = provider;
|
||||
private readonly string _categoryName = categoryName;
|
||||
private readonly LogLevel _logLevel = logLevel;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
=> logLevel >= LogLevel;
|
||||
=> logLevel >= _logLevel;
|
||||
|
||||
public void Log(LogLevel logLevel, string message, Exception? exception)
|
||||
{
|
||||
if (!IsEnabled(logLevel) || _provider.TestOutputHelper is null)
|
||||
if (!IsEnabled(logLevel))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var builder = GetEmptyBuilder();
|
||||
|
||||
var time = DateTime.Now.TimeOfDay;
|
||||
var leadingTimeStamp = $"[{time:hh\\:mm\\:ss\\.fffffff}] ";
|
||||
var leadingSpaces = new string(' ', leadingTimeStamp.Length);
|
||||
var lines = message.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
|
||||
|
||||
var isFirstLine = true;
|
||||
|
||||
builder.Append(leadingTimeStamp);
|
||||
|
||||
if (CategoryName is { } categoryName)
|
||||
{
|
||||
builder.Append($"[{categoryName}] ");
|
||||
isFirstLine = lines.Length == 1;
|
||||
}
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (!isFirstLine)
|
||||
{
|
||||
builder.AppendLine();
|
||||
builder.Append(leadingSpaces);
|
||||
}
|
||||
|
||||
builder.Append(line);
|
||||
|
||||
isFirstLine = false;
|
||||
}
|
||||
|
||||
var finalMessage = builder.ToString();
|
||||
var formattedMessage = LogMessageFormatter.FormatMessage(message, _categoryName, exception);
|
||||
|
||||
try
|
||||
{
|
||||
_provider.TestOutputHelper.WriteLine(finalMessage);
|
||||
|
||||
if (exception is not null)
|
||||
{
|
||||
_provider.TestOutputHelper.WriteLine(exception.ToString());
|
||||
}
|
||||
_provider.Output.WriteLine(formattedMessage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -96,22 +46,8 @@ internal partial class TestOutputLogger : ILogger
|
|||
innerExceptions.Add(exception);
|
||||
}
|
||||
|
||||
var aggregateException = new AggregateException($"An exception occurred while logging: {finalMessage}", innerExceptions);
|
||||
var aggregateException = new AggregateException($"An exception occurred while logging: {formattedMessage}", innerExceptions);
|
||||
throw aggregateException.Flatten();
|
||||
}
|
||||
}
|
||||
|
||||
private static StringBuilder GetEmptyBuilder()
|
||||
{
|
||||
if (g_builder is null)
|
||||
{
|
||||
g_builder = new();
|
||||
}
|
||||
else
|
||||
{
|
||||
g_builder.Clear();
|
||||
}
|
||||
|
||||
return g_builder;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,20 +8,11 @@ namespace Microsoft.AspNetCore.Razor.Test.Common.Logging;
|
|||
|
||||
internal class TestOutputLoggerProvider(ITestOutputHelper output, LogLevel logLevel = LogLevel.Trace) : ILoggerProvider
|
||||
{
|
||||
private ITestOutputHelper? _output = output;
|
||||
private readonly ITestOutputHelper _output = output;
|
||||
private readonly LogLevel _logLevel = logLevel;
|
||||
|
||||
public ITestOutputHelper? TestOutputHelper => _output;
|
||||
public ITestOutputHelper Output => _output;
|
||||
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
=> new TestOutputLogger(this, categoryName, _logLevel);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
internal void SetTestOutputHelper(ITestOutputHelper? testOutputHelper)
|
||||
{
|
||||
_output = testOutputHelper;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,9 +20,9 @@ using Xunit.Abstractions;
|
|||
namespace Microsoft.VisualStudio.Razor.IntegrationTests;
|
||||
|
||||
[LogIntegrationTest]
|
||||
public abstract class AbstractRazorEditorTest(ITestOutputHelper testOutputHelper) : AbstractIntegrationTest
|
||||
public abstract class AbstractRazorEditorTest(ITestOutputHelper testOutput) : AbstractIntegrationTest
|
||||
{
|
||||
private readonly ITestOutputHelper _testOutputHelper = testOutputHelper;
|
||||
private readonly ITestOutputHelper _testOutput = testOutput;
|
||||
private ILogger? _testLogger;
|
||||
private string? _projectFilePath;
|
||||
|
||||
|
@ -38,7 +38,7 @@ public abstract class AbstractRazorEditorTest(ITestOutputHelper testOutputHelper
|
|||
{
|
||||
await base.InitializeAsync();
|
||||
|
||||
_testLogger = await TestServices.Output.SetupIntegrationTestLoggerAsync(_testOutputHelper, ControlledHangMitigatingCancellationToken);
|
||||
_testLogger = await TestServices.Output.SetupIntegrationTestLoggerAsync(_testOutput, ControlledHangMitigatingCancellationToken);
|
||||
|
||||
_testLogger.LogInformation($"#### Razor integration test initialize.");
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.Test.Common.Logging;
|
||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||
using Microsoft.VisualStudio.Razor.IntegrationTests.Extensions;
|
||||
using Microsoft.VisualStudio.Razor.IntegrationTests.Logging;
|
||||
using Microsoft.VisualStudio.Razor.Settings;
|
||||
using Microsoft.VisualStudio.Shell;
|
||||
using Microsoft.VisualStudio.Shell.Interop;
|
||||
|
@ -20,7 +20,7 @@ internal partial class OutputInProcess
|
|||
{
|
||||
private const string RazorPaneName = "Razor Logger Output";
|
||||
|
||||
private TestOutputLoggerProvider? _testLoggerProvider;
|
||||
private IntegrationTestOutputLoggerProvider? _testLoggerProvider;
|
||||
|
||||
public async Task<ILogger> SetupIntegrationTestLoggerAsync(ITestOutputHelper testOutputHelper, CancellationToken cancellationToken)
|
||||
{
|
||||
|
@ -28,25 +28,25 @@ internal partial class OutputInProcess
|
|||
var clientSettingsManager = await TestServices.Shell.GetComponentModelServiceAsync<IClientSettingsManager>(cancellationToken);
|
||||
clientSettingsManager.Update(clientSettingsManager.GetClientSettings().AdvancedSettings with { LogLevel = LogLevel.Trace });
|
||||
|
||||
var logger = await TestServices.Shell.GetComponentModelServiceAsync<ILoggerFactory>(cancellationToken);
|
||||
var loggerFactory = await TestServices.Shell.GetComponentModelServiceAsync<ILoggerFactory>(cancellationToken);
|
||||
|
||||
// We can't remove logging providers, so we just keep track of ours so we can make sure it points to the right test output helper
|
||||
if (_testLoggerProvider is null)
|
||||
{
|
||||
_testLoggerProvider = new TestOutputLoggerProvider(testOutputHelper);
|
||||
logger.AddLoggerProvider(_testLoggerProvider);
|
||||
_testLoggerProvider = new IntegrationTestOutputLoggerProvider(testOutputHelper);
|
||||
loggerFactory.AddLoggerProvider(_testLoggerProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
_testLoggerProvider.SetTestOutputHelper(testOutputHelper);
|
||||
_testLoggerProvider.SetOutput(testOutputHelper);
|
||||
}
|
||||
|
||||
return logger.GetOrCreateLogger(GetType().Name);
|
||||
return loggerFactory.GetOrCreateLogger(GetType().Name);
|
||||
}
|
||||
|
||||
public void ClearIntegrationTestLogger()
|
||||
{
|
||||
_testLoggerProvider?.SetTestOutputHelper(null);
|
||||
_testLoggerProvider?.SetOutput(null);
|
||||
}
|
||||
|
||||
public async Task<bool> HasErrorsAsync(CancellationToken cancellationToken)
|
||||
|
@ -110,18 +110,4 @@ internal partial class OutputInProcess
|
|||
return textView;
|
||||
}
|
||||
}
|
||||
|
||||
private class NullLogger : ILogger
|
||||
{
|
||||
public static ILogger Instance { get; } = new NullLogger();
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Log(LogLevel logLevel, string message, Exception? exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||
|
||||
namespace Microsoft.VisualStudio.Razor.IntegrationTests.Logging;
|
||||
|
||||
internal partial class IntegrationTestOutputLogger(IntegrationTestOutputLoggerProvider provider, string categoryName, LogLevel logLevel = LogLevel.Trace) : ILogger
|
||||
{
|
||||
private readonly IntegrationTestOutputLoggerProvider _provider = provider;
|
||||
private readonly string _categoryName = categoryName;
|
||||
private readonly LogLevel _logLevel = logLevel;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
=> logLevel >= _logLevel;
|
||||
|
||||
public void Log(LogLevel logLevel, string message, Exception? exception)
|
||||
{
|
||||
if (!IsEnabled(logLevel) || !_provider.HasOutput)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var formattedMessage = LogMessageFormatter.FormatMessage(message, _categoryName, exception);
|
||||
|
||||
try
|
||||
{
|
||||
_provider.WriteLine(formattedMessage);
|
||||
}
|
||||
catch (InvalidOperationException ex) when (ex.Message == "There is no currently active test.")
|
||||
{
|
||||
// Ignore, something is logging a message outside of a test. Other loggers will capture it.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If an exception is thrown while writing a message, throw an AggregateException that includes
|
||||
// the message that was being logged, along with the exception that was thrown and any exception
|
||||
// that was being logged. This might provide clues to the cause.
|
||||
|
||||
var innerExceptions = new List<Exception>
|
||||
{
|
||||
ex
|
||||
};
|
||||
|
||||
// Were we logging an exception? If so, add that too.
|
||||
if (exception is not null)
|
||||
{
|
||||
innerExceptions.Add(exception);
|
||||
}
|
||||
|
||||
var aggregateException = new AggregateException($"An exception occurred while logging: {formattedMessage}", innerExceptions);
|
||||
throw aggregateException.Flatten();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.CodeAnalysis.Razor.Logging;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.VisualStudio.Razor.IntegrationTests.Logging;
|
||||
|
||||
internal class IntegrationTestOutputLoggerProvider(ITestOutputHelper output, LogLevel logLevel = LogLevel.Trace) : ILoggerProvider
|
||||
{
|
||||
private ITestOutputHelper? _output = output;
|
||||
private readonly LogLevel _logLevel = logLevel;
|
||||
|
||||
public bool HasOutput => _output is not null;
|
||||
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
=> new IntegrationTestOutputLogger(this, categoryName, _logLevel);
|
||||
|
||||
public void SetOutput(ITestOutputHelper? output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
public void WriteLine(string message)
|
||||
{
|
||||
_output?.WriteLine(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Utilities.Shared.Test;
|
||||
|
||||
public class MemoryBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
public void StartWithDefault()
|
||||
{
|
||||
using MemoryBuilder<int> builder = default;
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
builder.Append(i);
|
||||
}
|
||||
|
||||
var result = builder.AsMemory();
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
Assert.Equal(i, result.Span[i]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartWithNew()
|
||||
{
|
||||
using MemoryBuilder<int> builder = new();
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
builder.Append(i);
|
||||
}
|
||||
|
||||
var result = builder.AsMemory();
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
Assert.Equal(i, result.Span[i]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartWithInitialCapacity()
|
||||
{
|
||||
using MemoryBuilder<int> builder = new(1024);
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
builder.Append(i);
|
||||
}
|
||||
|
||||
var result = builder.AsMemory();
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
Assert.Equal(i, result.Span[i]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StartWithInitialArray()
|
||||
{
|
||||
using MemoryBuilder<int> builder = new(1024);
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
builder.Append(i);
|
||||
}
|
||||
|
||||
var result = builder.AsMemory();
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
Assert.Equal(i, result.Span[i]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppendChunks()
|
||||
{
|
||||
using MemoryBuilder<int> builder = default;
|
||||
|
||||
ReadOnlySpan<int> chunk = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
builder.Append(chunk);
|
||||
}
|
||||
|
||||
var result = builder.AsMemory();
|
||||
|
||||
for (var i = 0; i < 1000; i++)
|
||||
{
|
||||
for (var j = 0; j < chunk.Length; j++)
|
||||
{
|
||||
Assert.Equal(chunk[j], result.Span[(i * 8) + j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ internal static class ArgHelper
|
|||
#else
|
||||
if (argument is null)
|
||||
{
|
||||
ThrowArgumentNullException(paramName);
|
||||
ThrowHelper.ThrowArgumentNullException(paramName);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -31,36 +31,20 @@ internal static class ArgHelper
|
|||
#else
|
||||
if (argument is null)
|
||||
{
|
||||
ThrowArgumentNullException(paramName);
|
||||
ThrowHelper.ThrowArgumentNullException(paramName);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !NET8_0_OR_GREATER
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowArgumentNullException(string? paramName)
|
||||
{
|
||||
throw new ArgumentNullException(paramName);
|
||||
}
|
||||
#endif
|
||||
|
||||
public static void ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
ArgumentException.ThrowIfNullOrEmpty(argument, paramName);
|
||||
#else
|
||||
if (argument.IsNullOrEmpty())
|
||||
{
|
||||
ThrowException(argument, paramName);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowException(string? argument, string? paramName)
|
||||
{
|
||||
ThrowIfNull(argument, paramName);
|
||||
throw new ArgumentException(SR.The_value_cannot_be_an_empty_string, paramName);
|
||||
ThrowHelper.ThrowArgumentException(paramName, SR.The_value_cannot_be_an_empty_string);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -72,16 +56,9 @@ internal static class ArgHelper
|
|||
#else
|
||||
|
||||
if (argument.IsNullOrWhiteSpace())
|
||||
{
|
||||
ThrowException(argument, paramName);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowException(string? argument, string? paramName)
|
||||
{
|
||||
ThrowIfNull(argument, paramName);
|
||||
throw new ArgumentException(SR.The_value_cannot_be_an_empty_string_composed_entirely_of_whitespace, paramName);
|
||||
ThrowHelper.ThrowArgumentException(paramName, SR.The_value_cannot_be_an_empty_string_composed_entirely_of_whitespace);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -93,14 +70,7 @@ internal static class ArgHelper
|
|||
#else
|
||||
if (value == 0)
|
||||
{
|
||||
ThrowException(value, paramName);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowException(int value, string? paramName)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(paramName, value, SR.Format0_1_must_be_a_non_zero_value(paramName, value));
|
||||
ThrowHelper.ThrowArgumentOutOfRangeException(paramName, value, SR.Format0_1_must_be_a_non_zero_value(paramName, value));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -112,14 +82,7 @@ internal static class ArgHelper
|
|||
#else
|
||||
if (value < 0)
|
||||
{
|
||||
ThrowException(value, paramName);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowException(int value, string? paramName)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(paramName, value, SR.Format0_1_must_be_a_non_negative_value(paramName, value));
|
||||
ThrowHelper.ThrowArgumentOutOfRangeException(paramName, value, SR.Format0_1_must_be_a_non_negative_value(paramName, value));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -131,14 +94,7 @@ internal static class ArgHelper
|
|||
#else
|
||||
if (value <= 0)
|
||||
{
|
||||
ThrowException(value, paramName);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowException(int value, string? paramName)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(paramName, value, SR.Format0_1_must_be_a_non_negative_and_non_zero_value(paramName, value));
|
||||
ThrowHelper.ThrowArgumentOutOfRangeException(paramName, value, SR.Format0_1_must_be_a_non_negative_and_non_zero_value(paramName, value));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -151,14 +107,7 @@ internal static class ArgHelper
|
|||
#else
|
||||
if (EqualityComparer<T>.Default.Equals(value, other))
|
||||
{
|
||||
ThrowException(value, other, paramName);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowException(T value, T other, string? paramName)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(paramName, value, SR.Format0_1_must_not_be_equal_to_2(paramName, (object?)value ?? "null", (object?)other ?? "null"));
|
||||
ThrowHelper.ThrowArgumentOutOfRangeException(paramName, value, SR.Format0_1_must_not_be_equal_to_2(paramName, (object?)value ?? "null", (object?)other ?? "null"));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -171,14 +120,7 @@ internal static class ArgHelper
|
|||
#else
|
||||
if (!EqualityComparer<T>.Default.Equals(value, other))
|
||||
{
|
||||
ThrowException(value, other, paramName);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowException(T value, T other, string? paramName)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(paramName, value, SR.Format0_1_must_be_equal_to_2(paramName, (object?)value ?? "null", (object?)other ?? "null"));
|
||||
ThrowHelper.ThrowArgumentOutOfRangeException(paramName, value, SR.Format0_1_must_be_equal_to_2(paramName, (object?)value ?? "null", (object?)other ?? "null"));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -191,14 +133,7 @@ internal static class ArgHelper
|
|||
#else
|
||||
if (value.CompareTo(other) > 0)
|
||||
{
|
||||
ThrowException(value, other, paramName);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowException(T value, T other, string? paramName)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(paramName, value, SR.Format0_1_must_be_less_than_or_equal_to_2(paramName, value, other));
|
||||
ThrowHelper.ThrowArgumentOutOfRangeException(paramName, value, SR.Format0_1_must_be_less_than_or_equal_to_2(paramName, value, other));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -211,14 +146,7 @@ internal static class ArgHelper
|
|||
#else
|
||||
if (value.CompareTo(other) >= 0)
|
||||
{
|
||||
ThrowException(value, other, paramName);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowException(T value, T other, string? paramName)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(paramName, value, SR.Format0_1_must_be_less_than_2(paramName, value, other));
|
||||
ThrowHelper.ThrowArgumentOutOfRangeException(paramName, value, SR.Format0_1_must_be_less_than_2(paramName, value, other));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -231,14 +159,7 @@ internal static class ArgHelper
|
|||
#else
|
||||
if (value.CompareTo(other) < 0)
|
||||
{
|
||||
ThrowException(value, other, paramName);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowException(T value, T other, string? paramName)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(paramName, value, SR.Format0_1_must_be_greater_than_or_equal_to_2(paramName, value, other));
|
||||
ThrowHelper.ThrowArgumentOutOfRangeException(paramName, value, SR.Format0_1_must_be_greater_than_or_equal_to_2(paramName, value, other));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
@ -251,14 +172,7 @@ internal static class ArgHelper
|
|||
#else
|
||||
if (value.CompareTo(other) <= 0)
|
||||
{
|
||||
ThrowException(value, other, paramName);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
static void ThrowException(T value, T other, string? paramName)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(paramName, value, SR.Format0_1_must_be_greater_than_2(paramName, value, other));
|
||||
ThrowHelper.ThrowArgumentOutOfRangeException(paramName, value, SR.Format0_1_must_be_greater_than_2(paramName, value, other));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
|
|
@ -4,10 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
#if !NET
|
||||
using ThrowHelper = Microsoft.AspNetCore.Razor.Utilities.ThrowHelper;
|
||||
#endif
|
||||
using Microsoft.AspNetCore.Razor;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor;
|
||||
|
||||
|
@ -43,7 +40,7 @@ internal static class ArrayExtensions
|
|||
{
|
||||
if (!startIndex.Equals(Index.Start))
|
||||
{
|
||||
ThrowHelper.ThrowArgumentOutOfRange(nameof(startIndex));
|
||||
ThrowHelper.ThrowArgumentOutOfRangeException(nameof(startIndex));
|
||||
}
|
||||
|
||||
return default;
|
||||
|
@ -86,7 +83,7 @@ internal static class ArrayExtensions
|
|||
{
|
||||
if (!range.Start.Equals(Index.Start) || !range.End.Equals(Index.Start))
|
||||
{
|
||||
ThrowHelper.ThrowArgumentNull(nameof(array));
|
||||
ThrowHelper.ThrowArgumentNullException(nameof(array));
|
||||
}
|
||||
|
||||
return default;
|
||||
|
@ -128,7 +125,7 @@ internal static class ArrayExtensions
|
|||
{
|
||||
if (!startIndex.Equals(Index.Start))
|
||||
{
|
||||
ThrowHelper.ThrowArgumentOutOfRange(nameof(startIndex));
|
||||
ThrowHelper.ThrowArgumentOutOfRangeException(nameof(startIndex));
|
||||
}
|
||||
|
||||
return default;
|
||||
|
@ -172,7 +169,7 @@ internal static class ArrayExtensions
|
|||
{
|
||||
if (!range.Start.Equals(Index.Start) || !range.End.Equals(Index.Start))
|
||||
{
|
||||
ThrowHelper.ThrowArgumentNull(nameof(array));
|
||||
ThrowHelper.ThrowArgumentNullException(nameof(array));
|
||||
}
|
||||
|
||||
return default;
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
// Inspired by https://github.com/dotnet/runtime/blob/9c7ee976fd771c183e98cf629e3776bba4e45ccc/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/ValueListBuilder.cs
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor;
|
||||
|
||||
/// <summary>
|
||||
/// Temporary builder that uses <see cref="ArrayPool{T}"/> to back a <see cref="Memory{T}"/>.
|
||||
/// </summary>
|
||||
internal ref struct MemoryBuilder<T>
|
||||
{
|
||||
private Memory<T> _memory;
|
||||
private T[]? _arrayFromPool;
|
||||
private int _length;
|
||||
|
||||
public MemoryBuilder(int initialCapacity = 0)
|
||||
{
|
||||
ArgHelper.ThrowIfNegative(initialCapacity);
|
||||
|
||||
if (initialCapacity > 0)
|
||||
{
|
||||
_arrayFromPool = ArrayPool<T>.Shared.Rent(initialCapacity);
|
||||
_memory = _arrayFromPool;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
var toReturn = _arrayFromPool;
|
||||
if (toReturn is not null)
|
||||
{
|
||||
_memory = default;
|
||||
_arrayFromPool = null;
|
||||
ArrayPool<T>.Shared.Return(toReturn);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly ReadOnlyMemory<T> AsMemory()
|
||||
=> _memory[.._length];
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Append(T item)
|
||||
{
|
||||
var index = _length;
|
||||
var memory = _memory;
|
||||
|
||||
if ((uint)index < (uint)memory.Length)
|
||||
{
|
||||
memory.Span[index] = item;
|
||||
_length = index + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
AppendWithResize(item);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Append(ReadOnlySpan<T> source)
|
||||
{
|
||||
var index = _length;
|
||||
var memory = _memory;
|
||||
|
||||
if (source.Length == 1 && (uint)index < (uint)memory.Length)
|
||||
{
|
||||
memory.Span[index] = source[0];
|
||||
_length = index + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
AppendWithResize(source);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void AppendWithResize(T item)
|
||||
{
|
||||
Debug.Assert(_length == _memory.Length);
|
||||
var index = _length;
|
||||
Grow(1);
|
||||
_memory.Span[index] = item;
|
||||
_length = index + 1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void AppendWithResize(ReadOnlySpan<T> source)
|
||||
{
|
||||
if ((uint)(_length + source.Length) > (uint)_memory.Length)
|
||||
{
|
||||
Grow(_memory.Length - _length + source.Length);
|
||||
}
|
||||
|
||||
source.CopyTo(_memory.Span[_length..]);
|
||||
_length += source.Length;
|
||||
}
|
||||
|
||||
private void Grow(int additionalCapacityRequired = 1)
|
||||
{
|
||||
Debug.Assert(additionalCapacityRequired > 0);
|
||||
|
||||
const int ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength
|
||||
|
||||
// Double the size of the array. If it's currently empty, default to size 4.
|
||||
var nextCapacity = Math.Max(
|
||||
_memory.Length != 0 ? _memory.Length * 2 : 4,
|
||||
_memory.Length + additionalCapacityRequired);
|
||||
|
||||
// If nextCapacity exceeds the possible length of an array, then we want to downgrade to
|
||||
// either ArrayMaxLength, if that's large enough to hold an additional item, or
|
||||
// _memory.Length + 1, if that's larger than ArrayMaxLength. Essentially, we don't want
|
||||
// to simply clamp to ArrayMaxLength if that isn't actually large enough. Instead, if
|
||||
// we've grown too large, we want to OOM when Rent is called below.
|
||||
if ((uint)nextCapacity > ArrayMaxLength)
|
||||
{
|
||||
// Note: it's not possible for _memory.Length + 1 to overflow because that would mean
|
||||
// _memory is pointing to an array with length int.MaxValue, which is larger than
|
||||
// Array.MaxLength. We would have OOM'd before getting here.
|
||||
|
||||
nextCapacity = Math.Max(_memory.Length + 1, ArrayMaxLength);
|
||||
}
|
||||
|
||||
Debug.Assert(nextCapacity > _memory.Length);
|
||||
|
||||
var newArray = ArrayPool<T>.Shared.Rent(nextCapacity);
|
||||
_memory.Span.CopyTo(newArray);
|
||||
|
||||
var toReturn = _arrayFromPool;
|
||||
_memory = newArray;
|
||||
_arrayFromPool = newArray;
|
||||
|
||||
if (toReturn != null)
|
||||
{
|
||||
ArrayPool<T>.Shared.Return(toReturn);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ using System.Collections.Immutable;
|
|||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.AspNetCore.Razor;
|
||||
using Microsoft.AspNetCore.Razor.Utilities;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.PooledObjects;
|
||||
|
@ -1306,7 +1307,7 @@ internal partial struct PooledArrayBuilder<T> : IDisposable
|
|||
/// </summary>
|
||||
[DoesNotReturn]
|
||||
private static T ThrowInvalidOperation(string message)
|
||||
=> ThrowHelper.ThrowInvalidOperation<T>(message);
|
||||
=> ThrowHelper.ThrowInvalidOperationException<T>(message);
|
||||
|
||||
[MemberNotNull(nameof(_builder))]
|
||||
private void MoveInlineItemsToBuilder()
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.AspNetCore.Razor;
|
||||
using Microsoft.AspNetCore.Razor.PooledObjects;
|
||||
using Microsoft.AspNetCore.Razor.Utilities;
|
||||
|
||||
namespace System.Collections.Generic;
|
||||
|
||||
|
@ -169,7 +169,7 @@ internal static class ReadOnlyListExtensions
|
|||
/// The list is empty.
|
||||
/// </exception>
|
||||
public static T First<T>(this IReadOnlyList<T> list)
|
||||
=> list.Count > 0 ? list[0] : ThrowHelper.ThrowInvalidOperation<T>(SR.Contains_no_elements);
|
||||
=> list.Count > 0 ? list[0] : ThrowHelper.ThrowInvalidOperationException<T>(SR.Contains_no_elements);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first element in a list that satisfies a specified condition.
|
||||
|
@ -196,7 +196,7 @@ internal static class ReadOnlyListExtensions
|
|||
}
|
||||
}
|
||||
|
||||
return ThrowHelper.ThrowInvalidOperation<T>(SR.Contains_no_matching_elements);
|
||||
return ThrowHelper.ThrowInvalidOperationException<T>(SR.Contains_no_matching_elements);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -227,7 +227,7 @@ internal static class ReadOnlyListExtensions
|
|||
}
|
||||
}
|
||||
|
||||
return ThrowHelper.ThrowInvalidOperation<T>(SR.Contains_no_matching_elements);
|
||||
return ThrowHelper.ThrowInvalidOperationException<T>(SR.Contains_no_matching_elements);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -329,7 +329,7 @@ internal static class ReadOnlyListExtensions
|
|||
/// The list is empty.
|
||||
/// </exception>
|
||||
public static T Last<T>(this IReadOnlyList<T> list)
|
||||
=> list.Count > 0 ? list[^1] : ThrowHelper.ThrowInvalidOperation<T>(SR.Contains_no_elements);
|
||||
=> list.Count > 0 ? list[^1] : ThrowHelper.ThrowInvalidOperationException<T>(SR.Contains_no_elements);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the last element of a list that satisfies a specified condition.
|
||||
|
@ -356,7 +356,7 @@ internal static class ReadOnlyListExtensions
|
|||
}
|
||||
}
|
||||
|
||||
return ThrowHelper.ThrowInvalidOperation<T>(SR.Contains_no_matching_elements);
|
||||
return ThrowHelper.ThrowInvalidOperationException<T>(SR.Contains_no_matching_elements);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -387,7 +387,7 @@ internal static class ReadOnlyListExtensions
|
|||
}
|
||||
}
|
||||
|
||||
return ThrowHelper.ThrowInvalidOperation<T>(SR.Contains_no_matching_elements);
|
||||
return ThrowHelper.ThrowInvalidOperationException<T>(SR.Contains_no_matching_elements);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -559,8 +559,8 @@ internal static class ReadOnlyListExtensions
|
|||
return list.Count switch
|
||||
{
|
||||
1 => list[0],
|
||||
0 => ThrowHelper.ThrowInvalidOperation<T>(SR.Contains_no_elements),
|
||||
_ => ThrowHelper.ThrowInvalidOperation<T>(SR.Contains_more_than_one_element)
|
||||
0 => ThrowHelper.ThrowInvalidOperationException<T>(SR.Contains_no_elements),
|
||||
_ => ThrowHelper.ThrowInvalidOperationException<T>(SR.Contains_more_than_one_element)
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -594,7 +594,7 @@ internal static class ReadOnlyListExtensions
|
|||
{
|
||||
if (firstSeen)
|
||||
{
|
||||
return ThrowHelper.ThrowInvalidOperation<T>(SR.Contains_more_than_one_matching_element);
|
||||
return ThrowHelper.ThrowInvalidOperationException<T>(SR.Contains_more_than_one_matching_element);
|
||||
}
|
||||
|
||||
firstSeen = true;
|
||||
|
@ -604,7 +604,7 @@ internal static class ReadOnlyListExtensions
|
|||
|
||||
if (!firstSeen)
|
||||
{
|
||||
return ThrowHelper.ThrowInvalidOperation<T>(SR.Contains_no_matching_elements);
|
||||
return ThrowHelper.ThrowInvalidOperationException<T>(SR.Contains_no_matching_elements);
|
||||
}
|
||||
|
||||
return result!;
|
||||
|
@ -643,7 +643,7 @@ internal static class ReadOnlyListExtensions
|
|||
{
|
||||
if (firstSeen)
|
||||
{
|
||||
return ThrowHelper.ThrowInvalidOperation<T>(SR.Contains_more_than_one_matching_element);
|
||||
return ThrowHelper.ThrowInvalidOperationException<T>(SR.Contains_more_than_one_matching_element);
|
||||
}
|
||||
|
||||
firstSeen = true;
|
||||
|
@ -653,7 +653,7 @@ internal static class ReadOnlyListExtensions
|
|||
|
||||
if (!firstSeen)
|
||||
{
|
||||
return ThrowHelper.ThrowInvalidOperation<T>(SR.Contains_no_matching_elements);
|
||||
return ThrowHelper.ThrowInvalidOperationException<T>(SR.Contains_no_matching_elements);
|
||||
}
|
||||
|
||||
return result!;
|
||||
|
@ -679,7 +679,7 @@ internal static class ReadOnlyListExtensions
|
|||
{
|
||||
1 => list[0],
|
||||
0 => default,
|
||||
_ => ThrowHelper.ThrowInvalidOperation<T>(SR.Contains_more_than_one_element)
|
||||
_ => ThrowHelper.ThrowInvalidOperationException<T>(SR.Contains_more_than_one_element)
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -706,7 +706,7 @@ internal static class ReadOnlyListExtensions
|
|||
{
|
||||
1 => list[0],
|
||||
0 => defaultValue,
|
||||
_ => ThrowHelper.ThrowInvalidOperation<T>(SR.Contains_more_than_one_element)
|
||||
_ => ThrowHelper.ThrowInvalidOperationException<T>(SR.Contains_more_than_one_element)
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -738,7 +738,7 @@ internal static class ReadOnlyListExtensions
|
|||
{
|
||||
if (firstSeen)
|
||||
{
|
||||
return ThrowHelper.ThrowInvalidOperation<T>(SR.Contains_more_than_one_matching_element);
|
||||
return ThrowHelper.ThrowInvalidOperationException<T>(SR.Contains_more_than_one_matching_element);
|
||||
}
|
||||
|
||||
firstSeen = true;
|
||||
|
@ -780,7 +780,7 @@ internal static class ReadOnlyListExtensions
|
|||
{
|
||||
if (firstSeen)
|
||||
{
|
||||
return ThrowHelper.ThrowInvalidOperation<T>(SR.Contains_more_than_one_matching_element);
|
||||
return ThrowHelper.ThrowInvalidOperationException<T>(SR.Contains_more_than_one_matching_element);
|
||||
}
|
||||
|
||||
firstSeen = true;
|
||||
|
@ -822,7 +822,7 @@ internal static class ReadOnlyListExtensions
|
|||
{
|
||||
if (firstSeen)
|
||||
{
|
||||
return ThrowHelper.ThrowInvalidOperation<T>(SR.Contains_more_than_one_matching_element);
|
||||
return ThrowHelper.ThrowInvalidOperationException<T>(SR.Contains_more_than_one_matching_element);
|
||||
}
|
||||
|
||||
firstSeen = true;
|
||||
|
@ -867,7 +867,7 @@ internal static class ReadOnlyListExtensions
|
|||
{
|
||||
if (firstSeen)
|
||||
{
|
||||
return ThrowHelper.ThrowInvalidOperation<T>(SR.Contains_more_than_one_matching_element);
|
||||
return ThrowHelper.ThrowInvalidOperationException<T>(SR.Contains_more_than_one_matching_element);
|
||||
}
|
||||
|
||||
firstSeen = true;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
#if !NET
|
||||
using ThrowHelper = Microsoft.AspNetCore.Razor.Utilities.ThrowHelper;
|
||||
using Microsoft.AspNetCore.Razor;
|
||||
#endif
|
||||
|
||||
namespace System;
|
||||
|
@ -72,7 +72,7 @@ internal static class StringExtensions
|
|||
{
|
||||
if (!startIndex.Equals(Index.Start))
|
||||
{
|
||||
ThrowHelper.ThrowArgumentOutOfRange(nameof(startIndex));
|
||||
ThrowHelper.ThrowArgumentOutOfRangeException(nameof(startIndex));
|
||||
}
|
||||
|
||||
return default;
|
||||
|
@ -110,7 +110,7 @@ internal static class StringExtensions
|
|||
{
|
||||
if (!range.Start.Equals(Index.Start) || !range.End.Equals(Index.Start))
|
||||
{
|
||||
ThrowHelper.ThrowArgumentNull(nameof(text));
|
||||
ThrowHelper.ThrowArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
return default;
|
||||
|
@ -250,7 +250,7 @@ internal static class StringExtensions
|
|||
{
|
||||
if (!startIndex.Equals(Index.Start))
|
||||
{
|
||||
ThrowHelper.ThrowArgumentOutOfRange(nameof(startIndex));
|
||||
ThrowHelper.ThrowArgumentOutOfRangeException(nameof(startIndex));
|
||||
}
|
||||
|
||||
return default;
|
||||
|
@ -288,7 +288,7 @@ internal static class StringExtensions
|
|||
{
|
||||
if (!range.Start.Equals(Index.Start) || !range.End.Equals(Index.Start))
|
||||
{
|
||||
ThrowHelper.ThrowArgumentNull(nameof(text));
|
||||
ThrowHelper.ThrowArgumentNullException(nameof(text));
|
||||
}
|
||||
|
||||
return default;
|
||||
|
@ -552,6 +552,70 @@ internal static class StringExtensions
|
|||
return text.EndsWith(value);
|
||||
#else
|
||||
return text.Length > 0 && text[^1] == value;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encapsulates a method that receives a span of objects of type <typeparamref name="T"/>
|
||||
/// and a state object of type <typeparamref name="TArg"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">
|
||||
/// The type of the objects in the span.
|
||||
/// </typeparam>
|
||||
/// <typeparam name="TArg">
|
||||
/// The type of the object that represents the state.
|
||||
/// </typeparam>
|
||||
/// <param name="span">
|
||||
/// A span of objects of type <typeparamref name="T"/>.
|
||||
/// </param>
|
||||
/// <param name="arg">
|
||||
/// A state object of type <typeparamref name="TArg"/>.
|
||||
/// </param>
|
||||
public delegate void SpanAction<T, in TArg>(Span<T> span, TArg arg);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new string with a specific length and initializes it after creation by using the specified callback.
|
||||
/// </summary>
|
||||
/// <typeparam name="TState">
|
||||
/// The type of the element to pass to <paramref name="action"/>.
|
||||
/// </typeparam>
|
||||
/// <param name="length">
|
||||
/// The length of the string to create.
|
||||
/// </param>
|
||||
/// <param name="state">
|
||||
/// The element to pass to <paramref name="action"/>.
|
||||
/// </param>
|
||||
/// <param name="action">
|
||||
/// A callback to initialize the string
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The created string.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// The initial content of the destination span passed to <paramref name="action"/> is undefined.
|
||||
/// Therefore, it is the delegate's responsibility to ensure that every element of the span is assigned.
|
||||
/// Otherwise, the resulting string could contain random characters
|
||||
/// </remarks>
|
||||
public unsafe static string CreateString<TState>(int length, TState state, SpanAction<char, TState> action)
|
||||
{
|
||||
#if NET
|
||||
return string.Create(length, (action, state), static (span, state) => state.action(span, state.state));
|
||||
#else
|
||||
ArgHelper.ThrowIfNegative(length);
|
||||
|
||||
if (length == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var result = new string('\0', length);
|
||||
|
||||
fixed (char* ptr = result)
|
||||
{
|
||||
action(new Span<char>(ptr, length), state);
|
||||
}
|
||||
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,235 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor;
|
||||
|
||||
internal static class ThrowHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentException"/> with a parameter name and a message.
|
||||
/// </summary>
|
||||
/// <param name="paramName">
|
||||
/// The parameter name to include in the exception.
|
||||
/// </param>
|
||||
/// <param name="message">
|
||||
/// The message to include in the exception.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// This helps the JIT inline methods that need to throw an exceptions.
|
||||
/// </remarks>
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void ThrowArgumentException(string? paramName, string message)
|
||||
=> throw new ArgumentException(message, paramName);
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentException"/> with a parameter name and a message.
|
||||
/// </summary>
|
||||
/// <param name="paramName">
|
||||
/// The parameter name to include in the exception.
|
||||
/// </param>
|
||||
/// <param name="message">
|
||||
/// The message to include in the exception.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// This method does not return because it always throws an exception, but it is defined to return a
|
||||
/// <typeparamref name="T"/> value. This is useful for control flow scenarios where it is necessary to
|
||||
/// throw an exception and return from a method.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This helps the JIT inline methods that need to throw an exceptions.
|
||||
/// </remarks>
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static T ThrowArgumentException<T>(string? paramName, string message)
|
||||
=> throw new ArgumentException(message, paramName);
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentNullException"/> with a parameter name.
|
||||
/// </summary>
|
||||
/// <param name="paramName">
|
||||
/// The parameter name to include in the exception.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// This helps the JIT inline methods that need to throw an exceptions.
|
||||
/// </remarks>
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void ThrowArgumentNullException(string? paramName)
|
||||
=> throw new ArgumentNullException(paramName);
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentNullException"/> with a parameter name.
|
||||
/// </summary>
|
||||
/// <param name="paramName">
|
||||
/// The parameter name to include in the exception.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// This method does not return because it always throws an exception, but it is defined to return a
|
||||
/// <typeparamref name="T"/> value. This is useful for control flow scenarios where it is necessary to
|
||||
/// throw an exception and return from a method.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This helps the JIT inline methods that need to throw an exceptions.
|
||||
/// </remarks>
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static T ThrowArgumentNullException<T>(string? paramName)
|
||||
=> throw new ArgumentNullException(paramName);
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> with a parameter name, message, and
|
||||
/// the actual invalid value.
|
||||
/// </summary>
|
||||
/// <param name="paramName">
|
||||
/// The parameter name to include in the exception.
|
||||
/// </param>
|
||||
/// <param name="actualValue">
|
||||
/// The actual invalid value to include in the exception.
|
||||
/// </param>
|
||||
/// <param name="message">
|
||||
/// The message to include in the exception.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// This helps the JIT inline methods that need to throw an exceptions.
|
||||
/// </remarks>
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void ThrowArgumentOutOfRangeException(string? paramName, object? actualValue, string message)
|
||||
=> throw new ArgumentOutOfRangeException(paramName, actualValue, message);
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> with a parameter name and message.
|
||||
/// </summary>
|
||||
/// <param name="paramName">
|
||||
/// The parameter name to include in the exception.
|
||||
/// </param>
|
||||
/// <param name="message">
|
||||
/// The message to include in the exception.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// This helps the JIT inline methods that need to throw an exceptions.
|
||||
/// </remarks>
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void ThrowArgumentOutOfRangeException(string? paramName, string message)
|
||||
=> throw new ArgumentOutOfRangeException(paramName, message);
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> with a parameter name.
|
||||
/// </summary>
|
||||
/// <param name="paramName">
|
||||
/// The parameter name to include in the exception.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// This helps the JIT inline methods that need to throw an exceptions.
|
||||
/// </remarks>
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void ThrowArgumentOutOfRangeException(string? paramName)
|
||||
=> throw new ArgumentOutOfRangeException(paramName);
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> with a parameter name, message, and
|
||||
/// the actual invalid value.
|
||||
/// </summary>
|
||||
/// <param name="paramName">
|
||||
/// The parameter name to include in the exception.
|
||||
/// </param>
|
||||
/// <param name="actualValue">
|
||||
/// The actual invalid value to include in the exception.
|
||||
/// </param>
|
||||
/// <param name="message">
|
||||
/// The message to include in the exception.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// This method does not return because it always throws an exception, but it is defined to return a
|
||||
/// <typeparamref name="T"/> value. This is useful for control flow scenarios where it is necessary to
|
||||
/// throw an exception and return from a method.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This helps the JIT inline methods that need to throw an exceptions.
|
||||
/// </remarks>
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static T ThrowArgumentOutOfRangeException<T>(string? paramName, object? actualValue, string message)
|
||||
=> throw new ArgumentOutOfRangeException(paramName, actualValue, message);
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> with a parameter name and message.
|
||||
/// </summary>
|
||||
/// <param name="paramName">
|
||||
/// The parameter name to include in the exception.
|
||||
/// </param>
|
||||
/// <param name="message">
|
||||
/// The message to include in the exception.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// This method does not return because it always throws an exception, but it is defined to return a
|
||||
/// <typeparamref name="T"/> value. This is useful for control flow scenarios where it is necessary to
|
||||
/// throw an exception and return from a method.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This helps the JIT inline methods that need to throw an exceptions.
|
||||
/// </remarks>
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static T ThrowArgumentOutOfRangeException<T>(string? paramName, string message)
|
||||
=> throw new ArgumentOutOfRangeException(paramName, message);
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ArgumentOutOfRangeException"/> with a parameter name.
|
||||
/// </summary>
|
||||
/// <param name="paramName">
|
||||
/// The parameter name to include in the exception.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// This method does not return because it always throws an exception, but it is defined to return a
|
||||
/// <typeparamref name="T"/> value. This is useful for control flow scenarios where it is necessary to
|
||||
/// throw an exception and return from a method.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This helps the JIT inline methods that need to throw an exceptions.
|
||||
/// </remarks>
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static T ThrowArgumentOutOfRangeException<T>(string? paramName)
|
||||
=> throw new ArgumentOutOfRangeException(paramName);
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="InvalidOperationException"/> with a message.
|
||||
/// </summary>
|
||||
/// <param name="message">
|
||||
/// The message to include in the exception.
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// This helps the JIT inline methods that need to throw an exceptions.
|
||||
/// </remarks>
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void ThrowInvalidOperationException(string message)
|
||||
=> throw new InvalidOperationException(message);
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="InvalidOperationException"/> with a message.
|
||||
/// </summary>
|
||||
/// <param name="message">
|
||||
/// The message to include in the exception.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// This method does not return because it always throws an exception, but it is defined to return a
|
||||
/// <typeparamref name="T"/> value. This is useful for control flow scenarios where it is necessary to
|
||||
/// throw an exception and return from a method.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This helps the JIT inline methods that need to throw an exceptions.
|
||||
/// </remarks>
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static T ThrowInvalidOperationException<T>(string message)
|
||||
=> throw new InvalidOperationException(message);
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.Utilities;
|
||||
|
||||
internal static class ThrowHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// This is present to help the JIT inline methods that need to throw an <see cref="InvalidOperationException"/>.
|
||||
/// </summary>
|
||||
[DoesNotReturn]
|
||||
public static void ThrowArgumentNull(string paramName)
|
||||
=> throw new ArgumentNullException(paramName);
|
||||
|
||||
/// <summary>
|
||||
/// This is present to help the JIT inline methods that need to throw an <see cref="InvalidOperationException"/>.
|
||||
/// </summary>
|
||||
[DoesNotReturn]
|
||||
public static T ThrowArgumentNull<T>(string paramName)
|
||||
=> throw new ArgumentNullException(paramName);
|
||||
|
||||
/// <summary>
|
||||
/// This is present to help the JIT inline methods that need to throw an <see cref="InvalidOperationException"/>.
|
||||
/// </summary>
|
||||
[DoesNotReturn]
|
||||
public static void ThrowArgumentOutOfRange(string paramName)
|
||||
=> throw new ArgumentOutOfRangeException(paramName);
|
||||
|
||||
/// <summary>
|
||||
/// This is present to help the JIT inline methods that need to throw an <see cref="InvalidOperationException"/>.
|
||||
/// </summary>
|
||||
[DoesNotReturn]
|
||||
public static T ThrowArgumentOutOfRange<T>(string paramName)
|
||||
=> throw new ArgumentOutOfRangeException(paramName);
|
||||
|
||||
/// <summary>
|
||||
/// This is present to help the JIT inline methods that need to throw an <see cref="InvalidOperationException"/>.
|
||||
/// </summary>
|
||||
[DoesNotReturn]
|
||||
public static T ThrowInvalidOperation<T>(string message)
|
||||
=> throw new InvalidOperationException(message);
|
||||
}
|
Загрузка…
Ссылка в новой задаче