task, CancellationToken cancellationToken = null)
+>```
+>Summary: Creates an `System.Collections.Generic.IAsyncEnumerable`1` that awaits the specified task and yields its result as a single element.
+>
+>Parameters:
+> `task` - The task whose result will be yielded.
+> `cancellationToken` - A token to cancel the asynchronous operation before the task is awaited.
+>
+>Returns: An `System.Collections.Generic.IAsyncEnumerable`1` that yields the single result of `task`.
Generated by MarkdownCodeDoc version 1.2
diff --git a/docs/CodeDoc/Atc/Atc.Helpers.md b/docs/CodeDoc/Atc/Atc.Helpers.md
index e69c84f9..160a8382 100644
--- a/docs/CodeDoc/Atc/Atc.Helpers.md
+++ b/docs/CodeDoc/Atc/Atc.Helpers.md
@@ -796,11 +796,11 @@ DateTimeHelper.
### Static Methods
-#### TryParseShortDateUsingCurrentUiCulture
+#### TryParseShortDateUi
>```csharp
->bool TryParseShortDateUsingCurrentUiCulture(string value, out DateTime result)
+>bool TryParseShortDateUi(string value, out DateTime result)
>```
->Summary: Tries to parse a string representation of a short date using the current UI culture's date format.
+>Summary: Tries to parse a string representation of a short date using the current UI culture's date format. Use this variant when parsing input from a user interface.
>
>Parameters:
> `value` - The string to parse.
@@ -825,11 +825,11 @@ DateTimeHelper.
>
>Returns: if the parsing was successful; otherwise, .
-#### TryParseShortTimeUsingCurrentUiCulture
+#### TryParseShortTimeUi
>```csharp
->bool TryParseShortTimeUsingCurrentUiCulture(string value, out DateTime result)
+>bool TryParseShortTimeUi(string value, out DateTime result)
>```
->Summary: Tries to parse a string representation of a short time using the current UI culture's time format (12-hour or 24-hour).
+>Summary: Tries to parse a string representation of a short time using the current UI culture's time format (12-hour or 24-hour). Use this variant when parsing input from a user interface.
>
>Parameters:
> `value` - The string to parse.
@@ -839,11 +839,11 @@ DateTimeHelper.
>
>Returns: if the parsing was successful; otherwise, .
-#### TryParseShortTimeUsingCurrentUiCultureUtc
+#### TryParseShortTimeUiUtc
>```csharp
->bool TryParseShortTimeUsingCurrentUiCultureUtc(string value, out DateTime result)
+>bool TryParseShortTimeUiUtc(string value, out DateTime result)
>```
->Summary: Tries to parse a string representation of a short UTC time using the current UI culture's time format (12-hour or 24-hour).
+>Summary: Tries to parse a string representation of a short UTC time using the current UI culture's time format (12-hour or 24-hour). Use this variant when parsing input from a user interface.
>
>Parameters:
> `value` - The string to parse.
@@ -883,11 +883,11 @@ DateTimeHelper.
>
>Returns: if the parsing was successful; otherwise, .
-#### TryParseUsingCurrentUiCulture
+#### TryParseUi
>```csharp
->bool TryParseUsingCurrentUiCulture(string value, out DateTime result)
+>bool TryParseUi(string value, out DateTime result)
>```
->Summary: Tries to parse a string representation of a `DateTime` using the current UI culture's date and time format.
+>Summary: Tries to parse a string representation of a `DateTime` using the current UI culture's date and time format. Use this variant when parsing input from a user interface.
>
>Parameters:
> `value` - The string to parse.
@@ -924,11 +924,11 @@ DateTimeOffsetHelper.
### Static Methods
-#### TryParseShortDateUsingCurrentUiCulture
+#### TryParseShortDateUi
>```csharp
->bool TryParseShortDateUsingCurrentUiCulture(string value, out DateTime result)
+>bool TryParseShortDateUi(string value, out DateTime result)
>```
->Summary: Tries to parse a string representation of a short date using the current UI culture's date format.
+>Summary: Tries to parse a string representation of a short date using the current UI culture's date format. Use this variant when parsing input from a user interface.
>
>Parameters:
> `value` - The string to parse.
@@ -938,25 +938,84 @@ DateTimeOffsetHelper.
>
>Returns: if the parsing was successful; otherwise, .
-#### TryParseShortTimeUsingCurrentUiCulture
+#### TryParseShortDateUsingSpecificCulture
+>```csharp
+>bool TryParseShortDateUsingSpecificCulture(string value, CultureInfo cultureInfo, out DateTime result)
+>```
+>Summary: Tries to parse a string representation of a short date using a specific culture's date format.
+>
+>Parameters:
+> `value` - The string to parse.
+> `cultureInfo` - The culture info to use for parsing.
+> `result` -
+ When this method returns, contains the parsed DateTimeOffset,
+ if the parse operation was successful; otherwise, contains the default DateTimeOffset.
+
+>
+>Returns: if the parsing was successful; otherwise, .
+#### TryParseShortTimeUi
+>```csharp
+>bool TryParseShortTimeUi(string value, out DateTime result)
+>```
+>Summary: Tries to parse a string representation of a short time using the current UI culture's time format (12-hour or 24-hour). Use this variant when parsing input from a user interface.
+>
+>Parameters:
+> `value` - The string to parse.
+> `result` -
+ When this method returns, contains the parsed DateTimeOffset,
+ if the parse operation was successful; otherwise, contains the default DateTimeOffset.
+
+>
+>Returns: if the parsing was successful; otherwise, .
+#### TryParseShortTimeUiUtc
+>```csharp
+>bool TryParseShortTimeUiUtc(string value, out DateTime result)
+>```
+>Summary: Tries to parse a string representation of a short UTC time using the current UI culture's time format (12-hour or 24-hour). Use this variant when parsing input from a user interface.
+>
+>Parameters:
+> `value` - The string to parse.
+> `result` -
+ When this method returns, contains the parsed DateTimeOffset,
+ if the parse operation was successful; otherwise, contains the default DateTimeOffset.
+
+>
+>Returns: if the parsing was successful; otherwise, .
+#### TryParseShortTimeUsingSpecificCulture
>```csharp
->bool TryParseShortTimeUsingCurrentUiCulture(string value, out DateTime result)
+>bool TryParseShortTimeUsingSpecificCulture(string value, CultureInfo cultureInfo, out DateTime result)
>```
->Summary: Tries to parse a string representation of a short time using the current UI culture's time format (12-hour or 24-hour).
+>Summary: Tries to parse a string representation of a short time using a specific culture's time format (12-hour or 24-hour).
>
>Parameters:
> `value` - The string to parse.
+> `cultureInfo` - The culture info to use for parsing.
> `result` -
When this method returns, contains the parsed DateTimeOffset,
if the parse operation was successful; otherwise, contains the default DateTimeOffset.
>
>Returns: if the parsing was successful; otherwise, .
-#### TryParseShortTimeUsingCurrentUiCultureUtc
+#### TryParseShortTimeUsingSpecificCultureUtc
+>```csharp
+>bool TryParseShortTimeUsingSpecificCultureUtc(string value, CultureInfo cultureInfo, out DateTime result)
+>```
+>Summary: Tries to parse a string representation of a short UTC time using a specific culture's time format (12-hour or 24-hour).
+>
+>Parameters:
+> `value` - The string to parse.
+> `cultureInfo` - The culture info to use for parsing.
+> `result` -
+ When this method returns, contains the parsed DateTimeOffset in UTC,
+ if the parse operation was successful; otherwise, contains the default DateTimeOffset.
+
+>
+>Returns: if the parsing was successful; otherwise, .
+#### TryParseUi
>```csharp
->bool TryParseShortTimeUsingCurrentUiCultureUtc(string value, out DateTime result)
+>bool TryParseUi(string value, out DateTime result)
>```
->Summary: Tries to parse a string representation of a short UTC time using the current UI culture's time format (12-hour or 24-hour).
+>Summary: Tries to parse a string representation of a `DateTimeOffset` using the current UI culture's date and time format. Use this variant when parsing input from a user interface.
>
>Parameters:
> `value` - The string to parse.
@@ -966,14 +1025,15 @@ DateTimeOffsetHelper.
>
>Returns: if the parsing was successful; otherwise, .
-#### TryParseUsingCurrentUiCulture
+#### TryParseUsingSpecificCulture
>```csharp
->bool TryParseUsingCurrentUiCulture(string value, out DateTime result)
+>bool TryParseUsingSpecificCulture(string value, CultureInfo cultureInfo, out DateTime result)
>```
->Summary: Tries to parse a string representation of a `DateTimeOffset` using the current UI culture's date and time format.
+>Summary: Tries to parse a string representation of a `DateTimeOffset` using a specific culture's date and time format.
>
>Parameters:
> `value` - The string to parse.
+> `cultureInfo` - The culture info to use for parsing.
> `result` -
When this method returns, contains the parsed DateTimeOffset,
if the parse operation was successful; otherwise, contains the default DateTimeOffset.
@@ -1145,6 +1205,18 @@ Enumeration Helper: EnumHelper.
> `sortDirectionType` - Type of the sort direction.
> `byFlagIncludeBase` - if set to [by flag include base].
> `byFlagIncludeCombined` - if set to [by flag include combined].
+#### ConvertEnumToReadOnlyDictionary
+>```csharp
+>IReadOnlyDictionary ConvertEnumToReadOnlyDictionary(bool includeDefault = True, bool byFlagIncludeBase = True, bool byFlagIncludeCombined = True)
+>```
+>Summary: Builds a read-only map from each defined enum member to its underlying `System.Int32` value.
+>
+>Parameters:
+> `includeDefault` - If set to the 0-valued member is included.
+> `byFlagIncludeBase` - For enums, include the single-bit base values.
+> `byFlagIncludeCombined` - For enums, include the multi-bit combined values.
+>
+>Returns: An `System.Collections.Generic.IReadOnlyDictionary`2` keyed by the typed enum value with its underlying `System.Int32` as the value. Suitable for severity-rank or threshold lookup tables that previously required a hand-rolled `Dictionary<TEnum, int>` literal.
#### GetDescription
>```csharp
>string GetDescription(Enum enumeration)
@@ -2130,6 +2202,16 @@ Provides utility methods for checking network connectivity and retrieving networ
>Summary: Retrieves the public IP address of the current machine by querying an external service (api.ipify.org).
>
>Returns: The public `System.Net.IPAddress` if retrieval succeeds; otherwise, .
+#### GetPublicIpAddressAsync
+>```csharp
+>Task GetPublicIpAddressAsync(CancellationToken cancellationToken = null)
+>```
+>Summary: Asynchronously retrieves the public IP address of the current machine by querying an external service (api.ipify.org).
+>
+>Parameters:
+> `cancellationToken` - A token to cancel the asynchronous operation.
+>
+>Returns: The public `System.Net.IPAddress` if retrieval succeeds; otherwise, .
#### HasConnection
>```csharp
>bool HasConnection()
@@ -2144,6 +2226,26 @@ Provides utility methods for checking network connectivity and retrieving networ
>Summary: Determines whether there is network connectivity by pinging Google's DNS server (8.8.8.8).
>
>Returns: if a ping response is received; otherwise, .
+#### HasConnectionAsync
+>```csharp
+>Task HasConnectionAsync(CancellationToken cancellationToken = null)
+>```
+>Summary: Asynchronously determines whether there is network connectivity by pinging Google's DNS server (8.8.8.8).
+>
+>Parameters:
+> `cancellationToken` - A token to cancel the asynchronous operation.
+>
+>Returns: if a ping response is received; otherwise, .
+#### HasConnectionAsync
+>```csharp
+>Task HasConnectionAsync(IPAddress ipAddress, CancellationToken cancellationToken = null)
+>```
+>Summary: Asynchronously determines whether there is network connectivity by pinging Google's DNS server (8.8.8.8).
+>
+>Parameters:
+> `cancellationToken` - A token to cancel the asynchronous operation.
+>
+>Returns: if a ping response is received; otherwise, .
#### HasHttpConnection
>```csharp
>bool HasHttpConnection()
@@ -2158,6 +2260,26 @@ Provides utility methods for checking network connectivity and retrieving networ
>Summary: Determines whether there is HTTP connectivity by making a request to Google's website.
>
>Returns: if the HTTP request succeeds; otherwise, .
+#### HasHttpConnectionAsync
+>```csharp
+>Task HasHttpConnectionAsync(CancellationToken cancellationToken = null)
+>```
+>Summary: Asynchronously determines whether there is HTTP connectivity by making a request to Google's website.
+>
+>Parameters:
+> `cancellationToken` - A token to cancel the asynchronous operation.
+>
+>Returns: if the HTTP request succeeds; otherwise, .
+#### HasHttpConnectionAsync
+>```csharp
+>Task HasHttpConnectionAsync(Uri uri, CancellationToken cancellationToken = null)
+>```
+>Summary: Asynchronously determines whether there is HTTP connectivity by making a request to Google's website.
+>
+>Parameters:
+> `cancellationToken` - A token to cancel the asynchronous operation.
+>
+>Returns: if the HTTP request succeeds; otherwise, .
#### HasTcpConnection
>```csharp
>bool HasTcpConnection(IPAddress ipAddress, int port)
@@ -2169,6 +2291,18 @@ Provides utility methods for checking network connectivity and retrieving networ
> `port` - The port number to connect to.
>
>Returns: if the TCP connection succeeds; otherwise, .
+#### HasTcpConnectionAsync
+>```csharp
+>Task HasTcpConnectionAsync(IPAddress ipAddress, int port, CancellationToken cancellationToken = null)
+>```
+>Summary: Asynchronously determines whether a TCP connection can be established to the specified IP address and port. A connection timeout of 5 seconds is applied; pass a pre-cancelled token to impose a shorter deadline.
+>
+>Parameters:
+> `ipAddress` - The IP address to connect to.
+> `port` - The port number to connect to.
+> `cancellationToken` - A token to cancel the asynchronous operation.
+>
+>Returns: if the TCP connection succeeds within the timeout; otherwise, .
@@ -2712,6 +2846,34 @@ Provides utility methods for executing external processes, managing process life
> `cancellationToken` - A token to cancel the operation.
>
>Returns: A task that returns a tuple containing success status and output/error messages.
+#### ExecuteWithSeparateOutput
+>```csharp
+>Task> ExecuteWithSeparateOutput(FileInfo fileInfo, string arguments, bool runAsAdministrator = False, ushort timeoutInSec = 30, CancellationToken cancellationToken = null)
+>```
+>Summary: Executes a process with the specified file and arguments, returning standard output and standard error separately.
+>
+>Parameters:
+> `fileInfo` - The executable file to run.
+> `arguments` - The command-line arguments to pass to the executable.
+> `runAsAdministrator` - If , attempts to run the process with elevated privileges.
+> `timeoutInSec` - The maximum time in seconds to wait for the process to complete. Default is 30 seconds.
+> `cancellationToken` - A token to cancel the operation.
+>
+>Returns: A task that returns a tuple containing success status, standard output, and standard error streams separately.
+#### ExecuteWithSeparateOutput
+>```csharp
+>Task> ExecuteWithSeparateOutput(DirectoryInfo workingDirectory, FileInfo fileInfo, string arguments, bool runAsAdministrator = False, ushort timeoutInSec = 30, CancellationToken cancellationToken = null)
+>```
+>Summary: Executes a process with the specified file and arguments, returning standard output and standard error separately.
+>
+>Parameters:
+> `fileInfo` - The executable file to run.
+> `arguments` - The command-line arguments to pass to the executable.
+> `runAsAdministrator` - If , attempts to run the process with elevated privileges.
+> `timeoutInSec` - The maximum time in seconds to wait for the process to complete. Default is 30 seconds.
+> `cancellationToken` - A token to cancel the operation.
+>
+>Returns: A task that returns a tuple containing success status, standard output, and standard error streams separately.
#### KillById
>```csharp
>ValueTuple KillById(int processId, int timeoutInSec = 30)
diff --git a/docs/CodeDoc/Atc/Atc.Math.GeoSpatial.md b/docs/CodeDoc/Atc/Atc.Math.GeoSpatial.md
index 3a4ea2e5..59c364f0 100644
--- a/docs/CodeDoc/Atc/Atc.Math.GeoSpatial.md
+++ b/docs/CodeDoc/Atc/Atc.Math.GeoSpatial.md
@@ -36,6 +36,28 @@ Provides utility methods for geospatial calculations including distance measurem
### Static Methods
+#### Bearing
+>```csharp
+>double Bearing(CartesianCoordinate coordinate1, CartesianCoordinate coordinate2)
+>```
+>Summary: Calculates the initial bearing (forward azimuth) from one geographic coordinate to another. The bearing is the angle measured clockwise from true north (0°) to the direction of travel.
+>
+>Parameters:
+> `coordinate1` - The starting coordinate.
+> `coordinate2` - The destination coordinate.
+>
+>Returns: The initial bearing in degrees (0–360), where 0° is north, 90° east, 180° south, 270° west.
+#### Bearing
+>```csharp
+>double Bearing(double longitude1, double latitude1, double longitude2, double latitude2)
+>```
+>Summary: Calculates the initial bearing (forward azimuth) from one geographic coordinate to another. The bearing is the angle measured clockwise from true north (0°) to the direction of travel.
+>
+>Parameters:
+> `coordinate1` - The starting coordinate.
+> `coordinate2` - The destination coordinate.
+>
+>Returns: The initial bearing in degrees (0–360), where 0° is north, 90° east, 180° south, 270° west.
#### Distance
>```csharp
>double Distance(CartesianCoordinate coordinate1, CartesianCoordinate coordinate2, DistanceMeasurementType measurement)
@@ -50,7 +72,7 @@ Provides utility methods for geospatial calculations including distance measurem
>Returns: The distance between the two coordinates in the specified measurement unit.
#### Distance
>```csharp
->double Distance(double longitude1, double latitude1, double longitude2, double latitude2, DistanceMeasurementType measurement = Kilometers)
+>double Distance(double longitude1, double latitude1, double longitude2, double latitude2, DistanceMeasurementType measurement = Kilometers, double earthRadiusKm = 6371)
>```
>Summary: Calculates the great-circle distance between two geographic coordinates.
>
@@ -131,16 +153,26 @@ UniversalTransverseMercatorConverter
> `coordinate` - The coordinate.
#### ToWgs84
>```csharp
+>CartesianCoordinate ToWgs84(UniversalTransverseMercatorResult utmResult, int maxDecimalPrecision = 8)
+>```
+>Summary: Converts a `Atc.Math.GeoSpatial.UniversalTransverseMercatorResult` back to a WGS84 geographic coordinate. This is a convenience overload that unpacks the fields from the result returned by `Atc.Math.GeoSpatial.UniversalTransverseMercatorConverter.ToUtm(Atc.Structs.CartesianCoordinate)` or `Atc.Math.GeoSpatial.UniversalTransverseMercatorConverter.ToUtm(System.Double,System.Double)`.
+>
+>Parameters:
+> `utmResult` - The UTM result to convert.
+> `maxDecimalPrecision` - The maximum number of decimal places in the returned latitude/longitude values.
+>
+>Returns: A `Atc.Structs.CartesianCoordinate` containing the WGS84 latitude and longitude.
+#### ToWgs84
+>```csharp
>CartesianCoordinate ToWgs84(int utmZoneNumber, string utmZoneLetter, double utmEasting, double utmNorthing, int maxDecimalPrecision = 8)
>```
->Summary: To WGS84.
+>Summary: Converts a `Atc.Math.GeoSpatial.UniversalTransverseMercatorResult` back to a WGS84 geographic coordinate. This is a convenience overload that unpacks the fields from the result returned by `Atc.Math.GeoSpatial.UniversalTransverseMercatorConverter.ToUtm(Atc.Structs.CartesianCoordinate)` or `Atc.Math.GeoSpatial.UniversalTransverseMercatorConverter.ToUtm(System.Double,System.Double)`.
>
>Parameters:
-> `utmZoneNumber` - The utm zone number.
-> `utmZoneLetter` - The utm zone letter.
-> `utmEasting` - The utm easting.
-> `utmNorthing` - The utm northing.
-> `maxDecimalPrecision` - The maximum decimal precision.
+> `utmResult` - The UTM result to convert.
+> `maxDecimalPrecision` - The maximum number of decimal places in the returned latitude/longitude values.
+>
+>Returns: A `Atc.Structs.CartesianCoordinate` containing the WGS84 latitude and longitude.
diff --git a/docs/CodeDoc/Atc/Atc.Serialization.JsonConverters.md b/docs/CodeDoc/Atc/Atc.Serialization.JsonConverters.md
index a64a1773..bd90233b 100644
--- a/docs/CodeDoc/Atc/Atc.Serialization.JsonConverters.md
+++ b/docs/CodeDoc/Atc/Atc.Serialization.JsonConverters.md
@@ -174,7 +174,7 @@ JSON converter that enables deserialization of interface types by using the runt
## NumberToStringJsonConverter
JSON converter that converts numeric values to string representation and vice versa.
->Remarks: This converter handles conversion between JSON numbers and strings, allowing numeric values in JSON to be read as strings. During deserialization, JSON numbers are converted to their string representation using the current thread's culture. During serialization, any object is converted to its string representation.
+>Remarks: This converter handles conversion between JSON numbers and strings, allowing numeric values in JSON to be read as strings. During deserialization, JSON numbers are converted to their string representation using the invariant culture so the result is stable across machines and locales. During serialization, any object is converted to its string representation.
>```csharp
>public class NumberToStringJsonConverter : JsonConverter
@@ -304,7 +304,7 @@ JSON converter that serializes `System.Uri` objects to and from their absolute U
## VersionJsonConverter
JSON converter that serializes `System.Version` objects to and from their string representation.
->Remarks: This converter supports reading `System.Version` from both string format (e.g., "1.2.3.4") and object format with internal fields (_Major, _Minor, _Build, _Revision). During writing, `System.Version` is always serialized as a string. If parsing fails, a default empty `System.Version` is returned.
+>Remarks: This converter supports reading `System.Version` from both string format (e.g., "1.2.3.4") and object format with public properties (Major, Minor, Build, Revision) as produced by `System.Text.Json.JsonSerializer`. During writing, `System.Version` is always serialized as a string. If parsing fails, a default empty `System.Version` is returned.
>```csharp
>public class VersionJsonConverter : JsonConverter
diff --git a/docs/CodeDoc/Atc/Atc.Serialization.md b/docs/CodeDoc/Atc/Atc.Serialization.md
index 0344bb65..6ffa82ac 100644
--- a/docs/CodeDoc/Atc/Atc.Serialization.md
+++ b/docs/CodeDoc/Atc/Atc.Serialization.md
@@ -152,6 +152,65 @@ Configuration settings for creating `System.Text.Json.JsonSerializerOptions` ins
+## JsonSerializerHelper
+Provides async stream-based serialization and deserialization helpers using `System.Text.Json.JsonSerializer`.
+>Remarks: All overloads that omit `System.Text.Json.JsonSerializerOptions` use `Atc.Serialization.JsonSerializerOptionsFactory` default options.
+
+>```csharp
+>public static class JsonSerializerHelper
+>```
+
+### Static Methods
+
+#### DeserializeFromStreamAsync
+>```csharp
+>Task DeserializeFromStreamAsync(Stream stream, CancellationToken cancellationToken = null)
+>```
+>Summary: Asynchronously deserializes a value of type `T` from the specified UTF-8 JSON stream using the default serializer options.
+>
+>Parameters:
+> `stream` - The UTF-8 encoded JSON stream to read from.
+> `cancellationToken` - A token to cancel the asynchronous operation.
+>
+>Returns: A `System.Threading.Tasks.Task`1` that represents the asynchronous operation, containing the deserialized value, or if the stream contains a JSON null literal.
+#### DeserializeFromStreamAsync
+>```csharp
+>Task DeserializeFromStreamAsync(Stream stream, JsonSerializerOptions options, CancellationToken cancellationToken = null)
+>```
+>Summary: Asynchronously deserializes a value of type `T` from the specified UTF-8 JSON stream using the default serializer options.
+>
+>Parameters:
+> `stream` - The UTF-8 encoded JSON stream to read from.
+> `cancellationToken` - A token to cancel the asynchronous operation.
+>
+>Returns: A `System.Threading.Tasks.Task`1` that represents the asynchronous operation, containing the deserialized value, or if the stream contains a JSON null literal.
+#### SerializeToStreamAsync
+>```csharp
+>Task SerializeToStreamAsync(T value, Stream stream, CancellationToken cancellationToken = null)
+>```
+>Summary: Asynchronously serializes `value` as UTF-8 JSON into the specified stream using the default serializer options.
+>
+>Parameters:
+> `value` - The value to serialize.
+> `stream` - The stream to write JSON into.
+> `cancellationToken` - A token to cancel the asynchronous operation.
+>
+>Returns: A `System.Threading.Tasks.Task` that represents the asynchronous write operation.
+#### SerializeToStreamAsync
+>```csharp
+>Task SerializeToStreamAsync(T value, Stream stream, JsonSerializerOptions options, CancellationToken cancellationToken = null)
+>```
+>Summary: Asynchronously serializes `value` as UTF-8 JSON into the specified stream using the default serializer options.
+>
+>Parameters:
+> `value` - The value to serialize.
+> `stream` - The stream to write JSON into.
+> `cancellationToken` - A token to cancel the asynchronous operation.
+>
+>Returns: A `System.Threading.Tasks.Task` that represents the asynchronous write operation.
+
+
+
## JsonSerializerOptionsFactory
Factory class for creating preconfigured `System.Text.Json.JsonSerializerOptions` instances.
>Remarks: This factory provides convenient methods to create `System.Text.Json.JsonSerializerOptions` with common settings and custom converters. It supports both parameter-based and settings-based configuration.
@@ -160,6 +219,13 @@ Factory class for creating preconfigured `System.Text.Json.JsonSerializerOptions
>public static class JsonSerializerOptionsFactory
>```
+### Static Properties
+
+#### Default
+>```csharp
+>Default
+>```
+>Summary: Gets a cached, shared `System.Text.Json.JsonSerializerOptions` instance using the default settings (camelCase, null-values ignored, case-insensitive names, indented output). The instance is created once and reused; after the first serialization call it becomes read-only. Use `Atc.Serialization.JsonSerializerOptionsFactory.Create(System.Boolean,System.Boolean,System.Boolean,System.Boolean)` when you need a mutable copy.
### Static Methods
#### Create
diff --git a/docs/CodeDoc/Atc/Atc.Units.DigitalInformation.md b/docs/CodeDoc/Atc/Atc.Units.DigitalInformation.md
index 9f199bbe..3880a1e3 100644
--- a/docs/CodeDoc/Atc/Atc.Units.DigitalInformation.md
+++ b/docs/CodeDoc/Atc/Atc.Units.DigitalInformation.md
@@ -11,9 +11,35 @@
Represents a size in bytes.
>```csharp
->public struct ByteSize : IEquatable
+>public struct ByteSize : IEquatable, IComparable, IComparable
>```
+### Static Methods
+
+#### Parse
+>```csharp
+>ByteSize Parse(string value)
+>```
+>Summary: Parses a string of digits into a `Atc.Units.DigitalInformation.ByteSize`.
+>
+>Parameters:
+> `value` - The string to parse. Must represent a valid value.
+>
+>Returns: A `Atc.Units.DigitalInformation.ByteSize` with the parsed byte count.
+#### TryParse
+>```csharp
+>bool TryParse(string value, out byte result)
+>```
+>Summary: Tries to parse a string into a `Atc.Units.DigitalInformation.ByteSize`.
+>
+>Parameters:
+> `value` - The string to parse, or .
+> `result` -
+ When this method returns, contains the parsed
+ if parsing succeeded; otherwise, .
+
+>
+>Returns: if parsing succeeded; otherwise, .
### Properties
#### Value
@@ -23,6 +49,26 @@ Represents a size in bytes.
>Summary: Gets the size in bytes.
### Methods
+#### CompareTo
+>```csharp
+>int CompareTo(ByteSize other)
+>```
+>Summary: Compares this instance to another `Atc.Units.DigitalInformation.ByteSize` value.
+>
+>Parameters:
+> `other` - The other value to compare to.
+>
+>Returns: A negative number if this instance is less than `other`; zero if they are equal; a positive number if this instance is greater.
+#### CompareTo
+>```csharp
+>int CompareTo(object obj)
+>```
+>Summary: Compares this instance to another `Atc.Units.DigitalInformation.ByteSize` value.
+>
+>Parameters:
+> `other` - The other value to compare to.
+>
+>Returns: A negative number if this instance is less than `other`; zero if they are equal; a positive number if this instance is greater.
#### Equals
>```csharp
>bool Equals(ByteSize other)
@@ -174,6 +220,7 @@ Defines the suffix format for displaying byte sizes.
| 0 | None | None | No suffix is appended to the numeric value. |
| 1 | Short | Short | Short suffix format (e.g., "B", "KB", "MB", "GB"). |
| 2 | Full | Full | Full suffix format (e.g., "byte", "Kilobyte", "Megabyte", "Gigabyte"). |
+| 3 | ShortBinary | Short Binary | Short IEC binary suffix format (e.g., "B", "KiB", "MiB", "GiB"). Uses IEC 80000-13 notation to distinguish 1024-based units from SI decimal units. |
diff --git a/docs/CodeDoc/Atc/Atc.Units.InternationalSystemOfUnits.md b/docs/CodeDoc/Atc/Atc.Units.InternationalSystemOfUnits.md
index 8a3c668c..038d6a54 100644
--- a/docs/CodeDoc/Atc/Atc.Units.InternationalSystemOfUnits.md
+++ b/docs/CodeDoc/Atc/Atc.Units.InternationalSystemOfUnits.md
@@ -33,7 +33,7 @@ Enumeration: BaseUnitType
## InternationalSystemOfUnitsHelper
Provides utility methods for converting between International System of Units (SI) prefixes.
->Remarks: This helper class supports conversions between various SI unit prefixes such as kilo, mega, giga, milli, centi, etc. Note that not all prefix combinations are currently supported.
+>Remarks: This helper class supports conversions between all standard SI unit prefixes (Yotta through Yocto) using a table-driven exponent approach.
>```csharp
>public static class InternationalSystemOfUnitsHelper
@@ -50,10 +50,10 @@ Provides utility methods for converting between International System of Units (S
>Parameters:
> `prefixTypeFrom` - The source SI prefix type.
> `prefixTypeTo` - The target SI prefix type.
-> `numberOfDecimals` - The number of decimal places to round to (0 for no rounding).
+> `numberOfDecimals` - The number of decimal places to round to. Pass 0 for no rounding.
> `value` - The value to convert.
>
->Returns: The converted value in the target prefix type.
+>Returns: The converted value in the target prefix type, optionally rounded.
diff --git a/docs/CodeDoc/Atc/Atc.md b/docs/CodeDoc/Atc/Atc.md
index 80f9e88c..a7a6b770 100644
--- a/docs/CodeDoc/Atc/Atc.md
+++ b/docs/CodeDoc/Atc/Atc.md
@@ -165,12 +165,12 @@ Represents compass directions including the four cardinal points and intermediat
| 1024 | SouthWest | South West | SouthWest. |
| 2048 | WestSouthWest | West South West | WestSouthWest. |
| 4096 | West | West | West. |
-| 4625 | Simple | Simple | Simple = North | East | South | West. |
+| 4625 | Simple | Simple | Simple = North \| East \| South \| West. |
| 8192 | WestNorthWest | West North West | WestNorthWest. |
| 16384 | NorthWest | North West | NorthWest. |
-| 22101 | Medium | Medium | Medium = North | NorthEast | East | SouthEast | South | SouthWest | West | NorthWest. |
+| 22101 | Medium | Medium | Medium = North \| NorthEast \| East \| SouthEast \| South \| SouthWest \| West \| NorthWest. |
| 32768 | NorthNorthWest | North North West | NorthNorthWest. |
-| 65535 | Advanced | Advanced | Advanced = North | NorthNorthEast | NorthEast | EastNorthEast | East | EastSouthEast | SouthEast | SouthSouthEast | South | SouthSouthWest | SouthWest | WestSouthWest | West | WestNorthWest | NorthWest | NorthNorthWest. |
+| 65535 | Advanced | Advanced | Advanced = North \| NorthNorthEast \| NorthEast \| EastNorthEast \| East \| EastSouthEast \| SouthEast \| SouthSouthEast \| South \| SouthSouthWest \| SouthWest \| WestSouthWest \| West \| WestNorthWest \| NorthWest \| NorthNorthWest. |
@@ -536,6 +536,29 @@ Extension methods for enums.
>List> list = Enum.ToKeyValuePairsWithStringKey();
>Assert.Equal(7, list.Count);
>```
+#### ToReadOnlyDictionary
+>```csharp
+>IReadOnlyDictionary ToReadOnlyDictionary(bool includeDefault = True, bool byFlagIncludeBase = True, bool byFlagIncludeCombined = True)
+>```
+>Summary: Builds a read-only map from each defined enum member to its underlying `System.Int32` value. Useful for severity-rank or threshold lookup tables that previously required a hand-rolled `Dictionary<TEnum, int>` literal.
+>
+>Parameters:
+> `includeDefault` - If set to the 0-valued member is included.
+> `byFlagIncludeBase` - For enums, include the single-bit base values.
+> `byFlagIncludeCombined` - For enums, include the multi-bit combined values.
+>
+>Returns: An `System.Collections.Generic.IReadOnlyDictionary`2` keyed by the typed enum value with its underlying `System.Int32` as the value.
+>
+>Code usage:
+>```csharp
+>IReadOnlyDictionary map = Enum.ToReadOnlyDictionary();
+>```
+>
+>Code example:
+>```csharp
+>IReadOnlyDictionary map = Enum.ToReadOnlyDictionary();
+>Assert.Equal(0, map[DayOfWeek.Sunday]);
+>```
#### TryGetEnumValue
>```csharp
>bool TryGetEnumValue(string value, bool ignoreCase, out T returnedValue)
@@ -1193,7 +1216,7 @@ Provides custom string formatting based on specified case formatting options. ```csharp
>StringCaseFormatter Default
>```
->Summary: Static `Atc.StringCaseFormatter` using `System.Globalization.CultureInfo.CurrentCulture`.
+>Summary: Static `Atc.StringCaseFormatter` using `System.Globalization.CultureInfo.InvariantCulture`.
### Static Methods
#### Format
diff --git a/docs/CodeDoc/Atc/Index.md b/docs/CodeDoc/Atc/Index.md
index e0faf4d8..15160ab4 100644
--- a/docs/CodeDoc/Atc/Index.md
+++ b/docs/CodeDoc/Atc/Index.md
@@ -145,6 +145,7 @@
- [DynamicJson](Atc.Serialization.md#dynamicjson)
- [JsonSerializerFactorySettings](Atc.Serialization.md#jsonserializerfactorysettings)
+- [JsonSerializerHelper](Atc.Serialization.md#jsonserializerhelper)
- [JsonSerializerOptionsFactory](Atc.Serialization.md#jsonserializeroptionsfactory)
## [Atc.Serialization.JsonConverters](Atc.Serialization.JsonConverters.md)
@@ -266,6 +267,8 @@
## [System.Reflection](System.Reflection.md)
- [AssemblyExtensions](System.Reflection.md#assemblyextensions)
+- [ConstructorInfoExtensions](System.Reflection.md#constructorinfoextensions)
+- [EventInfoExtensions](System.Reflection.md#eventinfoextensions)
- [FieldInfoExtensions](System.Reflection.md#fieldinfoextensions)
- [MemberInfoExtensions](System.Reflection.md#memberinfoextensions)
- [MethodInfoExtensions](System.Reflection.md#methodinfoextensions)
diff --git a/docs/CodeDoc/Atc/IndexExtended.md b/docs/CodeDoc/Atc/IndexExtended.md
index a0198384..6ec4ace8 100644
--- a/docs/CodeDoc/Atc/IndexExtended.md
+++ b/docs/CodeDoc/Atc/IndexExtended.md
@@ -43,6 +43,7 @@
- ToDictionaryWithStringKey(DropDownFirstItemType dropDownFirstItemType = None, bool useDescriptionAttribute = True, bool includeDefault = True, SortDirectionType sortDirectionType = None, bool byFlagIncludeBase = True, bool byFlagIncludeCombined = True)
- ToKeyValuePairs(DropDownFirstItemType dropDownFirstItemType = None, bool useDescriptionAttribute = True, bool includeDefault = True, SortDirectionType sortDirectionType = None, bool byFlagIncludeBase = True, bool byFlagIncludeCombined = True)
- ToKeyValuePairsWithStringKey(DropDownFirstItemType dropDownFirstItemType = None, bool useDescriptionAttribute = True, bool includeDefault = True, SortDirectionType sortDirectionType = None, bool byFlagIncludeBase = True, bool byFlagIncludeCombined = True)
+ - ToReadOnlyDictionary(bool includeDefault = True, bool byFlagIncludeBase = True, bool byFlagIncludeCombined = True)
- TryGetEnumValue(Enum value, out T returnedValue)
- TryGetEnumValue(string value, bool ignoreCase, out T returnedValue)
- TryParse(string value, bool ignoreCase, out T returnedValue)
@@ -4393,6 +4394,7 @@
- GreaterThanOrEqualTo(SemanticVersion otherVersion, int significantParts = 4, int startingPart = 1)
- IsNewerThan(SemanticVersion otherVersion, bool withinMinorReleaseOnly = False, bool looseMode = False)
- ToString()
+ - ToString(string format, IFormatProvider formatProvider)
- ToVersion()
## [Atc.Factories](Atc.Factories.md)
@@ -4400,7 +4402,10 @@
- [AsyncEnumerableFactory](Atc.Factories.md#asyncenumerablefactory)
- Static Methods
- Empty()
- - FromSingleItem(T item)
+ - FromEnumerable(IEnumerable<T> source, CancellationToken cancellationToken = null)
+ - FromItems(T[] items, CancellationToken cancellationToken = null)
+ - FromSingleItem(T item, CancellationToken cancellationToken = null)
+ - FromTask(Task<T> task, CancellationToken cancellationToken = null)
## [Atc.Helpers](Atc.Helpers.md)
@@ -4503,20 +4508,24 @@
- TryValidateOutToValidationException(T data, out ValidationException validationException, bool validateAllProperties = True)
- [DateTimeHelper](Atc.Helpers.md#datetimehelper)
- Static Methods
- - TryParseShortDateUsingCurrentUiCulture(string value, out DateTime result)
+ - TryParseShortDateUi(string value, out DateTime result)
- TryParseShortDateUsingSpecificCulture(string value, CultureInfo cultureInfo, out DateTime result)
- - TryParseShortTimeUsingCurrentUiCulture(string value, out DateTime result)
- - TryParseShortTimeUsingCurrentUiCultureUtc(string value, out DateTime result)
+ - TryParseShortTimeUi(string value, out DateTime result)
+ - TryParseShortTimeUiUtc(string value, out DateTime result)
- TryParseShortTimeUsingSpecificCulture(string value, CultureInfo cultureInfo, out DateTime result)
- TryParseShortTimeUsingSpecificCultureUtc(string value, CultureInfo cultureInfo, out DateTime result)
- - TryParseUsingCurrentUiCulture(string value, out DateTime result)
+ - TryParseUi(string value, out DateTime result)
- TryParseUsingSpecificCulture(string value, CultureInfo cultureInfo, out DateTime result)
- [DateTimeOffsetHelper](Atc.Helpers.md#datetimeoffsethelper)
- Static Methods
- - TryParseShortDateUsingCurrentUiCulture(string value, out DateTime result)
- - TryParseShortTimeUsingCurrentUiCulture(string value, out DateTime result)
- - TryParseShortTimeUsingCurrentUiCultureUtc(string value, out DateTime result)
- - TryParseUsingCurrentUiCulture(string value, out DateTime result)
+ - TryParseShortDateUi(string value, out DateTime result)
+ - TryParseShortDateUsingSpecificCulture(string value, CultureInfo cultureInfo, out DateTime result)
+ - TryParseShortTimeUi(string value, out DateTime result)
+ - TryParseShortTimeUiUtc(string value, out DateTime result)
+ - TryParseShortTimeUsingSpecificCulture(string value, CultureInfo cultureInfo, out DateTime result)
+ - TryParseShortTimeUsingSpecificCultureUtc(string value, CultureInfo cultureInfo, out DateTime result)
+ - TryParseUi(string value, out DateTime result)
+ - TryParseUsingSpecificCulture(string value, CultureInfo cultureInfo, out DateTime result)
- [DayOfWeekHelper](Atc.Helpers.md#dayofweekhelper)
- Static Methods
- GetDescription(DayOfWeek dayOfWeek, CultureInfo culture = null)
@@ -4536,6 +4545,7 @@
- ConvertEnumToArray(Type enumType, DropDownFirstItemType dropDownFirstItemType = None, bool useDescriptionAttribute = False, bool includeDefault = True, SortDirectionType sortDirectionType = None, bool byFlagIncludeBase = True, bool byFlagIncludeCombined = True)
- ConvertEnumToDictionary(Type enumType, DropDownFirstItemType dropDownFirstItemType = None, bool useDescriptionAttribute = False, bool includeDefault = True, SortDirectionType sortDirectionType = None, bool byFlagIncludeBase = True, bool byFlagIncludeCombined = True)
- ConvertEnumToDictionaryWithStringKey(Type enumType, DropDownFirstItemType dropDownFirstItemType = None, bool useDescriptionAttribute = False, bool includeDefault = True, SortDirectionType sortDirectionType = None, bool byFlagIncludeBase = True, bool byFlagIncludeCombined = True)
+ - ConvertEnumToReadOnlyDictionary(bool includeDefault = True, bool byFlagIncludeBase = True, bool byFlagIncludeCombined = True)
- GetDescription(Enum enumeration)
- GetEnumValue(string value, bool ignoreCase = True)
- GetIndividualValues(bool includeDefault = True)
@@ -4648,11 +4658,17 @@
- [NetworkInformationHelper](Atc.Helpers.md#networkinformationhelper)
- Static Methods
- GetPublicIpAddress()
+ - GetPublicIpAddressAsync(CancellationToken cancellationToken = null)
- HasConnection()
- HasConnection(IPAddress ipAddress)
+ - HasConnectionAsync(CancellationToken cancellationToken = null)
+ - HasConnectionAsync(IPAddress ipAddress, CancellationToken cancellationToken = null)
- HasHttpConnection()
- HasHttpConnection(Uri uri)
+ - HasHttpConnectionAsync(CancellationToken cancellationToken = null)
+ - HasHttpConnectionAsync(Uri uri, CancellationToken cancellationToken = null)
- HasTcpConnection(IPAddress ipAddress, int port)
+ - HasTcpConnectionAsync(IPAddress ipAddress, int port, CancellationToken cancellationToken = null)
- [NumberHelper](Atc.Helpers.md#numberhelper)
- Static Methods
- IsDecimal(string value)
@@ -4712,6 +4728,8 @@
- ExecuteAsync(FileInfo fileInfo, string arguments, bool runAsAdministrator = False, ushort timeoutInSec = 30, CancellationToken cancellationToken = null)
- ExecuteAsync(ProcessStartInfo startInfo, ushort timeoutInSec = 30, CancellationToken cancellationToken = null)
- ExecutePrompt(DirectoryInfo workingDirectory, FileInfo fileInfo, string arguments, string[] inputLines, bool runAsAdministrator = False, ushort timeoutInSec = 1, CancellationToken cancellationToken = null)
+ - ExecuteWithSeparateOutput(DirectoryInfo workingDirectory, FileInfo fileInfo, string arguments, bool runAsAdministrator = False, ushort timeoutInSec = 30, CancellationToken cancellationToken = null)
+ - ExecuteWithSeparateOutput(FileInfo fileInfo, string arguments, bool runAsAdministrator = False, ushort timeoutInSec = 30, CancellationToken cancellationToken = null)
- KillById(int processId, int timeoutInSec = 30)
- KillByName(string processName, bool allowMultiKill = True, int timeoutInSec = 30)
- KillEntryCaller(int timeoutInSec = 30)
@@ -4796,13 +4814,16 @@
- [DistanceMeasurementType](Atc.Math.GeoSpatial.md#distancemeasurementtype)
- [GeoSpatialHelper](Atc.Math.GeoSpatial.md#geospatialhelper)
- Static Methods
+ - Bearing(CartesianCoordinate coordinate1, CartesianCoordinate coordinate2)
+ - Bearing(double longitude1, double latitude1, double longitude2, double latitude2)
- Distance(CartesianCoordinate coordinate1, CartesianCoordinate coordinate2, DistanceMeasurementType measurement)
- - Distance(double longitude1, double latitude1, double longitude2, double latitude2, DistanceMeasurementType measurement = Kilometers)
+ - Distance(double longitude1, double latitude1, double longitude2, double latitude2, DistanceMeasurementType measurement = Kilometers, double earthRadiusKm = 6371)
- [ReferenceEllipsoidType](Atc.Math.GeoSpatial.md#referenceellipsoidtype)
- [UniversalTransverseMercatorConverter](Atc.Math.GeoSpatial.md#universaltransversemercatorconverter)
- Methods
- ToUtm(CartesianCoordinate coordinate)
- ToUtm(double latitude, double longitude)
+ - ToWgs84(UniversalTransverseMercatorResult utmResult, int maxDecimalPrecision = 8)
- ToWgs84(int utmZoneNumber, string utmZoneLetter, double utmEasting, double utmNorthing, int maxDecimalPrecision = 8)
- [UniversalTransverseMercatorResult](Atc.Math.GeoSpatial.md#universaltransversemercatorresult)
- Properties
@@ -4880,7 +4901,15 @@
- UseConverterUnixDatetimeOffset
- UseConverterVersion
- WriteIndented
+- [JsonSerializerHelper](Atc.Serialization.md#jsonserializerhelper)
+ - Static Methods
+ - DeserializeFromStreamAsync(Stream stream, CancellationToken cancellationToken = null)
+ - DeserializeFromStreamAsync(Stream stream, JsonSerializerOptions options, CancellationToken cancellationToken = null)
+ - SerializeToStreamAsync(T value, Stream stream, CancellationToken cancellationToken = null)
+ - SerializeToStreamAsync(T value, Stream stream, JsonSerializerOptions options, CancellationToken cancellationToken = null)
- [JsonSerializerOptionsFactory](Atc.Serialization.md#jsonserializeroptionsfactory)
+ - Static Properties
+ - Default
- Static Methods
- Create(JsonSerializerFactorySettings settings)
- Create(bool useCamelCase = True, bool ignoreNullValues = True, bool propertyNameCaseInsensitive = True, bool writeIndented = True)
@@ -4966,9 +4995,14 @@
## [Atc.Units.DigitalInformation](Atc.Units.DigitalInformation.md)
- [ByteSize](Atc.Units.DigitalInformation.md#bytesize)
+ - Static Methods
+ - Parse(string value)
+ - TryParse(string value, out byte result)
- Properties
- Value
- Methods
+ - CompareTo(ByteSize other)
+ - CompareTo(object obj)
- Equals(ByteSize other)
- Equals(object obj)
- Format()
@@ -5062,53 +5096,74 @@
- [CharExtensions](System.md#charextensions)
- Static Methods
- IsAscii(this char value)
+ - IsAsciiDigit(this char value)
+ - IsAsciiLetter(this char value)
+ - IsHexDigit(this char value)
+ - IsVowel(this char value)
- [ConfigurationException](System.md#configurationexception)
+ - Static Methods
+ - ThrowIfInvalid(bool condition, string section, string key)
+ - ThrowIfMissing(string value, string section, string key)
- [DateTimeExtensions](System.md#datetimeextensions)
- Static Methods
- DateTimeDiff(this DateTime startDate, DateTime endDate, DateTimeDiffCompareType howToCompare)
+ - EndOfDay(this DateTime dateTime)
+ - EndOfMonth(this DateTime dateTime)
- GetPrettyTimeDiff(this DateTime startDate, DateTime endDate, int decimalPrecision = 3)
- GetPrettyTimeDiff(this DateTime startDate, int decimalPrecision = 3)
- GetWeekNumber(this DateTime date)
+ - GetWeekNumberUi(this DateTime date)
- IsBetween(this DateTime date, DateTime startDate, DateTime endDate)
+ - IsWeekend(this DateTime dateTime)
+ - StartOfDay(this DateTime dateTime)
+ - StartOfMonth(this DateTime dateTime)
- ToIso8601Date(this DateTime dateTime)
- ToIso8601UtcDate(this DateTime dateTime)
- ToLongDateString(this DateTime dateTime, DateTimeFormatInfo dateTimeFormatInfo)
- - ToLongDateStringUsingCurrentUiCulture(this DateTime dateTime)
+ - ToLongDateStringUi(this DateTime dateTime)
- ToLongDateStringUsingSpecificCulture(this DateTime dateTime, CultureInfo cultureInfo)
- ToLongTimeString(this DateTime dateTime, DateTimeFormatInfo dateTimeFormatInfo)
- - ToLongTimeStringUsingCurrentUiCulture(this DateTime dateTime)
+ - ToLongTimeStringUi(this DateTime dateTime)
- ToLongTimeStringUsingSpecificCulture(this DateTime dateTime, CultureInfo cultureInfo)
- ToShortDateString(this DateTime dateTime, DateTimeFormatInfo dateTimeFormatInfo)
- - ToShortDateStringUsingCurrentUiCulture(this DateTime dateTime)
+ - ToShortDateStringUi(this DateTime dateTime)
- ToShortDateStringUsingSpecificCulture(this DateTime dateTime, CultureInfo cultureInfo)
- ToShortTimeString(this DateTime dateTime, DateTimeFormatInfo dateTimeFormatInfo)
- - ToShortTimeStringUsingCurrentUiCulture(this DateTime dateTime)
+ - ToShortTimeStringUi(this DateTime dateTime)
- ToShortTimeStringUsingSpecificCulture(this DateTime dateTime, CultureInfo cultureInfo)
- [DateTimeOffsetExtensions](System.md#datetimeoffsetextensions)
- Static Methods
- DateTimeDiff(this DateTimeOffset startDate, DateTimeOffset endDate, DateTimeDiffCompareType howToCompare)
+ - EndOfDay(this DateTimeOffset dateTimeOffset)
+ - EndOfMonth(this DateTimeOffset dateTimeOffset)
- GetPrettyTimeDiff(this DateTimeOffset startDate, DateTimeOffset endDate, int decimalPrecision = 3)
- GetPrettyTimeDiff(this DateTimeOffset startDate, int decimalPrecision = 3)
- GetWeekNumber(this DateTimeOffset date)
+ - GetWeekNumberUi(this DateTimeOffset date)
- IsBetween(this DateTimeOffset dateTimeOffset, DateTimeOffset startDate, DateTimeOffset endDate)
+ - IsWeekend(this DateTimeOffset dateTimeOffset)
- ResetToStartOfCurrentHour(this DateTimeOffset dateTimeOffset)
- SetHourAndMinutes(this DateTimeOffset dateTimeOffset, int hour, int minutes)
+ - StartOfDay(this DateTimeOffset dateTimeOffset)
+ - StartOfMonth(this DateTimeOffset dateTimeOffset)
- ToIso8601Date(this DateTimeOffset dateTimeOffset)
- ToIso8601UtcDate(this DateTimeOffset dateTimeOffset)
- ToLongDateString(this DateTimeOffset dateTimeOffset, DateTimeFormatInfo dateTimeFormatInfo)
- - ToLongDateStringUsingCurrentUiCulture(this DateTimeOffset dateTimeOffset)
+ - ToLongDateStringUi(this DateTimeOffset dateTimeOffset)
- ToLongTimeString(this DateTimeOffset dateTimeOffset, DateTimeFormatInfo dateTimeFormatInfo)
- - ToLongTimeStringUsingCurrentUiCulture(this DateTimeOffset dateTimeOffset)
+ - ToLongTimeStringUi(this DateTimeOffset dateTimeOffset)
- ToShortDateString(this DateTimeOffset dateTimeOffset, DateTimeFormatInfo dateTimeFormatInfo)
- - ToShortDateStringUsingCurrentUiCulture(this DateTimeOffset dateTimeOffset)
+ - ToShortDateStringUi(this DateTimeOffset dateTimeOffset)
+ - ToShortDateStringUsingSpecificCulture(this DateTimeOffset dateTimeOffset, CultureInfo cultureInfo)
- ToShortTimeString(this DateTimeOffset dateTimeOffset, DateTimeFormatInfo dateTimeFormatInfo)
- - ToShortTimeStringUsingCurrentUiCulture(this DateTimeOffset dateTimeOffset)
+ - ToShortTimeStringUi(this DateTimeOffset dateTimeOffset)
- ToUnixTime(this DateTimeOffset dateTimeOffset)
- [DecimalExtensions](System.md#decimalextensions)
- Static Methods
- CurrencyRounding(this decimal value)
- CurrencyRounding(this decimal value, int digits)
- CurrencyRoundingAsInteger(this decimal value)
+ - CurrencyRoundingUi(this decimal value)
- IsEqual(this decimal a, decimal b)
- IsEqual(this decimal a, decimal b, int decimalPrecision)
- IsEqual(this decimal? a, decimal? b)
@@ -5127,6 +5182,7 @@
- CurrencyRounding(this double value)
- CurrencyRounding(this double value, int digits)
- CurrencyRoundingAsInteger(this double value)
+ - CurrencyRoundingUi(this double value)
- GreaterThanOrClose(this double value1, double value2)
- IsEqual(this double a, double b)
- IsEqual(this double a, double b, int decimalPrecision)
@@ -5179,6 +5235,7 @@
- MapTo(this Enum source, TTarget? defaultValue = null)
- ToStringLowerCase(this Enum enumeration)
- ToStringUpperCase(this Enum enumeration)
+ - TryMapTo(this Enum source, out TTarget result)
- [ExceptionExtensions](System.md#exceptionextensions)
- Static Methods
- Flatten(this Exception exception, string message = , bool includeStackTrace = False)
@@ -5192,9 +5249,12 @@
- [IntegerExtensions](System.md#integerextensions)
- Static Methods
- GetFirstDayOfWeekNumberByYear(this int year, int weekNumber)
+ - GetFirstDayOfWeekNumberByYearUi(this int year, int weekNumber)
- GetLastDayOfWeekNumberByYear(this int year, int weekNumber)
- - GetMonthNameByMonthNumber(this int month, bool pascalCased = False)
+ - GetLastDayOfWeekNumberByYearUi(this int year, int weekNumber)
+ - GetMonthNameByMonthNumberUi(this int month, bool pascalCased = False)
- GetNumberOfWeeksByYear(this int year)
+ - GetNumberOfWeeksByYearUi(this int year)
- IsBinarySequence(this int number)
- IsEqual(this int? a, int? b)
- IsEven(this int number)
@@ -5205,6 +5265,7 @@
- Static Methods
- FromUnixTime(this long valueInSeconds)
- FromUnixTimeMs(this long valueInMs)
+ - IsBinarySequence(this long number)
- [NullException](System.md#nullexception)
- [ObjectExtensions](System.md#objectextensions)
- Static Methods
@@ -5306,10 +5367,16 @@
- IsFormatXml(this string value)
- IsGuid(this string value)
- IsGuid(this string value, out Guid output)
+ - IsHostName(this string value)
+ - IsIPAddress(this string value)
+ - IsIPv4Address(this string value)
+ - IsIPv6Address(this string value)
- IsKey(this string value)
- IsLengthEven(this string value)
+ - IsMacAddress(this string value)
- IsNumericOnly(this string value)
- IsPersonCprNumber(this string cprNumber)
+ - IsPort(this string value)
- IsSentence(this string value)
- IsStringFormatParametersBalanced(this string value, bool isNumeric = True)
- IsTrue(this string value)
@@ -5319,6 +5386,9 @@
- IsWord(this string value)
- [StringNullOrEmptyException](System.md#stringnulloremptyexception)
- [SwitchCaseDefaultException](System.md#switchcasedefaultexception)
+ - Static Methods
+ - Throw(Enum value)
+ - Throw(object value)
- [TaskExtensions](System.md#taskextensions)
- Static Methods
- Forget(this Task task)
@@ -5328,6 +5398,7 @@
- [TimeSpanExtensions](System.md#timespanextensions)
- Static Methods
- GetPrettyTime(this TimeSpan timeSpan, int decimalPrecision = 3)
+ - GetPrettyTimeUi(this TimeSpan timeSpan, int decimalPrecision = 3)
- Max(this TimeSpan t1, TimeSpan t2)
- Min(this TimeSpan t1, TimeSpan t2)
- RemoveMilliseconds(this TimeSpan timeSpan)
@@ -5495,8 +5566,11 @@
- [StreamExtensions](System.IO.md#streamextensions)
- Static Methods
- CopyToStream(this Stream stream, int bufferSize = 4096)
+ - CopyToStreamAsync(this Stream stream, int bufferSize = 4096, CancellationToken cancellationToken = null)
- ToBytes(this Stream stream)
+ - ToBytesAsync(this Stream stream, CancellationToken cancellationToken = null)
- ToStringData(this Stream stream)
+ - ToStringDataAsync(this Stream stream, CancellationToken cancellationToken = null)
## [System.Net](System.Net.md)
@@ -5521,6 +5595,12 @@
- GetResourceManagers(this Assembly assembly)
- GetTypesInheritingFromType(this Assembly assembly, Type type)
- IsDebugBuild(this Assembly assembly)
+- [ConstructorInfoExtensions](System.Reflection.md#constructorinfoextensions)
+ - Static Methods
+ - BeautifyName(this ConstructorInfo constructorInfo, bool useFullName = False, bool useHtmlFormat = False)
+- [EventInfoExtensions](System.Reflection.md#eventinfoextensions)
+ - Static Methods
+ - BeautifyName(this EventInfo eventInfo, bool useFullName = False, bool useHtmlFormat = False, bool includeEventHandlerType = False)
- [FieldInfoExtensions](System.Reflection.md#fieldinfoextensions)
- Static Methods
- BeautifyName(this FieldInfo fieldInfo, bool useFullName = False, bool useHtmlFormat = False, bool includeReturnType = False)
diff --git a/docs/CodeDoc/Atc/System.IO.md b/docs/CodeDoc/Atc/System.IO.md
index 4dd1788e..3336b81d 100644
--- a/docs/CodeDoc/Atc/System.IO.md
+++ b/docs/CodeDoc/Atc/System.IO.md
@@ -180,7 +180,7 @@ Extensions for the `System.IO.MemoryStream` class.
>
>Parameters:
> `stream` - The memory stream to convert.
-> `encoding` - The encoding to use for the conversion. If , Unicode encoding is used.
+> `encoding` - The encoding to use for the conversion. Defaults to when .
>
>Returns: A string representation of the memory stream content.
@@ -206,6 +206,18 @@ Extensions for the `System.IO.Stream` class.
> `bufferSize` - The size of the buffer used for copying. Defaults to 4096 bytes.
>
>Returns: A new `System.IO.MemoryStream` containing the copied data with position set to 0.
+#### CopyToStreamAsync
+>```csharp
+>Task CopyToStreamAsync(this Stream stream, int bufferSize = 4096, CancellationToken cancellationToken = null)
+>```
+>Summary: Asynchronously copies the contents of the stream to a new `System.IO.MemoryStream`.
+>
+>Parameters:
+> `stream` - The source stream to copy from. The stream position will be reset to 0 before copying if the stream supports seeking.
+> `bufferSize` - The size of the buffer used for copying. Defaults to 4096 bytes.
+> `cancellationToken` - A token to cancel the asynchronous operation.
+>
+>Returns: A `System.Threading.Tasks.Task`1` that represents the asynchronous operation, containing a new `System.IO.MemoryStream` with position set to 0.
#### ToBytes
>```csharp
>byte[] ToBytes(this Stream stream)
@@ -216,6 +228,17 @@ Extensions for the `System.IO.Stream` class.
> `stream` - The stream to read from. The stream position will be reset to 0 before reading.
>
>Returns: A byte array containing all data from the stream.
+#### ToBytesAsync
+>```csharp
+>Task ToBytesAsync(this Stream stream, CancellationToken cancellationToken = null)
+>```
+>Summary: Asynchronously reads all bytes from the stream and returns them as a byte array.
+>
+>Parameters:
+> `stream` - The stream to read from. The stream position will be reset to 0 before reading if the stream supports seeking.
+> `cancellationToken` - A token to cancel the asynchronous operation.
+>
+>Returns: A `System.Threading.Tasks.Task`1` that represents the asynchronous operation, containing a byte array with all data from the stream.
#### ToStringData
>```csharp
>string ToStringData(this Stream stream)
@@ -226,4 +249,15 @@ Extensions for the `System.IO.Stream` class.
> `stream` - The stream to read from. The stream position will be reset to 0 before reading.
>
>Returns: A string containing all text content from the stream.
+#### ToStringDataAsync
+>```csharp
+>Task ToStringDataAsync(this Stream stream, CancellationToken cancellationToken = null)
+>```
+>Summary: Asynchronously reads all content from the stream and converts it to a string using UTF-8 encoding.
+>
+>Parameters:
+> `stream` - The stream to read from. The stream position will be reset to 0 before reading if the stream supports seeking.
+> `cancellationToken` - A token to cancel the asynchronous operation.
+>
+>Returns: A `System.Threading.Tasks.Task`1` that represents the asynchronous operation, containing a string with all text content from the stream.
Generated by MarkdownCodeDoc version 1.2
diff --git a/docs/CodeDoc/Atc/System.Reflection.md b/docs/CodeDoc/Atc/System.Reflection.md
index 7d673ccd..b46595e2 100644
--- a/docs/CodeDoc/Atc/System.Reflection.md
+++ b/docs/CodeDoc/Atc/System.Reflection.md
@@ -41,7 +41,7 @@ Extensions for the `System.Reflection.Assembly` class.
>```csharp
>Version GetFileVersion(this Assembly assembly)
>```
->Summary: Gets the file version of the assembly.
+>Summary: Gets the file version of the assembly. Falls back to `System.Reflection.AssemblyFileVersionAttribute` when the assembly location is unavailable (e.g., single-file published apps where `System.Reflection.Assembly.Location` is empty).
>
>Parameters:
> `assembly` - The assembly to query.
@@ -81,6 +81,55 @@ Extensions for the `System.Reflection.Assembly` class.
+## ConstructorInfoExtensions
+Extensions for the `System.Reflection.ConstructorInfo` class.
+
+>```csharp
+>public static class ConstructorInfoExtensions
+>```
+
+### Static Methods
+
+#### BeautifyName
+>```csharp
+>string BeautifyName(this ConstructorInfo constructorInfo, bool useFullName = False, bool useHtmlFormat = False)
+>```
+>Summary: Returns a human-readable representation of the constructor signature, optionally using full type names and HTML formatting for the parameter types.
+>
+>Parameters:
+> `constructorInfo` - The constructor information.
+> `useFullName` - If , parameter types are rendered with their fully-qualified names.
+> `useHtmlFormat` - If , parameter type names are wrapped in HTML tags.
+>
+>Returns: A string of the form `.ctor(TypeA paramA, TypeB paramB)`.
+
+
+
+## EventInfoExtensions
+Extensions for the `System.Reflection.EventInfo` class.
+
+>```csharp
+>public static class EventInfoExtensions
+>```
+
+### Static Methods
+
+#### BeautifyName
+>```csharp
+>string BeautifyName(this EventInfo eventInfo, bool useFullName = False, bool useHtmlFormat = False, bool includeEventHandlerType = False)
+>```
+>Summary: Returns a human-readable representation of the event, optionally including the event handler type and using full type names or HTML formatting.
+>
+>Parameters:
+> `eventInfo` - The event information.
+> `useFullName` - If , the event-handler type is rendered with its fully-qualified name.
+> `useHtmlFormat` - If , the event-handler type name is wrapped in HTML tags.
+> `includeEventHandlerType` - If , the event-handler type is prepended to the name.
+>
+>Returns: A string such as `MyEvent` or `EventHandler MyEvent` when `includeEventHandlerType` is .
+
+
+
## FieldInfoExtensions
Extensions for the `System.Reflection.FieldInfo` class.
diff --git a/docs/CodeDoc/Atc/System.md b/docs/CodeDoc/Atc/System.md
index c60f9a68..a90f9390 100644
--- a/docs/CodeDoc/Atc/System.md
+++ b/docs/CodeDoc/Atc/System.md
@@ -558,6 +558,46 @@ Extensions for the `System.Char` type.
> `value` - The character to check.
>
>Returns: if the character value is less than or equal to 127 (ASCII range); otherwise, .
+#### IsAsciiDigit
+>```csharp
+>bool IsAsciiDigit(this char value)
+>```
+>Summary: Determines whether the specified character is an ASCII decimal digit (0–9).
+>
+>Parameters:
+> `value` - The character to check.
+>
+>Returns: if the character is a digit 0 through 9; otherwise, .
+#### IsAsciiLetter
+>```csharp
+>bool IsAsciiLetter(this char value)
+>```
+>Summary: Determines whether the specified character is an ASCII letter (A–Z or a–z).
+>
+>Parameters:
+> `value` - The character to check.
+>
+>Returns: if the character is an ASCII letter; otherwise, .
+#### IsHexDigit
+>```csharp
+>bool IsHexDigit(this char value)
+>```
+>Summary: Determines whether the specified character is a hexadecimal digit (0–9, A–F, a–f).
+>
+>Parameters:
+> `value` - The character to check.
+>
+>Returns: if the character is a valid hexadecimal digit; otherwise, .
+#### IsVowel
+>```csharp
+>bool IsVowel(this char value)
+>```
+>Summary: Determines whether the specified character is an ASCII vowel (A, E, I, O, U — case-insensitive).
+>
+>Parameters:
+> `value` - The character to check.
+>
+>Returns: if the character is one of A, E, I, O, U (upper or lower case); otherwise, .
@@ -568,6 +608,28 @@ The exception that is thrown when a configuration error occurs, such as missing
>public class ConfigurationException : Exception, ISerializable
>```
+### Static Methods
+
+#### ThrowIfInvalid
+>```csharp
+>void ThrowIfInvalid(bool condition, string section, string key)
+>```
+>Summary: Throws a `System.ConfigurationException` if `condition` is .
+>
+>Parameters:
+> `condition` - The condition that indicates the configuration value is invalid.
+> `section` - The configuration section name.
+> `key` - The configuration key name.
+#### ThrowIfMissing
+>```csharp
+>void ThrowIfMissing(string value, string section, string key)
+>```
+>Summary: Throws a `System.ConfigurationException` if `value` is or empty.
+>
+>Parameters:
+> `value` - The configuration value to validate.
+> `section` - The configuration section name.
+> `key` - The configuration key name.
@@ -592,6 +654,26 @@ Extensions for the `System.DateTime` class.
> `howToCompare` - The how to compare.
>
>Returns: The number between start date and end date, depend on the DiffCompareType.
+#### EndOfDay
+>```csharp
+>DateTime EndOfDay(this DateTime dateTime)
+>```
+>Summary: Returns a new `System.DateTime` set to the very end of the same day (23:59:59.9999999). The `System.DateTime.Kind` of the result matches the input.
+>
+>Parameters:
+> `dateTime` - The date value.
+>
+>Returns: The last representable tick of `dateTime`'s date.
+#### EndOfMonth
+>```csharp
+>DateTime EndOfMonth(this DateTime dateTime)
+>```
+>Summary: Returns a new `System.DateTime` set to the last tick of the last day of the same month.
+>
+>Parameters:
+> `dateTime` - The date value.
+>
+>Returns: The last representable tick of the last day of the month containing `dateTime`.
#### GetPrettyTimeDiff
>```csharp
>string GetPrettyTimeDiff(this DateTime startDate, int decimalPrecision = 3)
@@ -620,6 +702,16 @@ Extensions for the `System.DateTime` class.
> `date` - The date.
>
>Returns: The week number from the given date.
+#### GetWeekNumberUi
+>```csharp
+>int GetWeekNumberUi(this DateTime date)
+>```
+>Summary: Gets the week number from a given date using the current UI culture's calendar. Use this variant when rendering the week number for display in a user interface.
+>
+>Parameters:
+> `date` - The date.
+>
+>Returns: The week number from the given date.
#### IsBetween
>```csharp
>bool IsBetween(this DateTime date, DateTime startDate, DateTime endDate)
@@ -632,6 +724,36 @@ Extensions for the `System.DateTime` class.
> `endDate` - End date to check for.
>
>Returns: boolean value indicating if the date is between or equal to one of the two values.
+#### IsWeekend
+>```csharp
+>bool IsWeekend(this DateTime dateTime)
+>```
+>Summary: Determines whether the specified date falls on a Saturday or Sunday.
+>
+>Parameters:
+> `dateTime` - The date to test.
+>
+>Returns: if the day of the week is `System.DayOfWeek.Saturday` or `System.DayOfWeek.Sunday`; otherwise, .
+#### StartOfDay
+>```csharp
+>DateTime StartOfDay(this DateTime dateTime)
+>```
+>Summary: Returns a new `System.DateTime` set to the very start of the same day (00:00:00.000). The `System.DateTime.Kind` of the result matches the input.
+>
+>Parameters:
+> `dateTime` - The date value.
+>
+>Returns: Midnight at the start of `dateTime`'s date.
+#### StartOfMonth
+>```csharp
+>DateTime StartOfMonth(this DateTime dateTime)
+>```
+>Summary: Returns a new `System.DateTime` set to the first day of the same month at midnight (00:00:00.000).
+>
+>Parameters:
+> `dateTime` - The date value.
+>
+>Returns: The first day of the month containing `dateTime`.
#### ToIso8601Date
>```csharp
>string ToIso8601Date(this DateTime dateTime)
@@ -664,11 +786,11 @@ Extensions for the `System.DateTime` class.
> `dateTimeFormatInfo` - The DateTimeFormatInfo specifying the format to use.
>
>Returns: A string representation of the DateTime using the long date pattern of the provided DateTimeFormatInfo.
-#### ToLongDateStringUsingCurrentUiCulture
+#### ToLongDateStringUi
>```csharp
->string ToLongDateStringUsingCurrentUiCulture(this DateTime dateTime)
+>string ToLongDateStringUi(this DateTime dateTime)
>```
->Summary: Converts a DateTime to a string using the long date pattern of the current UI culture.
+>Summary: Converts a DateTime to a string using the long date pattern of the current UI culture. Use this variant when rendering output for display in a user interface.
>
>Parameters:
> `dateTime` - The DateTime to format.
@@ -696,11 +818,11 @@ Extensions for the `System.DateTime` class.
> `dateTimeFormatInfo` - The DateTimeFormatInfo specifying the format to use.
>
>Returns: A string representation of the DateTime using the long time pattern of the provided DateTimeFormatInfo.
-#### ToLongTimeStringUsingCurrentUiCulture
+#### ToLongTimeStringUi
>```csharp
->string ToLongTimeStringUsingCurrentUiCulture(this DateTime dateTime)
+>string ToLongTimeStringUi(this DateTime dateTime)
>```
->Summary: Converts a DateTime to a string using the long time pattern of the current UI culture.
+>Summary: Converts a DateTime to a string using the long time pattern of the current UI culture. Use this variant when rendering output for display in a user interface.
>
>Parameters:
> `dateTime` - The DateTime to format.
@@ -728,11 +850,11 @@ Extensions for the `System.DateTime` class.
> `dateTimeFormatInfo` - The DateTimeFormatInfo specifying the format to use.
>
>Returns: A string representation of the DateTime using the short date pattern of the provided DateTimeFormatInfo.
-#### ToShortDateStringUsingCurrentUiCulture
+#### ToShortDateStringUi
>```csharp
->string ToShortDateStringUsingCurrentUiCulture(this DateTime dateTime)
+>string ToShortDateStringUi(this DateTime dateTime)
>```
->Summary: Converts a DateTime to a string using the short date pattern of the current UI culture.
+>Summary: Converts a DateTime to a string using the short date pattern of the current UI culture. Use this variant when rendering output for display in a user interface.
>
>Parameters:
> `dateTime` - The DateTime to format.
@@ -760,11 +882,11 @@ Extensions for the `System.DateTime` class.
> `dateTimeFormatInfo` - The DateTimeFormatInfo specifying the format to use.
>
>Returns: A string representation of the DateTime using the short time pattern of the provided DateTimeFormatInfo.
-#### ToShortTimeStringUsingCurrentUiCulture
+#### ToShortTimeStringUi
>```csharp
->string ToShortTimeStringUsingCurrentUiCulture(this DateTime dateTime)
+>string ToShortTimeStringUi(this DateTime dateTime)
>```
->Summary: Converts a DateTime to a string using the short time pattern of the current UI culture.
+>Summary: Converts a DateTime to a string using the short time pattern of the current UI culture. Use this variant when rendering output for display in a user interface.
>
>Parameters:
> `dateTime` - The DateTime to format.
@@ -805,6 +927,26 @@ Extensions for the `System.DateTimeOffset` class.
> `howToCompare` - The how to compare.
>
>Returns: The number between start date and end date, depend on the DiffCompareType.
+#### EndOfDay
+>```csharp
+>DateTimeOffset EndOfDay(this DateTimeOffset dateTimeOffset)
+>```
+>Summary: Returns a new `System.DateTimeOffset` set to the very end of the same day (23:59:59.9999999), preserving the original `System.DateTimeOffset.Offset`.
+>
+>Parameters:
+> `dateTimeOffset` - The date-time value.
+>
+>Returns: The last representable tick of the date, with the same UTC offset.
+#### EndOfMonth
+>```csharp
+>DateTimeOffset EndOfMonth(this DateTimeOffset dateTimeOffset)
+>```
+>Summary: Returns a new `System.DateTimeOffset` set to the last tick of the last day of the same month, preserving the original `System.DateTimeOffset.Offset`.
+>
+>Parameters:
+> `dateTimeOffset` - The date-time value.
+>
+>Returns: The last representable tick of the month, with the same UTC offset.
#### GetPrettyTimeDiff
>```csharp
>string GetPrettyTimeDiff(this DateTimeOffset startDate, int decimalPrecision = 3)
@@ -833,6 +975,16 @@ Extensions for the `System.DateTimeOffset` class.
> `date` - The date.
>
>Returns: The week number from the given date.
+#### GetWeekNumberUi
+>```csharp
+>int GetWeekNumberUi(this DateTimeOffset date)
+>```
+>Summary: Gets the week number from a given date using the current UI culture's calendar. Use this variant when rendering the week number for display in a user interface.
+>
+>Parameters:
+> `date` - The date.
+>
+>Returns: The week number from the given date.
#### IsBetween
>```csharp
>bool IsBetween(this DateTimeOffset dateTimeOffset, DateTimeOffset startDate, DateTimeOffset endDate)
@@ -845,6 +997,16 @@ Extensions for the `System.DateTimeOffset` class.
> `endDate` - End date to check for.
>
>Returns: boolean value indicating if the date is between or equal to one of the two values.
+#### IsWeekend
+>```csharp
+>bool IsWeekend(this DateTimeOffset dateTimeOffset)
+>```
+>Summary: Determines whether the date falls on a Saturday or Sunday.
+>
+>Parameters:
+> `dateTimeOffset` - The date to test.
+>
+>Returns: if the day of week is `System.DayOfWeek.Saturday` or `System.DayOfWeek.Sunday`; otherwise, .
#### ResetToStartOfCurrentHour
>```csharp
>DateTimeOffset ResetToStartOfCurrentHour(this DateTimeOffset dateTimeOffset)
@@ -867,6 +1029,26 @@ Extensions for the `System.DateTimeOffset` class.
> `minutes` - The minutes.
>
>Returns: The dateTimeOffset with the specified hour and minutes.
+#### StartOfDay
+>```csharp
+>DateTimeOffset StartOfDay(this DateTimeOffset dateTimeOffset)
+>```
+>Summary: Returns a new `System.DateTimeOffset` set to the very start of the same day (00:00:00.000), preserving the original `System.DateTimeOffset.Offset`.
+>
+>Parameters:
+> `dateTimeOffset` - The date-time value.
+>
+>Returns: Midnight at the start of the date, with the same UTC offset.
+#### StartOfMonth
+>```csharp
+>DateTimeOffset StartOfMonth(this DateTimeOffset dateTimeOffset)
+>```
+>Summary: Returns a new `System.DateTimeOffset` set to the first day of the same month at midnight, preserving the original `System.DateTimeOffset.Offset`.
+>
+>Parameters:
+> `dateTimeOffset` - The date-time value.
+>
+>Returns: The first day of the month, with the same UTC offset.
#### ToIso8601Date
>```csharp
>string ToIso8601Date(this DateTimeOffset dateTimeOffset)
@@ -894,16 +1076,16 @@ Extensions for the `System.DateTimeOffset` class.
> `dateTimeFormatInfo` - The DateTimeFormatInfo specifying the format to use.
>
>Returns: A string representation of the DateTime using the long date pattern of the provided DateTimeFormatInfo.
-#### ToLongDateStringUsingCurrentUiCulture
+#### ToLongDateStringUi
>```csharp
->string ToLongDateStringUsingCurrentUiCulture(this DateTimeOffset dateTimeOffset)
+>string ToLongDateStringUi(this DateTimeOffset dateTimeOffset)
>```
->Summary: Converts a DateTime to a string using the long date pattern of the current UI culture.
+>Summary: Converts a DateTimeOffset to a string using the long date pattern of the current UI culture. Use this variant when rendering output for display in a user interface.
>
>Parameters:
> `dateTimeOffset` - The DateTimeOffset to format.
>
->Returns: A string representation of the DateTime using the long date pattern of the current UI culture.
+>Returns: A string representation of the DateTimeOffset using the long date pattern of the current UI culture.
#### ToLongTimeString
>```csharp
>string ToLongTimeString(this DateTimeOffset dateTimeOffset, DateTimeFormatInfo dateTimeFormatInfo)
@@ -915,16 +1097,16 @@ Extensions for the `System.DateTimeOffset` class.
> `dateTimeFormatInfo` - The DateTimeFormatInfo specifying the format to use.
>
>Returns: A string representation of the DateTime using the long time pattern of the provided DateTimeFormatInfo.
-#### ToLongTimeStringUsingCurrentUiCulture
+#### ToLongTimeStringUi
>```csharp
->string ToLongTimeStringUsingCurrentUiCulture(this DateTimeOffset dateTimeOffset)
+>string ToLongTimeStringUi(this DateTimeOffset dateTimeOffset)
>```
->Summary: Converts a DateTime to a string using the long time pattern of the current UI culture.
+>Summary: Converts a DateTimeOffset to a string using the long time pattern of the current UI culture. Use this variant when rendering output for display in a user interface.
>
>Parameters:
> `dateTimeOffset` - The DateTimeOffset to format.
>
->Returns: A string representation of the DateTime using the long time pattern of the current UI culture.
+>Returns: A string representation of the DateTimeOffset using the long time pattern of the current UI culture.
#### ToShortDateString
>```csharp
>string ToShortDateString(this DateTimeOffset dateTimeOffset, DateTimeFormatInfo dateTimeFormatInfo)
@@ -936,16 +1118,27 @@ Extensions for the `System.DateTimeOffset` class.
> `dateTimeFormatInfo` - The DateTimeFormatInfo specifying the format to use.
>
>Returns: A string representation of the DateTime using the short date pattern of the provided DateTimeFormatInfo.
-#### ToShortDateStringUsingCurrentUiCulture
+#### ToShortDateStringUi
>```csharp
->string ToShortDateStringUsingCurrentUiCulture(this DateTimeOffset dateTimeOffset)
+>string ToShortDateStringUi(this DateTimeOffset dateTimeOffset)
>```
->Summary: Converts a DateTime to a string using the short date pattern of the current UI culture.
+>Summary: Converts a DateTimeOffset to a string using the short date pattern of the current UI culture. Use this variant when rendering output for display in a user interface.
>
>Parameters:
> `dateTimeOffset` - The DateTimeOffset to format.
>
->Returns: A string representation of the DateTime using the short date pattern of the current UI culture.
+>Returns: A string representation of the DateTimeOffset using the short date pattern of the current UI culture.
+#### ToShortDateStringUsingSpecificCulture
+>```csharp
+>string ToShortDateStringUsingSpecificCulture(this DateTimeOffset dateTimeOffset, CultureInfo cultureInfo)
+>```
+>Summary: Converts a `System.DateTimeOffset` to a string using the short date pattern of the specified culture.
+>
+>Parameters:
+> `dateTimeOffset` - The DateTimeOffset to format.
+> `cultureInfo` - The culture whose short date pattern is used.
+>
+>Returns: A string representation of the date using the short date pattern of `cultureInfo`.
#### ToShortTimeString
>```csharp
>string ToShortTimeString(this DateTimeOffset dateTimeOffset, DateTimeFormatInfo dateTimeFormatInfo)
@@ -957,16 +1150,16 @@ Extensions for the `System.DateTimeOffset` class.
> `dateTimeFormatInfo` - The DateTimeFormatInfo specifying the format to use.
>
>Returns: A string representation of the DateTime using the short time pattern of the provided DateTimeFormatInfo.
-#### ToShortTimeStringUsingCurrentUiCulture
+#### ToShortTimeStringUi
>```csharp
->string ToShortTimeStringUsingCurrentUiCulture(this DateTimeOffset dateTimeOffset)
+>string ToShortTimeStringUi(this DateTimeOffset dateTimeOffset)
>```
->Summary: Converts a DateTime to a string using the short time pattern of the current UI culture.
+>Summary: Converts a DateTimeOffset to a string using the short time pattern of the current UI culture. Use this variant when rendering output for display in a user interface.
>
>Parameters:
> `dateTimeOffset` - The DateTimeOffset to format.
>
->Returns: A string representation of the DateTime using the short time pattern of the current UI culture.
+>Returns: A string representation of the DateTimeOffset using the short time pattern of the current UI culture.
#### ToUnixTime
>```csharp
>long ToUnixTime(this DateTimeOffset dateTimeOffset)
@@ -1004,7 +1197,7 @@ Extensions for the `System.Decimal` class.
>```csharp
>decimal CurrencyRounding(this decimal value)
>```
->Summary: Rounds a decimal value using the currency decimal digits of the current UI culture.
+>Summary: Rounds a decimal value using the currency decimal digits of the current culture.
>
>Parameters:
> `value` - The decimal value to round.
@@ -1014,7 +1207,7 @@ Extensions for the `System.Decimal` class.
>```csharp
>decimal CurrencyRounding(this decimal value, int digits)
>```
->Summary: Rounds a decimal value using the currency decimal digits of the current UI culture.
+>Summary: Rounds a decimal value using the currency decimal digits of the current culture.
>
>Parameters:
> `value` - The decimal value to round.
@@ -1030,6 +1223,16 @@ Extensions for the `System.Decimal` class.
> `value` - The decimal value to round.
>
>Returns: The rounded value as an integer.
+#### CurrencyRoundingUi
+>```csharp
+>decimal CurrencyRoundingUi(this decimal value)
+>```
+>Summary: Rounds a decimal value using the currency decimal digits of the current UI culture. Use this variant when rounding for display in a user interface.
+>
+>Parameters:
+> `value` - The decimal value to round.
+>
+>Returns: The rounded decimal value.
#### IsEqual
>```csharp
>bool IsEqual(this decimal a, decimal b)
@@ -1142,7 +1345,7 @@ Extensions for the `System.Double` class.
>```csharp
>double DoubleEpsilon
>```
->Summary: The double epsilon.
+>Summary: The tolerance used by `System.DoubleExtensions.IsEqual(System.Double,System.Double)` and `System.DoubleExtensions.IsZero(System.Double)`. Values whose absolute difference is at or below this threshold are considered equal.
### Static Methods
#### AreClose
@@ -1160,17 +1363,17 @@ Extensions for the `System.Double` class.
>```csharp
>int CountDecimalPoints(this double value)
>```
->Summary: Counts the number of decimal places in the double value.
+>Summary: Counts the number of decimal places in the double value, up to a maximum of 15 (the limit of meaningful precision for a 64-bit floating-point number).
>
>Parameters:
> `value` - The double value to analyze.
>
->Returns: The number of decimal places in the value.
+>Returns: The number of decimal places, capped at 15.
#### CurrencyRounding
>```csharp
>double CurrencyRounding(this double value)
>```
->Summary: Rounds a double value using the currency decimal digits of the current UI culture.
+>Summary: Rounds a double value using the currency decimal digits of the current culture.
>
>Parameters:
> `value` - The double value to round.
@@ -1180,7 +1383,7 @@ Extensions for the `System.Double` class.
>```csharp
>double CurrencyRounding(this double value, int digits)
>```
->Summary: Rounds a double value using the currency decimal digits of the current UI culture.
+>Summary: Rounds a double value using the currency decimal digits of the current culture.
>
>Parameters:
> `value` - The double value to round.
@@ -1196,6 +1399,16 @@ Extensions for the `System.Double` class.
> `value` - The double value to round.
>
>Returns: The rounded value as an integer.
+#### CurrencyRoundingUi
+>```csharp
+>double CurrencyRoundingUi(this double value)
+>```
+>Summary: Rounds a double value using the currency decimal digits of the current UI culture. Use this variant when rounding for display in a user interface.
+>
+>Parameters:
+> `value` - The double value to round.
+>
+>Returns: The rounded double value.
#### GreaterThanOrClose
>```csharp
>bool GreaterThanOrClose(this double value1, double value2)
@@ -1211,56 +1424,56 @@ Extensions for the `System.Double` class.
>```csharp
>bool IsEqual(this double a, double b)
>```
->Summary: Compare two values. Return if they are equals.
+>Summary: Determines whether two double values are approximately equal within a tolerance of `System.DoubleExtensions.DoubleEpsilon` (1e-9). This handles common floating-point arithmetic rounding, for example `0.1 + 0.2 == 0.3`. Use `System.DoubleExtensions.IsEqual(System.Double,System.Double,System.Int32)` when an exact decimal-precision comparison is needed.
>
>Parameters:
> `a` - The first value.
> `b` - The second value.
>
->Returns: if the two values are equals, otherwise.
+>Returns: if the absolute difference between `a` and `b` is at most `System.DoubleExtensions.DoubleEpsilon`; otherwise, .
#### IsEqual
>```csharp
>bool IsEqual(this double? a, double? b)
>```
->Summary: Compare two values. Return if they are equals.
+>Summary: Determines whether two double values are approximately equal within a tolerance of `System.DoubleExtensions.DoubleEpsilon` (1e-9). This handles common floating-point arithmetic rounding, for example `0.1 + 0.2 == 0.3`. Use `System.DoubleExtensions.IsEqual(System.Double,System.Double,System.Int32)` when an exact decimal-precision comparison is needed.
>
>Parameters:
> `a` - The first value.
> `b` - The second value.
>
->Returns: if the two values are equals, otherwise.
+>Returns: if the absolute difference between `a` and `b` is at most `System.DoubleExtensions.DoubleEpsilon`; otherwise, .
#### IsEqual
>```csharp
>bool IsEqual(this double a, double b, int decimalPrecision)
>```
->Summary: Compare two values. Return if they are equals.
+>Summary: Determines whether two double values are approximately equal within a tolerance of `System.DoubleExtensions.DoubleEpsilon` (1e-9). This handles common floating-point arithmetic rounding, for example `0.1 + 0.2 == 0.3`. Use `System.DoubleExtensions.IsEqual(System.Double,System.Double,System.Int32)` when an exact decimal-precision comparison is needed.
>
>Parameters:
> `a` - The first value.
> `b` - The second value.
>
->Returns: if the two values are equals, otherwise.
+>Returns: if the absolute difference between `a` and `b` is at most `System.DoubleExtensions.DoubleEpsilon`; otherwise, .
#### IsEqual
>```csharp
>bool IsEqual(this double? a, double? b, int decimalPrecision)
>```
->Summary: Compare two values. Return if they are equals.
+>Summary: Determines whether two double values are approximately equal within a tolerance of `System.DoubleExtensions.DoubleEpsilon` (1e-9). This handles common floating-point arithmetic rounding, for example `0.1 + 0.2 == 0.3`. Use `System.DoubleExtensions.IsEqual(System.Double,System.Double,System.Int32)` when an exact decimal-precision comparison is needed.
>
>Parameters:
> `a` - The first value.
> `b` - The second value.
>
->Returns: if the two values are equals, otherwise.
+>Returns: if the absolute difference between `a` and `b` is at most `System.DoubleExtensions.DoubleEpsilon`; otherwise, .
#### IsZero
>```csharp
>bool IsZero(this double value)
>```
->Summary: Determines whether the specified double value is approximately zero.
+>Summary: Determines whether the specified double value is approximately zero within a tolerance of `System.DoubleExtensions.DoubleEpsilon` (1e-9).
>
>Parameters:
> `value` - The value to check.
>
->Returns: if the absolute value is less than epsilon; otherwise, .
+>Returns: if the absolute value is at most `System.DoubleExtensions.DoubleEpsilon`; otherwise, .
#### RoundOff
>```csharp
>double RoundOff(this double value, int numberOfDecimals)
@@ -1688,13 +1901,13 @@ Extension methods for enumerations.
>```csharp
>bool IsSet(this Enum enumeration, Enum matchTo)
>```
->Summary: Determines whether the specified enumeration match another enumeration.
+>Summary: Determines whether all bits of `matchTo` are set in `enumeration`, equivalent to `enumeration.HasFlag(matchTo)`. Works for all underlying numeric types including -backed enumerations.
>
>Parameters:
-> `enumeration` - The enumeration.
-> `matchTo` - The enumeration to match.
+> `enumeration` - The enumeration to check.
+> `matchTo` - The flags that must all be present in .
>
->Returns: true on match; otherwise false.
+>Returns: if every bit in `matchTo` is also set in `enumeration`; otherwise, .
>
>Code usage:
>```csharp
@@ -1709,13 +1922,13 @@ Extension methods for enumerations.
>```csharp
>bool IsSet(this T enumeration, T flags)
>```
->Summary: Determines whether the specified enumeration match another enumeration.
+>Summary: Determines whether all bits of `matchTo` are set in `enumeration`, equivalent to `enumeration.HasFlag(matchTo)`. Works for all underlying numeric types including -backed enumerations.
>
>Parameters:
-> `enumeration` - The enumeration.
-> `matchTo` - The enumeration to match.
+> `enumeration` - The enumeration to check.
+> `matchTo` - The flags that must all be present in .
>
->Returns: true on match; otherwise false.
+>Returns: if every bit in `matchTo` is also set in `enumeration`; otherwise, .
>
>Code usage:
>```csharp
@@ -1753,6 +1966,19 @@ Extension methods for enumerations.
>
>Parameters:
> `enumeration` - The enum.
+#### TryMapTo
+>```csharp
+>bool TryMapTo(this Enum source, out TTarget result)
+>```
+>Summary: Tries to map the current enum value to a target enum type by matching the name (case-insensitive). Unlike `System.EnumExtensions.MapTo``1(System.Enum,System.Nullable{``0})`, this method never throws — it returns when no match is found.
+>
+>Parameters:
+> `source` - The source enum value.
+> `result` -
+ When this method returns , the matched target value; otherwise, the default value of .
+
+>
+>Returns: if a matching named value was found in `TTarget`; otherwise, .
@@ -1865,6 +2091,17 @@ Extensions for the `System.Int32` class.
> `weekNumber` - The week number.
>
>Returns: The date of the first day in the given year and week number.
+#### GetFirstDayOfWeekNumberByYearUi
+>```csharp
+>DateTime GetFirstDayOfWeekNumberByYearUi(this int year, int weekNumber)
+>```
+>Summary: Gets the date of the first day of a given week in a given year using the current UI culture's calendar. Use this variant when rendering dates for display in a user interface.
+>
+>Parameters:
+> `year` - The four-digit year.
+> `weekNumber` - The ISO week number (1–53).
+>
+>Returns: The `System.DateTime` of the Monday that starts the requested week.
#### GetLastDayOfWeekNumberByYear
>```csharp
>DateTime GetLastDayOfWeekNumberByYear(this int year, int weekNumber)
@@ -1876,11 +2113,22 @@ Extensions for the `System.Int32` class.
> `weekNumber` - The week number.
>
>Returns: The date of the last day in the given year and week number.
-#### GetMonthNameByMonthNumber
+#### GetLastDayOfWeekNumberByYearUi
>```csharp
->string GetMonthNameByMonthNumber(this int month, bool pascalCased = False)
+>DateTime GetLastDayOfWeekNumberByYearUi(this int year, int weekNumber)
>```
->Summary: Gets the month name by month number.
+>Summary: Gets the date of the last day of a given week in a given year using the current UI culture's calendar. Use this variant when rendering dates for display in a user interface.
+>
+>Parameters:
+> `year` - The four-digit year.
+> `weekNumber` - The ISO week number (1–53).
+>
+>Returns: The `System.DateTime` of the Sunday that ends the requested week.
+#### GetMonthNameByMonthNumberUi
+>```csharp
+>string GetMonthNameByMonthNumberUi(this int month, bool pascalCased = False)
+>```
+>Summary: Gets the month name by month number using the current UI culture. Use this variant when rendering output for display in a user interface.
>
>Parameters:
> `month` - The month.
@@ -1897,6 +2145,16 @@ Extensions for the `System.Int32` class.
> `year` - The year.
>
>Returns: The get number of weeks.
+#### GetNumberOfWeeksByYearUi
+>```csharp
+>int GetNumberOfWeeksByYearUi(this int year)
+>```
+>Summary: Returns the number of ISO weeks in the given year using the current UI culture's calendar. Use this variant when rendering the value for display in a user interface.
+>
+>Parameters:
+> `year` - The four-digit year.
+>
+>Returns: 52 or 53 depending on the year and calendar.
#### IsBinarySequence
>```csharp
>bool IsBinarySequence(this int number)
@@ -2023,6 +2281,16 @@ Extensions for the `System.Int64` class.
>long unixTime = 0; // Equivalent to 1-1-1970
>DateTimeOffset dateTimeOffset = unixTime.FromUnixTimeMs();
>```
+#### IsBinarySequence
+>```csharp
+>bool IsBinarySequence(this long number)
+>```
+>Summary: Determines whether the value is a binary sequence (a power of two), meaning exactly one bit is set.
+>
+>Parameters:
+> `number` - The number to evaluate.
+>
+>Returns: if `number` is a positive power of two; otherwise, .
@@ -2813,12 +3081,14 @@ Extensions for the string class.
>```csharp
>string XmlDecode(this string xml)
>```
->Summary: Decodes an XML string by unescaping special character entities (&, ', <, >, ").
+>Summary: Decodes an XML string by unescaping special character entities (&, ', <, >, ").
>
>Parameters:
> `xml` - The XML string to decode.
>
>Returns: The decoded XML string with special character entities replaced.
+>
+>Remarks: The ampersand entity (`&`) is decoded last so that already-decoded content is not re-interpreted, keeping `System.StringExtensions.XmlEncode(System.String)`/`System.StringExtensions.XmlDecode(System.String)` a faithful round-trip.
#### XmlEncode
>```csharp
>string XmlEncode(this string xml)
@@ -3026,6 +3296,48 @@ StringHasIsExtensions.
> `value` - The string to work on.
>
>Returns: if the specified string is a System.Guid; otherwise, .
+#### IsHostName
+>```csharp
+>bool IsHostName(this string value)
+>```
+>Summary: Determines whether the specified value is a syntactically valid DNS host name (RFC 1123).
+>
+>Parameters:
+> `value` - The string to validate.
+>
+>Returns: if the value is a valid host name; otherwise, .
+>
+>Remarks: Accepts single-label names (e.g. `localhost`) and an optional trailing dot (e.g. `example.com.`). Each label is 1-63 ASCII alphanumeric/hyphen characters and may not start or end with a hyphen; the total length is limited to 253 characters. Underscores and raw Unicode (non-punycode IDN) are not allowed. This is a purely syntactic check and does not perform any DNS resolution.
+#### IsIPAddress
+>```csharp
+>bool IsIPAddress(this string value)
+>```
+>Summary: Determines whether the specified value is a valid IPv4 or IPv6 address.
+>
+>Parameters:
+> `value` - The string to validate.
+>
+>Returns: if the value is a valid IP address; otherwise, .
+#### IsIPv4Address
+>```csharp
+>bool IsIPv4Address(this string value)
+>```
+>Summary: Determines whether the specified value is a valid IPv4 address.
+>
+>Parameters:
+> `value` - The string to validate.
+>
+>Returns: if the value is a valid IPv4 address; otherwise, .
+#### IsIPv6Address
+>```csharp
+>bool IsIPv6Address(this string value)
+>```
+>Summary: Determines whether the specified value is a valid IPv6 address.
+>
+>Parameters:
+> `value` - The string to validate.
+>
+>Returns: if the value is a valid IPv6 address; otherwise, .
#### IsKey
>```csharp
>bool IsKey(this string value)
@@ -3046,6 +3358,18 @@ StringHasIsExtensions.
> `value` - The string to work on.
>
>Returns: if the specified string length is even; otherwise, .
+#### IsMacAddress
+>```csharp
+>bool IsMacAddress(this string value)
+>```
+>Summary: Determines whether the specified value is a valid MAC address.
+>
+>Parameters:
+> `value` - The string to validate.
+>
+>Returns: if the value is a valid 48-bit MAC address; otherwise, .
+>
+>Remarks: Accepts the three most common notations: colon-separated (`AA:BB:CC:DD:EE:FF`), hyphen-separated (`AA-BB-CC-DD-EE-FF`), and dot-separated Cisco style (`AABB.CCDD.EEFF`), as well as the compact twelve-hex-digit form (`AABBCCDDEEFF`). The check is case-insensitive.
#### IsNumericOnly
>```csharp
>bool IsNumericOnly(this string value)
@@ -3066,6 +3390,16 @@ StringHasIsExtensions.
> `cprNumber` - The CPR number.
>
>Returns: if the specified person CPR number is a valid number; otherwise, .
+#### IsPort
+>```csharp
+>bool IsPort(this string value)
+>```
+>Summary: Determines whether the specified value is a valid TCP/UDP port number.
+>
+>Parameters:
+> `value` - The string to validate.
+>
+>Returns: if the value parses to an integer in the range 1–65535; otherwise, .
#### IsSentence
>```csharp
>bool IsSentence(this string value)
@@ -3156,6 +3490,24 @@ The exception that is thrown when an unexpected value is encountered in a switch
>public class SwitchCaseDefaultException : Exception, ISerializable
>```
+### Static Methods
+
+#### Throw
+>```csharp
+>void Throw(Enum value)
+>```
+>Summary: Throws a `System.SwitchCaseDefaultException` for the given enum value.
+>
+>Parameters:
+> `value` - The unexpected enum value encountered in the switch default case.
+#### Throw
+>```csharp
+>void Throw(object value)
+>```
+>Summary: Throws a `System.SwitchCaseDefaultException` for the given enum value.
+>
+>Parameters:
+> `value` - The unexpected enum value encountered in the switch default case.
@@ -3229,6 +3581,17 @@ Extensions for the `System.TimeSpan` class.
> `decimalPrecision` - The number of decimal places to display (default is 3).
>
>Returns: A formatted string representing the time in the most appropriate unit (days, hours, minutes, seconds, or milliseconds).
+#### GetPrettyTimeUi
+>```csharp
+>string GetPrettyTimeUi(this TimeSpan timeSpan, int decimalPrecision = 3)
+>```
+>Summary: Converts a TimeSpan to a human-readable string representation with appropriate time units, using the current UI culture for number formatting and unit label casing. Use this variant when rendering output for display in a user interface.
+>
+>Parameters:
+> `timeSpan` - The TimeSpan to format.
+> `decimalPrecision` - The number of decimal places to display (default is 3).
+>
+>Returns: A formatted string representing the time in the most appropriate unit (days, hours, minutes, seconds, or milliseconds).
#### Max
>```csharp
>TimeSpan Max(this TimeSpan t1, TimeSpan t2)
diff --git a/sample/Demo.Atc.Console.Spectre.Cli/Demo.Atc.Console.Spectre.Cli.csproj b/sample/Demo.Atc.Console.Spectre.Cli/Demo.Atc.Console.Spectre.Cli.csproj
index c1fac715..334a4dfd 100644
--- a/sample/Demo.Atc.Console.Spectre.Cli/Demo.Atc.Console.Spectre.Cli.csproj
+++ b/sample/Demo.Atc.Console.Spectre.Cli/Demo.Atc.Console.Spectre.Cli.csproj
@@ -8,10 +8,10 @@
-
-
-
-
+
+
+
+
diff --git a/sample/Demo.Atc.Dotnet.Cli/Demo.Atc.Dotnet.Cli.csproj b/sample/Demo.Atc.Dotnet.Cli/Demo.Atc.Dotnet.Cli.csproj
index d79c00bc..062ff94a 100644
--- a/sample/Demo.Atc.Dotnet.Cli/Demo.Atc.Dotnet.Cli.csproj
+++ b/sample/Demo.Atc.Dotnet.Cli/Demo.Atc.Dotnet.Cli.csproj
@@ -8,10 +8,10 @@
-
-
-
-
+
+
+
+
diff --git a/src/Atc.CodeAnalysis.CSharp/Extensions/EnumDeclarationSyntaxExtensions.cs b/src/Atc.CodeAnalysis.CSharp/Extensions/EnumDeclarationSyntaxExtensions.cs
index c55c6978..57aaa7bb 100644
--- a/src/Atc.CodeAnalysis.CSharp/Extensions/EnumDeclarationSyntaxExtensions.cs
+++ b/src/Atc.CodeAnalysis.CSharp/Extensions/EnumDeclarationSyntaxExtensions.cs
@@ -85,9 +85,16 @@ public static bool HasAttributeOfAttributeType(
throw new ArgumentNullException(nameof(attributeType));
}
- var attributeName = attributeType.Name.Replace("Attribute", string.Empty, StringComparison.Ordinal);
+ // Strip the conventional "Attribute" suffix so both [Flags] and [FlagsAttribute] match FlagsAttribute.
+ var shortName = attributeType.Name.Replace("Attribute", string.Empty, StringComparison.Ordinal);
+ var fullName = attributeType.Name; // "FlagsAttribute"
return enumDeclaration
.Select()
- .Any(x => attributeName.Equals(x.Name.ToString(), StringComparison.Ordinal));
+ .Any(x =>
+ {
+ var attrName = x.Name.ToString();
+ return attrName.Equals(shortName, StringComparison.Ordinal) ||
+ attrName.Equals(fullName, StringComparison.Ordinal);
+ });
}
}
\ No newline at end of file
diff --git a/src/Atc.CodeAnalysis.CSharp/Extensions/InterfaceDeclarationSyntaxExtensions.cs b/src/Atc.CodeAnalysis.CSharp/Extensions/InterfaceDeclarationSyntaxExtensions.cs
index a8c1ea51..8ad033c6 100644
--- a/src/Atc.CodeAnalysis.CSharp/Extensions/InterfaceDeclarationSyntaxExtensions.cs
+++ b/src/Atc.CodeAnalysis.CSharp/Extensions/InterfaceDeclarationSyntaxExtensions.cs
@@ -6,6 +6,51 @@ namespace Microsoft.CodeAnalysis.CSharp.Syntax;
///
public static class InterfaceDeclarationSyntaxExtensions
{
+ ///
+ /// Adds a to the interface declaration.
+ ///
+ /// The interface declaration to modify.
+ /// The suppress message attribute to add.
+ /// A new with the attribute added.
+ /// Thrown when or is null.
+ /// Thrown when the justification in is invalid.
+ public static InterfaceDeclarationSyntax AddSuppressMessageAttribute(
+ this InterfaceDeclarationSyntax interfaceDeclaration,
+ SuppressMessageAttribute suppressMessage)
+ {
+ if (interfaceDeclaration is null)
+ {
+ throw new ArgumentNullException(nameof(interfaceDeclaration));
+ }
+
+ if (suppressMessage is null)
+ {
+ throw new ArgumentNullException(nameof(suppressMessage));
+ }
+
+ if (string.IsNullOrEmpty(suppressMessage.Justification))
+ {
+ throw new ArgumentException("Justification is invalid.", nameof(suppressMessage));
+ }
+
+ var attributeArgumentList = SyntaxFactory.AttributeArgumentList(
+ SyntaxFactory.SeparatedList(
+ SyntaxFactory.NodeOrTokenList(
+ SyntaxFactory.AttributeArgument(SyntaxLiteralExpressionFactory.Create(suppressMessage.Category)),
+ SyntaxTokenFactory.Comma(),
+ SyntaxFactory.AttributeArgument(SyntaxLiteralExpressionFactory.Create(suppressMessage.CheckId)),
+ SyntaxTokenFactory.Comma(),
+ SyntaxFactory
+ .AttributeArgument(SyntaxLiteralExpressionFactory.Create(suppressMessage.Justification!))
+ .WithNameEquals(
+ SyntaxNameEqualsFactory
+ .Create(nameof(SuppressMessageAttribute.Justification))
+ .WithEqualsToken(SyntaxTokenFactory.Equals())))));
+
+ return interfaceDeclaration
+ .AddAttributeLists(SyntaxAttributeListFactory.Create(nameof(SuppressMessageAttribute), attributeArgumentList));
+ }
+
///
/// Adds a to the interface declaration.
///
diff --git a/src/Atc.CodeAnalysis.CSharp/Extensions/RecordDeclarationSyntaxExtensions.cs b/src/Atc.CodeAnalysis.CSharp/Extensions/RecordDeclarationSyntaxExtensions.cs
new file mode 100644
index 00000000..caeec03e
--- /dev/null
+++ b/src/Atc.CodeAnalysis.CSharp/Extensions/RecordDeclarationSyntaxExtensions.cs
@@ -0,0 +1,91 @@
+// ReSharper disable once CheckNamespace
+namespace Microsoft.CodeAnalysis.CSharp.Syntax;
+
+///
+/// Extension methods for .
+///
+public static class RecordDeclarationSyntaxExtensions
+{
+ ///
+ /// Adds a to the record declaration.
+ ///
+ /// The record declaration to modify.
+ /// The suppress message attribute to add.
+ /// A new with the attribute added.
+ /// Thrown when or is null.
+ /// Thrown when the justification in is invalid.
+ public static RecordDeclarationSyntax AddSuppressMessageAttribute(
+ this RecordDeclarationSyntax recordDeclaration,
+ SuppressMessageAttribute suppressMessage)
+ {
+ if (recordDeclaration is null)
+ {
+ throw new ArgumentNullException(nameof(recordDeclaration));
+ }
+
+ if (suppressMessage is null)
+ {
+ throw new ArgumentNullException(nameof(suppressMessage));
+ }
+
+ if (string.IsNullOrEmpty(suppressMessage.Justification))
+ {
+ throw new ArgumentException("Justification is invalid.", nameof(suppressMessage));
+ }
+
+ var attributeArgumentList = SyntaxFactory.AttributeArgumentList(
+ SyntaxFactory.SeparatedList(
+ SyntaxFactory.NodeOrTokenList(
+ SyntaxFactory.AttributeArgument(SyntaxLiteralExpressionFactory.Create(suppressMessage.Category)),
+ SyntaxTokenFactory.Comma(),
+ SyntaxFactory.AttributeArgument(SyntaxLiteralExpressionFactory.Create(suppressMessage.CheckId)),
+ SyntaxTokenFactory.Comma(),
+ SyntaxFactory
+ .AttributeArgument(SyntaxLiteralExpressionFactory.Create(suppressMessage.Justification!))
+ .WithNameEquals(
+ SyntaxNameEqualsFactory
+ .Create(nameof(SuppressMessageAttribute.Justification))
+ .WithEqualsToken(SyntaxTokenFactory.Equals())))));
+
+ return recordDeclaration
+ .AddAttributeLists(SyntaxAttributeListFactory.Create(nameof(SuppressMessageAttribute), attributeArgumentList));
+ }
+
+ ///
+ /// Adds a to the record declaration.
+ ///
+ /// The record declaration to modify.
+ /// The name of the code generation tool.
+ /// The version of the code generation tool.
+ /// A new with the attribute added.
+ /// Thrown when , , or is null.
+ public static RecordDeclarationSyntax AddGeneratedCodeAttribute(
+ this RecordDeclarationSyntax recordDeclaration,
+ string toolName,
+ string version)
+ {
+ if (recordDeclaration is null)
+ {
+ throw new ArgumentNullException(nameof(recordDeclaration));
+ }
+
+ if (toolName is null)
+ {
+ throw new ArgumentNullException(nameof(toolName));
+ }
+
+ if (version is null)
+ {
+ throw new ArgumentNullException(nameof(version));
+ }
+
+ var attributeArgumentList = SyntaxFactory.AttributeArgumentList(
+ SyntaxFactory.SeparatedList(SyntaxFactory.NodeOrTokenList(
+ SyntaxFactory.AttributeArgument(SyntaxLiteralExpressionFactory.Create(toolName)),
+ SyntaxTokenFactory.Comma(),
+ SyntaxFactory.AttributeArgument(SyntaxLiteralExpressionFactory.Create(version)))));
+
+ return recordDeclaration
+ .AddAttributeLists(SyntaxAttributeListFactory.Create(nameof(GeneratedCodeAttribute), attributeArgumentList));
+ }
+}
\ No newline at end of file
diff --git a/src/Atc.CodeAnalysis.CSharp/Extensions/StructDeclarationSyntaxExtensions.cs b/src/Atc.CodeAnalysis.CSharp/Extensions/StructDeclarationSyntaxExtensions.cs
new file mode 100644
index 00000000..82c186b4
--- /dev/null
+++ b/src/Atc.CodeAnalysis.CSharp/Extensions/StructDeclarationSyntaxExtensions.cs
@@ -0,0 +1,91 @@
+// ReSharper disable once CheckNamespace
+namespace Microsoft.CodeAnalysis.CSharp.Syntax;
+
+///
+/// Extension methods for .
+///
+public static class StructDeclarationSyntaxExtensions
+{
+ ///
+ /// Adds a to the struct declaration.
+ ///
+ /// The struct declaration to modify.
+ /// The suppress message attribute to add.
+ /// A new with the attribute added.
+ /// Thrown when or is null.
+ /// Thrown when the justification in is invalid.
+ public static StructDeclarationSyntax AddSuppressMessageAttribute(
+ this StructDeclarationSyntax structDeclaration,
+ SuppressMessageAttribute suppressMessage)
+ {
+ if (structDeclaration is null)
+ {
+ throw new ArgumentNullException(nameof(structDeclaration));
+ }
+
+ if (suppressMessage is null)
+ {
+ throw new ArgumentNullException(nameof(suppressMessage));
+ }
+
+ if (string.IsNullOrEmpty(suppressMessage.Justification))
+ {
+ throw new ArgumentException("Justification is invalid.", nameof(suppressMessage));
+ }
+
+ var attributeArgumentList = SyntaxFactory.AttributeArgumentList(
+ SyntaxFactory.SeparatedList(
+ SyntaxFactory.NodeOrTokenList(
+ SyntaxFactory.AttributeArgument(SyntaxLiteralExpressionFactory.Create(suppressMessage.Category)),
+ SyntaxTokenFactory.Comma(),
+ SyntaxFactory.AttributeArgument(SyntaxLiteralExpressionFactory.Create(suppressMessage.CheckId)),
+ SyntaxTokenFactory.Comma(),
+ SyntaxFactory
+ .AttributeArgument(SyntaxLiteralExpressionFactory.Create(suppressMessage.Justification!))
+ .WithNameEquals(
+ SyntaxNameEqualsFactory
+ .Create(nameof(SuppressMessageAttribute.Justification))
+ .WithEqualsToken(SyntaxTokenFactory.Equals())))));
+
+ return structDeclaration
+ .AddAttributeLists(SyntaxAttributeListFactory.Create(nameof(SuppressMessageAttribute), attributeArgumentList));
+ }
+
+ ///
+ /// Adds a to the struct declaration.
+ ///
+ /// The struct declaration to modify.
+ /// The name of the code generation tool.
+ /// The version of the code generation tool.
+ /// A new with the attribute added.
+ /// Thrown when , , or is null.
+ public static StructDeclarationSyntax AddGeneratedCodeAttribute(
+ this StructDeclarationSyntax structDeclaration,
+ string toolName,
+ string version)
+ {
+ if (structDeclaration is null)
+ {
+ throw new ArgumentNullException(nameof(structDeclaration));
+ }
+
+ if (toolName is null)
+ {
+ throw new ArgumentNullException(nameof(toolName));
+ }
+
+ if (version is null)
+ {
+ throw new ArgumentNullException(nameof(version));
+ }
+
+ var attributeArgumentList = SyntaxFactory.AttributeArgumentList(
+ SyntaxFactory.SeparatedList(SyntaxFactory.NodeOrTokenList(
+ SyntaxFactory.AttributeArgument(SyntaxLiteralExpressionFactory.Create(toolName)),
+ SyntaxTokenFactory.Comma(),
+ SyntaxFactory.AttributeArgument(SyntaxLiteralExpressionFactory.Create(version)))));
+
+ return structDeclaration
+ .AddAttributeLists(SyntaxAttributeListFactory.Create(nameof(GeneratedCodeAttribute), attributeArgumentList));
+ }
+}
\ No newline at end of file
diff --git a/src/Atc.CodeAnalysis.CSharp/Extensions/SyntaxNodeExtensions.cs b/src/Atc.CodeAnalysis.CSharp/Extensions/SyntaxNodeExtensions.cs
index 40fbcec2..d56a2531 100644
--- a/src/Atc.CodeAnalysis.CSharp/Extensions/SyntaxNodeExtensions.cs
+++ b/src/Atc.CodeAnalysis.CSharp/Extensions/SyntaxNodeExtensions.cs
@@ -60,7 +60,8 @@ public static string[] GetUsedUsingStatements(this SyntaxNode syntaxNode)
return syntaxNode
.Select()
- .Select(x => x.Name!.ToFullString())
+ .Where(x => x.Name is not null)
+ .Select(x => x.Name!.ToString())
.ToArray();
}
@@ -80,8 +81,8 @@ public static string[] GetUsedUsingStatementsWithoutAlias(
return syntaxNode
.Select()
- .Where(x => x.Alias is null)
- .Select(x => x.Name!.ToFullString())
+ .Where(x => x.Alias is null && x.Name is not null)
+ .Select(x => x.Name!.ToString())
.ToArray();
}
}
\ No newline at end of file
diff --git a/src/Atc.CodeAnalysis.CSharp/Extensions/UsingDirectiveSyntaxExtensions.cs b/src/Atc.CodeAnalysis.CSharp/Extensions/UsingDirectiveSyntaxExtensions.cs
index 8c53b046..ecf4f7fd 100644
--- a/src/Atc.CodeAnalysis.CSharp/Extensions/UsingDirectiveSyntaxExtensions.cs
+++ b/src/Atc.CodeAnalysis.CSharp/Extensions/UsingDirectiveSyntaxExtensions.cs
@@ -20,9 +20,9 @@ internal static SyntaxList Sort(
.OrderBy(Compare)
.ThenBy(x => x.Alias?.ToString(), StringComparer.Ordinal)
.ThenByDescending(x => placeSystemNamespaceFirst &&
- x.Name!
- .ToString()
- .StartsWith(nameof(System), StringComparison.Ordinal))
+ (x.Name?.ToString() is { } n &&
+ (n == nameof(System) ||
+ n.StartsWith(nameof(System) + ".", StringComparison.Ordinal))))
.ThenBy(x => x.Name!.ToString(), StringComparer.Ordinal));
private static int Compare(UsingDirectiveSyntax directive)
diff --git a/src/Atc.CodeAnalysis.CSharp/SyntaxFactories/SyntaxInterpolatedFactory.cs b/src/Atc.CodeAnalysis.CSharp/SyntaxFactories/SyntaxInterpolatedFactory.cs
index 17ffcdd6..91bf601a 100644
--- a/src/Atc.CodeAnalysis.CSharp/SyntaxFactories/SyntaxInterpolatedFactory.cs
+++ b/src/Atc.CodeAnalysis.CSharp/SyntaxFactories/SyntaxInterpolatedFactory.cs
@@ -11,14 +11,28 @@ public static class SyntaxInterpolatedFactory
/// The text value to include in the interpolated string.
/// An representing the text.
public static InterpolatedStringContentSyntax StringText(string value)
- => SyntaxFactory.InterpolatedStringText()
+ {
+ if (value is null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ // In the raw source text of an interpolated string, '{' and '}' must be doubled to
+ // avoid being interpreted as interpolation holes. The valueText (semantic value) keeps
+ // the original characters because that is what the runtime sees at execution time.
+ var rawText = value
+ .Replace("{", "{{", StringComparison.Ordinal)
+ .Replace("}", "}}", StringComparison.Ordinal);
+
+ return SyntaxFactory.InterpolatedStringText()
.WithTextToken(
SyntaxFactory.Token(
SyntaxFactory.TriviaList(),
SyntaxKind.InterpolatedStringTextToken,
- value,
+ rawText,
value,
SyntaxFactory.TriviaList()));
+ }
///
/// Creates interpolated string text for a colon and space (": ").
diff --git a/src/Atc.CodeAnalysis.CSharp/SyntaxFactories/SyntaxLiteralExpressionFactory.cs b/src/Atc.CodeAnalysis.CSharp/SyntaxFactories/SyntaxLiteralExpressionFactory.cs
index ca1f3a93..f893e276 100644
--- a/src/Atc.CodeAnalysis.CSharp/SyntaxFactories/SyntaxLiteralExpressionFactory.cs
+++ b/src/Atc.CodeAnalysis.CSharp/SyntaxFactories/SyntaxLiteralExpressionFactory.cs
@@ -30,8 +30,11 @@ public static LiteralExpressionSyntax Create(
return SyntaxFactory.LiteralExpression(syntaxKind, SyntaxFactory.Literal(parsedInt));
}
- value = value.Replace(',', '.');
- if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedDouble))
+ // Support European decimal notation (comma as decimal separator): "12,345" → 12.345.
+ // Thousands-separator values (e.g. "1,000") are not supported — they would silently
+ // become 1.0, so callers must normalise such values before passing them in.
+ var normalised = value.Replace(',', '.');
+ if (double.TryParse(normalised, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedDouble))
{
return SyntaxFactory.LiteralExpression(syntaxKind, SyntaxFactory.Literal(parsedDouble));
}
@@ -50,4 +53,44 @@ public static LiteralExpressionSyntax Create(
/// A node representing the integer.
public static LiteralExpressionSyntax Create(int value)
=> SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(value));
+
+ ///
+ /// Creates a numeric literal expression from a long value.
+ ///
+ /// The long value for the literal expression.
+ /// A node representing the long.
+ public static LiteralExpressionSyntax Create(long value)
+ => SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(value));
+
+ ///
+ /// Creates a numeric literal expression from a double value.
+ ///
+ /// The double value for the literal expression.
+ /// A node representing the double.
+ public static LiteralExpressionSyntax Create(double value)
+ => SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(value));
+
+ ///
+ /// Creates a boolean literal expression.
+ ///
+ /// The boolean value.
+ /// A node representing or .
+ public static LiteralExpressionSyntax Create(bool value)
+ => SyntaxFactory.LiteralExpression(
+ value ? SyntaxKind.TrueLiteralExpression : SyntaxKind.FalseLiteralExpression);
+
+ ///
+ /// Creates a character literal expression.
+ ///
+ /// The character value.
+ /// A node representing the character literal.
+ public static LiteralExpressionSyntax Create(char value)
+ => SyntaxFactory.LiteralExpression(SyntaxKind.CharacterLiteralExpression, SyntaxFactory.Literal(value));
+
+ ///
+ /// Creates a literal expression.
+ ///
+ /// A node representing .
+ public static LiteralExpressionSyntax CreateNull()
+ => SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression);
}
\ No newline at end of file
diff --git a/src/Atc.CodeAnalysis.CSharp/SyntaxFactories/SyntaxObjectCreationExpressionFactory.cs b/src/Atc.CodeAnalysis.CSharp/SyntaxFactories/SyntaxObjectCreationExpressionFactory.cs
index 40c7e3d2..95f7b487 100644
--- a/src/Atc.CodeAnalysis.CSharp/SyntaxFactories/SyntaxObjectCreationExpressionFactory.cs
+++ b/src/Atc.CodeAnalysis.CSharp/SyntaxFactories/SyntaxObjectCreationExpressionFactory.cs
@@ -47,4 +47,181 @@ public static ObjectCreationExpressionSyntax Create(
SyntaxFactory.IdentifierName(namespaceName),
SyntaxFactory.IdentifierName(identifierName)));
}
+
+ ///
+ /// Creates an object creation expression for a type with an explicit argument list.
+ ///
+ /// The name of the type to instantiate.
+ /// The argument list to pass to the constructor.
+ /// An node with the given arguments.
+ /// Thrown when or is null.
+ public static ObjectCreationExpressionSyntax Create(
+ string identifierName,
+ ArgumentListSyntax argumentList)
+ {
+ if (identifierName is null)
+ {
+ throw new ArgumentNullException(nameof(identifierName));
+ }
+
+ if (argumentList is null)
+ {
+ throw new ArgumentNullException(nameof(argumentList));
+ }
+
+ return SyntaxFactory.ObjectCreationExpression(SyntaxFactory.IdentifierName(identifierName))
+ .WithArgumentList(argumentList);
+ }
+
+ ///
+ /// Creates an object creation expression for a qualified type name with an explicit argument list.
+ ///
+ /// The namespace containing the type.
+ /// The name of the type to instantiate.
+ /// The argument list to pass to the constructor.
+ /// An node with the given arguments.
+ /// Thrown when any parameter is null.
+ public static ObjectCreationExpressionSyntax Create(
+ string namespaceName,
+ string identifierName,
+ ArgumentListSyntax argumentList)
+ {
+ if (namespaceName is null)
+ {
+ throw new ArgumentNullException(nameof(namespaceName));
+ }
+
+ if (identifierName is null)
+ {
+ throw new ArgumentNullException(nameof(identifierName));
+ }
+
+ if (argumentList is null)
+ {
+ throw new ArgumentNullException(nameof(argumentList));
+ }
+
+ return SyntaxFactory.ObjectCreationExpression(
+ SyntaxFactory.QualifiedName(
+ SyntaxFactory.IdentifierName(namespaceName),
+ SyntaxFactory.IdentifierName(identifierName)))
+ .WithArgumentList(argumentList);
+ }
+
+ ///
+ /// Creates a generic object creation expression (e.g. new List<T>() ).
+ ///
+ /// The name of the generic type to instantiate.
+ /// The type argument list (e.g. <T> ).
+ /// An node for the generic type.
+ /// Thrown when or is null.
+ public static ObjectCreationExpressionSyntax CreateGeneric(
+ string identifierName,
+ TypeArgumentListSyntax typeArgumentList)
+ {
+ if (identifierName is null)
+ {
+ throw new ArgumentNullException(nameof(identifierName));
+ }
+
+ if (typeArgumentList is null)
+ {
+ throw new ArgumentNullException(nameof(typeArgumentList));
+ }
+
+ return SyntaxFactory.ObjectCreationExpression(
+ SyntaxFactory.GenericName(
+ SyntaxFactory.Identifier(identifierName))
+ .WithTypeArgumentList(typeArgumentList));
+ }
+
+ ///
+ /// Creates a generic object creation expression with a single named type argument (e.g. new List<MyType>() ).
+ ///
+ /// The name of the generic type to instantiate.
+ /// The name of the single type argument.
+ /// An node for the generic type.
+ /// Thrown when any parameter is null.
+ public static ObjectCreationExpressionSyntax CreateGeneric(
+ string identifierName,
+ string typeArgumentName)
+ {
+ if (identifierName is null)
+ {
+ throw new ArgumentNullException(nameof(identifierName));
+ }
+
+ if (typeArgumentName is null)
+ {
+ throw new ArgumentNullException(nameof(typeArgumentName));
+ }
+
+ return CreateGeneric(identifierName, SyntaxTypeArgumentListFactory.CreateWithOneItem(typeArgumentName));
+ }
+
+ ///
+ /// Creates a generic object creation expression with an explicit argument list (e.g. new Dictionary<K,V>(capacity) ).
+ ///
+ /// The name of the generic type to instantiate.
+ /// The type argument list.
+ /// The argument list to pass to the constructor.
+ /// An node for the generic type with arguments.
+ /// Thrown when any parameter is null.
+ public static ObjectCreationExpressionSyntax CreateGeneric(
+ string identifierName,
+ TypeArgumentListSyntax typeArgumentList,
+ ArgumentListSyntax argumentList)
+ {
+ if (identifierName is null)
+ {
+ throw new ArgumentNullException(nameof(identifierName));
+ }
+
+ if (typeArgumentList is null)
+ {
+ throw new ArgumentNullException(nameof(typeArgumentList));
+ }
+
+ if (argumentList is null)
+ {
+ throw new ArgumentNullException(nameof(argumentList));
+ }
+
+ return SyntaxFactory.ObjectCreationExpression(
+ SyntaxFactory.GenericName(
+ SyntaxFactory.Identifier(identifierName))
+ .WithTypeArgumentList(typeArgumentList))
+ .WithArgumentList(argumentList);
+ }
+
+ ///
+ /// Creates a generic object creation expression with a single named type argument and an argument list.
+ ///
+ /// The name of the generic type to instantiate.
+ /// The name of the single type argument.
+ /// The argument list to pass to the constructor.
+ /// An node for the generic type with arguments.
+ /// Thrown when any parameter is null.
+ public static ObjectCreationExpressionSyntax CreateGeneric(
+ string identifierName,
+ string typeArgumentName,
+ ArgumentListSyntax argumentList)
+ {
+ if (identifierName is null)
+ {
+ throw new ArgumentNullException(nameof(identifierName));
+ }
+
+ if (typeArgumentName is null)
+ {
+ throw new ArgumentNullException(nameof(typeArgumentName));
+ }
+
+ if (argumentList is null)
+ {
+ throw new ArgumentNullException(nameof(argumentList));
+ }
+
+ return CreateGeneric(identifierName, SyntaxTypeArgumentListFactory.CreateWithOneItem(typeArgumentName), argumentList);
+ }
}
\ No newline at end of file
diff --git a/src/Atc.CodeDocumentation/AssemblyCommentHelper.cs b/src/Atc.CodeDocumentation/AssemblyCommentHelper.cs
index 7b4a85ed..48026743 100644
--- a/src/Atc.CodeDocumentation/AssemblyCommentHelper.cs
+++ b/src/Atc.CodeDocumentation/AssemblyCommentHelper.cs
@@ -18,7 +18,32 @@ internal static class AssemblyCommentHelper
}
var xmlFile = GetXmlFileForAssembly(type.Assembly);
- return CollectExportedTypesWithComments(type.Assembly, xmlFile, namespaceMatch: null, excludeSourceTypes: null)
+ return CollectExportedTypesWithCommentsCore(type.Assembly, xmlFile, namespaceMatch: null, excludeSourceTypes: null)
+ .FirstOrDefault(x => string.Equals(x.FullName, type.FullName, StringComparison.Ordinal));
+ }
+
+ ///
+ /// Collects XML documentation comments for a specific type using an explicit XML documentation file.
+ ///
+ /// The type to collect documentation for.
+ /// The explicit path to the XML documentation file.
+ /// The type comments, or if the type was not found.
+ /// Thrown when or is null.
+ public static TypeComments? CollectExportedTypeWithComments(
+ Type type,
+ FileInfo xmlDocPath)
+ {
+ if (type is null)
+ {
+ throw new ArgumentNullException(nameof(type));
+ }
+
+ if (xmlDocPath is null)
+ {
+ throw new ArgumentNullException(nameof(xmlDocPath));
+ }
+
+ return CollectExportedTypesWithCommentsCore(type.Assembly, xmlDocPath, namespaceMatch: null, excludeSourceTypes: null)
.FirstOrDefault(x => string.Equals(x.FullName, type.FullName, StringComparison.Ordinal));
}
@@ -40,7 +65,36 @@ public static TypeComments[] CollectExportedTypesWithMissingComments(
}
var xmlFile = GetXmlFileForAssembly(assembly);
- return CollectExportedTypesWithMissingComments(assembly, xmlFile, namespaceMatch, excludeSourceTypes);
+ return CollectExportedTypesWithMissingCommentsCore(assembly, xmlFile, namespaceMatch, excludeSourceTypes);
+ }
+
+ ///
+ /// Collects all public types from an assembly that are missing XML documentation comments,
+ /// using an explicit XML documentation file path.
+ ///
+ /// The assembly to scan for types.
+ /// The explicit path to the XML documentation file.
+ /// Optional regex pattern to filter types by namespace.
+ /// Optional list of types to exclude from the results.
+ /// An array of type comments for types missing documentation.
+ /// Thrown when or is null.
+ public static TypeComments[] CollectExportedTypesWithMissingComments(
+ Assembly assembly,
+ FileInfo xmlDocPath,
+ string? namespaceMatch = null,
+ List? excludeSourceTypes = null)
+ {
+ if (assembly is null)
+ {
+ throw new ArgumentNullException(nameof(assembly));
+ }
+
+ if (xmlDocPath is null)
+ {
+ throw new ArgumentNullException(nameof(xmlDocPath));
+ }
+
+ return CollectExportedTypesWithMissingCommentsCore(assembly, xmlDocPath, namespaceMatch, excludeSourceTypes);
}
///
@@ -61,7 +115,36 @@ public static TypeComments[] CollectExportedTypesWithComments(
}
var xmlFile = GetXmlFileForAssembly(assembly);
- return CollectExportedTypesWithComments(assembly, xmlFile, namespaceMatch, excludeSourceTypes);
+ return CollectExportedTypesWithCommentsCore(assembly, xmlFile, namespaceMatch, excludeSourceTypes);
+ }
+
+ ///
+ /// Collects all public types from an assembly along with their XML documentation comments,
+ /// using an explicit XML documentation file path.
+ ///
+ /// The assembly to scan for types.
+ /// The explicit path to the XML documentation file.
+ /// Optional regex pattern to filter types by namespace.
+ /// Optional list of types to exclude from the results.
+ /// An array of type comments for all matching types.
+ /// Thrown when or is null.
+ public static TypeComments[] CollectExportedTypesWithComments(
+ Assembly assembly,
+ FileInfo xmlDocPath,
+ string? namespaceMatch = null,
+ List? excludeSourceTypes = null)
+ {
+ if (assembly is null)
+ {
+ throw new ArgumentNullException(nameof(assembly));
+ }
+
+ if (xmlDocPath is null)
+ {
+ throw new ArgumentNullException(nameof(xmlDocPath));
+ }
+
+ return CollectExportedTypesWithCommentsCore(assembly, xmlDocPath, namespaceMatch, excludeSourceTypes);
}
///
@@ -85,12 +168,22 @@ public static string GetTypesAsRenderText(
private static FileInfo GetXmlFileForAssembly(Assembly assembly)
{
- var xmlFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, assembly.GetName().Name + ".xml");
+ var assemblyDir = string.IsNullOrEmpty(assembly.Location)
+ ? AppDomain.CurrentDomain.BaseDirectory
+ : Path.GetDirectoryName(assembly.Location) ?? AppDomain.CurrentDomain.BaseDirectory;
+
+ var xmlFile = Path.Combine(assemblyDir, assembly.GetName().Name + ".xml");
if (File.Exists(xmlFile))
{
return new FileInfo(xmlFile);
}
+ var fallback = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, assembly.GetName().Name + ".xml");
+ if (File.Exists(fallback))
+ {
+ return new FileInfo(fallback);
+ }
+
throw new IOException($"No xml document found for the assembly: {assembly.FullName}, expected file: {xmlFile}");
}
@@ -100,7 +193,7 @@ private static bool IsRequiredNamespace(
=> regex is null ||
regex.IsMatch(type.Namespace ?? string.Empty);
- private static TypeComments[] CollectExportedTypesWithMissingComments(
+ private static TypeComments[] CollectExportedTypesWithMissingCommentsCore(
Assembly assembly,
FileSystemInfo xmlPath,
string? namespaceMatch,
@@ -121,7 +214,7 @@ private static TypeComments[] CollectExportedTypesWithMissingComments(
throw new IOException($"File don't exist: {xmlPath.FullName}");
}
- var collectExportedTypesWithComments = CollectExportedTypesWithComments(assembly, xmlPath, namespaceMatch, excludeSourceTypes);
+ var collectExportedTypesWithComments = CollectExportedTypesWithCommentsCore(assembly, xmlPath, namespaceMatch, excludeSourceTypes);
var collectExportedTypesWithMissingComments = collectExportedTypesWithComments
.Where(x => !x.HasComments)
@@ -131,7 +224,7 @@ private static TypeComments[] CollectExportedTypesWithMissingComments(
return collectExportedTypesWithMissingComments;
}
- private static TypeComments[] CollectExportedTypesWithComments(
+ private static TypeComments[] CollectExportedTypesWithCommentsCore(
Assembly assembly,
FileSystemInfo xmlPath,
string? namespaceMatch,
diff --git a/src/Atc.CodeDocumentation/DocumentationHelper.cs b/src/Atc.CodeDocumentation/DocumentationHelper.cs
index e24ea397..059a5abe 100644
--- a/src/Atc.CodeDocumentation/DocumentationHelper.cs
+++ b/src/Atc.CodeDocumentation/DocumentationHelper.cs
@@ -14,6 +14,17 @@ public static class DocumentationHelper
Type type)
=> AssemblyCommentHelper.CollectExportedTypeWithComments(type);
+ ///
+ /// Collects XML documentation comments for a specific type using an explicit XML documentation file.
+ ///
+ /// The type to collect documentation for.
+ /// The explicit path to the XML documentation file.
+ /// The type comments, or if the type was not found.
+ public static TypeComments? CollectExportedTypeWithCommentsFromType(
+ Type type,
+ FileInfo xmlDocPath)
+ => AssemblyCommentHelper.CollectExportedTypeWithComments(type, xmlDocPath);
+
///
/// Collects all public types from an assembly that are missing XML documentation comments.
///
@@ -28,6 +39,24 @@ public static TypeComments[] CollectExportedTypesWithMissingCommentsFromAssembly
namespaceMatch: null,
excludeTypes);
+ ///
+ /// Collects all public types from an assembly that are missing XML documentation comments,
+ /// using an explicit XML documentation file path.
+ ///
+ /// The assembly to scan for types.
+ /// The explicit path to the XML documentation file.
+ /// Optional list of types to exclude from the results.
+ /// An array of type comments for types missing documentation.
+ public static TypeComments[] CollectExportedTypesWithMissingCommentsFromAssembly(
+ Assembly assembly,
+ FileInfo xmlDocPath,
+ List? excludeTypes = null)
+ => AssemblyCommentHelper.CollectExportedTypesWithMissingComments(
+ assembly,
+ xmlDocPath,
+ namespaceMatch: null,
+ excludeTypes);
+
///
/// Collects all public types from an assembly that are missing XML documentation and generates a formatted text report.
///
diff --git a/src/Atc.CodeDocumentation/Markdown/MarkdownBuilder.cs b/src/Atc.CodeDocumentation/Markdown/MarkdownBuilder.cs
index 0c43139f..d46a3298 100644
--- a/src/Atc.CodeDocumentation/Markdown/MarkdownBuilder.cs
+++ b/src/Atc.CodeDocumentation/Markdown/MarkdownBuilder.cs
@@ -37,13 +37,12 @@ public void AppendLine(
int indentSpaces,
string text)
{
- var sbLocal = new StringBuilder();
for (var i = 0; i < indentSpaces; i++)
{
- sbLocal.Append(" ");
+ sb.Append(" ");
}
- sb.AppendLine(sbLocal + text);
+ sb.AppendLine(text);
}
///
@@ -192,7 +191,7 @@ public void Table(
sb.Append("| ");
foreach (var item in headers)
{
- sb.Append(item);
+ sb.Append(EscapeTableCell(item));
sb.Append(" | ");
}
@@ -212,7 +211,7 @@ public void Table(
sb.Append("| ");
foreach (var item2 in item)
{
- sb.Append(item2);
+ sb.Append(EscapeTableCell(item2));
sb.Append(" | ");
}
@@ -222,6 +221,26 @@ public void Table(
sb.AppendLine();
}
+ ///
+ /// Escapes a value for safe use inside a markdown table cell by neutralizing the
+ /// column delimiter and collapsing line breaks, which would otherwise break the table layout.
+ ///
+ /// The raw cell value.
+ /// The escaped cell value.
+ private static string EscapeTableCell(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ return value;
+ }
+
+ return value
+ .Replace("\r\n", " ", StringComparison.Ordinal)
+ .Replace("\r", " ", StringComparison.Ordinal)
+ .Replace("\n", " ", StringComparison.Ordinal)
+ .Replace("|", "\\|", StringComparison.Ordinal);
+ }
+
///
/// Appends an unordered list item (no nesting).
///
diff --git a/src/Atc.CodeDocumentation/Markdown/MarkdownCodeDocGenerator.cs b/src/Atc.CodeDocumentation/Markdown/MarkdownCodeDocGenerator.cs
index 83435579..38e60cb2 100644
--- a/src/Atc.CodeDocumentation/Markdown/MarkdownCodeDocGenerator.cs
+++ b/src/Atc.CodeDocumentation/Markdown/MarkdownCodeDocGenerator.cs
@@ -55,6 +55,23 @@ public static void Run(
{
var assemblyName = assembly.GetName().Name!;
var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
+
+ // Walk up the directory tree until we find a segment whose name matches the assembly name.
+ // This avoids a fragile string-index search that breaks when the assembly name appears
+ // multiple times in the path.
+ var dir = new DirectoryInfo(baseDirectory);
+ while (dir is not null)
+ {
+ if (dir.Name.Equals(assemblyName, StringComparison.Ordinal))
+ {
+ return new DirectoryInfo(Path.Combine(dir.FullName, "CodeDoc"));
+ }
+
+ dir = dir.Parent;
+ }
+
+ // Fall back to the original index-based search for cases where the assembly name
+ // does not appear as a standalone directory segment (e.g. single-file publish).
var index = baseDirectory.IndexOf(assemblyName, StringComparison.Ordinal);
if (index == -1)
{
@@ -73,7 +90,7 @@ private static void PrepareOutputPath(DirectoryInfo outputPath)
}
else
{
- foreach (var file in Directory.GetFiles(outputPath.FullName, "*.md", SearchOption.AllDirectories))
+ foreach (var file in Directory.GetFiles(outputPath.FullName, "*.md", SearchOption.TopDirectoryOnly))
{
File.Delete(file);
}
diff --git a/src/Atc.CodeDocumentation/Markdown/MarkdownHelper.cs b/src/Atc.CodeDocumentation/Markdown/MarkdownHelper.cs
index a3926e82..e30fedd3 100644
--- a/src/Atc.CodeDocumentation/Markdown/MarkdownHelper.cs
+++ b/src/Atc.CodeDocumentation/Markdown/MarkdownHelper.cs
@@ -350,7 +350,7 @@ private static void AppendBodyForEnum(
.GetNames(typeComments.Type)
.Select(x => new
{
- Value = (int)Enum.Parse(typeComments.Type, x),
+ Value = Convert.ToInt64(Enum.Parse(typeComments.Type, x), GlobalizationConstants.EnglishCultureInfo),
Name = x,
Description = (Enum.Parse(typeComments.Type, x) as Enum)!.GetDescription(),
})
@@ -410,7 +410,7 @@ private static void BuildTable(
var data = seq
.Select(item =>
{
- var summary = docs.FirstOrDefault(x => string.Equals(x.MemberName, name(item), StringComparison.Ordinal) || x.MemberName!.StartsWith(name(item) + "`", StringComparison.Ordinal))?.Summary ?? string.Empty;
+ var summary = docs.FirstOrDefault(x => string.Equals(x.MemberName, name(item), StringComparison.Ordinal) || x.MemberName?.StartsWith(name(item) + "`", StringComparison.Ordinal) == true)?.Summary ?? string.Empty;
return new[]
{
xType(item),
@@ -432,7 +432,7 @@ private static void BuildTable(
var data = seq
.Select(item =>
{
- var summary = docs.FirstOrDefault(x => string.Equals(x.MemberName, name(item), StringComparison.Ordinal) || x.MemberName!.StartsWith(name(item) + "`", StringComparison.Ordinal))?.Summary ?? string.Empty;
+ var summary = docs.FirstOrDefault(x => string.Equals(x.MemberName, name(item), StringComparison.Ordinal) || x.MemberName?.StartsWith(name(item) + "`", StringComparison.Ordinal) == true)?.Summary ?? string.Empty;
return new[]
{
xType(item),
@@ -485,7 +485,7 @@ private static void Build(
// ReSharper disable once PossibleMultipleEnumeration
var commentForMember = docs.FirstOrDefault(x =>
string.Equals(x.MemberName, name(item), StringComparison.Ordinal) ||
- x.MemberName!.StartsWith(name(item) + "`", StringComparison.Ordinal));
+ x.MemberName?.StartsWith(name(item) + "`", StringComparison.Ordinal) == true);
if (commentForMember is null || string.IsNullOrEmpty(commentForMember.Summary))
{
diff --git a/src/Atc.CodeDocumentation/XmlDocument/XmlDocumentCommentParser.cs b/src/Atc.CodeDocumentation/XmlDocument/XmlDocumentCommentParser.cs
index f4746f10..a6365319 100644
--- a/src/Atc.CodeDocumentation/XmlDocument/XmlDocumentCommentParser.cs
+++ b/src/Atc.CodeDocumentation/XmlDocument/XmlDocumentCommentParser.cs
@@ -2,6 +2,36 @@ namespace Atc.CodeDocumentation.XmlDocument;
internal static class XmlDocumentCommentParser
{
+ private static readonly Regex MemberAttributeRegex = new(
+ @"(.):(.+)\.([^.()]+)?(\(.+\)|$)",
+ RegexOptions.Compiled,
+ TimeSpan.FromSeconds(5));
+
+ private static readonly Regex ParaTagRegex = new(
+ @" |<\/para>",
+ RegexOptions.Compiled,
+ TimeSpan.FromSeconds(1));
+
+ private static readonly Regex SeeCrefRegex = new(
+ @" ",
+ RegexOptions.Compiled,
+ TimeSpan.FromSeconds(1));
+
+ private static readonly Regex ParamRefRegex = new(
+ @"<(type)*paramref name=""([^\""]*)""\s*\/>",
+ RegexOptions.Compiled,
+ TimeSpan.FromSeconds(1));
+
+ private static readonly Regex CodeTagRegex = new(
+ @"]*>(.*?)<\/c>",
+ RegexOptions.Compiled,
+ TimeSpan.FromSeconds(1));
+
+ private static readonly Regex TypeNameSuffixRegex = new(
+ @"\.(?:.(?!\.))+$",
+ RegexOptions.Compiled,
+ TimeSpan.FromSeconds(1));
+
[SuppressMessage("Design", "MA0051:Method is too long", Justification = "OK.")]
internal static XmlDocumentComment?[] ParseXmlComment(
XDocument xDocument,
@@ -16,7 +46,7 @@ internal static class XmlDocumentCommentParser
return null;
}
- var match = Regex.Match(attributeValue, @"(.):(.+)\.([^.()]+)?(\(.+\)|$)", RegexOptions.None, TimeSpan.FromSeconds(5));
+ var match = MemberAttributeRegex.Match(attributeValue);
if (!match.Groups[1].Success)
{
return null;
@@ -132,7 +162,7 @@ private static string ResolveSeeElement(
}
return typeName.StartsWith(ns, StringComparison.Ordinal)
- ? $"[{typeName}]({Regex.Replace(typeName, "\\.(?:.(?!\\.))+$", me => me.Groups[0].Value.Replace('.', '#').ToLower(GlobalizationConstants.EnglishCultureInfo), RegexOptions.None, TimeSpan.FromSeconds(1))})"
+ ? $"[{typeName}]({TypeNameSuffixRegex.Replace(typeName, me => me.Groups[0].Value.Replace('.', '#').ToLower(GlobalizationConstants.EnglishCultureInfo))})"
: $"`{typeName}`";
}
@@ -148,12 +178,14 @@ private static string ParseElementText(
}
innerXml = innerXml.Replace("", string.Empty, StringComparison.Ordinal);
- innerXml = innerXml.Replace(Environment.NewLine, " ", StringComparison.Ordinal);
+ innerXml = innerXml.Replace("\r\n", " ", StringComparison.Ordinal);
+ innerXml = innerXml.Replace("\n", " ", StringComparison.Ordinal);
+ innerXml = innerXml.Replace("\r", " ", StringComparison.Ordinal);
innerXml = Regex.Replace(innerXml, @$"<\/?{name}>", string.Empty, RegexOptions.None, TimeSpan.FromSeconds(1)).Trim();
- innerXml = Regex.Replace(innerXml, @" |<\/para>", Environment.NewLine, RegexOptions.None, TimeSpan.FromSeconds(1));
- innerXml = Regex.Replace(innerXml, @" ", m => ResolveSeeElement(m, @namespace), RegexOptions.None, TimeSpan.FromSeconds(1));
- innerXml = Regex.Replace(innerXml, @"<(type)*paramref name=""([^\""]*)""\s*\/>", e => $"`{e.Groups[2].Value}`", RegexOptions.None, TimeSpan.FromSeconds(1));
- innerXml = Regex.Replace(innerXml, @"]*>(.*?)<\/c>", e => $"`{e.Groups[1].Value}`", RegexOptions.None, TimeSpan.FromSeconds(1));
+ innerXml = ParaTagRegex.Replace(innerXml, Environment.NewLine);
+ innerXml = SeeCrefRegex.Replace(innerXml, m => ResolveSeeElement(m, @namespace));
+ innerXml = ParamRefRegex.Replace(innerXml, e => $"`{e.Groups[2].Value}`");
+ innerXml = CodeTagRegex.Replace(innerXml, e => $"`{e.Groups[1].Value}`");
innerXml = innerXml.TrimExtended();
var lines = innerXml
diff --git a/src/Atc.Console.Spectre/Atc.Console.Spectre.csproj b/src/Atc.Console.Spectre/Atc.Console.Spectre.csproj
index e50e6c61..4e88b6e0 100644
--- a/src/Atc.Console.Spectre/Atc.Console.Spectre.csproj
+++ b/src/Atc.Console.Spectre/Atc.Console.Spectre.csproj
@@ -8,8 +8,8 @@
-
-
+
+
diff --git a/src/Atc.Console.Spectre/Logging/ConsoleLogger.cs b/src/Atc.Console.Spectre/Logging/ConsoleLogger.cs
index 986870ef..a856ed5b 100644
--- a/src/Atc.Console.Spectre/Logging/ConsoleLogger.cs
+++ b/src/Atc.Console.Spectre/Logging/ConsoleLogger.cs
@@ -12,21 +12,49 @@ namespace Atc.Console.Spectre.Logging;
public class ConsoleLogger : ILogger
{
private readonly string categoryName;
+ private readonly string escapedCategoryName;
private readonly ConsoleLoggerConfiguration config;
private readonly IAnsiConsole console;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class with a shared console.
+ /// Prefer this constructor when the logger is created by a so
+ /// that all loggers in the same provider share one instance.
///
/// The category name for the logger.
/// The console logger configuration.
- /// Thrown when is null.
+ /// A shared instance to write to.
+ /// Thrown when or is null.
public ConsoleLogger(
string categoryName,
- ConsoleLoggerConfiguration config)
+ ConsoleLoggerConfiguration config,
+ IAnsiConsole console)
{
this.categoryName = categoryName;
+ this.escapedCategoryName = Markup.Escape(categoryName ?? string.Empty);
this.config = config ?? throw new ArgumentNullException(nameof(config));
+ this.console = console ?? throw new ArgumentNullException(nameof(console));
+ }
+
+ ///
+ /// Initializes a new instance of the class, creating its own
+ /// from the configuration. Use this only when creating a logger
+ /// outside of a ; for provider-managed loggers prefer the
+ /// overload that accepts a shared .
+ ///
+ /// The category name for the logger.
+ /// The console logger configuration.
+ /// Thrown when is null.
+ public ConsoleLogger(
+ string categoryName,
+ ConsoleLoggerConfiguration config)
+ : this(categoryName, config, CreateConsole(config))
+ {
+ }
+
+ private static IAnsiConsole CreateConsole(ConsoleLoggerConfiguration config)
+ {
+ ArgumentNullException.ThrowIfNull(config);
var settings = config.ConsoleSettings ?? new AnsiConsoleSettings
{
@@ -34,12 +62,14 @@ public ConsoleLogger(
ColorSystem = ColorSystemSupport.Detect,
};
- console = AnsiConsole.Create(settings);
- config.ConsoleConfiguration?.Invoke(console);
+ var c = AnsiConsole.Create(settings);
+ config.ConsoleConfiguration?.Invoke(c);
+ return c;
}
///
- public IDisposable BeginScope(TState state) => default!;
+ public IDisposable BeginScope(TState state)
+ => new LogScope(state);
///
public bool IsEnabled(LogLevel logLevel)
@@ -51,7 +81,7 @@ public void Log(
EventId eventId,
TState state,
Exception? exception,
- Func formatter)
+ Func formatter)
{
ArgumentNullException.ThrowIfNull(formatter);
@@ -60,11 +90,20 @@ public void Log(
return;
}
- var stateStr = formatter(state, exception!);
+ var stateStr = formatter(state, exception);
var message = config.AllowMarkup
? stateStr
: Markup.Escape(stateStr);
+ if (config.IncludeScopes)
+ {
+ var scopeText = LogScope.BuildScopeText();
+ if (scopeText.Length > 0)
+ {
+ message = $"[grey]{Markup.Escape(scopeText)}[/] {message}";
+ }
+ }
+
var exceptionMessage = exception?.GetMessage(
includeInnerMessage: config.IncludeInnerMessageForException,
includeExceptionName: config.IncludeInnerMessageForException);
@@ -297,7 +336,7 @@ private string GetTimeStampWithMarkup()
=> $"[white]{GetTimeStamp()}[/]";
private string GetCategoryNameWithMarkup()
- => $"[grey]{categoryName}[/]";
+ => $"[grey]{escapedCategoryName}[/]";
private string GetTimeStampAndCategoryNameWithMarkup()
=> $"{GetTimeStampWithMarkup()} {GetCategoryNameWithMarkup()}";
@@ -306,4 +345,63 @@ private string GetMessageWithMarkup(
LogLevel logLevel,
string message)
=> $"{GetLogLevelMarkupStartTag(logLevel)}{message}[/]";
+
+ ///
+ /// Tracks a single log scope entry in a per-async-context linked list.
+ /// Disposing removes this scope from the ambient context.
+ ///
+ private sealed class LogScope : IDisposable
+ {
+ private static readonly AsyncLocal Current = new();
+
+ private readonly LogScope? parent;
+ private bool disposed;
+
+ internal LogScope(object? state)
+ {
+ State = state;
+ parent = Current.Value;
+ Current.Value = this;
+ }
+
+ internal object? State { get; }
+
+ ///
+ /// Builds a formatted string from all active scopes in the current async context,
+ /// from outermost to innermost, separated by " => " .
+ /// Returns when no scopes are active.
+ ///
+ internal static string BuildScopeText()
+ {
+ var scope = Current.Value;
+ if (scope is null)
+ {
+ return string.Empty;
+ }
+
+ var parts = new List();
+ while (scope is not null)
+ {
+ var text = scope.State?.ToString();
+ if (!string.IsNullOrEmpty(text))
+ {
+ parts.Add(text);
+ }
+
+ scope = scope.parent;
+ }
+
+ parts.Reverse();
+ return string.Join(" => ", parts);
+ }
+
+ public void Dispose()
+ {
+ if (!disposed)
+ {
+ Current.Value = parent;
+ disposed = true;
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/src/Atc.Console.Spectre/Logging/ConsoleLoggerConfiguration.cs b/src/Atc.Console.Spectre/Logging/ConsoleLoggerConfiguration.cs
index 16c747c7..7c8742e8 100644
--- a/src/Atc.Console.Spectre/Logging/ConsoleLoggerConfiguration.cs
+++ b/src/Atc.Console.Spectre/Logging/ConsoleLoggerConfiguration.cs
@@ -80,6 +80,13 @@ public ConsoleLoggerConfiguration()
///
public bool AllowMarkup { get; set; }
+ ///
+ /// Gets or sets a value indicating whether log scope values are included in the output.
+ /// When enabled, active scopes opened via
+ /// are rendered as a grey prefix before the log message.
+ ///
+ public bool IncludeScopes { get; set; }
+
///
/// Gets or sets a value indicating whether the Timestamp should be rendered as UTC.
///
@@ -100,5 +107,5 @@ public ConsoleLoggerConfiguration()
///
public override string ToString()
- => $"{nameof(MinimumLogLevel)}: {MinimumLogLevel}, {nameof(RenderingMode)}: {RenderingMode}, {nameof(UseTimestamp)}: {UseTimestamp}, {nameof(UseShortNameForLogLevel)}: {UseShortNameForLogLevel}, {nameof(IncludeInnerMessageForException)}: {IncludeInnerMessageForException}, {nameof(IncludeExceptionNameForException)}: {IncludeExceptionNameForException}, {nameof(AllowMarkup)}: {AllowMarkup}, {nameof(UseTimestampUtc)}: {UseTimestampUtc}, {nameof(TimestampFormat)}: {TimestampFormat}";
+ => $"{nameof(MinimumLogLevel)}: {MinimumLogLevel}, {nameof(RenderingMode)}: {RenderingMode}, {nameof(UseTimestamp)}: {UseTimestamp}, {nameof(UseShortNameForLogLevel)}: {UseShortNameForLogLevel}, {nameof(IncludeInnerMessageForException)}: {IncludeInnerMessageForException}, {nameof(IncludeExceptionNameForException)}: {IncludeExceptionNameForException}, {nameof(AllowMarkup)}: {AllowMarkup}, {nameof(IncludeScopes)}: {IncludeScopes}, {nameof(UseTimestampUtc)}: {UseTimestampUtc}, {nameof(TimestampFormat)}: {TimestampFormat}";
}
\ No newline at end of file
diff --git a/src/Atc.Console.Spectre/Logging/ConsoleLoggerProvider.cs b/src/Atc.Console.Spectre/Logging/ConsoleLoggerProvider.cs
index 032fef98..4210f957 100644
--- a/src/Atc.Console.Spectre/Logging/ConsoleLoggerProvider.cs
+++ b/src/Atc.Console.Spectre/Logging/ConsoleLoggerProvider.cs
@@ -6,24 +6,40 @@ namespace Atc.Console.Spectre.Logging;
public class ConsoleLoggerProvider : ILoggerProvider
{
private readonly ConsoleLoggerConfiguration config;
+ private readonly IAnsiConsole console;
private readonly ConcurrentDictionary loggers = new(StringComparer.Ordinal);
///
/// Initializes a new instance of the class.
+ /// A single is created here and shared across all loggers produced
+ /// by this provider, preventing torn interleaved output under concurrent log calls.
///
/// The console logger configuration to use.
+ /// Thrown when is null.
public ConsoleLoggerProvider(ConsoleLoggerConfiguration config)
{
+ ArgumentNullException.ThrowIfNull(config);
+
this.config = config;
+
+ var settings = config.ConsoleSettings ?? new AnsiConsoleSettings
+ {
+ Ansi = AnsiSupport.Detect,
+ ColorSystem = ColorSystemSupport.Detect,
+ };
+
+ console = AnsiConsole.Create(settings);
+ config.ConsoleConfiguration?.Invoke(console);
}
///
/// Creates a new instance for the specified category.
+ /// All loggers share the provider's instance.
///
/// The category name for the logger.
/// A instance.
public ILogger CreateLogger(string categoryName)
- => loggers.GetOrAdd(categoryName, name => new ConsoleLogger(name, config));
+ => loggers.GetOrAdd(categoryName, name => new ConsoleLogger(name, config, console));
///
public void Dispose()
diff --git a/src/Atc.DotNet/DotnetBuildHelper.cs b/src/Atc.DotNet/DotnetBuildHelper.cs
index cfc8721a..3cb53ca9 100644
--- a/src/Atc.DotNet/DotnetBuildHelper.cs
+++ b/src/Atc.DotNet/DotnetBuildHelper.cs
@@ -7,6 +7,11 @@ namespace Atc.DotNet;
public static class DotnetBuildHelper
{
private const int DefaultTimeoutInSec = 1200;
+
+ // Sentinel prefix used internally when there are multiple candidate build files so the
+ // caller can detect the ambiguity without relying on a localised dotnet CLI message.
+ private const string MultipleFilesOutputPrefix = "Please specify which";
+
private static readonly ConcurrentDictionary RegexCache = new(StringComparer.Ordinal);
///
@@ -19,6 +24,7 @@ public static class DotnetBuildHelper
/// Whether to build in Release mode. If false, builds in Debug mode. Default is true.
/// Build timeout in seconds. Default is 1200 seconds (20 minutes).
/// Optional prefix for log messages.
+ /// Additional arguments appended to the dotnet build command, such as -p:TreatWarningsAsErrors=false or -f net9.0 .
/// Token to cancel the build operation.
/// A dictionary mapping error codes to their occurrence counts.
/// Thrown when is null.
@@ -26,6 +32,7 @@ public static class DotnetBuildHelper
/// This is a convenience overload that uses ; for build
/// progress visibility prefer the overload accepting an .
///
+ [SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", Justification = "OK.")]
public static Task> BuildAndCollectErrors(
DirectoryInfo rootPath,
int? runNumber = null,
@@ -34,6 +41,7 @@ public static Task> BuildAndCollectErrors(
bool useConfigurationReleaseMode = true,
int timeoutInSec = DefaultTimeoutInSec,
string logPrefix = "",
+ string additionalBuildArguments = "",
CancellationToken cancellationToken = default)
=> BuildAndCollectErrors(
NullLogger.Instance,
@@ -44,6 +52,7 @@ public static Task> BuildAndCollectErrors(
useConfigurationReleaseMode,
timeoutInSec,
logPrefix,
+ additionalBuildArguments,
cancellationToken);
///
@@ -57,6 +66,7 @@ public static Task> BuildAndCollectErrors(
/// Whether to build in Release mode. If false, builds in Debug mode. Default is true.
/// Build timeout in seconds. Default is 1200 seconds (20 minutes).
/// Optional prefix for log messages.
+ /// Additional arguments appended to the dotnet build command, such as -p:TreatWarningsAsErrors=false or -f net9.0 .
/// Token to cancel the build operation.
/// A dictionary mapping error codes to their occurrence counts.
/// Thrown when or is null.
@@ -70,19 +80,99 @@ public static Task> BuildAndCollectErrors(
bool useConfigurationReleaseMode = true,
int timeoutInSec = DefaultTimeoutInSec,
string logPrefix = "",
+ string additionalBuildArguments = "",
CancellationToken cancellationToken = default)
{
- if (logger is null)
- {
- throw new ArgumentNullException(nameof(logger));
- }
+ ArgumentNullException.ThrowIfNull(logger);
+ ArgumentNullException.ThrowIfNull(rootPath);
- if (rootPath is null)
- {
- throw new ArgumentNullException(nameof(rootPath));
- }
+ return InvokeBuildAndCollect(
+ logger,
+ rootPath,
+ runNumber,
+ buildFile,
+ useNugetRestore,
+ useConfigurationReleaseMode,
+ timeoutInSec,
+ logPrefix,
+ additionalBuildArguments,
+ collectWarnings: false,
+ cancellationToken);
+ }
- return InvokeBuildAndCollectErrors(
+ ///
+ /// Builds a .NET project or solution and collects compilation warnings grouped by warning code.
+ ///
+ /// The root directory containing the project or solution to build.
+ /// Optional run number for logging purposes.
+ /// Optional specific solution or project file to build. If not specified, discovers the build file automatically.
+ /// Whether to perform NuGet restore before building. Default is true.
+ /// Whether to build in Release mode. If false, builds in Debug mode. Default is true.
+ /// Build timeout in seconds. Default is 1200 seconds (20 minutes).
+ /// Optional prefix for log messages.
+ /// Additional arguments appended to the dotnet build command, such as -p:NoWarn=CS0168 or -f net9.0 .
+ /// Token to cancel the build operation.
+ /// A dictionary mapping warning codes to their occurrence counts.
+ /// Thrown when is null.
+ ///
+ /// This is a convenience overload that uses ; for build
+ /// progress visibility prefer the overload accepting an .
+ ///
+ [SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", Justification = "OK.")]
+ public static Task> BuildAndCollectWarnings(
+ DirectoryInfo rootPath,
+ int? runNumber = null,
+ FileInfo? buildFile = null,
+ bool useNugetRestore = true,
+ bool useConfigurationReleaseMode = true,
+ int timeoutInSec = DefaultTimeoutInSec,
+ string logPrefix = "",
+ string additionalBuildArguments = "",
+ CancellationToken cancellationToken = default)
+ => BuildAndCollectWarnings(
+ NullLogger.Instance,
+ rootPath,
+ runNumber,
+ buildFile,
+ useNugetRestore,
+ useConfigurationReleaseMode,
+ timeoutInSec,
+ logPrefix,
+ additionalBuildArguments,
+ cancellationToken);
+
+ ///
+ /// Builds a .NET project or solution with logging support and collects compilation warnings grouped by warning code.
+ ///
+ /// The logger to use for build progress and results.
+ /// The root directory containing the project or solution to build.
+ /// Optional run number for logging purposes.
+ /// Optional specific solution or project file to build. If not specified, discovers the build file automatically.
+ /// Whether to perform NuGet restore before building. Default is true.
+ /// Whether to build in Release mode. If false, builds in Debug mode. Default is true.
+ /// Build timeout in seconds. Default is 1200 seconds (20 minutes).
+ /// Optional prefix for log messages.
+ /// Additional arguments appended to the dotnet build command, such as -p:NoWarn=CS0168 or -f net9.0 .
+ /// Token to cancel the build operation.
+ /// A dictionary mapping warning codes to their occurrence counts.
+ /// Thrown when or is null.
+ [SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", Justification = "OK.")]
+ public static Task> BuildAndCollectWarnings(
+ ILogger logger,
+ DirectoryInfo rootPath,
+ int? runNumber = null,
+ FileInfo? buildFile = null,
+ bool useNugetRestore = true,
+ bool useConfigurationReleaseMode = true,
+ int timeoutInSec = DefaultTimeoutInSec,
+ string logPrefix = "",
+ string additionalBuildArguments = "",
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(logger);
+ ArgumentNullException.ThrowIfNull(rootPath);
+
+ return InvokeBuildAndCollect(
logger,
rootPath,
runNumber,
@@ -91,11 +181,13 @@ public static Task> BuildAndCollectErrors(
useConfigurationReleaseMode,
timeoutInSec,
logPrefix,
+ additionalBuildArguments,
+ collectWarnings: true,
cancellationToken);
}
[SuppressMessage("Major Code Smell", "S107:Methods should not have too many parameters", Justification = "OK.")]
- private static async Task> InvokeBuildAndCollectErrors(
+ private static async Task> InvokeBuildAndCollect(
ILogger logger,
DirectoryInfo rootPath,
int? runNumber,
@@ -104,6 +196,8 @@ private static async Task> InvokeBuildAndCollectErrors(
bool useConfigurationReleaseMode,
int timeoutInSec,
string logPrefix,
+ string additionalBuildArguments,
+ bool collectWarnings,
CancellationToken cancellationToken)
{
logger.LogInformation(runNumber is > 0
@@ -118,33 +212,38 @@ private static async Task> InvokeBuildAndCollectErrors(
useNugetRestore,
useConfigurationReleaseMode,
timeoutInSec,
+ additionalBuildArguments,
cancellationToken)
.ConfigureAwait(false);
- if (output.StartsWith("Please specify which", StringComparison.Ordinal) &&
+ if (output.StartsWith(MultipleFilesOutputPrefix, StringComparison.Ordinal) &&
output.Contains("option: --buildFile", StringComparison.Ordinal))
{
stopwatch.Stop();
throw new IOException(output);
}
- var parsedErrors = ParseBuildOutput(output);
- int totalErrors = parsedErrors.Sum(parsedError => parsedError.Value);
+ var parsed = collectWarnings
+ ? ParseBuildOutput(output, diagnostic: "warning")
+ : ParseBuildOutput(output, diagnostic: "error");
+
+ int total = parsed.Sum(x => x.Value);
stopwatch.Stop();
- if (totalErrors > 0)
+ if (total > 0)
{
+ var kind = collectWarnings ? "warnings" : "errors";
logger.LogError(runNumber is > 0
- ? $"{logPrefix}Found {totalErrors} errors divided into {parsedErrors.Count} rules in Build ({runNumber})"
- : $"{logPrefix}Found {totalErrors} errors divided into {parsedErrors.Count} rules");
+ ? $"{logPrefix}Found {total} {kind} divided into {parsed.Count} rules in Build ({runNumber})"
+ : $"{logPrefix}Found {total} {kind} divided into {parsed.Count} rules");
}
logger.LogInformation(runNumber is > 0
? $"{logPrefix}Build ({runNumber}) time: {stopwatch.Elapsed.GetPrettyTime()}"
: $"{logPrefix}Build time: {stopwatch.Elapsed.GetPrettyTime()}");
- return parsedErrors;
+ return parsed;
}
private static async Task<(
@@ -155,6 +254,7 @@ private static async Task> InvokeBuildAndCollectErrors(
bool useNugetRestore,
bool useConfigurationReleaseMode,
int timeoutInSec,
+ string additionalBuildArguments,
CancellationToken cancellationToken)
{
var argumentNugetRestore = useNugetRestore
@@ -165,38 +265,44 @@ private static async Task> InvokeBuildAndCollectErrors(
? " -c Release"
: " -c Debug";
+ var argumentAdditional = string.IsNullOrWhiteSpace(additionalBuildArguments)
+ ? string.Empty
+ : $" {additionalBuildArguments.Trim()}";
+
string arguments;
if (buildFile is not null && buildFile.Exists)
{
- arguments = $"build {buildFile.FullName}{argumentNugetRestore}{argumentConfigurationReleaseMode} -v q -clp:NoSummary";
+ arguments = $"build {buildFile.FullName}{argumentNugetRestore}{argumentConfigurationReleaseMode}{argumentAdditional} -v q -clp:NoSummary";
}
else
{
- arguments = $"build{argumentNugetRestore}{argumentConfigurationReleaseMode} -v q -clp:NoSummary";
+ arguments = $"build{argumentNugetRestore}{argumentConfigurationReleaseMode}{argumentAdditional} -v q -clp:NoSummary";
var slnFiles = Directory.GetFiles(rootPath.FullName, "*.sln");
if (slnFiles.Length > 1)
{
- var files = await slnFiles
+#pragma warning disable AsyncFixer02
+ var files = slnFiles
.Select(x => new FileInfo(x).Name)
- .ToListAsync(cancellationToken)
- .ConfigureAwait(false);
+ .ToList();
+#pragma warning restore AsyncFixer02
return (
IsSuccessful: false,
- Output: $"Please specify which solution file to use:{Environment.NewLine} - {string.Join($"{Environment.NewLine} - ", files)}{Environment.NewLine} Specify the solution file using this option: --buildFile");
+ Output: $"{MultipleFilesOutputPrefix} solution file to use:{Environment.NewLine} - {string.Join($"{Environment.NewLine} - ", files)}{Environment.NewLine} Specify the solution file using this option: --buildFile");
}
var csprojFiles = Directory.GetFiles(rootPath.FullName, "*.csproj");
if (csprojFiles.Length > 1)
{
- var files = await csprojFiles
+#pragma warning disable AsyncFixer02
+ var files = csprojFiles
.Select(x => new FileInfo(x).Name)
- .ToListAsync(cancellationToken)
- .ConfigureAwait(false);
+ .ToList();
+#pragma warning restore AsyncFixer02
return (
IsSuccessful: false,
- Output: $"Please specify which C# project file to use:{Environment.NewLine} - {string.Join($"{Environment.NewLine} - ", files)}{Environment.NewLine} Specify the C# project file using this option: --buildFile");
+ Output: $"{MultipleFilesOutputPrefix} C# project file to use:{Environment.NewLine} - {string.Join($"{Environment.NewLine} - ", files)}{Environment.NewLine} Specify the C# project file using this option: --buildFile");
}
}
@@ -207,25 +313,49 @@ private static async Task> InvokeBuildAndCollectErrors(
.ConfigureAwait(false);
}
- private static Dictionary ParseBuildOutput(string buildResult)
+ ///
+ /// Parses raw dotnet build output and returns error codes grouped by their occurrence count.
+ /// Recognises MSBuild errors (MSB prefix), NuGet errors (NU prefix), and general compiler errors
+ /// (e.g. CS, CA). The project-file suffix that MSBuild appends — [project.csproj] — is
+ /// optional; errors emitted without it are still counted.
+ ///
+ /// The raw text output from a dotnet build invocation.
+ /// A dictionary mapping each error code to the number of times it appeared.
+ public static Dictionary ParseErrors(string buildOutput)
+ => ParseBuildOutput(buildOutput, diagnostic: "error");
+
+ ///
+ /// Parses raw dotnet build output and returns warning codes grouped by their occurrence count.
+ /// Recognises MSBuild warnings (MSB prefix), NuGet warnings (NU prefix), and general compiler warnings
+ /// (e.g. CS, CA). The project-file suffix that MSBuild appends — [project.csproj] — is
+ /// optional; warnings emitted without it are still counted.
+ ///
+ /// The raw text output from a dotnet build invocation.
+ /// A dictionary mapping each warning code to the number of times it appeared.
+ public static Dictionary ParseWarnings(string buildOutput)
+ => ParseBuildOutput(buildOutput, diagnostic: "warning");
+
+ private static Dictionary ParseBuildOutput(
+ string buildResult,
+ string diagnostic)
{
- const string? regexPatternMsBuild = @": error MSB(\S+?): (.*)";
- const string? regexPatternNuget = @": error NU(\S+?): (.*)";
- const string? regexPatternGeneral = @": error ([A-Z]\S+?): (.*) \[";
+ var patternMsBuild = $@": {diagnostic} MSB(\S+?): (.*)";
+ var patternNuget = $@": {diagnostic} NU(\S+?): (.*)";
+ var patternGeneral = $@": {diagnostic} ([A-Z]\S+?): (.+)";
- var errors = ParseBuildOutputHelper(buildResult, regexPatternMsBuild, "MSB");
- if (errors.Any())
+ var results = ParseBuildOutputHelper(buildResult, patternMsBuild, "MSB");
+ if (results.Any())
{
- return errors;
+ return results;
}
- errors = ParseBuildOutputHelper(buildResult, regexPatternNuget, "NU");
- if (errors.Any())
+ results = ParseBuildOutputHelper(buildResult, patternNuget, "NU");
+ if (results.Any())
{
- return errors;
+ return results;
}
- return ParseBuildOutputHelper(buildResult, regexPatternGeneral);
+ return ParseBuildOutputHelper(buildResult, patternGeneral);
}
private static Dictionary ParseBuildOutputHelper(
diff --git a/src/Atc.DotNet/DotnetHelper.cs b/src/Atc.DotNet/DotnetHelper.cs
index 5d31995b..15dd35bd 100644
--- a/src/Atc.DotNet/DotnetHelper.cs
+++ b/src/Atc.DotNet/DotnetHelper.cs
@@ -17,16 +17,21 @@ public static class DotnetHelper
///
public static DirectoryInfo GetDotnetDirectory()
{
- var pathEnvironmentVariable = Environment.GetEnvironmentVariable("path");
- if (pathEnvironmentVariable is not null &&
- pathEnvironmentVariable.Contains("dotnet", StringComparison.Ordinal))
+ var dotnetFilename = OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet";
+ var pathEnvironmentVariable = Environment.GetEnvironmentVariable("PATH");
+ if (pathEnvironmentVariable is not null)
{
- var sa = pathEnvironmentVariable.Split(Path.PathSeparator);
- foreach (var s in sa)
+ foreach (var segment in pathEnvironmentVariable.Split(Path.PathSeparator))
{
- if (s.Contains("dotnet", StringComparison.Ordinal))
+ if (string.IsNullOrWhiteSpace(segment))
{
- return new DirectoryInfo(s);
+ continue;
+ }
+
+ var candidate = Path.Combine(segment, dotnetFilename);
+ if (File.Exists(candidate))
+ {
+ return new DirectoryInfo(segment);
}
}
}
diff --git a/src/Atc.DotNet/DotnetNugetHelper.cs b/src/Atc.DotNet/DotnetNugetHelper.cs
index 2d313242..05523f7e 100644
--- a/src/Atc.DotNet/DotnetNugetHelper.cs
+++ b/src/Atc.DotNet/DotnetNugetHelper.cs
@@ -49,51 +49,20 @@ public static List GetAllPackageReferences(
throw new DataException("Expect xml content");
}
- var data = new List();
- foreach (var line in fileContent.EnsureEnvironmentNewLinesAndSplit())
- {
- if (!line.Contains(" ", string.Empty, StringComparison.Ordinal)
- .Replace(">", string.Empty, StringComparison.Ordinal)
- .Trim()
- .Split(' ');
-
- var packageId = string.Empty;
- var version = string.Empty;
-
- foreach (var attribute in attributes)
+ var xDoc = XDocument.Parse(fileContent);
+ var data = xDoc
+ .Descendants("PackageReference")
+ .Select(e => new
{
- if (attribute.StartsWith("Include=", StringComparison.Ordinal))
- {
- packageId = attribute
- .Replace("Include=", string.Empty, StringComparison.Ordinal)
- .Replace("\"", string.Empty, StringComparison.Ordinal);
- }
- else if (attribute.StartsWith("Version=", StringComparison.Ordinal))
- {
- version = attribute
- .Replace("Version=", string.Empty, StringComparison.Ordinal)
- .Replace("\"", string.Empty, StringComparison.Ordinal);
- }
- }
-
- if (!string.IsNullOrEmpty(packageId) &&
- !string.IsNullOrEmpty(version))
- {
- data.Add(new DotnetNugetPackageMetadataBase(packageId, version));
- }
- }
-
- return data
+ PackageId = e.Attribute("Include")?.Value,
+ Version = e.Attribute("Version")?.Value
+ ?? e.Element("Version")?.Value,
+ })
+ .Where(x => !string.IsNullOrEmpty(x.PackageId) && !string.IsNullOrEmpty(x.Version))
+ .Select(x => new DotnetNugetPackageMetadataBase(x.PackageId!, x.Version!))
.OrderBy(x => x.PackageId, StringComparer.Ordinal)
.ToList();
+
+ return data;
}
}
\ No newline at end of file
diff --git a/src/Atc.OpenApi/Extensions/OpenApiResponsesExtensions.cs b/src/Atc.OpenApi/Extensions/OpenApiResponsesExtensions.cs
index 53e9b30b..73c97271 100644
--- a/src/Atc.OpenApi/Extensions/OpenApiResponsesExtensions.cs
+++ b/src/Atc.OpenApi/Extensions/OpenApiResponsesExtensions.cs
@@ -242,7 +242,7 @@ public static bool IsSchemaUsingBinaryFormatForOkResponse(
{
foreach (var (key, value) in responses.OrderBy(x => x.Key, StringComparer.Ordinal))
{
- if (!key.Equals(((int)HttpStatusCode.OK).ToString(CultureInfo.CurrentCulture), StringComparison.OrdinalIgnoreCase))
+ if (!key.Equals(((int)HttpStatusCode.OK).ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal))
{
continue;
}
diff --git a/src/Atc.OpenApi/Extensions/OpenApiSchemaExtensions.cs b/src/Atc.OpenApi/Extensions/OpenApiSchemaExtensions.cs
index f26289e7..78fcd32e 100644
--- a/src/Atc.OpenApi/Extensions/OpenApiSchemaExtensions.cs
+++ b/src/Atc.OpenApi/Extensions/OpenApiSchemaExtensions.cs
@@ -604,6 +604,11 @@ public static bool HasAnyPropertiesFormatTypeFromSystemNamespace(
this OpenApiSchema schema,
IDictionary componentSchemas)
{
+ if (componentSchemas is null)
+ {
+ throw new ArgumentNullException(nameof(componentSchemas));
+ }
+
if (!schema.HasAnyProperties())
{
return false;
@@ -642,6 +647,11 @@ public static bool HasAnyPropertiesFormatTypeFromSystemCollectionGenericNamespac
this OpenApiSchema schema,
IDictionary componentSchemas)
{
+ if (componentSchemas is null)
+ {
+ throw new ArgumentNullException(nameof(componentSchemas));
+ }
+
if (!schema.HasAnyProperties())
{
return false;
@@ -1459,7 +1469,12 @@ public static Tuple GetEnumSchema(
continue;
}
- var enumName = schemaProperty.Value.Reference?.Id ?? schema.Reference.Id;
+ var enumName = schemaProperty.Value.Reference?.Id ?? schema.Reference?.Id;
+ if (enumName is null)
+ {
+ throw new ItemNotFoundException("Enum schema is missing a Reference.Id.");
+ }
+
return Tuple.Create(enumName, schemaProperty.Value);
}
@@ -1519,9 +1534,8 @@ private static bool HasAnyPropertiesFormatTypeFromSystemNamespaceHelper(
return false;
}
- var componentSchema = componentSchemas.FirstOrDefault(x => x.Key == modelName);
- return !string.IsNullOrEmpty(componentSchema.Key) &&
- componentSchema.Value.HasAnyPropertiesFormatTypeFromSystemNamespace(componentSchemas);
+ return componentSchemas.TryGetValue(modelName!, out var componentSchemaValue) &&
+ componentSchemaValue.HasAnyPropertiesFormatTypeFromSystemNamespace(componentSchemas);
}
private static bool HasAnyPropertiesFormatTypeFromSystemCollectionGenericNamespaceHelper(
@@ -1539,8 +1553,7 @@ private static bool HasAnyPropertiesFormatTypeFromSystemCollectionGenericNamespa
return false;
}
- var componentSchema = componentSchemas.FirstOrDefault(x => x.Key == modelName);
- return !string.IsNullOrEmpty(componentSchema.Key) &&
- componentSchema.Value.HasAnyPropertiesFormatTypeFromSystemCollectionGenericNamespace(componentSchemas);
+ return componentSchemas.TryGetValue(modelName!, out var componentSchemaValue) &&
+ componentSchemaValue.HasAnyPropertiesFormatTypeFromSystemCollectionGenericNamespace(componentSchemas);
}
}
\ No newline at end of file
diff --git a/src/Atc.Rest.Extended/Atc.Rest.Extended.csproj b/src/Atc.Rest.Extended/Atc.Rest.Extended.csproj
index 5dc5ca94..894d3b5a 100644
--- a/src/Atc.Rest.Extended/Atc.Rest.Extended.csproj
+++ b/src/Atc.Rest.Extended/Atc.Rest.Extended.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/src/Atc.Rest.Extended/Filters/SwaggerEnumDescriptionsDocumentFilter.cs b/src/Atc.Rest.Extended/Filters/SwaggerEnumDescriptionsDocumentFilter.cs
index 9c5bcd21..5abf97f9 100644
--- a/src/Atc.Rest.Extended/Filters/SwaggerEnumDescriptionsDocumentFilter.cs
+++ b/src/Atc.Rest.Extended/Filters/SwaggerEnumDescriptionsDocumentFilter.cs
@@ -115,9 +115,7 @@ private static string DescribeEnum(
private static Type? GetEnumTypeByName(string enumTypeName)
=> AppDomain.CurrentDomain
.GetAssemblies()
- .SelectMany(x => x
- .GetTypes()
- .Where(t => t.IsEnum))
+ .SelectMany(GetEnumTypesFromAssembly)
.Where(x => string.Equals(x.Name, enumTypeName, StringComparison.Ordinal))
.ToArray()
switch
@@ -125,4 +123,21 @@ private static string DescribeEnum(
{ Length: 1 } a => a[0],
_ => null,
};
+
+ [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Assembly.GetExportedTypes can fail for many reasons; fall back to empty on any error.")]
+ private static IEnumerable GetEnumTypesFromAssembly(Assembly assembly)
+ {
+ try
+ {
+ return assembly.GetExportedTypes().Where(t => t.IsEnum);
+ }
+ catch (ReflectionTypeLoadException ex)
+ {
+ return ex.Types.Where(t => t?.IsEnum == true).Select(t => t!);
+ }
+ catch
+ {
+ return [];
+ }
+ }
}
\ No newline at end of file
diff --git a/src/Atc.Rest.Extended/GlobalUsings.cs b/src/Atc.Rest.Extended/GlobalUsings.cs
index 9bac7546..f2a28811 100644
--- a/src/Atc.Rest.Extended/GlobalUsings.cs
+++ b/src/Atc.Rest.Extended/GlobalUsings.cs
@@ -16,7 +16,6 @@
global using FluentValidation;
global using FluentValidation.AspNetCore;
-global using Microsoft.ApplicationInsights;
global using Microsoft.AspNetCore.Authentication;
global using Microsoft.AspNetCore.Authentication.JwtBearer;
global using Microsoft.AspNetCore.Authorization;
@@ -32,8 +31,6 @@
global using Microsoft.Extensions.Logging;
global using Microsoft.Extensions.Options;
global using Microsoft.IdentityModel.Logging;
-global using Microsoft.IdentityModel.Protocols;
-global using Microsoft.IdentityModel.Protocols.OpenIdConnect;
global using Microsoft.IdentityModel.Tokens;
global using Microsoft.OpenApi;
diff --git a/src/Atc.Rest.Extended/Options/ConfigureApiVersioningOptions.cs b/src/Atc.Rest.Extended/Options/ConfigureApiVersioningOptions.cs
index c5355941..17b150f1 100644
--- a/src/Atc.Rest.Extended/Options/ConfigureApiVersioningOptions.cs
+++ b/src/Atc.Rest.Extended/Options/ConfigureApiVersioningOptions.cs
@@ -6,15 +6,11 @@ namespace Atc.Rest.Extended.Options;
///
public class ConfigureApiVersioningOptions : IConfigureOptions
{
- private readonly TelemetryClient telemetry;
-
///
/// Initializes a new instance of the class.
///
- /// The Application Insights telemetry client.
- public ConfigureApiVersioningOptions(TelemetryClient telemetry)
+ public ConfigureApiVersioningOptions()
{
- this.telemetry = telemetry;
}
///
diff --git a/src/Atc.Rest.Extended/Options/ConfigureAuthorizationOptions.cs b/src/Atc.Rest.Extended/Options/ConfigureAuthorizationOptions.cs
index df02c693..7d51ac0d 100644
--- a/src/Atc.Rest.Extended/Options/ConfigureAuthorizationOptions.cs
+++ b/src/Atc.Rest.Extended/Options/ConfigureAuthorizationOptions.cs
@@ -5,14 +5,13 @@ namespace Atc.Rest.Extended.Options;
///
/// Post-configures JWT Bearer authentication and authorization options based on .
-/// Handles issuer signing key retrieval from OpenID Connect configuration and token validation setup.
+/// Signing-key discovery is delegated to JwtBearer's built-in ,
+/// which fetches and caches the OIDC discovery document on the first authentication request using the set here.
///
public class ConfigureAuthorizationOptions :
IPostConfigureOptions,
IPostConfigureOptions
{
- private const string WellKnownOpenidConfiguration = ".well-known/openid-configuration";
- private static readonly TimeSpan SigningKeyFetchTimeout = TimeSpan.FromSeconds(30);
private readonly IWebHostEnvironment? environment;
private readonly RestApiExtendedOptions apiOptions;
private readonly ILogger? logger;
@@ -34,7 +33,9 @@ public ConfigureAuthorizationOptions(
}
///
- /// Post-configures JWT Bearer options with token validation parameters and issuer signing keys.
+ /// Post-configures JWT Bearer options with token validation parameters.
+ /// Signing keys are not pre-fetched; JwtBearer's built-in
+ /// discovers and caches them from the OIDC discovery endpoint on the first authentication request.
///
/// The name of the options instance being configured.
/// The to configure.
@@ -92,39 +93,44 @@ public void PostConfigure(
options.Authority = $"{apiOptions.Authorization.Instance}/{apiOptions.Authorization.TenantId}/";
}
- options.TokenValidationParameters = new TokenValidationParameters
+ var tvp = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = apiOptions.Authorization.Audience,
ValidAudiences = apiOptions.Authorization.ValidAudiences,
ValidateIssuer = !string.IsNullOrWhiteSpace(apiOptions.Authorization.Issuer) ||
apiOptions.Authorization.ValidIssuers?.Any() == true,
+
+ // Always validate the token signature (fail-closed). This is never relaxed below,
+ // so a transient key-fetch failure can never cause unverified tokens to be accepted.
+ ValidateIssuerSigningKey = true,
};
- if (!options.TokenValidationParameters.ValidateIssuer)
+ if (!string.IsNullOrEmpty(apiOptions.Authorization.RoleClaimType))
{
- return;
+ tvp.RoleClaimType = apiOptions.Authorization.RoleClaimType;
}
- options.TokenValidationParameters.ValidIssuer = apiOptions.Authorization.Issuer;
- options.TokenValidationParameters.ValidIssuers = apiOptions.Authorization.ValidIssuers ?? new List();
+ if (!string.IsNullOrEmpty(apiOptions.Authorization.NameClaimType))
+ {
+ tvp.NameClaimType = apiOptions.Authorization.NameClaimType;
+ }
+
+ options.TokenValidationParameters = tvp;
- // Run the async fetch on a thread-pool thread with a hard timeout so we cannot deadlock
- // on a captured sync-context and cannot hang application startup indefinitely if an
- // identity provider is unreachable.
- var fetchTask = Task.Run(() => GetIssuerSigningKeysAsync(options));
- if (!fetchTask.Wait(SigningKeyFetchTimeout))
+ if (!options.TokenValidationParameters.ValidateIssuer)
{
- logger?.LogWarning(
- "Timed out fetching issuer signing keys after {TimeoutSeconds}s. Token validation will fall back to empty key set; signing-key validation disabled.",
- SigningKeyFetchTimeout.TotalSeconds);
- options.TokenValidationParameters.IssuerSigningKeys = Array.Empty();
- options.TokenValidationParameters.ValidateIssuerSigningKey = false;
return;
}
- options.TokenValidationParameters.IssuerSigningKeys = fetchTask.Result;
- options.TokenValidationParameters.ValidateIssuerSigningKey = options.TokenValidationParameters.IssuerSigningKeys.Any();
+ options.TokenValidationParameters.ValidIssuer = apiOptions.Authorization.Issuer;
+ options.TokenValidationParameters.ValidIssuers = apiOptions.Authorization.ValidIssuers ?? new List();
+
+ // Signing keys are discovered lazily by JwtBearer's built-in ConfigurationManager via options.Authority.
+ // Pre-fetching here blocked application startup for up to 30 s and duplicated JwtBearer's own mechanism.
+ logger?.LogInformation(
+ "JWT signing-key discovery deferred: keys will be fetched from {Authority} on the first authentication request.",
+ options.Authority);
}
///
@@ -141,68 +147,6 @@ public void PostConfigure(
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}
- ///
- /// Retrieves issuer signing keys from the OpenID Connect configuration endpoint.
- ///
- /// The issuer URL.
- ///
- /// A collection of security keys for token validation, or an empty array if retrieval failed
- /// (the failure is logged via the configured ; callers should treat an
- /// empty result as "signing keys unavailable").
- ///
- [SuppressMessage("Microsoft.Design", "CA1031:Do not catch general exception types", Justification = "Failure to fetch keys must not abort startup; we log and return empty so the caller decides.")]
- private async Task> GetIssuerSigningKeysAsync(
- string issuer)
- {
- try
- {
- var configurationManager = new ConfigurationManager(
- $"{issuer}/{WellKnownOpenidConfiguration}",
- new OpenIdConnectConfigurationRetriever());
-
- var configuration = await configurationManager.GetConfigurationAsync();
- return configuration.SigningKeys;
- }
- catch (Exception e)
- {
- logger?.LogWarning(
- e,
- "Failed to retrieve OpenID Connect signing keys from {Issuer}. Token validation will fall back to an empty key set for this issuer.",
- issuer);
- return Array.Empty();
- }
- }
-
- private async Task> GetIssuerSigningKeysAsync(
- JwtBearerOptions options)
- {
- var issuerSigningKeys = new List();
-
- if (!string.IsNullOrEmpty(options.Authority))
- {
- issuerSigningKeys.AddRange(
- await GetIssuerSigningKeysAsync(
- options.Authority));
- }
-
- if (apiOptions.Authorization is not null &&
- !string.IsNullOrWhiteSpace(apiOptions.Authorization.Issuer))
- {
- issuerSigningKeys.AddRange(
- await GetIssuerSigningKeysAsync(
- apiOptions.Authorization.Issuer));
- }
-
- foreach (var issuer in options.TokenValidationParameters.ValidIssuers)
- {
- issuerSigningKeys.AddRange(
- await GetIssuerSigningKeysAsync(
- issuer));
- }
-
- return issuerSigningKeys;
- }
-
private void SanityCheck(JwtBearerOptions options)
{
ArgumentNullException.ThrowIfNull(options);
diff --git a/src/Atc.Rest.Extended/Options/ConfigureSwaggerOptions.cs b/src/Atc.Rest.Extended/Options/ConfigureSwaggerOptions.cs
index 84decb60..83f315dd 100644
--- a/src/Atc.Rest.Extended/Options/ConfigureSwaggerOptions.cs
+++ b/src/Atc.Rest.Extended/Options/ConfigureSwaggerOptions.cs
@@ -47,6 +47,7 @@ public void Configure(SwaggerUIOptions options)
/// Configures Swagger generation options including API versioning, XML documentation, and security filters.
///
/// The to configure.
+ [SuppressMessage("Design", "MA0051:Method is too long", Justification = "OK.")]
public void Configure(SwaggerGenOptions options)
{
options.TagActionsBy(api =>
@@ -84,7 +85,7 @@ public void Configure(SwaggerGenOptions options)
description.GroupName,
new OpenApiInfo
{
- Title = Assembly.GetEntryAssembly()!.GetApiName(),
+ Title = (Assembly.GetEntryAssembly() ?? Assembly.GetCallingAssembly()).GetApiName(),
Version = description.ApiVersion.ToString(),
});
}
@@ -106,6 +107,33 @@ public void Configure(SwaggerGenOptions options)
{
options.OperationFilter();
options.OperationFilter();
+
+ var authOptions = restApiOptions.Authorization;
+ if (authOptions?.IsSecurityEnabled() == true &&
+ !string.IsNullOrEmpty(authOptions.Instance) &&
+ !string.IsNullOrEmpty(authOptions.TenantId))
+ {
+ var baseUrl = authOptions.Instance.TrimEnd('/');
+ var tenant = authOptions.TenantId;
+ options.AddSecurityDefinition(
+ "OAuth2",
+ new OpenApiSecurityScheme
+ {
+ Type = SecuritySchemeType.OAuth2,
+ Flows = new OpenApiOAuthFlows
+ {
+ AuthorizationCode = new OpenApiOAuthFlow
+ {
+ AuthorizationUrl = new Uri($"{baseUrl}/{tenant}/oauth2/v2.0/authorize"),
+ TokenUrl = new Uri($"{baseUrl}/{tenant}/oauth2/v2.0/token"),
+ Scopes = new Dictionary(StringComparer.Ordinal)
+ {
+ [$"api://{authOptions.ClientId}/.default"] = "Default scope",
+ },
+ },
+ },
+ });
+ }
}
}
diff --git a/src/Atc.Rest.FluentAssertions/Assertions/ContentResultAssertionsBase.cs b/src/Atc.Rest.FluentAssertions/Assertions/ContentResultAssertionsBase.cs
index 0de80b50..0ca642b8 100644
--- a/src/Atc.Rest.FluentAssertions/Assertions/ContentResultAssertionsBase.cs
+++ b/src/Atc.Rest.FluentAssertions/Assertions/ContentResultAssertionsBase.cs
@@ -78,7 +78,7 @@ public AndWhichConstraint WithContentOfType(
.BecauseOf(because, becauseArgs)
.WithDefaultIdentifier($"content type of {Identifier}")
.Given(() => Subject.ContentType)
- .ForCondition(contentType => contentType is not null && contentType.Equals(MediaTypeNames.Application.Json, StringComparison.Ordinal))
+ .ForCondition(IsJsonContentType)
.FailWith("Expected {context} to be {0}{reason}, but found {1}.", _ => MediaTypeNames.Application.Json, x => x);
var parseSuccess = TryContentValueAs(out var result);
@@ -119,6 +119,29 @@ protected bool TryContentValueAs([NotNullWhen(true)] out T content)
return false;
}
+ ///
+ /// Determines whether the supplied content type denotes JSON, ignoring any
+ /// parameters such as ; charset=utf-8 and comparing case-insensitively.
+ ///
+ /// The raw value.
+ /// if the media type is application/json ; otherwise, .
+ private static bool IsJsonContentType(string? contentType)
+ {
+ if (string.IsNullOrEmpty(contentType))
+ {
+ return false;
+ }
+
+ var separatorIndex = contentType.IndexOf(';', StringComparison.Ordinal);
+ var mediaType = separatorIndex >= 0
+ ? contentType[..separatorIndex]
+ : contentType;
+
+ return mediaType
+ .Trim()
+ .Equals(MediaTypeNames.Application.Json, StringComparison.OrdinalIgnoreCase);
+ }
+
private bool TryContentValueAs(
Type type,
out object content)
diff --git a/src/Atc.Rest.FluentAssertions/Assertions/ErrorContentResultAssertions.cs b/src/Atc.Rest.FluentAssertions/Assertions/ErrorContentResultAssertions.cs
index ed06a41e..ddbc145c 100644
--- a/src/Atc.Rest.FluentAssertions/Assertions/ErrorContentResultAssertions.cs
+++ b/src/Atc.Rest.FluentAssertions/Assertions/ErrorContentResultAssertions.cs
@@ -34,7 +34,7 @@ public AndWhichConstraint WithErrorMessage(
if (TryContentValueAs(out var pd))
{
- actualErrorMessage = pd.Detail;
+ actualErrorMessage = pd.Detail ?? string.Empty;
}
else if (TryContentValueAs(out var details))
{
diff --git a/src/Atc.Rest.FluentAssertions/Assertions/OkResultAssertions.cs b/src/Atc.Rest.FluentAssertions/Assertions/OkResultAssertions.cs
index 0d372682..44789350 100644
--- a/src/Atc.Rest.FluentAssertions/Assertions/OkResultAssertions.cs
+++ b/src/Atc.Rest.FluentAssertions/Assertions/OkResultAssertions.cs
@@ -35,6 +35,23 @@ public AndWhichConstraint WithContentOfType(
}
}
+ ///
+ /// Asserts that the OK result has no body content (the result value is ).
+ ///
+ /// Optional explanation of why the assertion is needed.
+ /// Optional formatting arguments for the parameter.
+ /// An for chaining further assertions.
+ public AndConstraint WithEmptyContent(
+ string because = "",
+ params object[] becauseArgs)
+ {
+ Execute.Assertion
+ .BecauseOf(because, becauseArgs)
+ .ForCondition(Subject.Value is null)
+ .FailWith("Expected content of {0} to be empty{{reason}}, but found {1}.", Identifier, Subject.Value);
+ return new AndConstraint(this);
+ }
+
///
/// Asserts that the OK result contains content equivalent to the specified expected content.
///
diff --git a/src/Atc.Rest.FluentAssertions/Assertions/ResultAssertions.cs b/src/Atc.Rest.FluentAssertions/Assertions/ResultAssertions.cs
index 8c0dfa01..44f680cb 100644
--- a/src/Atc.Rest.FluentAssertions/Assertions/ResultAssertions.cs
+++ b/src/Atc.Rest.FluentAssertions/Assertions/ResultAssertions.cs
@@ -47,6 +47,24 @@ public OkResultAssertions BeOkResult(
return new OkResultAssertions(okSubject);
}
+ ///
+ /// Asserts that the action result is a 200 OK result whose content is equivalent to .
+ /// This is a convenience shorthand for BeOkResult().WithContent(expectedContent) .
+ ///
+ /// The type of the expected content.
+ /// The expected content value to compare against.
+ /// Optional explanation of why the assertion is needed.
+ /// Optional formatting arguments for the parameter.
+ /// An for further assertions.
+ public AndWhichConstraint BeOkResultWithContent(
+ T expectedContent,
+ string because = "",
+ params object[] becauseArgs)
+ {
+ var okAssertions = BeOkResult(because, becauseArgs);
+ return okAssertions.WithContent(expectedContent, because, becauseArgs);
+ }
+
///
/// Asserts that the action result is a with HTTP status code 201 (Created).
///
diff --git a/src/Atc.Rest.HealthChecks/Atc.Rest.HealthChecks.csproj b/src/Atc.Rest.HealthChecks/Atc.Rest.HealthChecks.csproj
index 1725035a..a883e009 100644
--- a/src/Atc.Rest.HealthChecks/Atc.Rest.HealthChecks.csproj
+++ b/src/Atc.Rest.HealthChecks/Atc.Rest.HealthChecks.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/src/Atc.Rest.HealthChecks/Extensions/HealthReportEntryExtensions.cs b/src/Atc.Rest.HealthChecks/Extensions/HealthReportEntryExtensions.cs
index 681d933a..2ae15308 100644
--- a/src/Atc.Rest.HealthChecks/Extensions/HealthReportEntryExtensions.cs
+++ b/src/Atc.Rest.HealthChecks/Extensions/HealthReportEntryExtensions.cs
@@ -60,8 +60,12 @@ private static IReadOnlyDictionary SanitizeData(
IReadOnlyDictionary data)
=> data.ToDictionary(
kvp => kvp.Key,
- kvp => kvp.Value is Exception ex
- ? ex.Message
- : kvp.Value,
+ kvp => kvp.Value switch
+ {
+ null => (object)string.Empty,
+ Exception ex => ex.Message,
+ string s => s,
+ _ => kvp.Value.ToString() ?? string.Empty,
+ },
StringComparer.Ordinal);
}
\ No newline at end of file
diff --git a/src/Atc.Rest.HealthChecks/Extensions/ResourceHealthCheckExtensions.cs b/src/Atc.Rest.HealthChecks/Extensions/ResourceHealthCheckExtensions.cs
index e6d17396..757db6a8 100644
--- a/src/Atc.Rest.HealthChecks/Extensions/ResourceHealthCheckExtensions.cs
+++ b/src/Atc.Rest.HealthChecks/Extensions/ResourceHealthCheckExtensions.cs
@@ -12,13 +12,19 @@ public static class ResourceHealthCheckExtensions
/// A read-only dictionary where keys are resource names and values are anonymous objects containing status, duration, and description.
public static IReadOnlyDictionary ToIReadOnlyDictionary(
this IEnumerable resourceHealthCheck)
- => resourceHealthCheck.ToDictionary(
- keySelector: key => key.Name,
- elementSelector: e => (object)new
- {
- e.Status,
- e.Duration,
- e.Description,
- },
- StringComparer.Ordinal);
+ => resourceHealthCheck
+ .GroupBy(e => e.Name, StringComparer.Ordinal)
+ .ToDictionary(
+ g => g.Key,
+ g =>
+ {
+ var e = g.First();
+ return (object)new
+ {
+ e.Status,
+ e.Duration,
+ e.Description,
+ };
+ },
+ StringComparer.Ordinal);
}
\ No newline at end of file
diff --git a/src/Atc.Rest.HealthChecks/Factories/HealthCheckOptionsFactory.cs b/src/Atc.Rest.HealthChecks/Factories/HealthCheckOptionsFactory.cs
index f2f1a498..be8ef80f 100644
--- a/src/Atc.Rest.HealthChecks/Factories/HealthCheckOptionsFactory.cs
+++ b/src/Atc.Rest.HealthChecks/Factories/HealthCheckOptionsFactory.cs
@@ -33,10 +33,11 @@ public static HealthCheckOptions CreateJson(
r.Status,
r.TotalDuration);
- await c.Response.WriteAsync(
- JsonSerializer.Serialize(
- response,
- jsonSerializerOptions ?? JsonSerializerOptionsFactory.Create()));
+ await JsonSerializer.SerializeAsync(
+ c.Response.Body,
+ response,
+ jsonSerializerOptions ?? JsonSerializerOptionsFactory.Default,
+ c.RequestAborted);
},
};
}
\ No newline at end of file
diff --git a/src/Atc.Rest.HealthChecks/GlobalUsings.cs b/src/Atc.Rest.HealthChecks/GlobalUsings.cs
index f42fecc3..4affb6a3 100644
--- a/src/Atc.Rest.HealthChecks/GlobalUsings.cs
+++ b/src/Atc.Rest.HealthChecks/GlobalUsings.cs
@@ -4,5 +4,4 @@
global using Atc.Rest.HealthChecks.Models;
global using Atc.Serialization;
global using Microsoft.AspNetCore.Diagnostics.HealthChecks;
-global using Microsoft.AspNetCore.Http;
global using Microsoft.Extensions.Diagnostics.HealthChecks;
\ No newline at end of file
diff --git a/src/Atc.Rest/Extensions/ApplicationBuilder/RestApiBuilderExtensions.cs b/src/Atc.Rest/Extensions/ApplicationBuilder/RestApiBuilderExtensions.cs
index 1613dc4e..33ce9bc9 100644
--- a/src/Atc.Rest/Extensions/ApplicationBuilder/RestApiBuilderExtensions.cs
+++ b/src/Atc.Rest/Extensions/ApplicationBuilder/RestApiBuilderExtensions.cs
@@ -110,8 +110,11 @@ public static IApplicationBuilder ConfigureRestApi(
options.AllowCredentials();
});
}
- else
+ else if (env.IsDevelopment())
{
+ // Only allow all origins in Development; in other environments with no configured
+ // origins the CORS middleware is simply not added, which means the browser's
+ // same-origin policy applies and no CORS headers are emitted.
app.UseCors(options =>
{
options.AllowAnyHeader();
diff --git a/src/Atc.Rest/Extensions/HeaderDictionaryExtensions.cs b/src/Atc.Rest/Extensions/HeaderDictionaryExtensions.cs
index 53b439eb..41ec7652 100644
--- a/src/Atc.Rest/Extensions/HeaderDictionaryExtensions.cs
+++ b/src/Atc.Rest/Extensions/HeaderDictionaryExtensions.cs
@@ -58,7 +58,11 @@ public static string AddCorrelationId(
if (headers.TryGetValue(WellKnownHttpHeaders.RequestId, out var header))
{
- return header.FirstOrDefault();
+ var value = header.FirstOrDefault();
+ if (!string.IsNullOrEmpty(value) && IsSafeRequestId(value!))
+ {
+ return value;
+ }
}
var requestId = Guid
@@ -93,4 +97,13 @@ private static bool IsValidCorrelationId(string value)
=> value.Length <= 68 &&
!value.AsSpan().ContainsAny('\r', '\n') &&
Guid.TryParse(value, out _);
+
+ ///
+ /// Validates that a request ID value is safe to echo and log: bounded length and free of
+ /// CR/LF control characters, guarding against header-injection and log-forging. Unlike the
+ /// correlation ID, a non-GUID format is permitted so legitimate upstream request IDs pass through.
+ ///
+ private static bool IsSafeRequestId(string value)
+ => value.Length <= 128 &&
+ !value.AsSpan().ContainsAny('\r', '\n');
}
\ No newline at end of file
diff --git a/src/Atc.Rest/Extensions/HttpRequestExExtensions.cs b/src/Atc.Rest/Extensions/HttpRequestExExtensions.cs
index 7f3f5926..4c05add9 100644
--- a/src/Atc.Rest/Extensions/HttpRequestExExtensions.cs
+++ b/src/Atc.Rest/Extensions/HttpRequestExExtensions.cs
@@ -20,8 +20,19 @@ public static async Task GetRawBodyStringAsync(
encoding ??= Encoding.UTF8;
- using var reader = new StreamReader(request.Body, encoding);
- return await reader.ReadToEndAsync();
+ // Allow the body to be read more than once so downstream model binding still works.
+ request.EnableBuffering();
+
+ string body;
+ using (var reader = new StreamReader(request.Body, encoding, leaveOpen: true))
+ {
+ body = await reader.ReadToEndAsync();
+ }
+
+ // Rewind so subsequent reads (e.g. model binding) start from the beginning.
+ request.Body.Position = 0;
+
+ return body;
}
///
diff --git a/src/Atc.Rest/Extensions/ServiceCollection/ServiceCollectionExtensions.cs b/src/Atc.Rest/Extensions/ServiceCollection/ServiceCollectionExtensions.cs
index 53d73439..999f4a6e 100644
--- a/src/Atc.Rest/Extensions/ServiceCollection/ServiceCollectionExtensions.cs
+++ b/src/Atc.Rest/Extensions/ServiceCollection/ServiceCollectionExtensions.cs
@@ -34,23 +34,23 @@ public static void AutoRegistrateServices(
.DefinedTypes
.ToArray();
+ // Build a lookup keyed by interface FullName to avoid an O(interfaces × types) double loop.
+ var implementationLookup = implementationTypes
+ .SelectMany(t => t.GetInterfaces(), (t, i) => (InterfaceName: i.FullName, Type: t))
+ .Where(x => x.InterfaceName is not null)
+ .ToLookup(x => x.InterfaceName!, x => x.Type, StringComparer.Ordinal);
+
foreach (var implementationInterface in implementationInterfaces)
{
- foreach (var implementationType in implementationTypes)
+ var matchingType = implementationLookup[implementationInterface.FullName ?? string.Empty].FirstOrDefault();
+ if (matchingType is null)
{
- if (implementationType
- .GetInterfaces()
- .FirstOrDefault(x => string.Equals(x.FullName, implementationInterface.FullName, StringComparison.Ordinal)) is null)
- {
- continue;
- }
-
- if (!IsImplementationTypeRegistered(services, implementationType))
- {
- services.AddTransient(implementationInterface, implementationType);
- }
+ continue;
+ }
- break;
+ if (!IsImplementationTypeRegistered(services, matchingType))
+ {
+ services.AddTransient(implementationInterface, matchingType);
}
}
}
@@ -68,11 +68,12 @@ public static void ValidateServiceRegistrations(
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(apiAssembly);
+ var registeredServiceTypes = new HashSet(services.Select(x => x.ServiceType));
+
var notRegistered = apiAssembly
.DefinedTypes
.Where(x => x.IsInterface)
- .Where(typeInfo => services
- .All(x => x.ServiceType != typeInfo))
+ .Where(typeInfo => !registeredServiceTypes.Contains(typeInfo))
.ToList();
if (notRegistered.Count <= 0)
diff --git a/src/Atc.Rest/Filters/ErrorHandlingExceptionFilterAttribute.cs b/src/Atc.Rest/Filters/ErrorHandlingExceptionFilterAttribute.cs
index 1a7966e0..120e37d8 100644
--- a/src/Atc.Rest/Filters/ErrorHandlingExceptionFilterAttribute.cs
+++ b/src/Atc.Rest/Filters/ErrorHandlingExceptionFilterAttribute.cs
@@ -17,7 +17,6 @@ namespace Microsoft.AspNetCore.Mvc.Filters;
///
/// → 400 Bad Request
/// → 401 Unauthorized
-/// → 409 Conflict
/// → 501 Not Implemented
/// - All other exceptions → 500 Internal Server Error
///
@@ -92,10 +91,6 @@ private static HttpStatusCode GetHttpStatusCodeByExceptionType(
{
statusCode = HttpStatusCode.Unauthorized;
}
- else if (exceptionType == typeof(InvalidOperationException))
- {
- statusCode = HttpStatusCode.Conflict;
- }
else if (exceptionType == typeof(NotImplementedException))
{
statusCode = HttpStatusCode.NotImplemented;
@@ -106,14 +101,24 @@ private static HttpStatusCode GetHttpStatusCodeByExceptionType(
private void HandleException(ExceptionContext context)
{
- context.Result = new ContentResult
+ var statusCode = (int)GetHttpStatusCodeByExceptionType(context);
+
+ if (useProblemDetailsAsResponseBody)
{
- ContentType = MediaTypeNames.Application.Json,
- StatusCode = (int)GetHttpStatusCodeByExceptionType(context),
- Content = useProblemDetailsAsResponseBody
- ? JsonSerializer.Serialize(CreateProblemDetails(context))
- : CreateMessage(context),
- };
+ context.Result = new ObjectResult(CreateProblemDetails(context))
+ {
+ StatusCode = statusCode,
+ };
+ }
+ else
+ {
+ context.Result = new ContentResult
+ {
+ ContentType = MediaTypeNames.Application.Json,
+ StatusCode = statusCode,
+ Content = CreateMessage(context),
+ };
+ }
}
[SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "OK.")]
diff --git a/src/Atc.Rest/Middleware/ExceptionTelemetryMiddleware.cs b/src/Atc.Rest/Middleware/ExceptionTelemetryMiddleware.cs
index 849d385e..1bf24e2d 100644
--- a/src/Atc.Rest/Middleware/ExceptionTelemetryMiddleware.cs
+++ b/src/Atc.Rest/Middleware/ExceptionTelemetryMiddleware.cs
@@ -10,19 +10,14 @@ namespace Atc.Rest.Middleware;
public class ExceptionTelemetryMiddleware
{
private readonly RequestDelegate next;
- private readonly TelemetryClient client;
///
/// Initializes a new instance of the class.
///
/// The next middleware delegate in the pipeline.
- /// The Application Insights telemetry client.
- public ExceptionTelemetryMiddleware(
- RequestDelegate next,
- TelemetryClient client)
+ public ExceptionTelemetryMiddleware(RequestDelegate next)
{
this.next = next;
- this.client = client;
}
///
@@ -47,7 +42,8 @@ private async Task InternalInvokeAsync(HttpContext context)
}
catch (Exception ex)
{
- client.TrackException(ex);
+ var telemetryClient = context.RequestServices.GetService();
+ telemetryClient?.TrackException(ex);
if (context.Response.HasStarted)
{
@@ -57,12 +53,16 @@ private async Task InternalInvokeAsync(HttpContext context)
requestFailed = true;
}
- if (requestFailed)
+ if (requestFailed && !context.RequestAborted.IsCancellationRequested)
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
- await context
- .Response
- .WriteAsync($"Something is broken. Please contact the development team with the value of the returned header named '{WellKnownHttpHeaders.CorrelationId}'");
+ var problem = new ProblemDetails
+ {
+ Status = (int)HttpStatusCode.InternalServerError,
+ Title = "An unexpected error occurred.",
+ Detail = $"Contact the development team with the value of the '{WellKnownHttpHeaders.CorrelationId}' response header.",
+ };
+ await context.Response.WriteAsJsonAsync(problem, context.RequestAborted);
}
}
}
\ No newline at end of file
diff --git a/src/Atc.Rest/Middleware/RequestResponseLoggerMiddleware.cs b/src/Atc.Rest/Middleware/RequestResponseLoggerMiddleware.cs
index 9841ab7e..403a30bc 100644
--- a/src/Atc.Rest/Middleware/RequestResponseLoggerMiddleware.cs
+++ b/src/Atc.Rest/Middleware/RequestResponseLoggerMiddleware.cs
@@ -167,8 +167,19 @@ private static async Task ReadBodyFromRequest(
request.EnableBuffering();
}
- using var streamReader = new StreamReader(request.Body, leaveOpen: true);
- var requestBody = await streamReader.ReadToEndAsync();
+ string requestBody;
+ if (maxBufferSize > 0)
+ {
+ // Read at most maxBufferSize bytes so the documented cap is enforced.
+ var buffer = new byte[maxBufferSize];
+ var bytesRead = await request.Body.ReadAsync(buffer.AsMemory(0, buffer.Length));
+ requestBody = Encoding.UTF8.GetString(buffer, 0, bytesRead);
+ }
+ else
+ {
+ using var streamReader = new StreamReader(request.Body, leaveOpen: true);
+ requestBody = await streamReader.ReadToEndAsync();
+ }
// Reset the request's body stream position for next middleware in the pipeline.
request.Body.Position = 0;
diff --git a/src/Atc.Rest/Options/AuthorizationOptions.cs b/src/Atc.Rest/Options/AuthorizationOptions.cs
index 349853b0..3b5490dd 100644
--- a/src/Atc.Rest/Options/AuthorizationOptions.cs
+++ b/src/Atc.Rest/Options/AuthorizationOptions.cs
@@ -72,6 +72,24 @@ public class AuthorizationOptions
///
public List ValidIssuers { get; set; } = new();
+ ///
+ /// Gets or sets the JWT claim type used to populate ASP.NET Core roles for
+ /// [Authorize(Roles=…)] . For Azure AD access tokens the claim is "roles" ;
+ /// for client-credentials tokens the scope claim is "scp" .
+ /// When or empty, the framework default
+ /// (ClaimTypes.Role = the long URI form) is used, which does not match
+ /// the short-form claims issued by Azure AD.
+ ///
+ public string? RoleClaimType { get; set; }
+
+ ///
+ /// Gets or sets the JWT claim type used to populate the user's identity name ( ).
+ /// For Azure AD access tokens the claim is typically "name" or "preferred_username" .
+ /// When or empty, the framework default
+ /// (ClaimTypes.Name = the long URI form) is used.
+ ///
+ public string? NameClaimType { get; set; }
+
///
/// Determines whether any security settings are configured.
///
diff --git a/src/Atc.Rest/Options/ConfigureApiBehaviorOptions.cs b/src/Atc.Rest/Options/ConfigureApiBehaviorOptions.cs
index 027b77ce..63dd9e99 100644
--- a/src/Atc.Rest/Options/ConfigureApiBehaviorOptions.cs
+++ b/src/Atc.Rest/Options/ConfigureApiBehaviorOptions.cs
@@ -9,17 +9,24 @@ namespace Atc.Rest.Options;
/// - Suppress automatic binding source inference for better control
/// - Return ValidationProblemDetails for invalid model state
/// - Include correlation ID in validation error responses
-/// - Track validation errors in Application Insights telemetry
+/// - Track validation errors in Application Insights telemetry when a
is provided
///
///
public class ConfigureApiBehaviorOptions : IConfigureOptions
{
- private readonly TelemetryClient telemetry;
+ private readonly TelemetryClient? telemetry;
+
+ ///
+ /// Initializes a new instance of the class without telemetry.
+ ///
+ public ConfigureApiBehaviorOptions()
+ {
+ }
///
/// Initializes a new instance of the class.
///
- /// The Application Insights telemetry client.
+ /// The Application Insights telemetry client used to track validation errors.
public ConfigureApiBehaviorOptions(TelemetryClient telemetry)
{
this.telemetry = telemetry;
@@ -44,11 +51,14 @@ public void Configure(ApiBehaviorOptions options)
},
};
- telemetry.TrackTrace(
+ // Log only the field names (not values) to avoid capturing PII or user-submitted data in telemetry.
+ var invalidFields = string.Join(", ", error.Errors.Keys);
+ telemetry?.TrackTrace(
"BadRequest",
new Dictionary(StringComparer.Ordinal)
{
- { "Response.Body", JsonSerializer.Serialize(error) },
+ { "InvalidFields", invalidFields },
+ { "TraceId", error.Extensions.TryGetValue("traceId", out var traceId) ? traceId?.ToString() ?? string.Empty : string.Empty },
});
return new BadRequestObjectResult(error);
diff --git a/src/Atc.Rest/Options/RestApiOptions.cs b/src/Atc.Rest/Options/RestApiOptions.cs
index 97a4fe96..3989cf80 100644
--- a/src/Atc.Rest/Options/RestApiOptions.cs
+++ b/src/Atc.Rest/Options/RestApiOptions.cs
@@ -76,8 +76,11 @@ public class RestApiOptions
/// Gets or sets the allowed CORS origins for the API.
///
///
- /// When null or empty, a permissive policy (AllowAnyOrigin, AllowAnyMethod, AllowAnyHeader) is used.
- /// When specified, only the listed origins are allowed. Set this in production to prevent CSRF attacks.
+ /// When null or empty and the environment is Development, a permissive policy
+ /// (AllowAnyOrigin, AllowAnyMethod, AllowAnyHeader) is applied.
+ /// When null or empty in non-Development environments, no CORS middleware is added and the
+ /// browser's same-origin policy applies — no CORS headers are emitted.
+ /// When specified, only the listed origins are allowed in all environments.
///
[SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "OK.")]
public List? AllowedCorsOrigins { get; set; }
diff --git a/src/Atc.Rest/Results/Pagination.cs b/src/Atc.Rest/Results/Pagination.cs
index 0de1bbef..367fad00 100644
--- a/src/Atc.Rest/Results/Pagination.cs
+++ b/src/Atc.Rest/Results/Pagination.cs
@@ -102,7 +102,7 @@ public Pagination(
///
/// Gets the total number of pages based on TotalCount and PageSize.
///
- public int? TotalPages => TotalCount is null
+ public int? TotalPages => TotalCount is null || PageSize <= 0
? default(int?)
: (int)System.Math.Ceiling((double)TotalCount / PageSize);
diff --git a/src/Atc.Rest/Results/ResultFactory.cs b/src/Atc.Rest/Results/ResultFactory.cs
index 0838ded9..d5c1568a 100644
--- a/src/Atc.Rest/Results/ResultFactory.cs
+++ b/src/Atc.Rest/Results/ResultFactory.cs
@@ -61,7 +61,7 @@ public static ContentResult CreateContentResultWithProblemDetails(
{
ContentType = contentType,
StatusCode = (int)statusCode,
- Content = JsonSerializer.Serialize(CreateProblemDetails(statusCode, message)),
+ Content = JsonSerializer.Serialize(CreateProblemDetails(statusCode, message), JsonSerializerOptionsFactory.Create(writeIndented: false)),
};
///
@@ -93,11 +93,11 @@ public static ContentResult CreateContentResultWithProblemDetails(
var message = SimpleTypeHelper.IsSimpleType(beautifyTypeName)
? value.ToString()
- : JsonSerializer.Serialize(value);
+ : JsonSerializer.Serialize(value, JsonSerializerOptionsFactory.Create(writeIndented: false));
var problemDetails = CreateProblemDetails(statusCode, message);
- result.Content = JsonSerializer.Serialize(problemDetails);
+ result.Content = JsonSerializer.Serialize(problemDetails, JsonSerializerOptionsFactory.Create(writeIndented: false));
return result;
}
@@ -117,7 +117,7 @@ public static ContentResult CreateContentResultWithValidationProblemDetails(
{
ContentType = contentType,
StatusCode = (int)statusCode,
- Content = JsonSerializer.Serialize(CreateValidationProblemDetails(statusCode, new Dictionary(StringComparer.Ordinal), message)),
+ Content = JsonSerializer.Serialize(CreateValidationProblemDetails(statusCode, new Dictionary(StringComparer.Ordinal), message), JsonSerializerOptionsFactory.Create(writeIndented: false)),
};
///
@@ -137,7 +137,60 @@ public static ContentResult CreateContentResultWithValidationProblemDetails(
{
ContentType = contentType,
StatusCode = (int)statusCode,
- Content = JsonSerializer.Serialize(CreateValidationProblemDetails(statusCode, errors, message)),
+ Content = JsonSerializer.Serialize(CreateValidationProblemDetails(statusCode, errors, message), JsonSerializerOptionsFactory.Create(writeIndented: false)),
+ };
+
+ ///
+ /// Creates an containing ProblemDetails, allowing ASP.NET Core's
+ /// output formatters to serialize it using the app-configured .
+ /// Prefer this over
+ /// when consistent casing with the rest of the API is required.
+ ///
+ /// The HTTP status code.
+ /// The detail message describing the problem.
+ /// An wrapping a instance.
+ public static ObjectResult CreateObjectResultWithProblemDetails(
+ HttpStatusCode statusCode,
+ string? message)
+ => new(CreateProblemDetails(statusCode, message))
+ {
+ StatusCode = (int)statusCode,
+ };
+
+ ///
+ /// Creates an containing ValidationProblemDetails without field-specific errors,
+ /// allowing ASP.NET Core's output formatters to serialize it using the app-configured .
+ /// Prefer this over
+ /// when consistent casing with the rest of the API is required.
+ ///
+ /// The HTTP status code.
+ /// The detail message describing the validation failure.
+ /// An wrapping a instance.
+ public static ObjectResult CreateObjectResultWithValidationProblemDetails(
+ HttpStatusCode statusCode,
+ string? message)
+ => new(CreateValidationProblemDetails(statusCode, new Dictionary(StringComparer.Ordinal), message))
+ {
+ StatusCode = (int)statusCode,
+ };
+
+ ///
+ /// Creates an containing ValidationProblemDetails with field-specific errors,
+ /// allowing ASP.NET Core's output formatters to serialize it using the app-configured .
+ /// Prefer this over
+ /// when consistent casing with the rest of the API is required.
+ ///
+ /// The HTTP status code.
+ /// A dictionary of field names and their associated validation errors.
+ /// The detail message describing the validation failure.
+ /// An wrapping a instance.
+ public static ObjectResult CreateObjectResultWithValidationProblemDetails(
+ HttpStatusCode statusCode,
+ Dictionary errors,
+ string? message)
+ => new(CreateValidationProblemDetails(statusCode, errors, message))
+ {
+ StatusCode = (int)statusCode,
};
///
diff --git a/src/Atc.XUnit/Atc.XUnit.csproj b/src/Atc.XUnit/Atc.XUnit.csproj
index 7f508788..208b7945 100644
--- a/src/Atc.XUnit/Atc.XUnit.csproj
+++ b/src/Atc.XUnit/Atc.XUnit.csproj
@@ -13,7 +13,7 @@
-
+
NU1701
diff --git a/src/Atc.XUnit/CodeComplianceDocumentationHelper.cs b/src/Atc.XUnit/CodeComplianceDocumentationHelper.cs
index ac731a30..ff38a1ba 100644
--- a/src/Atc.XUnit/CodeComplianceDocumentationHelper.cs
+++ b/src/Atc.XUnit/CodeComplianceDocumentationHelper.cs
@@ -23,6 +23,53 @@ public static void AssertExportedTypeWithMissingComments(Type type)
TestResultHelper.AssertOnTestResults(testResults);
}
+ ///
+ /// Asserts that all exported types in an assembly have XML documentation comments,
+ /// using an explicit XML documentation file path instead of relying on automatic path resolution.
+ /// Use this overload when the XML documentation file is not located next to the assembly or in
+ /// base directory.
+ ///
+ /// The assembly to validate.
+ /// The explicit path to the XML documentation file for .
+ /// Optional list of types to exclude from validation.
+ /// Thrown when or is null.
+ public static void AssertExportedTypesWithMissingComments(
+ Assembly assembly,
+ FileInfo xmlDocPath,
+ List? excludeTypes = null)
+ {
+ if (assembly is null)
+ {
+ throw new ArgumentNullException(nameof(assembly));
+ }
+
+ if (xmlDocPath is null)
+ {
+ throw new ArgumentNullException(nameof(xmlDocPath));
+ }
+
+ // Due to some build issue with GenerateDocumentationFile=true and xml-file location, this hack is made for now.
+ if (!OperatingSystem.IsWindows())
+ {
+ return;
+ }
+
+ var typesWithMissingCommentsGroups = DocumentationHelper
+ .CollectExportedTypesWithMissingCommentsFromAssembly(assembly, xmlDocPath, excludeTypes)
+ .OrderBy(x => x.Type.FullName, StringComparer.Ordinal)
+ .GroupBy(x => x.Type.BeautifyName(useFullName: true), StringComparer.Ordinal)
+ .ToArray();
+
+ var testResults = new List
+ {
+ new($"Assembly: {assembly.GetName()}"),
+ };
+
+ testResults.AddRange(typesWithMissingCommentsGroups.Select(item => new TestResult(isError: false, 1, $"Type: {item.Key}")));
+
+ TestResultHelper.AssertOnTestResults(testResults);
+ }
+
///
/// Asserts that all exported types in an assembly have XML documentation comments.
/// Fails the test if any types are missing documentation.
diff --git a/src/Atc.XUnit/CodeComplianceTestHelper.cs b/src/Atc.XUnit/CodeComplianceTestHelper.cs
index 1b2f85f0..701596b0 100644
--- a/src/Atc.XUnit/CodeComplianceTestHelper.cs
+++ b/src/Atc.XUnit/CodeComplianceTestHelper.cs
@@ -13,11 +13,13 @@ public static class CodeComplianceTestHelper
/// The source type to validate for test coverage.
/// The test type containing unit tests for the source type.
/// If set to true , use full type names in output.
+ /// A token to cancel the analysis operation.
public static void AssertExportedMethodsWithMissingTests(
DecompilerType decompilerType,
Type sourceType,
Type testType,
- bool useFullName = false)
+ bool useFullName = false,
+ CancellationToken cancellationToken = default)
{
if (sourceType is null)
{
@@ -32,7 +34,8 @@ public static void AssertExportedMethodsWithMissingTests(
var methodsWithMissingTests = AssemblyTestHelper.CollectExportedMethodsWithMissingTests(
decompilerType,
sourceType,
- testType);
+ testType,
+ cancellationToken);
TestResultHelper.AssertOnTestResultsFromMethodsWithMissingTests(
sourceType.Assembly.GetName().Name!,
methodsWithMissingTests,
@@ -47,11 +50,13 @@ public static void AssertExportedMethodsWithMissingTests(
/// The source type to validate for test coverage.
/// The test assembly to search for unit tests.
/// If set to true , use full type names in output.
+ /// A token to cancel the analysis operation.
public static void AssertExportedMethodsWithMissingTests(
DecompilerType decompilerType,
Type sourceType,
Assembly testAssembly,
- bool useFullName = false)
+ bool useFullName = false,
+ CancellationToken cancellationToken = default)
{
if (sourceType is null)
{
@@ -61,7 +66,8 @@ public static void AssertExportedMethodsWithMissingTests(
var methodsWithMissingTests = AssemblyTestHelper.CollectExportedMethodsWithMissingTests(
decompilerType,
sourceType,
- testAssembly);
+ testAssembly,
+ cancellationToken);
TestResultHelper.AssertOnTestResultsFromMethodsWithMissingTests(
sourceType.Assembly.GetName().Name!,
methodsWithMissingTests,
@@ -77,12 +83,14 @@ public static void AssertExportedMethodsWithMissingTests(
/// The test assembly containing unit tests.
/// Optional list of source types to exclude from validation.
/// If set to true , use full type names in output.
+ /// A token to cancel the analysis operation.
public static void AssertExportedMethodsWithMissingTests(
DecompilerType decompilerType,
Assembly sourceAssembly,
Assembly testAssembly,
List? excludeSourceTypes = null,
- bool useFullName = false)
+ bool useFullName = false,
+ CancellationToken cancellationToken = default)
{
if (sourceAssembly is null)
{
@@ -98,7 +106,8 @@ public static void AssertExportedMethodsWithMissingTests(
decompilerType,
sourceAssembly,
testAssembly,
- excludeSourceTypes);
+ excludeSourceTypes,
+ cancellationToken);
TestResultHelper.AssertOnTestResultsFromMethodsWithMissingTests(
sourceAssembly.GetName().Name!,
methodsWithMissingTests,
@@ -112,12 +121,14 @@ public static void AssertExportedMethodsWithMissingTests(
/// The source assembly to analyze.
/// The test assembly to search for unit tests.
/// Optional list of source types to exclude from analysis.
+ /// A token to cancel the analysis operation.
/// An array of types that have methods missing test coverage.
public static Type[] CollectExportedTypesWithMissingTests(
DecompilerType decompilerType,
Assembly sourceAssembly,
Assembly testAssembly,
- List? excludeSourceTypes = null)
+ List? excludeSourceTypes = null,
+ CancellationToken cancellationToken = default)
{
if (sourceAssembly is null)
{
@@ -133,7 +144,8 @@ public static Type[] CollectExportedTypesWithMissingTests(
decompilerType,
sourceAssembly,
testAssembly,
- excludeSourceTypes);
+ excludeSourceTypes,
+ cancellationToken);
}
///
@@ -145,13 +157,15 @@ public static Type[] CollectExportedTypesWithMissingTests(
/// The test assembly to search for unit tests.
/// Optional list of source types to exclude from analysis.
/// If set to true , use full type names in output.
+ /// A token to cancel the analysis operation.
/// A formatted C# code snippet containing a list of typeof() expressions for types missing tests.
public static string CollectExportedTypesWithMissingTestsAndGenerateText(
DecompilerType decompilerType,
Assembly sourceAssembly,
Assembly testAssembly,
List? excludeSourceTypes = null,
- bool useFullName = false)
+ bool useFullName = false,
+ CancellationToken cancellationToken = default)
{
if (sourceAssembly is null)
{
@@ -167,7 +181,8 @@ public static string CollectExportedTypesWithMissingTestsAndGenerateText(
decompilerType,
sourceAssembly,
testAssembly,
- excludeSourceTypes);
+ excludeSourceTypes,
+ cancellationToken);
var sb = new StringBuilder();
sb.AppendLine(12, "var excludeTypes = new List");
@@ -203,12 +218,14 @@ public static string CollectExportedTypesWithMissingTestsAndGenerateText(
/// The source assembly to analyze.
/// The test assembly to search for unit tests.
/// Optional list of source types to exclude from analysis.
+ /// A token to cancel the analysis operation.
/// An array of objects representing methods missing test coverage.
public static MethodInfo[] CollectExportedMethodsWithMissingTestsFromAssembly(
DecompilerType decompilerType,
Assembly sourceAssembly,
Assembly testAssembly,
- List? excludeSourceTypes = null)
+ List? excludeSourceTypes = null,
+ CancellationToken cancellationToken = default)
{
if (sourceAssembly is null)
{
@@ -220,7 +237,7 @@ public static MethodInfo[] CollectExportedMethodsWithMissingTestsFromAssembly(
throw new ArgumentNullException(nameof(testAssembly));
}
- return AssemblyTestHelper.CollectExportedMethodsWithMissingTests(decompilerType, sourceAssembly, testAssembly, excludeSourceTypes);
+ return AssemblyTestHelper.CollectExportedMethodsWithMissingTests(decompilerType, sourceAssembly, testAssembly, excludeSourceTypes, cancellationToken);
}
///
@@ -231,13 +248,15 @@ public static MethodInfo[] CollectExportedMethodsWithMissingTestsFromAssembly(
/// The test assembly to search for unit tests.
/// Optional list of source types to exclude from analysis.
/// If set to true , use full type names in output.
+ /// A token to cancel the analysis operation.
/// An array of strings containing beautified method signatures.
public static string[] CollectExportedMethodsWithMissingTestsAndGenerateTextLines(
DecompilerType decompilerType,
Assembly sourceAssembly,
Assembly testAssembly,
List? excludeSourceTypes = null,
- bool useFullName = false)
+ bool useFullName = false,
+ CancellationToken cancellationToken = default)
{
if (sourceAssembly is null)
{
@@ -249,7 +268,7 @@ public static string[] CollectExportedMethodsWithMissingTestsAndGenerateTextLine
throw new ArgumentNullException(nameof(testAssembly));
}
- var methodsWithMissingTests = AssemblyTestHelper.CollectExportedMethodsWithMissingTests(decompilerType, sourceAssembly, testAssembly, excludeSourceTypes);
+ var methodsWithMissingTests = AssemblyTestHelper.CollectExportedMethodsWithMissingTests(decompilerType, sourceAssembly, testAssembly, excludeSourceTypes, cancellationToken);
return AssemblyTestHelper.GetMethodsAsRenderTextLines(methodsWithMissingTests, useFullName);
}
@@ -261,13 +280,15 @@ public static string[] CollectExportedMethodsWithMissingTestsAndGenerateTextLine
/// The test assembly to search for unit tests.
/// Optional list of source types to exclude from analysis.
/// If set to true , use full type names in output.
+ /// A token to cancel the analysis operation.
/// A multi-line string containing all method signatures missing tests.
public static string CollectExportedMethodsWithMissingTestsAndGenerateText(
DecompilerType decompilerType,
Assembly sourceAssembly,
Assembly testAssembly,
List? excludeSourceTypes = null,
- bool useFullName = false)
+ bool useFullName = false,
+ CancellationToken cancellationToken = default)
{
if (sourceAssembly is null)
{
@@ -279,22 +300,24 @@ public static string CollectExportedMethodsWithMissingTestsAndGenerateText(
throw new ArgumentNullException(nameof(testAssembly));
}
- var methodsWithMissingTests = AssemblyTestHelper.CollectExportedMethodsWithMissingTests(decompilerType, sourceAssembly, testAssembly, excludeSourceTypes);
+ var methodsWithMissingTests = AssemblyTestHelper.CollectExportedMethodsWithMissingTests(decompilerType, sourceAssembly, testAssembly, excludeSourceTypes, cancellationToken);
return AssemblyTestHelper.GetMethodsAsRenderText(methodsWithMissingTests, useFullName);
}
///
- /// Collects exported methods with missing tests and exports them to an Excel file at C:\Temp.
+ /// Collects exported methods with missing tests and exports them to an Excel file at the system temp directory.
///
/// The to use for analyzing test method bodies.
/// The source assembly to analyze.
/// The test assembly to search for unit tests.
/// Optional list of source types to exclude from analysis.
+ /// A token to cancel the analysis operation.
public static void CollectExportedMethodsWithMissingTestsToExcel(
DecompilerType decompilerType,
Assembly sourceAssembly,
Assembly testAssembly,
- List? excludeSourceTypes = null)
+ List? excludeSourceTypes = null,
+ CancellationToken cancellationToken = default)
{
if (sourceAssembly is null)
{
@@ -308,10 +331,11 @@ public static void CollectExportedMethodsWithMissingTestsToExcel(
CollectExportedMethodsWithMissingTestsToExcel(
decompilerType,
- new DirectoryInfo(@"C:\Temp"),
+ new DirectoryInfo(Path.GetTempPath()),
sourceAssembly,
testAssembly,
- excludeSourceTypes);
+ excludeSourceTypes,
+ cancellationToken);
}
///
@@ -322,12 +346,14 @@ public static void CollectExportedMethodsWithMissingTestsToExcel(
/// The source assembly to analyze.
/// The test assembly to search for unit tests.
/// Optional list of source types to exclude from analysis.
+ /// A token to cancel the analysis operation.
public static void CollectExportedMethodsWithMissingTestsToExcel(
DecompilerType decompilerType,
DirectoryInfo reportDirectory,
Assembly sourceAssembly,
Assembly testAssembly,
- List? excludeSourceTypes = null)
+ List? excludeSourceTypes = null,
+ CancellationToken cancellationToken = default)
{
if (reportDirectory is null)
{
@@ -348,7 +374,8 @@ public static void CollectExportedMethodsWithMissingTestsToExcel(
decompilerType,
sourceAssembly,
testAssembly,
- excludeSourceTypes);
+ excludeSourceTypes,
+ cancellationToken);
TestResultHelper.ToExcelTestResultsFromMethodsWithMissingTests(
reportDirectory,
sourceAssembly.GetName().Name!,
diff --git a/src/Atc.XUnit/GlobalUsings.cs b/src/Atc.XUnit/GlobalUsings.cs
index 6f2ece8a..8438af06 100644
--- a/src/Atc.XUnit/GlobalUsings.cs
+++ b/src/Atc.XUnit/GlobalUsings.cs
@@ -1,4 +1,5 @@
global using System.Collections;
+global using System.Collections.Concurrent;
global using System.Diagnostics;
global using System.Diagnostics.CodeAnalysis;
global using System.Globalization;
diff --git a/src/Atc.XUnit/IntegrationTestCliBase.cs b/src/Atc.XUnit/IntegrationTestCliBase.cs
index b71037e4..e7015daa 100644
--- a/src/Atc.XUnit/IntegrationTestCliBase.cs
+++ b/src/Atc.XUnit/IntegrationTestCliBase.cs
@@ -261,6 +261,10 @@ private static (string CliFileNameExe, DirectoryInfo SearchFromPath) GetCliFileE
.BaseDirectory;
var testAssemblyName = GetTestAssemblyName();
+ if (string.IsNullOrEmpty(testAssemblyName))
+ {
+ return (GetCliFileName(programTypeForCliExe), new DirectoryInfo(currentDomainBaseDirectory));
+ }
var searchFromPath = new DirectoryInfo(currentDomainBaseDirectory.Split(testAssemblyName, StringSplitOptions.RemoveEmptyEntries)[0]);
if (searchFromPath.Parent is not null)
diff --git a/src/Atc.XUnit/Internal/AbstractSyntaxTree/AnalyzerHelper.cs b/src/Atc.XUnit/Internal/AbstractSyntaxTree/AnalyzerHelper.cs
index ddd8e06d..46616e3d 100644
--- a/src/Atc.XUnit/Internal/AbstractSyntaxTree/AnalyzerHelper.cs
+++ b/src/Atc.XUnit/Internal/AbstractSyntaxTree/AnalyzerHelper.cs
@@ -42,11 +42,6 @@ internal static MethodInfo[] GetSourceMethodsWithMissingTest(
{
methodsWithTest.Add(method);
}
- else if (debugLimitData is not null)
- {
- // Dummy for breakpoint
- throw new Exception("Whoops..");
- }
}
}
}
@@ -148,7 +143,7 @@ private static bool IsMethodUsedByTestMethod(
{
if (method.DeclaringType is null)
{
- throw new Exception("method.DeclaringType is null...");
+ return false;
}
var parameters = method.GetParameters();
diff --git a/src/Atc.XUnit/Internal/AbstractSyntaxTree/DecompilerHelper.cs b/src/Atc.XUnit/Internal/AbstractSyntaxTree/DecompilerHelper.cs
index 473ed41b..d3fa4ecf 100644
--- a/src/Atc.XUnit/Internal/AbstractSyntaxTree/DecompilerHelper.cs
+++ b/src/Atc.XUnit/Internal/AbstractSyntaxTree/DecompilerHelper.cs
@@ -3,9 +3,19 @@ namespace Atc.XUnit.Internal.AbstractSyntaxTree;
internal static class DecompilerHelper
{
+ private static readonly ConcurrentDictionary> Cache = new(StringComparer.OrdinalIgnoreCase);
+
internal static CSharpDecompiler GetDecompiler(Assembly assembly)
{
var assemblyFileName = assembly.Location;
+ return Cache.GetOrAdd(assemblyFileName, static path =>
+ new Lazy(() => CreateDecompiler(path), LazyThreadSafetyMode.ExecutionAndPublication)).Value;
+ }
+
+ private static CSharpDecompiler CreateDecompiler(string assemblyFileName)
+ {
+ // PEFile is used only to validate the assembly path; the resolver is the long-lived handle.
+ // Close the validation PEFile immediately to avoid keeping the native handle open.
using var module = new PEFile(assemblyFileName);
var resolver = new UniversalAssemblyResolver(assemblyFileName, false, targetFramework: null);
return new CSharpDecompiler(assemblyFileName, resolver, GetSettings());
@@ -24,6 +34,11 @@ internal static Tuple[] GetTestMethodsWithDecompi
var testMethods = new List>();
foreach ((Type testType, MethodInfo[] testMethodInfos) in testTypeMethods)
{
+ if (testType.FullName is null)
+ {
+ continue;
+ }
+
var fullTypeName = new FullTypeName(testType.FullName);
var syntaxTree = decompiler.DecompileType(fullTypeName);
var astNodes = syntaxTree
diff --git a/src/Atc.XUnit/Internal/AbstractSyntaxTree/ParametersNamingMatchHelper.cs b/src/Atc.XUnit/Internal/AbstractSyntaxTree/ParametersNamingMatchHelper.cs
index 0ed0b6f6..3b70053a 100644
--- a/src/Atc.XUnit/Internal/AbstractSyntaxTree/ParametersNamingMatchHelper.cs
+++ b/src/Atc.XUnit/Internal/AbstractSyntaxTree/ParametersNamingMatchHelper.cs
@@ -252,7 +252,12 @@ private static bool ParameterCheckForMemberReferenceExpression(
return true;
}
- if (astNodeTypeName.Contains("Constants", StringComparison.Ordinal))
+ // Only auto-pass a TypeReferenceExpression for a constants type when the parameter
+ // itself is of type System.Type (i.e., the test passes typeof(SomeConstants)).
+ // The previous broad Contains("Constants") check caused any test referencing a
+ // *Constants* type to be silently marked as covered, causing false negatives.
+ if (astNodeTypeName.EndsWith("Constants", StringComparison.Ordinal) &&
+ parameter.ParameterType == typeof(Type))
{
return true;
}
diff --git a/src/Atc.XUnit/Internal/AssemblyAnalyzerHelper.cs b/src/Atc.XUnit/Internal/AssemblyAnalyzerHelper.cs
index 60ff4a5f..0904b98f 100644
--- a/src/Atc.XUnit/Internal/AssemblyAnalyzerHelper.cs
+++ b/src/Atc.XUnit/Internal/AssemblyAnalyzerHelper.cs
@@ -164,7 +164,13 @@ private static Type[] CollectFilteredAssemblyTypes(
var className = classType.Name.Replace("Extensions", string.Empty, StringComparison.Ordinal);
var classNamePrefixSimplified = GetSimpleTypeName(className);
- var firstParameterType = method.GetParameters()[0].ParameterType;
+ var parameters = method.GetParameters();
+ if (parameters.Length == 0)
+ {
+ return "Extension method has no parameters.";
+ }
+
+ var firstParameterType = parameters[0].ParameterType;
var firstParameterNameSimplified = GetSimpleTypeName(firstParameterType);
if (classNamePrefixSimplified.Equals(firstParameterNameSimplified, StringComparison.Ordinal) ||
("I" + classNamePrefixSimplified).Equals(firstParameterNameSimplified, StringComparison.Ordinal) ||
diff --git a/src/Atc.XUnit/Internal/AssemblyTestHelper.cs b/src/Atc.XUnit/Internal/AssemblyTestHelper.cs
index f379ba74..34e9f27b 100644
--- a/src/Atc.XUnit/Internal/AssemblyTestHelper.cs
+++ b/src/Atc.XUnit/Internal/AssemblyTestHelper.cs
@@ -25,9 +25,10 @@ internal static Type[] CollectExportedTypesWithMissingTests(
DecompilerType decompilerType,
Assembly sourceAssembly,
Assembly testAssembly,
- List? excludeSourceTypes)
+ List? excludeSourceTypes,
+ CancellationToken cancellationToken = default)
{
- var methodsWithMissingTests = CollectExportedMethodsWithMissingTests(decompilerType, sourceAssembly, testAssembly, excludeSourceTypes);
+ var methodsWithMissingTests = CollectExportedMethodsWithMissingTests(decompilerType, sourceAssembly, testAssembly, excludeSourceTypes, cancellationToken);
var methodsWithMissingTestsGroups = methodsWithMissingTests
.OrderBy(x => x.DeclaringType?.FullName, StringComparer.Ordinal)
@@ -43,7 +44,8 @@ internal static Type[] CollectExportedTypesWithMissingTests(
internal static MethodInfo[] CollectExportedMethodsWithMissingTests(
DecompilerType decompilerType,
Type sourceType,
- Type testType)
+ Type testType,
+ CancellationToken cancellationToken = default)
{
if (sourceType is null)
{
@@ -55,6 +57,8 @@ internal static MethodInfo[] CollectExportedMethodsWithMissingTests(
throw new ArgumentNullException(nameof(testType));
}
+ cancellationToken.ThrowIfCancellationRequested();
+
var sourceTypes = new[] { sourceType };
var testTypes = new[] { testType };
@@ -76,7 +80,8 @@ internal static MethodInfo[] CollectExportedMethodsWithMissingTests(
internal static MethodInfo[] CollectExportedMethodsWithMissingTests(
DecompilerType decompilerType,
Type sourceType,
- Assembly testAssembly)
+ Assembly testAssembly,
+ CancellationToken cancellationToken = default)
{
if (sourceType is null)
{
@@ -88,6 +93,8 @@ internal static MethodInfo[] CollectExportedMethodsWithMissingTests(
throw new ArgumentNullException(nameof(testAssembly));
}
+ cancellationToken.ThrowIfCancellationRequested();
+
var sourceTypes = new[] { sourceType };
var testTypes = testAssembly.ExportedTypes.ToArray();
@@ -111,7 +118,8 @@ internal static MethodInfo[] CollectExportedMethodsWithMissingTests(
DecompilerType decompilerType,
Assembly sourceAssembly,
Assembly testAssembly,
- List? excludeSourceTypes)
+ List? excludeSourceTypes,
+ CancellationToken cancellationToken = default)
{
if (sourceAssembly is null)
{
@@ -123,6 +131,8 @@ internal static MethodInfo[] CollectExportedMethodsWithMissingTests(
throw new ArgumentNullException(nameof(testAssembly));
}
+ cancellationToken.ThrowIfCancellationRequested();
+
var sourceTypes = sourceAssembly.ExportedTypes
.Where(x => !x.IsInterface &&
!x.IsNested &&
@@ -142,6 +152,8 @@ internal static MethodInfo[] CollectExportedMethodsWithMissingTests(
.ToArray();
}
+ cancellationToken.ThrowIfCancellationRequested();
+
var testTypes = testAssembly.ExportedTypes.ToArray();
var testTypeMethods = TypeAndMethodAndParameterHelper.GetTypeMethodsWithTestAttributes(testTypes);
switch (decompilerType)
diff --git a/src/Atc.XUnit/Internal/MonoReflection/AnalyzerHelper.cs b/src/Atc.XUnit/Internal/MonoReflection/AnalyzerHelper.cs
index e5d93143..17de035f 100644
--- a/src/Atc.XUnit/Internal/MonoReflection/AnalyzerHelper.cs
+++ b/src/Atc.XUnit/Internal/MonoReflection/AnalyzerHelper.cs
@@ -40,11 +40,6 @@ public static MethodInfo[] GetSourceMethodsWithMissingTest(
{
methodsWithTest.Add(method);
}
- else if (debugLimitData is not null)
- {
- // Dummy for breakpoint
- throw new Exception("Ups..");
- }
}
}
}
@@ -70,7 +65,7 @@ internal static MethodInfo[] GetUsedSourceMethods(
Type[] sourceTypes,
Tuple[] testTypeMethods)
{
- var list = new List();
+ var set = new HashSet();
foreach (var tuple in testTypeMethods)
{
foreach (var method in tuple.Item2)
@@ -78,12 +73,12 @@ internal static MethodInfo[] GetUsedSourceMethods(
var instructions = method.GetInstructions();
foreach (var instruction in instructions)
{
- ProcessInstruction(instruction, sourceTypes, list);
+ ProcessInstruction(instruction, sourceTypes, set);
}
}
}
- return list
+ return set
.OrderBy(x => x.DeclaringType?.Name, StringComparer.Ordinal)
.ThenBy(x => x.Name, StringComparer.Ordinal)
.ToArray();
@@ -92,7 +87,7 @@ internal static MethodInfo[] GetUsedSourceMethods(
private static void ProcessInstruction(
Instruction instruction,
Type[] sourceTypes,
- List list)
+ HashSet set)
{
if (instruction.Operand is not MethodInfo usedMethodInTest)
{
@@ -111,9 +106,9 @@ private static void ProcessInstruction(
{
// Try to find the original method from the state machine
var originalMethod = TryGetOriginalMethodFromStateMachine(type, sourceTypes);
- if (originalMethod is not null && !list.Contains(originalMethod))
+ if (originalMethod is not null)
{
- list.Add(originalMethod);
+ set.Add(originalMethod);
return;
}
}
@@ -135,10 +130,7 @@ private static void ProcessInstruction(
? usedMethodInTest.GetGenericMethodDefinition()
: usedMethodInTest;
- if (!list.Contains(methodToAdd))
- {
- list.Add(methodToAdd);
- }
+ set.Add(methodToAdd);
}
[SuppressMessage("Performance", "MA0009:Regular expressions should not be vulnerable to Denial of Service attacks", Justification = "OK - Simple pattern for compiler-generated names")]
diff --git a/src/Atc/Atc.csproj b/src/Atc/Atc.csproj
index 441491fa..f599855c 100644
--- a/src/Atc/Atc.csproj
+++ b/src/Atc/Atc.csproj
@@ -9,13 +9,13 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
+
@@ -76,7 +76,7 @@
-
+
\ No newline at end of file
diff --git a/src/Atc/Collections/ConcurrentHashSet.cs b/src/Atc/Collections/ConcurrentHashSet.cs
index 67ffc979..8af6eb0e 100644
--- a/src/Atc/Collections/ConcurrentHashSet.cs
+++ b/src/Atc/Collections/ConcurrentHashSet.cs
@@ -35,19 +35,23 @@ public int Count
}
///
+ ///
+ /// Returns an enumerator over a point-in-time snapshot taken under a read lock,
+ /// so iteration is safe even if the set is mutated concurrently afterwards.
+ ///
public IEnumerator GetEnumerator()
{
- readerWriterLock.EnterWriteLock();
+ readerWriterLock.EnterReadLock();
try
{
- return hashSet.GetEnumerator();
+ return hashSet.ToList().GetEnumerator();
}
finally
{
- if (readerWriterLock.IsWriteLockHeld)
+ if (readerWriterLock.IsReadLockHeld)
{
- readerWriterLock.ExitWriteLock();
+ readerWriterLock.ExitReadLock();
}
}
}
diff --git a/src/Atc/Comparers/ByteArrayEqualityComparer.cs b/src/Atc/Comparers/ByteArrayEqualityComparer.cs
index 7de8c49a..1329beca 100644
--- a/src/Atc/Comparers/ByteArrayEqualityComparer.cs
+++ b/src/Atc/Comparers/ByteArrayEqualityComparer.cs
@@ -35,6 +35,7 @@ public bool Equals(
return false;
}
+#if NETSTANDARD2_0
for (var i = 0; i < x.Length; i++)
{
if (x[i] != y[i])
@@ -44,6 +45,9 @@ public bool Equals(
}
return true;
+#else
+ return x.AsSpan().SequenceEqual(y);
+#endif
}
///
diff --git a/src/Atc/Comparers/NumericAlphaComparer.cs b/src/Atc/Comparers/NumericAlphaComparer.cs
index 1e9939b3..bacdb496 100644
--- a/src/Atc/Comparers/NumericAlphaComparer.cs
+++ b/src/Atc/Comparers/NumericAlphaComparer.cs
@@ -111,7 +111,7 @@ private static string ExtractLetters(string value)
.Replace(".", string.Empty, StringComparison.Ordinal)
.Replace(",", string.Empty, StringComparison.Ordinal);
return value
- .Replace(ExtractNumber(value).ToString(Thread.CurrentThread.CurrentCulture), string.Empty, StringComparison.Ordinal)
+ .Replace(ExtractNumber(value).ToString(GlobalizationConstants.EnglishCultureInfo), string.Empty, StringComparison.Ordinal)
.Trim();
}
}
\ No newline at end of file
diff --git a/src/Atc/Data/AssemblyInformationFactory.cs b/src/Atc/Data/AssemblyInformationFactory.cs
index b3406ae7..ea3d3f66 100644
--- a/src/Atc/Data/AssemblyInformationFactory.cs
+++ b/src/Atc/Data/AssemblyInformationFactory.cs
@@ -65,7 +65,7 @@ private static bool IsAssemblyCompliedToDebug(
return false;
}
- return attributes[0] is DebuggableAttribute { IsJITTrackingEnabled: true };
+ return attributes[0] is DebuggableAttribute { IsJITOptimizerDisabled: true };
}
catch (IOException)
{
diff --git a/src/Atc/Data/Models/LogItem.cs b/src/Atc/Data/Models/LogItem.cs
index 15949dc4..8db987d2 100644
--- a/src/Atc/Data/Models/LogItem.cs
+++ b/src/Atc/Data/Models/LogItem.cs
@@ -11,7 +11,7 @@ public class LogItem
///
public LogItem()
{
- TimeStamp = DateTime.Now;
+ TimeStamp = DateTime.UtcNow;
Severity = LogCategoryType.Information;
Message = string.Empty;
}
diff --git a/src/Atc/Data/SemVer/Identifier.cs b/src/Atc/Data/SemVer/Identifier.cs
index 0ce1135b..92edc9aa 100644
--- a/src/Atc/Data/SemVer/Identifier.cs
+++ b/src/Atc/Data/SemVer/Identifier.cs
@@ -43,7 +43,7 @@ private void SetNumeric()
{
if (!int.TryParse(
Value,
- NumberStyles.Any,
+ NumberStyles.None,
GlobalizationConstants.EnglishCultureInfo,
out var x))
{
diff --git a/src/Atc/Data/SemVer/SemanticVersion.cs b/src/Atc/Data/SemVer/SemanticVersion.cs
index 32464123..1dcfda48 100644
--- a/src/Atc/Data/SemVer/SemanticVersion.cs
+++ b/src/Atc/Data/SemVer/SemanticVersion.cs
@@ -9,7 +9,11 @@ namespace Atc.Data.SemVer;
/// Represents a version object, compliant with the Semantic Version standard 2.0 (http://semver.org).
///
[Serializable]
-public sealed class SemanticVersion : IComparable, IComparable, IEquatable
+#if NET7_0_OR_GREATER
+public sealed class SemanticVersion : IComparable, IComparable, IEquatable, IFormattable, ISpanParsable
+#else
+public sealed class SemanticVersion : IComparable, IComparable, IEquatable, IFormattable
+#endif
{
private static readonly Regex StrictModeRegex = new(
@"^
@@ -107,7 +111,7 @@ public SemanticVersion(
if (looseMode &&
!string.IsNullOrEmpty(PreRelease) &&
string.IsNullOrEmpty(Build) &&
- PreRelease.StartsWith('.') &&
+ PreRelease.StartsWith(".", StringComparison.Ordinal) &&
int.TryParse(
PreRelease.Replace(".", string.Empty, StringComparison.Ordinal),
NumberStyles.Any,
@@ -457,6 +461,18 @@ public override string ToString()
return $"{Major}.{Minor}.{Patch}{preReleaseString}{buildString}";
}
+ ///
+ /// Formats the value of the current instance using the specified format.
+ /// Both parameters are ignored; uses a fixed, culture-invariant format.
+ ///
+ /// Format specifier — not used by this type.
+ /// Culture provider — not used by this type.
+ /// The string representation of the current instance.
+ public string ToString(
+ string? format,
+ IFormatProvider? formatProvider)
+ => ToString();
+
///
/// Converts to based on Major, Minor and Patch.
///
@@ -475,6 +491,72 @@ public Version ToVersion()
return new(Major, Minor, Patch);
}
+#if NET7_0_OR_GREATER
+ ///
+ /// Parses a string into a .
+ ///
+ /// The string to parse.
+ /// Ignored; parsing is culture-invariant.
+ /// The parsed .
+ /// Thrown when is not a valid semantic version string.
+ static SemanticVersion IParsable.Parse(
+ string s,
+ IFormatProvider? provider)
+ => Parse(s);
+
+ ///
+ /// Tries to parse a string into a .
+ ///
+ /// The string to parse, or .
+ /// Ignored; parsing is culture-invariant.
+ ///
+ /// When this method returns, contains the parsed ,
+ /// or if parsing fails.
+ ///
+ /// if parsing succeeded; otherwise, .
+ static bool IParsable.TryParse(
+ string? s,
+ IFormatProvider? provider,
+ [NotNullWhen(true)] out SemanticVersion? result)
+ {
+ if (s is null)
+ {
+ result = null;
+ return false;
+ }
+
+ return TryParse(s, out result);
+ }
+
+ ///
+ /// Parses a character span into a .
+ ///
+ /// The character span to parse.
+ /// Ignored; parsing is culture-invariant.
+ /// The parsed .
+ /// Thrown when is not a valid semantic version string.
+ static SemanticVersion ISpanParsable.Parse(
+ ReadOnlySpan s,
+ IFormatProvider? provider)
+ => Parse(s.ToString());
+
+ ///
+ /// Tries to parse a character span into a .
+ ///
+ /// The character span to parse.
+ /// Ignored; parsing is culture-invariant.
+ ///
+ /// When this method returns, contains the parsed ,
+ /// or if parsing fails.
+ ///
+ /// if parsing succeeded; otherwise, .
+ static bool ISpanParsable.TryParse(
+ ReadOnlySpan s,
+ IFormatProvider? provider,
+ [NotNullWhen(true)] out SemanticVersion? result)
+ => TryParse(s.ToString(), out result);
+#endif
+
public static bool operator ==(
SemanticVersion? a,
SemanticVersion? b)
diff --git a/src/Atc/Exceptions/ArgumentNullOrDefaultException.cs b/src/Atc/Exceptions/ArgumentNullOrDefaultException.cs
index c335b3d0..a3429ac2 100644
--- a/src/Atc/Exceptions/ArgumentNullOrDefaultException.cs
+++ b/src/Atc/Exceptions/ArgumentNullOrDefaultException.cs
@@ -64,7 +64,11 @@ public ArgumentNullOrDefaultException(
private ArgumentNullOrDefaultException(
SerializationInfo serializationInfo,
StreamingContext streamingContext)
+#if NETSTANDARD2_0
+ : base(serializationInfo, streamingContext)
+#else
: base(ExceptionMessage)
+#endif
{
}
}
\ No newline at end of file
diff --git a/src/Atc/Exceptions/ArgumentNullOrDefaultPropertyException.cs b/src/Atc/Exceptions/ArgumentNullOrDefaultPropertyException.cs
index 26e88e75..fb6b8100 100644
--- a/src/Atc/Exceptions/ArgumentNullOrDefaultPropertyException.cs
+++ b/src/Atc/Exceptions/ArgumentNullOrDefaultPropertyException.cs
@@ -64,7 +64,11 @@ public ArgumentNullOrDefaultPropertyException(
private ArgumentNullOrDefaultPropertyException(
SerializationInfo serializationInfo,
StreamingContext streamingContext)
+#if NETSTANDARD2_0
+ : base(serializationInfo, streamingContext)
+#else
: base(ExceptionMessage)
+#endif
{
}
}
\ No newline at end of file
diff --git a/src/Atc/Exceptions/ArgumentNullPropertyException.cs b/src/Atc/Exceptions/ArgumentNullPropertyException.cs
index 97081e9e..e6174ea9 100644
--- a/src/Atc/Exceptions/ArgumentNullPropertyException.cs
+++ b/src/Atc/Exceptions/ArgumentNullPropertyException.cs
@@ -64,7 +64,11 @@ public ArgumentNullPropertyException(
private ArgumentNullPropertyException(
SerializationInfo serializationInfo,
StreamingContext streamingContext)
+#if NETSTANDARD2_0
+ : base(serializationInfo, streamingContext)
+#else
: base(ExceptionMessage)
+#endif
{
}
}
\ No newline at end of file
diff --git a/src/Atc/Exceptions/ArgumentPropertyException.cs b/src/Atc/Exceptions/ArgumentPropertyException.cs
index 0e0a7171..f3398d84 100644
--- a/src/Atc/Exceptions/ArgumentPropertyException.cs
+++ b/src/Atc/Exceptions/ArgumentPropertyException.cs
@@ -64,7 +64,11 @@ public ArgumentPropertyException(
private ArgumentPropertyException(
SerializationInfo serializationInfo,
StreamingContext streamingContext)
+#if NETSTANDARD2_0
+ : base(serializationInfo, streamingContext)
+#else
: base(ExceptionMessage)
+#endif
{
}
}
\ No newline at end of file
diff --git a/src/Atc/Exceptions/ArgumentPropertyNullException.cs b/src/Atc/Exceptions/ArgumentPropertyNullException.cs
index 1dd4e35e..e2ec0670 100644
--- a/src/Atc/Exceptions/ArgumentPropertyNullException.cs
+++ b/src/Atc/Exceptions/ArgumentPropertyNullException.cs
@@ -64,7 +64,11 @@ public ArgumentPropertyNullException(
private ArgumentPropertyNullException(
SerializationInfo serializationInfo,
StreamingContext streamingContext)
+#if NETSTANDARD2_0
+ : base(serializationInfo, streamingContext)
+#else
: base(ExceptionMessage)
+#endif
{
}
}
\ No newline at end of file
diff --git a/src/Atc/Exceptions/CertificateValidationException.cs b/src/Atc/Exceptions/CertificateValidationException.cs
index 9c934a7e..a3ea499e 100644
--- a/src/Atc/Exceptions/CertificateValidationException.cs
+++ b/src/Atc/Exceptions/CertificateValidationException.cs
@@ -47,7 +47,11 @@ public CertificateValidationException(
protected CertificateValidationException(
SerializationInfo serializationInfo,
StreamingContext streamingContext)
+#if NETSTANDARD2_0
+ : base(serializationInfo, streamingContext)
+#else
: base(ExceptionMessage)
+#endif
{
}
}
\ No newline at end of file
diff --git a/src/Atc/Exceptions/ConfigurationException.cs b/src/Atc/Exceptions/ConfigurationException.cs
index bc57f98c..9b4c1974 100644
--- a/src/Atc/Exceptions/ConfigurationException.cs
+++ b/src/Atc/Exceptions/ConfigurationException.cs
@@ -32,7 +32,7 @@ public ConfigurationException(string message)
///
/// The configuration section name.
/// The configuration key name.
- /// A value indicating whether the configuration is missing (true) or invalid (false).
+ /// A value indicating whether the configuration is missing ( ) or invalid ( ).
public ConfigurationException(
string section,
string key,
@@ -41,6 +41,24 @@ public ConfigurationException(
{
}
+ ///
+ /// Initializes a new instance of the class with a configuration section, key, status, and inner exception.
+ ///
+ /// The configuration section name.
+ /// The configuration key name.
+ /// A value indicating whether the configuration is missing ( ) or invalid ( ).
+ /// The exception that is the cause of the current exception.
+ public ConfigurationException(
+ string section,
+ string key,
+ bool isMissing,
+ Exception innerException)
+ : base(
+ $"Configuration with section '{section}' and key '{key}' is {GetTermForIsMissing(isMissing)}.",
+ innerException)
+ {
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -53,6 +71,40 @@ public ConfigurationException(
{
}
+ ///
+ /// Throws a if is or empty.
+ ///
+ /// The configuration value to validate.
+ /// The configuration section name.
+ /// The configuration key name.
+ public static void ThrowIfMissing(
+ [NotNull] string? value,
+ string section,
+ string key)
+ {
+ if (value is null || value.Length == 0)
+ {
+ throw new ConfigurationException(section, key, isMissing: true);
+ }
+ }
+
+ ///
+ /// Throws a if is .
+ ///
+ /// The condition that indicates the configuration value is invalid.
+ /// The configuration section name.
+ /// The configuration key name.
+ public static void ThrowIfInvalid(
+ bool condition,
+ string section,
+ string key)
+ {
+ if (condition)
+ {
+ throw new ConfigurationException(section, key, isMissing: false);
+ }
+ }
+
///
/// Initializes a new instance of the class with serialized data.
///
@@ -61,7 +113,11 @@ public ConfigurationException(
protected ConfigurationException(
SerializationInfo serializationInfo,
StreamingContext streamingContext)
+#if NETSTANDARD2_0
+ : base(serializationInfo, streamingContext)
+#else
: base(ExceptionMessage)
+#endif
{
}
diff --git a/src/Atc/Exceptions/DesignTimeUseOnlyException.cs b/src/Atc/Exceptions/DesignTimeUseOnlyException.cs
index 3ca55c71..d06e3b58 100644
--- a/src/Atc/Exceptions/DesignTimeUseOnlyException.cs
+++ b/src/Atc/Exceptions/DesignTimeUseOnlyException.cs
@@ -51,7 +51,11 @@ public DesignTimeUseOnlyException(
protected DesignTimeUseOnlyException(
SerializationInfo serializationInfo,
StreamingContext streamingContext)
+#if NETSTANDARD2_0
+ : base(serializationInfo, streamingContext)
+#else
: base(ExceptionMessage)
+#endif
{
}
}
\ No newline at end of file
diff --git a/src/Atc/Exceptions/EntityStoreException.cs b/src/Atc/Exceptions/EntityStoreException.cs
index 4e8327d8..ad5cd7b8 100644
--- a/src/Atc/Exceptions/EntityStoreException.cs
+++ b/src/Atc/Exceptions/EntityStoreException.cs
@@ -47,7 +47,11 @@ public EntityStoreException(
protected EntityStoreException(
SerializationInfo serializationInfo,
StreamingContext streamingContext)
+#if NETSTANDARD2_0
+ : base(serializationInfo, streamingContext)
+#else
: base(ExceptionMessage)
+#endif
{
}
}
\ No newline at end of file
diff --git a/src/Atc/Exceptions/ItemNotFoundException.cs b/src/Atc/Exceptions/ItemNotFoundException.cs
index 64882f46..3b28f1fd 100644
--- a/src/Atc/Exceptions/ItemNotFoundException.cs
+++ b/src/Atc/Exceptions/ItemNotFoundException.cs
@@ -47,7 +47,11 @@ public ItemNotFoundException(
protected ItemNotFoundException(
SerializationInfo serializationInfo,
StreamingContext streamingContext)
+#if NETSTANDARD2_0
+ : base(serializationInfo, streamingContext)
+#else
: base(ExceptionMessage)
+#endif
{
}
}
\ No newline at end of file
diff --git a/src/Atc/Exceptions/NullException.cs b/src/Atc/Exceptions/NullException.cs
index a105e597..300d2f77 100644
--- a/src/Atc/Exceptions/NullException.cs
+++ b/src/Atc/Exceptions/NullException.cs
@@ -47,7 +47,11 @@ public NullException(
protected NullException(
SerializationInfo serializationInfo,
StreamingContext streamingContext)
+#if NETSTANDARD2_0
+ : base(serializationInfo, streamingContext)
+#else
: base(ExceptionMessage)
+#endif
{
}
}
\ No newline at end of file
diff --git a/src/Atc/Exceptions/PermissionException.cs b/src/Atc/Exceptions/PermissionException.cs
index 6074820d..b38d34dd 100644
--- a/src/Atc/Exceptions/PermissionException.cs
+++ b/src/Atc/Exceptions/PermissionException.cs
@@ -47,7 +47,11 @@ public PermissionException(
protected PermissionException(
SerializationInfo serializationInfo,
StreamingContext streamingContext)
+#if NETSTANDARD2_0
+ : base(serializationInfo, streamingContext)
+#else
: base(ExceptionMessage)
+#endif
{
}
}
\ No newline at end of file
diff --git a/src/Atc/Exceptions/StringNullOrEmptyException.cs b/src/Atc/Exceptions/StringNullOrEmptyException.cs
index 7db94ffe..160dbdc1 100644
--- a/src/Atc/Exceptions/StringNullOrEmptyException.cs
+++ b/src/Atc/Exceptions/StringNullOrEmptyException.cs
@@ -47,7 +47,11 @@ public StringNullOrEmptyException(
protected StringNullOrEmptyException(
SerializationInfo serializationInfo,
StreamingContext streamingContext)
+#if NETSTANDARD2_0
+ : base(serializationInfo, streamingContext)
+#else
: base(ExceptionMessage)
+#endif
{
}
}
\ No newline at end of file
diff --git a/src/Atc/Exceptions/SwitchCaseDefaultException.cs b/src/Atc/Exceptions/SwitchCaseDefaultException.cs
index cde401e1..3bd630e6 100644
--- a/src/Atc/Exceptions/SwitchCaseDefaultException.cs
+++ b/src/Atc/Exceptions/SwitchCaseDefaultException.cs
@@ -36,15 +36,9 @@ public SwitchCaseDefaultException(string message)
///
/// The unexpected enum value that was encountered.
/// Thrown when is null.
- [SuppressMessage("Major Code Smell", "S5766:Deserializing objects without performing data validation is security-sensitive", Justification = "OK.")]
public SwitchCaseDefaultException(Enum value)
+ : base(BuildMessage(value))
{
- if (value is null)
- {
- throw new ArgumentNullException(nameof(value));
- }
-
- ReflectionHelper.SetPrivateField(this, "_message", $"Unexpected value.{Environment.NewLine}Enum name: {value.GetType().FullName}{Environment.NewLine}Enum value: {value}");
}
///
@@ -53,22 +47,22 @@ public SwitchCaseDefaultException(Enum value)
/// The unexpected enum value that was encountered.
/// The custom error message that describes the error.
/// Thrown when or is null.
- [SuppressMessage("Major Code Smell", "S5766:Deserializing objects without performing data validation is security-sensitive", Justification = "OK.")]
public SwitchCaseDefaultException(
Enum value,
string message)
+ : base(BuildMessage(value, message))
{
- if (value is null)
- {
- throw new ArgumentNullException(nameof(value));
- }
-
- if (message is null)
- {
- throw new ArgumentNullException(nameof(message));
- }
+ }
- ReflectionHelper.SetPrivateField(this, "_message", $"{message}{Environment.NewLine}Enum name: {value.GetType().FullName}{Environment.NewLine}Enum value: {value}");
+ ///
+ /// Initializes a new instance of the class with any non-enum value.
+ ///
+ ///
+ /// The unexpected value that was encountered. Accepts any type, including .
+ ///
+ public SwitchCaseDefaultException(object? value)
+ : base(BuildMessageForObject(value))
+ {
}
///
@@ -83,6 +77,22 @@ public SwitchCaseDefaultException(
{
}
+ ///
+ /// Throws a for the given enum value.
+ ///
+ /// The unexpected enum value encountered in the switch default case.
+ [DoesNotReturn]
+ public static void Throw(Enum value)
+ => throw new SwitchCaseDefaultException(value);
+
+ ///
+ /// Throws a for the given value.
+ ///
+ /// The unexpected value encountered in the switch default case.
+ [DoesNotReturn]
+ public static void Throw(object? value)
+ => throw new SwitchCaseDefaultException(value);
+
///
/// Initializes a new instance of the class with serialized data.
///
@@ -91,7 +101,43 @@ public SwitchCaseDefaultException(
protected SwitchCaseDefaultException(
SerializationInfo serializationInfo,
StreamingContext streamingContext)
+#if NETSTANDARD2_0
+ : base(serializationInfo, streamingContext)
+#else
: base(ExceptionMessage)
+#endif
{
}
+
+ private static string BuildMessage(Enum value)
+ {
+ if (value is null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ return $"Unexpected value.{Environment.NewLine}Enum name: {value.GetType().FullName}{Environment.NewLine}Enum value: {value}";
+ }
+
+ private static string BuildMessage(
+ Enum value,
+ string message)
+ {
+ if (value is null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ if (message is null)
+ {
+ throw new ArgumentNullException(nameof(message));
+ }
+
+ return $"{message}{Environment.NewLine}Enum name: {value.GetType().FullName}{Environment.NewLine}Enum value: {value}";
+ }
+
+ private static string BuildMessageForObject(object? value)
+ => value is null
+ ? $"Unexpected value.{Environment.NewLine}Value: "
+ : $"Unexpected value.{Environment.NewLine}Type: {value.GetType().FullName}{Environment.NewLine}Value: {value}";
}
\ No newline at end of file
diff --git a/src/Atc/Exceptions/TcpException.cs b/src/Atc/Exceptions/TcpException.cs
index c04ee54d..2317857b 100644
--- a/src/Atc/Exceptions/TcpException.cs
+++ b/src/Atc/Exceptions/TcpException.cs
@@ -47,7 +47,11 @@ public TcpException(
protected TcpException(
SerializationInfo serializationInfo,
StreamingContext streamingContext)
+#if NETSTANDARD2_0
+ : base(serializationInfo, streamingContext)
+#else
: base(ExceptionMessage)
+#endif
{
}
}
\ No newline at end of file
diff --git a/src/Atc/Exceptions/UnexpectedTypeException.cs b/src/Atc/Exceptions/UnexpectedTypeException.cs
index 8cb94de9..739c461b 100644
--- a/src/Atc/Exceptions/UnexpectedTypeException.cs
+++ b/src/Atc/Exceptions/UnexpectedTypeException.cs
@@ -33,34 +33,11 @@ public UnexpectedTypeException(string message)
/// The actual type that was encountered.
/// The type that was expected.
/// Thrown when or is null.
- [SuppressMessage("Major Code Smell", "S5766:Deserializing objects without performing data validation is security-sensitive", Justification = "OK.")]
public UnexpectedTypeException(
Type actualType,
Type expectedType)
+ : base(BuildMessage(actualType, expectedType))
{
- if (actualType is null)
- {
- throw new ArgumentNullException(nameof(actualType));
- }
-
- if (expectedType is null)
- {
- throw new ArgumentNullException(nameof(expectedType));
- }
-
- var actualTypeName = actualType.FullName!;
- if (actualType.IsSimple())
- {
- actualTypeName = actualType.BeautifyTypeName();
- }
-
- var expectedTypeName = expectedType.FullName!;
- if (expectedType.IsSimple())
- {
- expectedTypeName = expectedType.BeautifyTypeName();
- }
-
- ReflectionHelper.SetPrivateField(this, "_message", $"Unexpected type.{Environment.NewLine}ActualType name: {actualTypeName}{Environment.NewLine}ExpectedType name: {expectedTypeName}");
}
///
@@ -70,40 +47,12 @@ public UnexpectedTypeException(
/// The type that was expected.
/// The custom error message that describes the error.
/// Thrown when , , or is null.
- [SuppressMessage("Major Code Smell", "S5766:Deserializing objects without performing data validation is security-sensitive", Justification = "OK.")]
public UnexpectedTypeException(
Type actualType,
Type expectedType,
string message)
+ : base(BuildMessage(actualType, expectedType, message))
{
- if (actualType is null)
- {
- throw new ArgumentNullException(nameof(actualType));
- }
-
- if (expectedType is null)
- {
- throw new ArgumentNullException(nameof(expectedType));
- }
-
- if (message is null)
- {
- throw new ArgumentNullException(nameof(message));
- }
-
- var actualTypeName = actualType.FullName!;
- if (actualType.IsSimple())
- {
- actualTypeName = actualType.BeautifyTypeName();
- }
-
- var expectedTypeName = expectedType.FullName!;
- if (expectedType.IsSimple())
- {
- expectedTypeName = expectedType.BeautifyTypeName();
- }
-
- ReflectionHelper.SetPrivateField(this, "_message", $"{message}{Environment.NewLine}ActualType name: {actualTypeName}{Environment.NewLine}ExpectedType name: {expectedTypeName}");
}
///
@@ -126,7 +75,54 @@ public UnexpectedTypeException(
protected UnexpectedTypeException(
SerializationInfo serializationInfo,
StreamingContext streamingContext)
+#if NETSTANDARD2_0
+ : base(serializationInfo, streamingContext)
+#else
: base(ExceptionMessage)
+#endif
{
}
+
+ private static string GetTypeName(Type type)
+ => type.IsSimple() ? type.BeautifyTypeName() : type.FullName!;
+
+ private static string BuildMessage(
+ Type actualType,
+ Type expectedType)
+ {
+ if (actualType is null)
+ {
+ throw new ArgumentNullException(nameof(actualType));
+ }
+
+ if (expectedType is null)
+ {
+ throw new ArgumentNullException(nameof(expectedType));
+ }
+
+ return $"Unexpected type.{Environment.NewLine}ActualType name: {GetTypeName(actualType)}{Environment.NewLine}ExpectedType name: {GetTypeName(expectedType)}";
+ }
+
+ private static string BuildMessage(
+ Type actualType,
+ Type expectedType,
+ string message)
+ {
+ if (actualType is null)
+ {
+ throw new ArgumentNullException(nameof(actualType));
+ }
+
+ if (expectedType is null)
+ {
+ throw new ArgumentNullException(nameof(expectedType));
+ }
+
+ if (message is null)
+ {
+ throw new ArgumentNullException(nameof(message));
+ }
+
+ return $"{message}{Environment.NewLine}ActualType name: {GetTypeName(actualType)}{Environment.NewLine}ExpectedType name: {GetTypeName(expectedType)}";
+ }
}
\ No newline at end of file
diff --git a/src/Atc/Exceptions/UserNotFoundException.cs b/src/Atc/Exceptions/UserNotFoundException.cs
index c97b40ac..58ca05cc 100644
--- a/src/Atc/Exceptions/UserNotFoundException.cs
+++ b/src/Atc/Exceptions/UserNotFoundException.cs
@@ -47,7 +47,11 @@ public UserNotFoundException(
protected UserNotFoundException(
SerializationInfo serializationInfo,
StreamingContext streamingContext)
+#if NETSTANDARD2_0
+ : base(serializationInfo, streamingContext)
+#else
: base(ExceptionMessage)
+#endif
{
}
}
\ No newline at end of file
diff --git a/src/Atc/Exceptions/ViewModelException.cs b/src/Atc/Exceptions/ViewModelException.cs
index 0ed9590e..0c1a3ba9 100644
--- a/src/Atc/Exceptions/ViewModelException.cs
+++ b/src/Atc/Exceptions/ViewModelException.cs
@@ -47,7 +47,11 @@ public ViewModelException(
protected ViewModelException(
SerializationInfo serializationInfo,
StreamingContext streamingContext)
+#if NETSTANDARD2_0
+ : base(serializationInfo, streamingContext)
+#else
: base(ExceptionMessage)
+#endif
{
}
}
\ No newline at end of file
diff --git a/src/Atc/Extensions/BaseTypes/ByteExtensions.cs b/src/Atc/Extensions/BaseTypes/ByteExtensions.cs
index aa34ca53..840f9dee 100644
--- a/src/Atc/Extensions/BaseTypes/ByteExtensions.cs
+++ b/src/Atc/Extensions/BaseTypes/ByteExtensions.cs
@@ -29,10 +29,7 @@ public static byte[] TakeBytes(
return Array.Empty();
}
- return value
- .Skip(startPosition)
- .Take(length)
- .ToArray();
+ return new ReadOnlySpan(value, startPosition, length).ToArray();
}
///
@@ -58,10 +55,10 @@ public static int TakeBytesAndConvertToInt(
return -1;
}
- if (length < sizeof(int))
+ if (bytes.Length < sizeof(int))
{
bytes = bytes
- .Concat(ByteHelper.CreateZeroArray(length))
+ .Concat(ByteHelper.CreateZeroArray(sizeof(int) - bytes.Length))
.ToArray();
}
@@ -95,10 +92,10 @@ public static long TakeBytesAndConvertToLong(
return -1;
}
- if (length < sizeof(long))
+ if (bytes.Length < sizeof(long))
{
bytes = bytes
- .Concat(ByteHelper.CreateZeroArray(length))
+ .Concat(ByteHelper.CreateZeroArray(sizeof(long) - bytes.Length))
.ToArray();
}
diff --git a/src/Atc/Extensions/BaseTypes/CharExtensions.cs b/src/Atc/Extensions/BaseTypes/CharExtensions.cs
index b047f0e0..925acfbf 100644
--- a/src/Atc/Extensions/BaseTypes/CharExtensions.cs
+++ b/src/Atc/Extensions/BaseTypes/CharExtensions.cs
@@ -13,4 +13,39 @@ public static class CharExtensions
/// if the character value is less than or equal to 127 (ASCII range); otherwise, .
public static bool IsAscii(this char value)
=> value <= sbyte.MaxValue;
+
+ ///
+ /// Determines whether the specified character is an ASCII letter (A–Z or a–z).
+ ///
+ /// The character to check.
+ /// if the character is an ASCII letter; otherwise, .
+ public static bool IsAsciiLetter(this char value)
+ => (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z');
+
+ ///
+ /// Determines whether the specified character is an ASCII decimal digit (0–9).
+ ///
+ /// The character to check.
+ /// if the character is a digit 0 through 9; otherwise, .
+ public static bool IsAsciiDigit(this char value)
+ => value >= '0' && value <= '9';
+
+ ///
+ /// Determines whether the specified character is a hexadecimal digit (0–9, A–F, a–f).
+ ///
+ /// The character to check.
+ /// if the character is a valid hexadecimal digit; otherwise, .
+ public static bool IsHexDigit(this char value)
+ => (value >= '0' && value <= '9')
+ || (value >= 'A' && value <= 'F')
+ || (value >= 'a' && value <= 'f');
+
+ ///
+ /// Determines whether the specified character is an ASCII vowel (A, E, I, O, U — case-insensitive).
+ ///
+ /// The character to check.
+ /// if the character is one of A, E, I, O, U (upper or lower case); otherwise, .
+ public static bool IsVowel(this char value)
+ => value is 'A' or 'E' or 'I' or 'O' or 'U'
+ or 'a' or 'e' or 'i' or 'o' or 'u';
}
\ No newline at end of file
diff --git a/src/Atc/Extensions/BaseTypes/DateTimeExtensions.cs b/src/Atc/Extensions/BaseTypes/DateTimeExtensions.cs
index feaae93c..15e0489d 100644
--- a/src/Atc/Extensions/BaseTypes/DateTimeExtensions.cs
+++ b/src/Atc/Extensions/BaseTypes/DateTimeExtensions.cs
@@ -53,6 +53,15 @@ public static string GetPrettyTimeDiff(
/// The date.
/// The week number from the given date.
public static int GetWeekNumber(this DateTime date)
+ => CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(date, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
+
+ ///
+ /// Gets the week number from a given date using the current UI culture's calendar.
+ /// Use this variant when rendering the week number for display in a user interface.
+ ///
+ /// The date.
+ /// The week number from the given date.
+ public static int GetWeekNumberUi(this DateTime date)
=> CultureInfo.CurrentUICulture.Calendar.GetWeekOfYear(date, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
///
@@ -113,15 +122,13 @@ public static string ToIso8601UtcDate(this DateTime dateTime)
.ToString(GlobalizationConstants.DateTimeIso8601, GlobalizationConstants.EnglishCultureInfo);
///
- /// Converts a DateTime to a string using the long date pattern
- /// of the current UI culture.
+ /// Converts a DateTime to a string using the long date pattern of the current UI culture.
+ /// Use this variant when rendering output for display in a user interface.
///
/// The DateTime to format.
- /// A string representation of the DateTime using the
- /// long date pattern of the current UI culture.
- public static string ToLongDateStringUsingCurrentUiCulture(
- this DateTime dateTime)
- => dateTime.ToLongDateString(Thread.CurrentThread.CurrentUICulture.DateTimeFormat);
+ /// A string representation of the DateTime using the long date pattern of the current UI culture.
+ public static string ToLongDateStringUi(this DateTime dateTime)
+ => dateTime.ToLongDateString(CultureInfo.CurrentUICulture.DateTimeFormat);
///
/// Converts a DateTime to a string using the long date pattern of a specific culture.
@@ -166,15 +173,13 @@ public static string ToLongDateString(
}
///
- /// Converts a DateTime to a string using the long time pattern
- /// of the current UI culture.
+ /// Converts a DateTime to a string using the long time pattern of the current UI culture.
+ /// Use this variant when rendering output for display in a user interface.
///
/// The DateTime to format.
- /// A string representation of the DateTime using the
- /// long time pattern of the current UI culture.
- public static string ToLongTimeStringUsingCurrentUiCulture(
- this DateTime dateTime)
- => dateTime.ToLongTimeString(Thread.CurrentThread.CurrentUICulture.DateTimeFormat);
+ /// A string representation of the DateTime using the long time pattern of the current UI culture.
+ public static string ToLongTimeStringUi(this DateTime dateTime)
+ => dateTime.ToLongTimeString(CultureInfo.CurrentUICulture.DateTimeFormat);
///
/// Converts a DateTime to a string using the long time pattern of a specific culture.
@@ -218,15 +223,13 @@ public static string ToLongTimeString(
}
///
- /// Converts a DateTime to a string using the short date pattern
- /// of the current UI culture.
+ /// Converts a DateTime to a string using the short date pattern of the current UI culture.
+ /// Use this variant when rendering output for display in a user interface.
///
/// The DateTime to format.
- /// A string representation of the DateTime using the
- /// short date pattern of the current UI culture.
- public static string ToShortDateStringUsingCurrentUiCulture(
- this DateTime dateTime)
- => dateTime.ToShortDateString(Thread.CurrentThread.CurrentUICulture.DateTimeFormat);
+ /// A string representation of the DateTime using the short date pattern of the current UI culture.
+ public static string ToShortDateStringUi(this DateTime dateTime)
+ => dateTime.ToShortDateString(CultureInfo.CurrentUICulture.DateTimeFormat);
///
/// Converts a DateTime to a string using the short date pattern of a specific culture.
@@ -270,15 +273,13 @@ public static string ToShortDateString(
}
///
- /// Converts a DateTime to a string using the short time pattern
- /// of the current UI culture.
+ /// Converts a DateTime to a string using the short time pattern of the current UI culture.
+ /// Use this variant when rendering output for display in a user interface.
///
/// The DateTime to format.
- /// A string representation of the DateTime using the
- /// short time pattern of the current UI culture.
- public static string ToShortTimeStringUsingCurrentUiCulture(
- this DateTime dateTime)
- => dateTime.ToShortTimeString(Thread.CurrentThread.CurrentUICulture.DateTimeFormat);
+ /// A string representation of the DateTime using the short time pattern of the current UI culture.
+ public static string ToShortTimeStringUi(this DateTime dateTime)
+ => dateTime.ToShortTimeString(CultureInfo.CurrentUICulture.DateTimeFormat);
///
/// Converts a DateTime to a string using the short time pattern of a specific culture.
@@ -320,4 +321,48 @@ public static string ToShortTimeString(
dateTimeFormatInfo.ShortTimePattern,
dateTimeFormatInfo);
}
+
+ ///
+ /// Returns a new set to the very start of the same day (00:00:00.000).
+ /// The of the result matches the input.
+ ///
+ /// The date value.
+ /// Midnight at the start of 's date.
+ public static DateTime StartOfDay(this DateTime dateTime)
+ => dateTime.Date;
+
+ ///
+ /// Returns a new set to the very end of the same day (23:59:59.9999999).
+ /// The of the result matches the input.
+ ///
+ /// The date value.
+ /// The last representable tick of 's date.
+ public static DateTime EndOfDay(this DateTime dateTime)
+ => dateTime.Date.AddDays(1).AddTicks(-1);
+
+ ///
+ /// Returns a new set to the first day of the same month at midnight (00:00:00.000).
+ ///
+ /// The date value.
+ /// The first day of the month containing .
+ public static DateTime StartOfMonth(this DateTime dateTime)
+ => new(dateTime.Year, dateTime.Month, 1, 0, 0, 0, dateTime.Kind);
+
+ ///
+ /// Returns a new set to the last tick of the last day of the same month.
+ ///
+ /// The date value.
+ /// The last representable tick of the last day of the month containing .
+ public static DateTime EndOfMonth(this DateTime dateTime)
+ => new DateTime(dateTime.Year, dateTime.Month, DateTime.DaysInMonth(dateTime.Year, dateTime.Month), 0, 0, 0, dateTime.Kind)
+ .AddDays(1)
+ .AddTicks(-1);
+
+ ///
+ /// Determines whether the specified date falls on a Saturday or Sunday.
+ ///
+ /// The date to test.
+ /// if the day of the week is or ; otherwise, .
+ public static bool IsWeekend(this DateTime dateTime)
+ => dateTime.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
}
\ No newline at end of file
diff --git a/src/Atc/Extensions/BaseTypes/DateTimeOffsetExtensions.cs b/src/Atc/Extensions/BaseTypes/DateTimeOffsetExtensions.cs
index d4355416..828e09e8 100644
--- a/src/Atc/Extensions/BaseTypes/DateTimeOffsetExtensions.cs
+++ b/src/Atc/Extensions/BaseTypes/DateTimeOffsetExtensions.cs
@@ -53,6 +53,15 @@ public static string GetPrettyTimeDiff(
/// The date.
/// The week number from the given date.
public static int GetWeekNumber(this DateTimeOffset date)
+ => CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(date.DateTime, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
+
+ ///
+ /// Gets the week number from a given date using the current UI culture's calendar.
+ /// Use this variant when rendering the week number for display in a user interface.
+ ///
+ /// The date.
+ /// The week number from the given date.
+ public static int GetWeekNumberUi(this DateTimeOffset date)
=> CultureInfo.CurrentUICulture.Calendar.GetWeekOfYear(date.DateTime, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
///
@@ -127,7 +136,7 @@ public static DateTimeOffset SetHourAndMinutes(
hour,
minutes,
0,
- TimeSpan.Zero);
+ dateTimeOffset.Offset);
/// Converts the DateTimeOffset to a unix time - seconds starting from 1-1-1970.
/// The date time offset.
@@ -157,15 +166,13 @@ public static string ToIso8601UtcDate(this DateTimeOffset dateTimeOffset)
.ToString(GlobalizationConstants.DateTimeIso8601, GlobalizationConstants.EnglishCultureInfo);
///
- /// Converts a DateTime to a string using the long date pattern
- /// of the current UI culture.
+ /// Converts a DateTimeOffset to a string using the long date pattern of the current UI culture.
+ /// Use this variant when rendering output for display in a user interface.
///
/// The DateTimeOffset to format.
- /// A string representation of the DateTime using the
- /// long date pattern of the current UI culture.
- public static string ToLongDateStringUsingCurrentUiCulture(
- this DateTimeOffset dateTimeOffset)
- => dateTimeOffset.ToLongDateString(Thread.CurrentThread.CurrentUICulture.DateTimeFormat);
+ /// A string representation of the DateTimeOffset using the long date pattern of the current UI culture.
+ public static string ToLongDateStringUi(this DateTimeOffset dateTimeOffset)
+ => dateTimeOffset.ToLongDateString(CultureInfo.CurrentUICulture.DateTimeFormat);
///
/// Converts a DateTime to a string using the long date pattern of the provided DateTimeFormatInfo.
@@ -191,15 +198,13 @@ public static string ToLongDateString(
}
///
- /// Converts a DateTime to a string using the long time pattern
- /// of the current UI culture.
+ /// Converts a DateTimeOffset to a string using the long time pattern of the current UI culture.
+ /// Use this variant when rendering output for display in a user interface.
///
/// The DateTimeOffset to format.
- /// A string representation of the DateTime using the
- /// long time pattern of the current UI culture.
- public static string ToLongTimeStringUsingCurrentUiCulture(
- this DateTimeOffset dateTimeOffset)
- => dateTimeOffset.ToLongTimeString(Thread.CurrentThread.CurrentUICulture.DateTimeFormat);
+ /// A string representation of the DateTimeOffset using the long time pattern of the current UI culture.
+ public static string ToLongTimeStringUi(this DateTimeOffset dateTimeOffset)
+ => dateTimeOffset.ToLongTimeString(CultureInfo.CurrentUICulture.DateTimeFormat);
///
/// Converts a DateTime to a string using the long time pattern of the provided DateTimeFormatInfo.
@@ -224,15 +229,28 @@ public static string ToLongTimeString(
}
///
- /// Converts a DateTime to a string using the short date pattern
- /// of the current UI culture.
+ /// Converts a DateTimeOffset to a string using the short date pattern of the current UI culture.
+ /// Use this variant when rendering output for display in a user interface.
///
/// The DateTimeOffset to format.
- /// A string representation of the DateTime using the
- /// short date pattern of the current UI culture.
- public static string ToShortDateStringUsingCurrentUiCulture(
- this DateTimeOffset dateTimeOffset)
- => dateTimeOffset.ToShortDateString(Thread.CurrentThread.CurrentUICulture.DateTimeFormat);
+ /// A string representation of the DateTimeOffset using the short date pattern of the current UI culture.
+ public static string ToShortDateStringUi(this DateTimeOffset dateTimeOffset)
+ => dateTimeOffset.ToShortDateString(CultureInfo.CurrentUICulture.DateTimeFormat);
+
+ ///
+ /// Converts a to a string using the short date pattern of the specified culture.
+ ///
+ /// The DateTimeOffset to format.
+ /// The culture whose short date pattern is used.
+ /// A string representation of the date using the short date pattern of .
+ /// Thrown when is null.
+ public static string ToShortDateStringUsingSpecificCulture(
+ this DateTimeOffset dateTimeOffset,
+ CultureInfo cultureInfo)
+ {
+ ArgumentNullException.ThrowIfNull(cultureInfo);
+ return dateTimeOffset.ToShortDateString(cultureInfo.DateTimeFormat);
+ }
///
/// Converts a DateTime to a string using the short date pattern of the provided DateTimeFormatInfo.
@@ -257,15 +275,13 @@ public static string ToShortDateString(
}
///
- /// Converts a DateTime to a string using the short time pattern
- /// of the current UI culture.
+ /// Converts a DateTimeOffset to a string using the short time pattern of the current UI culture.
+ /// Use this variant when rendering output for display in a user interface.
///
/// The DateTimeOffset to format.
- /// A string representation of the DateTime using the
- /// short time pattern of the current UI culture.
- public static string ToShortTimeStringUsingCurrentUiCulture(
- this DateTimeOffset dateTimeOffset)
- => dateTimeOffset.ToShortTimeString(Thread.CurrentThread.CurrentUICulture.DateTimeFormat);
+ /// A string representation of the DateTimeOffset using the short time pattern of the current UI culture.
+ public static string ToShortTimeStringUi(this DateTimeOffset dateTimeOffset)
+ => dateTimeOffset.ToShortTimeString(CultureInfo.CurrentUICulture.DateTimeFormat);
///
/// Converts a DateTime to a string using the short time pattern of the provided DateTimeFormatInfo.
@@ -288,4 +304,53 @@ public static string ToShortTimeString(
dateTimeFormatInfo.ShortTimePattern,
dateTimeFormatInfo);
}
+
+ ///
+ /// Returns a new set to the very start of the same day (00:00:00.000),
+ /// preserving the original .
+ ///
+ /// The date-time value.
+ /// Midnight at the start of the date, with the same UTC offset.
+ public static DateTimeOffset StartOfDay(this DateTimeOffset dateTimeOffset)
+ => new(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day, 0, 0, 0, dateTimeOffset.Offset);
+
+ ///
+ /// Returns a new set to the very end of the same day (23:59:59.9999999),
+ /// preserving the original .
+ ///
+ /// The date-time value.
+ /// The last representable tick of the date, with the same UTC offset.
+ public static DateTimeOffset EndOfDay(this DateTimeOffset dateTimeOffset)
+ => new DateTimeOffset(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day, 0, 0, 0, dateTimeOffset.Offset)
+ .AddDays(1)
+ .AddTicks(-1);
+
+ ///
+ /// Returns a new set to the first day of the same month at midnight,
+ /// preserving the original .
+ ///
+ /// The date-time value.
+ /// The first day of the month, with the same UTC offset.
+ public static DateTimeOffset StartOfMonth(
+ this DateTimeOffset dateTimeOffset)
+ => new(dateTimeOffset.Year, dateTimeOffset.Month, 1, 0, 0, 0, dateTimeOffset.Offset);
+
+ ///
+ /// Returns a new set to the last tick of the last day of the same month,
+ /// preserving the original .
+ ///
+ /// The date-time value.
+ /// The last representable tick of the month, with the same UTC offset.
+ public static DateTimeOffset EndOfMonth(this DateTimeOffset dateTimeOffset)
+ => new DateTimeOffset(dateTimeOffset.Year, dateTimeOffset.Month, DateTime.DaysInMonth(dateTimeOffset.Year, dateTimeOffset.Month), 0, 0, 0, dateTimeOffset.Offset)
+ .AddDays(1)
+ .AddTicks(-1);
+
+ ///
+ /// Determines whether the date falls on a Saturday or Sunday.
+ ///
+ /// The date to test.
+ /// if the day of week is or ; otherwise, .
+ public static bool IsWeekend(this DateTimeOffset dateTimeOffset)
+ => dateTimeOffset.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
}
\ No newline at end of file
diff --git a/src/Atc/Extensions/BaseTypes/DecimalExtensions.cs b/src/Atc/Extensions/BaseTypes/DecimalExtensions.cs
index c92bae6f..43482bf3 100644
--- a/src/Atc/Extensions/BaseTypes/DecimalExtensions.cs
+++ b/src/Atc/Extensions/BaseTypes/DecimalExtensions.cs
@@ -51,13 +51,9 @@ public static bool IsEqual(
decimal b,
int decimalPrecision)
{
- var sa = a
- .ToString(CultureInfo.InvariantCulture)
- .Replace(',', '.');
-
- var sb = b
- .ToString(CultureInfo.InvariantCulture)
- .Replace(',', '.');
+ // InvariantCulture always uses '.' as decimal separator — no Replace needed.
+ var sa = a.ToString(CultureInfo.InvariantCulture);
+ var sb = b.ToString(CultureInfo.InvariantCulture);
var saa = sa.Split('.');
var sab = sb.Split('.');
@@ -126,12 +122,12 @@ public static int CurrencyRoundingAsInteger(this decimal value)
=> (int)CurrencyRounding(value, 0);
///
- /// Rounds a decimal value using the currency decimal digits of the current UI culture.
+ /// Rounds a decimal value using the currency decimal digits of the current culture.
///
/// The decimal value to round.
/// The rounded decimal value.
public static decimal CurrencyRounding(this decimal value)
- => CurrencyRounding(value, Thread.CurrentThread.CurrentUICulture.NumberFormat.CurrencyDecimalDigits);
+ => CurrencyRounding(value, CultureInfo.CurrentCulture.NumberFormat.CurrencyDecimalDigits);
///
/// Rounds a decimal value to a specified number of decimal digits using midpoint rounding away from zero.
@@ -151,6 +147,15 @@ public static decimal CurrencyRounding(
return Math.Round(value, digits, MidpointRounding.AwayFromZero);
}
+ ///
+ /// Rounds a decimal value using the currency decimal digits of the current UI culture.
+ /// Use this variant when rounding for display in a user interface.
+ ///
+ /// The decimal value to round.
+ /// The rounded decimal value.
+ public static decimal CurrencyRoundingUi(this decimal value)
+ => CurrencyRounding(value, CultureInfo.CurrentUICulture.NumberFormat.CurrencyDecimalDigits);
+
///
/// Rounds a decimal value to 2 decimal places.
///
diff --git a/src/Atc/Extensions/BaseTypes/DoubleExtensions.cs b/src/Atc/Extensions/BaseTypes/DoubleExtensions.cs
index 29ead502..33ac8a8b 100644
--- a/src/Atc/Extensions/BaseTypes/DoubleExtensions.cs
+++ b/src/Atc/Extensions/BaseTypes/DoubleExtensions.cs
@@ -7,22 +7,26 @@ namespace System;
public static class DoubleExtensions
{
///
- /// The double epsilon.
+ /// The tolerance used by and .
+ /// Values whose absolute difference is at or below this threshold are considered equal.
///
- public const double DoubleEpsilon = double.Epsilon;
+ public const double DoubleEpsilon = 1e-9;
///
- /// Compare two values. Return if they are equals.
+ /// Determines whether two double values are approximately equal within a tolerance of (1e-9).
+ /// This handles common floating-point arithmetic rounding, for example 0.1 + 0.2 == 0.3 .
+ /// Use when an exact decimal-precision comparison is needed.
///
/// The first value.
/// The second value.
///
- /// if the two values are equals, otherwise.
+ /// if the absolute difference between and
+ /// is at most ; otherwise, .
///
public static bool IsEqual(
this double a,
double b)
- => Math.Abs(a - b) < double.Epsilon;
+ => Math.Abs(a - b) <= DoubleEpsilon;
///
/// Compare two values. Return if they are equals.
@@ -109,12 +113,12 @@ public static bool GreaterThanOrClose(
=> value1 > value2 || AreClose(value1, value2);
///
- /// Determines whether the specified double value is approximately zero.
+ /// Determines whether the specified double value is approximately zero within a tolerance of (1e-9).
///
/// The value to check.
- /// if the absolute value is less than epsilon; otherwise, .
+ /// if the absolute value is at most ; otherwise, .
public static bool IsZero(this double value)
- => Math.Abs(value) < DoubleEpsilon;
+ => Math.Abs(value) <= DoubleEpsilon;
///
/// Rounds a double value using currency rounding rules and returns it as an integer.
@@ -125,12 +129,12 @@ public static int CurrencyRoundingAsInteger(this double value)
=> (int)CurrencyRounding(value, 0);
///
- /// Rounds a double value using the currency decimal digits of the current UI culture.
+ /// Rounds a double value using the currency decimal digits of the current culture.
///
/// The double value to round.
/// The rounded double value.
public static double CurrencyRounding(this double value)
- => CurrencyRounding(value, Thread.CurrentThread.CurrentUICulture.NumberFormat.CurrencyDecimalDigits);
+ => CurrencyRounding(value, CultureInfo.CurrentCulture.NumberFormat.CurrencyDecimalDigits);
///
/// Rounds a double value to a specified number of decimal digits using midpoint rounding away from zero.
@@ -150,6 +154,15 @@ public static double CurrencyRounding(
return Math.Round(value, digits, MidpointRounding.AwayFromZero);
}
+ ///
+ /// Rounds a double value using the currency decimal digits of the current UI culture.
+ /// Use this variant when rounding for display in a user interface.
+ ///
+ /// The double value to round.
+ /// The rounded double value.
+ public static double CurrencyRoundingUi(this double value)
+ => CurrencyRounding(value, CultureInfo.CurrentUICulture.NumberFormat.CurrencyDecimalDigits);
+
///
/// Rounds a double value to 2 decimal places.
///
@@ -186,16 +199,25 @@ public static double RoundOffPercent(this double percent)
=> RoundOff(percent, 2);
///
- /// Counts the number of decimal places in the double value.
+ /// Counts the number of decimal places in the double value, up to a maximum of 15
+ /// (the limit of meaningful precision for a 64-bit floating-point number).
///
/// The double value to analyze.
- /// The number of decimal places in the value.
+ /// The number of decimal places, capped at 15.
public static int CountDecimalPoints(this double value)
{
+ const int maxPrecision = 15;
+ const double tolerance = 1e-9;
var precision = 0;
- while (Math.Abs((value * Math.Pow(10, precision)) - Math.Round(value * Math.Pow(10, precision))) > double.Epsilon)
+ while (precision < maxPrecision)
{
+ var scaled = value * Math.Pow(10, precision);
+ if (Math.Abs(scaled - Math.Round(scaled)) <= tolerance)
+ {
+ break;
+ }
+
precision++;
}
diff --git a/src/Atc/Extensions/BaseTypes/IntegerExtensions.cs b/src/Atc/Extensions/BaseTypes/IntegerExtensions.cs
index 44b2f0d8..ab613f85 100644
--- a/src/Atc/Extensions/BaseTypes/IntegerExtensions.cs
+++ b/src/Atc/Extensions/BaseTypes/IntegerExtensions.cs
@@ -92,13 +92,14 @@ public static bool IsBinarySequence(this int number)
=> number > 0 && (number & (number - 1)) == 0;
///
- /// Gets the month name by month number.
+ /// Gets the month name by month number using the current UI culture.
+ /// Use this variant when rendering output for display in a user interface.
///
/// The month.
/// if set to [pascal cased].
/// The name of the month.
[SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", Justification = "OK.")]
- public static string GetMonthNameByMonthNumber(
+ public static string GetMonthNameByMonthNumberUi(
this int month,
bool pascalCased = false)
{
@@ -111,7 +112,7 @@ public static string GetMonthNameByMonthNumber(
var time = timeBegin.AddMonths(month - 1);
// ReSharper disable once StringLiteralTypo
- var str = time.ToString("MMMM", Thread.CurrentThread.CurrentUICulture);
+ var str = time.ToString("MMMM", CultureInfo.CurrentUICulture);
if (str.Length <= 0)
{
return string.Empty;
@@ -128,6 +129,15 @@ public static string GetMonthNameByMonthNumber(
/// The year.
/// The get number of weeks.
public static int GetNumberOfWeeksByYear(this int year)
+ => CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(new DateTime(year, 12, 28), CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
+
+ ///
+ /// Returns the number of ISO weeks in the given year using the current UI culture's calendar.
+ /// Use this variant when rendering the value for display in a user interface.
+ ///
+ /// The four-digit year.
+ /// 52 or 53 depending on the year and calendar.
+ public static int GetNumberOfWeeksByYearUi(this int year)
=> CultureInfo.CurrentUICulture.Calendar.GetWeekOfYear(new DateTime(year, 12, 28), CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
///
@@ -139,6 +149,34 @@ public static int GetNumberOfWeeksByYear(this int year)
public static DateTime GetFirstDayOfWeekNumberByYear(
this int year,
int weekNumber)
+ {
+ var calendar = CultureInfo.CurrentCulture.Calendar;
+ var firstOfYear = new DateTime(year, 1, 1, calendar);
+ var daysOffset = DayOfWeek.Thursday - firstOfYear.DayOfWeek;
+
+ var firstThursday = firstOfYear.AddDays(daysOffset);
+ var firstWeek = CultureInfo.CurrentCulture.Calendar.GetWeekOfYear(firstThursday, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
+
+ var weekNum = weekNumber;
+ if (firstWeek <= 1)
+ {
+ weekNum -= 1;
+ }
+
+ var result = firstThursday.AddDays(weekNum * 7);
+ return result.AddDays(-3);
+ }
+
+ ///
+ /// Gets the date of the first day of a given week in a given year using the current UI culture's calendar.
+ /// Use this variant when rendering dates for display in a user interface.
+ ///
+ /// The four-digit year.
+ /// The ISO week number (1–53).
+ /// The of the Monday that starts the requested week.
+ public static DateTime GetFirstDayOfWeekNumberByYearUi(
+ this int year,
+ int weekNumber)
{
var calendar = CultureInfo.CurrentUICulture.Calendar;
var firstOfYear = new DateTime(year, 1, 1, calendar);
@@ -167,4 +205,16 @@ public static DateTime GetLastDayOfWeekNumberByYear(
this int year,
int weekNumber)
=> GetFirstDayOfWeekNumberByYear(year, weekNumber).AddDays(6);
+
+ ///
+ /// Gets the date of the last day of a given week in a given year using the current UI culture's calendar.
+ /// Use this variant when rendering dates for display in a user interface.
+ ///
+ /// The four-digit year.
+ /// The ISO week number (1–53).
+ /// The of the Sunday that ends the requested week.
+ public static DateTime GetLastDayOfWeekNumberByYearUi(
+ this int year,
+ int weekNumber)
+ => GetFirstDayOfWeekNumberByYearUi(year, weekNumber).AddDays(6);
}
\ No newline at end of file
diff --git a/src/Atc/Extensions/BaseTypes/LongExtensions.cs b/src/Atc/Extensions/BaseTypes/LongExtensions.cs
index 23395360..3e6eac95 100644
--- a/src/Atc/Extensions/BaseTypes/LongExtensions.cs
+++ b/src/Atc/Extensions/BaseTypes/LongExtensions.cs
@@ -26,7 +26,13 @@ public static DateTimeOffset FromUnixTime(this long valueInSeconds)
/// DateTimeOffset dateTimeOffset = unixTime.FromUnixTimeMs();
/// ]]>
public static DateTimeOffset FromUnixTimeMs(this long valueInMs)
- => valueInMs >= 1000
- ? DateTimeOffset.FromUnixTimeSeconds(valueInMs / 1000)
- : DateTimeOffset.FromUnixTimeSeconds(valueInMs);
+ => DateTimeOffset.FromUnixTimeMilliseconds(valueInMs);
+
+ ///
+ /// Determines whether the value is a binary sequence (a power of two), meaning exactly one bit is set.
+ ///
+ /// The number to evaluate.
+ /// if is a positive power of two; otherwise, .
+ public static bool IsBinarySequence(this long number)
+ => number > 0 && (number & (number - 1)) == 0;
}
\ No newline at end of file
diff --git a/src/Atc/Extensions/BaseTypes/TimeSpanExtensions.cs b/src/Atc/Extensions/BaseTypes/TimeSpanExtensions.cs
index 33f58fbf..a12689ce 100644
--- a/src/Atc/Extensions/BaseTypes/TimeSpanExtensions.cs
+++ b/src/Atc/Extensions/BaseTypes/TimeSpanExtensions.cs
@@ -61,30 +61,72 @@ public static string GetPrettyTime(
{
if ((int)timeSpan.TotalDays > 0)
{
- return $"{timeSpan.TotalDays.ToString("N" + decimalPrecision, Thread.CurrentThread.CurrentUICulture)} " +
- $"{DateAndTime.Days.ToLower(Thread.CurrentThread.CurrentUICulture)}";
+ return $"{timeSpan.TotalDays.ToString("N" + decimalPrecision, CultureInfo.CurrentCulture)} " +
+ $"{DateAndTime.Days.ToLower(CultureInfo.CurrentCulture)}";
}
if ((int)timeSpan.TotalHours > 0)
{
- return $"{timeSpan.TotalHours.ToString("N" + decimalPrecision, Thread.CurrentThread.CurrentUICulture)} " +
- $"{DateAndTime.Hours.ToLower(Thread.CurrentThread.CurrentUICulture)}";
+ return $"{timeSpan.TotalHours.ToString("N" + decimalPrecision, CultureInfo.CurrentCulture)} " +
+ $"{DateAndTime.Hours.ToLower(CultureInfo.CurrentCulture)}";
}
if ((int)timeSpan.TotalMinutes > 0)
{
- return $"{timeSpan.TotalMinutes.ToString("N" + decimalPrecision, Thread.CurrentThread.CurrentUICulture)} " +
- $"{DateAndTime.MinuteAsAbbreviation.ToLower(Thread.CurrentThread.CurrentUICulture)}";
+ return $"{timeSpan.TotalMinutes.ToString("N" + decimalPrecision, CultureInfo.CurrentCulture)} " +
+ $"{DateAndTime.MinuteAsAbbreviation.ToLower(CultureInfo.CurrentCulture)}";
}
// ReSharper disable once ConvertIfStatementToReturnStatement
if ((int)timeSpan.TotalSeconds > 0)
{
- return $"{timeSpan.TotalSeconds.ToString("N" + decimalPrecision, Thread.CurrentThread.CurrentUICulture)} " +
- $"{DateAndTime.SecondAsAbbreviation.ToLower(Thread.CurrentThread.CurrentUICulture)}";
+ return $"{timeSpan.TotalSeconds.ToString("N" + decimalPrecision, CultureInfo.CurrentCulture)} " +
+ $"{DateAndTime.SecondAsAbbreviation.ToLower(CultureInfo.CurrentCulture)}";
}
- return $"{timeSpan.TotalMilliseconds.ToString("N" + decimalPrecision, Thread.CurrentThread.CurrentUICulture)} " +
- $"{DateAndTime.MillisecondAsAbbreviation1.ToLower(Thread.CurrentThread.CurrentUICulture)}";
+ return $"{timeSpan.TotalMilliseconds.ToString("N" + decimalPrecision, CultureInfo.CurrentCulture)} " +
+ $"{DateAndTime.MillisecondAsAbbreviation1.ToLower(CultureInfo.CurrentCulture)}";
+ }
+
+ ///
+ /// Converts a TimeSpan to a human-readable string representation with appropriate time units,
+ /// using the current UI culture for number formatting and unit label casing.
+ /// Use this variant when rendering output for display in a user interface.
+ ///
+ /// The TimeSpan to format.
+ /// The number of decimal places to display (default is 3).
+ /// A formatted string representing the time in the most appropriate unit (days, hours, minutes, seconds, or milliseconds).
+ [SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "OK.")]
+ public static string GetPrettyTimeUi(
+ this TimeSpan timeSpan,
+ int decimalPrecision = 3)
+ {
+ if ((int)timeSpan.TotalDays > 0)
+ {
+ return $"{timeSpan.TotalDays.ToString("N" + decimalPrecision, CultureInfo.CurrentUICulture)} " +
+ $"{DateAndTime.Days.ToLower(CultureInfo.CurrentUICulture)}";
+ }
+
+ if ((int)timeSpan.TotalHours > 0)
+ {
+ return $"{timeSpan.TotalHours.ToString("N" + decimalPrecision, CultureInfo.CurrentUICulture)} " +
+ $"{DateAndTime.Hours.ToLower(CultureInfo.CurrentUICulture)}";
+ }
+
+ if ((int)timeSpan.TotalMinutes > 0)
+ {
+ return $"{timeSpan.TotalMinutes.ToString("N" + decimalPrecision, CultureInfo.CurrentUICulture)} " +
+ $"{DateAndTime.MinuteAsAbbreviation.ToLower(CultureInfo.CurrentUICulture)}";
+ }
+
+ // ReSharper disable once ConvertIfStatementToReturnStatement
+ if ((int)timeSpan.TotalSeconds > 0)
+ {
+ return $"{timeSpan.TotalSeconds.ToString("N" + decimalPrecision, CultureInfo.CurrentUICulture)} " +
+ $"{DateAndTime.SecondAsAbbreviation.ToLower(CultureInfo.CurrentUICulture)}";
+ }
+
+ return $"{timeSpan.TotalMilliseconds.ToString("N" + decimalPrecision, CultureInfo.CurrentUICulture)} " +
+ $"{DateAndTime.MillisecondAsAbbreviation1.ToLower(CultureInfo.CurrentUICulture)}";
}
}
\ No newline at end of file
diff --git a/src/Atc/Extensions/DataTableExtensions.cs b/src/Atc/Extensions/DataTableExtensions.cs
index c2f9473f..1447f783 100644
--- a/src/Atc/Extensions/DataTableExtensions.cs
+++ b/src/Atc/Extensions/DataTableExtensions.cs
@@ -166,7 +166,7 @@ where d is not null
using (var ds = new DataSet("DataSet"))
{
ds.Locale = GlobalizationConstants.EnglishCultureInfo;
- ds.Tables.Add(dataTable);
+ ds.Tables.Add(dataTable.Copy());
xmlDocument.LoadXml(ds.GetXml());
}
diff --git a/src/Atc/Extensions/DictionaryExtensions.cs b/src/Atc/Extensions/DictionaryExtensions.cs
index 2f414813..4e1e5ede 100644
--- a/src/Atc/Extensions/DictionaryExtensions.cs
+++ b/src/Atc/Extensions/DictionaryExtensions.cs
@@ -112,5 +112,132 @@ public static bool TryUpdate(
dictionaryValue = valueFactory(key, dictionaryValue);
return true;
}
+#else
+ ///
+ /// Retrieves the value associated with the specified key or adds a new value if the key does not exist.
+ ///
+ /// The type of the dictionary keys.
+ /// The type of the dictionary values.
+ /// The dictionary instance.
+ /// The key whose value to get or add.
+ /// The value to add if the key does not exist.
+ /// The existing or newly added value.
+ public static TValue GetOrAdd(
+ this Dictionary dict,
+ TKey key,
+ TValue value)
+ where TKey : notnull
+ {
+ if (dict is null)
+ {
+ throw new ArgumentNullException(nameof(dict));
+ }
+
+ if (dict.TryGetValue(key, out var existing))
+ {
+ return existing;
+ }
+
+ dict.Add(key, value);
+ return value;
+ }
+
+ ///
+ /// Retrieves the value associated with the specified key or adds a new value generated by the provided factory function if the key does not exist.
+ ///
+ /// The type of the dictionary keys.
+ /// The type of the dictionary values.
+ /// The dictionary instance.
+ /// The key whose value to get or add.
+ /// A function to generate the value if the key does not exist.
+ /// The existing or newly added value.
+ public static TValue GetOrAdd(
+ this Dictionary dict,
+ TKey key,
+ Func valueFactory)
+ where TKey : notnull
+ {
+ if (dict is null)
+ {
+ throw new ArgumentNullException(nameof(dict));
+ }
+
+ if (valueFactory is null)
+ {
+ throw new ArgumentNullException(nameof(valueFactory));
+ }
+
+ if (dict.TryGetValue(key, out var existing))
+ {
+ return existing;
+ }
+
+ var newValue = valueFactory(key);
+ dict.Add(key, newValue);
+ return newValue;
+ }
+
+ ///
+ /// Attempts to update the value of an existing key in the dictionary.
+ ///
+ /// The type of the dictionary keys.
+ /// The type of the dictionary values.
+ /// The dictionary instance.
+ /// The key whose value should be updated.
+ /// The new value to assign.
+ /// true if the key exists and the value was updated; otherwise, false .
+ public static bool TryUpdate(
+ this Dictionary dict,
+ TKey key,
+ TValue value)
+ where TKey : notnull
+ {
+ if (dict is null)
+ {
+ throw new ArgumentNullException(nameof(dict));
+ }
+
+ if (!dict.ContainsKey(key))
+ {
+ return false;
+ }
+
+ dict[key] = value;
+ return true;
+ }
+
+ ///
+ /// Attempts to update the value of an existing key in the dictionary using a factory function.
+ ///
+ /// The type of the dictionary keys.
+ /// The type of the dictionary values.
+ /// The dictionary instance.
+ /// The key whose value should be updated.
+ /// A function to generate the new value based on the existing value.
+ /// true if the key exists and the value was updated; otherwise, false .
+ public static bool TryUpdate(
+ this Dictionary dict,
+ TKey key,
+ Func valueFactory)
+ where TKey : notnull
+ {
+ if (dict is null)
+ {
+ throw new ArgumentNullException(nameof(dict));
+ }
+
+ if (valueFactory is null)
+ {
+ throw new ArgumentNullException(nameof(valueFactory));
+ }
+
+ if (!dict.TryGetValue(key, out var existing))
+ {
+ return false;
+ }
+
+ dict[key] = valueFactory(key, existing);
+ return true;
+ }
#endif
}
\ No newline at end of file
diff --git a/src/Atc/Extensions/EnumExtensions.cs b/src/Atc/Extensions/EnumExtensions.cs
index 8b2a0310..8ee3ca10 100644
--- a/src/Atc/Extensions/EnumExtensions.cs
+++ b/src/Atc/Extensions/EnumExtensions.cs
@@ -25,10 +25,14 @@ public static bool AreFlagsSet(
Enum flags)
=> IsSet(enumeration, flags);
- /// Determines whether the specified enumeration match another enumeration.
- /// The enumeration.
- /// The enumeration to match.
- /// true on match; otherwise false.
+ ///
+ /// Determines whether all bits of are set in ,
+ /// equivalent to enumeration.HasFlag(matchTo) . Works for all underlying numeric types including
+ /// -backed enumerations.
+ ///
+ /// The enumeration to check.
+ /// The flags that must all be present in .
+ /// if every bit in is also set in ; otherwise, .
///
///
@@ -195,6 +199,33 @@ public static TTarget MapTo(
return defaultValue ?? throw new InvalidOperationException($"Cannot map '{source}' from {source.GetType().Name} to {typeof(TTarget).Name}.");
}
+ ///
+ /// Tries to map the current enum value to a target enum type by matching the name (case-insensitive).
+ /// Unlike , this method never throws — it returns when no match is found.
+ ///
+ /// The target enum type.
+ /// The source enum value.
+ ///
+ /// When this method returns , the matched target value; otherwise, the default value of .
+ ///
+ /// if a matching named value was found in ; otherwise, .
+ public static bool TryMapTo(
+ this Enum source,
+ out TTarget result)
+ where TTarget : struct, Enum
+ {
+ ArgumentNullException.ThrowIfNull(source);
+
+ if (Enum.TryParse(source.ToString(), ignoreCase: true, out result) &&
+ Enum.IsDefined(typeof(TTarget), result))
+ {
+ return true;
+ }
+
+ result = default;
+ return false;
+ }
+
/// Gets the attribute value.
/// The type.
/// The type of the expected.
diff --git a/src/Atc/Extensions/ExceptionExtensions.cs b/src/Atc/Extensions/ExceptionExtensions.cs
index b336b84f..76df4065 100644
--- a/src/Atc/Extensions/ExceptionExtensions.cs
+++ b/src/Atc/Extensions/ExceptionExtensions.cs
@@ -86,16 +86,12 @@ public static string Flatten(
while (currentException is not null)
{
sb.AppendLine(currentException.Message);
- if (includeStackTrace && exception.StackTrace is not null)
+ if (includeStackTrace && currentException.StackTrace is not null)
{
- sb.Append(exception.StackTrace);
+ sb.AppendLine(currentException.StackTrace);
}
currentException = currentException.InnerException;
- if (includeStackTrace && exception.StackTrace is not null)
- {
- sb.AppendLine();
- }
}
return sb.ToString();
@@ -128,7 +124,11 @@ public static XDocument ToXml(this Exception exception)
{
var xElements = from frame
in exception.StackTrace.Split('\n')
- let prettierFrame = frame[6..].Trim()
+ let trimmedFrame = frame.Trim()
+ where trimmedFrame.Length > 0
+ let prettierFrame = trimmedFrame.StartsWith("at ", StringComparison.Ordinal)
+ ? trimmedFrame[3..].Trim()
+ : trimmedFrame
select new XElement("Frame", prettierFrame);
root.Add(new XElement("StackTrace", xElements));
}
diff --git a/src/Atc/Extensions/LoggerExtensions.cs b/src/Atc/Extensions/LoggerExtensions.cs
index ffe9e9e4..4b709e32 100644
--- a/src/Atc/Extensions/LoggerExtensions.cs
+++ b/src/Atc/Extensions/LoggerExtensions.cs
@@ -29,26 +29,26 @@ public static void LogKeyValueItem(
switch (logKeyValueItem.LogCategory)
{
case LogCategoryType.Critical:
- logger.LogCritical(message);
+ logger.LogCritical("{Message}", message);
break;
case LogCategoryType.Error:
- logger.LogError(message);
+ logger.LogError("{Message}", message);
break;
case LogCategoryType.Warning:
- logger.LogWarning(message);
+ logger.LogWarning("{Message}", message);
break;
case LogCategoryType.Security:
case LogCategoryType.Audit:
case LogCategoryType.Service:
case LogCategoryType.UI:
case LogCategoryType.Information:
- logger.LogInformation(message);
+ logger.LogInformation("{Message}", message);
break;
case LogCategoryType.Debug:
- logger.LogDebug(message);
+ logger.LogDebug("{Message}", message);
break;
case LogCategoryType.Trace:
- logger.LogTrace(message);
+ logger.LogTrace("{Message}", message);
break;
default:
throw new SwitchCaseDefaultException(logKeyValueItem.LogCategory);
diff --git a/src/Atc/Extensions/MemoryStreamExtensions.cs b/src/Atc/Extensions/MemoryStreamExtensions.cs
index 65122d69..1d8bb55c 100644
--- a/src/Atc/Extensions/MemoryStreamExtensions.cs
+++ b/src/Atc/Extensions/MemoryStreamExtensions.cs
@@ -10,7 +10,7 @@ public static class MemoryStreamExtensions
/// Converts the memory stream content to a string using the specified encoding.
///
/// The memory stream to convert.
- /// The encoding to use for the conversion. If , Unicode encoding is used.
+ /// The encoding to use for the conversion. Defaults to when .
/// A string representation of the memory stream content.
/// Thrown when is .
public static string ToString(
@@ -22,7 +22,7 @@ public static string ToString(
throw new ArgumentNullException(nameof(stream));
}
- encoding ??= Encoding.Unicode;
+ encoding ??= Encoding.UTF8;
return encoding.GetString(stream.ToArray());
}
diff --git a/src/Atc/Extensions/ProcessExtensions.cs b/src/Atc/Extensions/ProcessExtensions.cs
index a2beb924..499fb8c4 100644
--- a/src/Atc/Extensions/ProcessExtensions.cs
+++ b/src/Atc/Extensions/ProcessExtensions.cs
@@ -11,8 +11,7 @@ public static class ProcessExtensions
{
private static readonly TimeSpan DefaultKillTimeout = TimeSpan.FromSeconds(30);
- [SuppressMessage("Major Code Smell", "S4457:Parameter validation in \"async\"/\"await\" methods should be wrapped", Justification = "OK.")]
- public static async Task WaitForExitAsync(
+ public static Task WaitForExitAsync(
this Process process,
CancellationToken cancellationToken = default)
{
@@ -21,6 +20,18 @@ public static async Task WaitForExitAsync(
throw new ArgumentNullException(nameof(process));
}
+#if NET5_0_OR_GREATER
+ return process.WaitForExitAsync(cancellationToken);
+#else
+ return WaitForExitAsyncCore(process, cancellationToken);
+#endif
+ }
+
+#if !NET5_0_OR_GREATER
+ private static async Task WaitForExitAsyncCore(
+ Process process,
+ CancellationToken cancellationToken)
+ {
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
void ProcessExited(
@@ -54,6 +65,7 @@ void ProcessExited(
process.Exited -= ProcessExited;
}
}
+#endif
///
/// Synchronously terminates the process and any child processes it started, using a default
@@ -204,18 +216,15 @@ private static (int ExitCode, string Output) RunProcessAndReadOutput(
return (-1, string.Empty);
}
+ var outputTask = Task.Run(() => process.StandardOutput.ReadToEnd());
if (process.WaitForExit((int)timeout.TotalMilliseconds))
{
- return (
- process.ExitCode,
- process.StandardOutput.ReadToEnd());
+ return (process.ExitCode, outputTask.GetAwaiter().GetResult());
}
process.Kill();
- return (
- process.ExitCode,
- string.Empty);
+ return (process.ExitCode, string.Empty);
}
private static void RunProcessAndIgnoreOutput(
diff --git a/src/Atc/Extensions/ReadOnlyListExtensions.cs b/src/Atc/Extensions/ReadOnlyListExtensions.cs
index 3b663254..8f07e176 100644
--- a/src/Atc/Extensions/ReadOnlyListExtensions.cs
+++ b/src/Atc/Extensions/ReadOnlyListExtensions.cs
@@ -12,16 +12,7 @@ public static IEnumerable> GetUniqueCombinations(
throw new ArgumentNullException(nameof(list));
}
- var result = new List>();
- var uniqueCombinations = GetUniqueCombinationsAsCommaSeparated(list);
- foreach (var uniqueCombination in uniqueCombinations)
- {
- var combinations = new List();
- combinations.AddRange(uniqueCombination.Split(','));
- result.Add(combinations);
- }
-
- return result;
+ return GetPowerSet(list).Where(subset => subset.Any());
}
public static IEnumerable GetUniqueCombinationsAsCommaSeparated(
@@ -46,6 +37,11 @@ public static IEnumerable> GetPowerSet(
throw new ArgumentNullException(nameof(list));
}
+ if (list.Count >= 31)
+ {
+ throw new ArgumentOutOfRangeException(nameof(list), $"Cannot compute a power set for a list with {list.Count} elements; the bitmask overflows int at 31.");
+ }
+
return from m in Enumerable.Range(0, 1 << list.Count)
select
from i in Enumerable.Range(0, list.Count)
diff --git a/src/Atc/Extensions/Reflection/AssemblyExtensions.cs b/src/Atc/Extensions/Reflection/AssemblyExtensions.cs
index b6c50c46..6a858135 100644
--- a/src/Atc/Extensions/Reflection/AssemblyExtensions.cs
+++ b/src/Atc/Extensions/Reflection/AssemblyExtensions.cs
@@ -9,6 +9,8 @@ public static class AssemblyExtensions
{
///
/// Gets the file version of the assembly.
+ /// Falls back to when the assembly location is
+ /// unavailable (e.g., single-file published apps where is empty).
///
/// The assembly to query.
/// The file version, or 1.0.0.0 if the version cannot be determined.
@@ -20,15 +22,23 @@ public static Version GetFileVersion(this Assembly assembly)
throw new ArgumentNullException(nameof(assembly));
}
- var fileVersion = FileVersionInfo.GetVersionInfo(assembly.Location).FileVersion;
- if (fileVersion is null)
+ var location = assembly.Location;
+ if (!string.IsNullOrEmpty(location))
{
- return new Version(1, 0, 0, 0);
+ var fileVersion = FileVersionInfo.GetVersionInfo(location).FileVersion;
+ if (fileVersion is not null && Version.TryParse(fileVersion, out var fvVersion))
+ {
+ return fvVersion;
+ }
+ }
+
+ var attr = assembly.GetCustomAttribute();
+ if (attr is not null && Version.TryParse(attr.Version, out var attrVersion))
+ {
+ return attrVersion;
}
- return Version.TryParse(fileVersion, out var version)
- ? version
- : new Version(1, 0, 0, 0);
+ return new Version(1, 0, 0, 0);
}
///
@@ -46,7 +56,7 @@ public static bool IsDebugBuild(this Assembly assembly)
return assembly
.GetCustomAttributes(false)
.OfType()
- .Select(att => att.IsJITTrackingEnabled)
+ .Select(att => att.IsJITOptimizerDisabled)
.FirstOrDefault();
}
diff --git a/src/Atc/Extensions/Reflection/ConstructorInfoExtensions.cs b/src/Atc/Extensions/Reflection/ConstructorInfoExtensions.cs
new file mode 100644
index 00000000..10f0c1b3
--- /dev/null
+++ b/src/Atc/Extensions/Reflection/ConstructorInfoExtensions.cs
@@ -0,0 +1,53 @@
+// ReSharper disable once CheckNamespace
+namespace System.Reflection;
+
+///
+/// Extensions for the class.
+///
+public static class ConstructorInfoExtensions
+{
+ ///
+ /// Returns a human-readable representation of the constructor signature, optionally using full type names
+ /// and HTML formatting for the parameter types.
+ ///
+ /// The constructor information.
+ /// If , parameter types are rendered with their fully-qualified names.
+ /// If , parameter type names are wrapped in HTML tags.
+ /// A string of the form .ctor(TypeA paramA, TypeB paramB) .
+ /// Thrown when is null.
+ public static string BeautifyName(
+ this ConstructorInfo constructorInfo,
+ bool useFullName = false,
+ bool useHtmlFormat = false)
+ {
+ ArgumentNullException.ThrowIfNull(constructorInfo);
+
+ var seq = constructorInfo
+ .GetParameters()
+ .Select(x =>
+ {
+ var typeName = x.ParameterType.BeautifyName(useFullName, useHtmlFormat);
+
+ // ReSharper disable once InvertIf
+ if (x.ParameterType.Name.EndsWith('&'))
+ {
+ if (x.IsIn)
+ {
+ typeName = "in " + typeName.Replace("&", string.Empty, StringComparison.Ordinal);
+ }
+ else if (x.IsOut)
+ {
+ typeName = "out " + typeName.Replace("&", string.Empty, StringComparison.Ordinal);
+ }
+ else
+ {
+ typeName = "ref " + typeName.Replace("&", string.Empty, StringComparison.Ordinal);
+ }
+ }
+
+ return typeName + " " + x.Name;
+ });
+
+ return $".ctor({string.Join(", ", seq)})";
+ }
+}
\ No newline at end of file
diff --git a/src/Atc/Extensions/Reflection/EventInfoExtensions.cs b/src/Atc/Extensions/Reflection/EventInfoExtensions.cs
new file mode 100644
index 00000000..93655ad5
--- /dev/null
+++ b/src/Atc/Extensions/Reflection/EventInfoExtensions.cs
@@ -0,0 +1,31 @@
+// ReSharper disable once CheckNamespace
+namespace System.Reflection;
+
+///
+/// Extensions for the class.
+///
+public static class EventInfoExtensions
+{
+ ///
+ /// Returns a human-readable representation of the event, optionally including the event handler type
+ /// and using full type names or HTML formatting.
+ ///
+ /// The event information.
+ /// If , the event-handler type is rendered with its fully-qualified name.
+ /// If , the event-handler type name is wrapped in HTML tags.
+ /// If , the event-handler type is prepended to the name.
+ /// A string such as MyEvent or EventHandler MyEvent when is .
+ /// Thrown when is null.
+ public static string BeautifyName(
+ this EventInfo eventInfo,
+ bool useFullName = false,
+ bool useHtmlFormat = false,
+ bool includeEventHandlerType = false)
+ {
+ ArgumentNullException.ThrowIfNull(eventInfo);
+
+ return includeEventHandlerType && eventInfo.EventHandlerType is not null
+ ? $"{eventInfo.EventHandlerType.BeautifyName(useFullName, useHtmlFormat)} {eventInfo.Name}"
+ : eventInfo.Name;
+ }
+}
\ No newline at end of file
diff --git a/src/Atc/Extensions/Reflection/MemberInfoExtensions.cs b/src/Atc/Extensions/Reflection/MemberInfoExtensions.cs
index cc0e7c9b..47c9b501 100644
--- a/src/Atc/Extensions/Reflection/MemberInfoExtensions.cs
+++ b/src/Atc/Extensions/Reflection/MemberInfoExtensions.cs
@@ -15,9 +15,10 @@ public static class MemberInfoExtensions
[SuppressMessage("", "CA2263:Prefer the generic overload", Justification = "OK")]
public static bool AnyCustomAttributes(this MemberInfo element)
where T : Attribute
- => element
- .GetCustomAttributes(typeof(T))
- .Any();
+ {
+ ArgumentNullException.ThrowIfNull(element);
+ return element.IsDefined(typeof(T), inherit: false);
+ }
///
/// Determines whether [has exclude from code coverage attribute].
@@ -35,8 +36,7 @@ public static bool HasExcludeFromCodeCoverageAttribute(
throw new ArgumentNullException(nameof(memberInfo));
}
- var attributeData = memberInfo.CustomAttributes.FirstOrDefault(x => x.AttributeType == typeof(ExcludeFromCodeCoverageAttribute));
- return attributeData is not null;
+ return memberInfo.IsDefined(typeof(ExcludeFromCodeCoverageAttribute), inherit: false);
}
///
@@ -52,8 +52,7 @@ public static bool HasCompilerGeneratedAttribute(this MemberInfo memberInfo)
throw new ArgumentNullException(nameof(memberInfo));
}
- var attributeData = memberInfo.CustomAttributes.FirstOrDefault(x => x.AttributeType == typeof(CompilerGeneratedAttribute));
- return attributeData is not null;
+ return memberInfo.IsDefined(typeof(CompilerGeneratedAttribute), inherit: false);
}
///
@@ -71,8 +70,7 @@ public static bool HasIgnoreDisplayAttribute(this MemberInfo memberInfo)
throw new ArgumentNullException(nameof(memberInfo));
}
- var attributeData = memberInfo.CustomAttributes.FirstOrDefault(x => x.AttributeType == typeof(IgnoreDisplayAttribute));
- return attributeData is not null;
+ return memberInfo.IsDefined(typeof(IgnoreDisplayAttribute), inherit: false);
}
///
@@ -90,8 +88,7 @@ public static bool HasRequiredAttribute(this MemberInfo memberInfo)
throw new ArgumentNullException(nameof(memberInfo));
}
- var attributeData = memberInfo.CustomAttributes.FirstOrDefault(x => x.AttributeType == typeof(RequiredAttribute));
- return attributeData is not null;
+ return memberInfo.IsDefined(typeof(RequiredAttribute), inherit: false);
}
///
diff --git a/src/Atc/Extensions/StreamExtensions.cs b/src/Atc/Extensions/StreamExtensions.cs
index a3909d84..f9bacb02 100644
--- a/src/Atc/Extensions/StreamExtensions.cs
+++ b/src/Atc/Extensions/StreamExtensions.cs
@@ -22,15 +22,13 @@ public static Stream CopyToStream(
throw new ArgumentNullException(nameof(stream));
}
- stream.Position = 0;
- var buffer = new byte[bufferSize];
- int nRead;
- var destination = new MemoryStream();
- while ((nRead = stream.Read(buffer, 0, bufferSize)) > 0)
+ if (stream.CanSeek)
{
- destination.Write(buffer, 0, nRead);
+ stream.Position = 0;
}
+ var destination = new MemoryStream();
+ stream.CopyTo(destination, bufferSize);
destination.Position = 0;
return destination;
}
@@ -48,19 +46,14 @@ public static byte[] ToBytes(this Stream stream)
throw new ArgumentNullException(nameof(stream));
}
- stream.Position = 0;
- var buffer = new byte[32768];
- using var ms = new MemoryStream();
- while (true)
+ if (stream.CanSeek)
{
- var read = stream.Read(buffer, 0, buffer.Length);
- if (read <= 0)
- {
- return ms.ToArray();
- }
-
- ms.Write(buffer, 0, read);
+ stream.Position = 0;
}
+
+ using var ms = new MemoryStream();
+ stream.CopyTo(ms);
+ return ms.ToArray();
}
///
@@ -76,9 +69,97 @@ public static string ToStringData(this Stream stream)
throw new ArgumentNullException(nameof(stream));
}
- stream.Position = 0;
- using var reader = new StreamReader(stream);
- var val = reader.ReadToEnd();
- return val;
+ if (stream.CanSeek)
+ {
+ stream.Position = 0;
+ }
+
+ using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: -1, leaveOpen: true);
+ return reader.ReadToEnd();
+ }
+
+ ///
+ /// Asynchronously copies the contents of the stream to a new .
+ ///
+ /// The source stream to copy from. The stream position will be reset to 0 before copying if the stream supports seeking.
+ /// The size of the buffer used for copying. Defaults to 4096 bytes.
+ /// A token to cancel the asynchronous operation.
+ /// A that represents the asynchronous operation, containing a new with position set to 0.
+ /// Thrown when is .
+ public static async Task CopyToStreamAsync(
+ this Stream stream,
+ int bufferSize = 4096,
+ CancellationToken cancellationToken = default)
+ {
+ if (stream is null)
+ {
+ throw new ArgumentNullException(nameof(stream));
+ }
+
+ if (stream.CanSeek)
+ {
+ stream.Position = 0;
+ }
+
+ var destination = new MemoryStream();
+ await stream.CopyToAsync(destination, bufferSize, cancellationToken).ConfigureAwait(false);
+ destination.Position = 0;
+ return destination;
+ }
+
+ ///
+ /// Asynchronously reads all bytes from the stream and returns them as a byte array.
+ ///
+ /// The stream to read from. The stream position will be reset to 0 before reading if the stream supports seeking.
+ /// A token to cancel the asynchronous operation.
+ /// A that represents the asynchronous operation, containing a byte array with all data from the stream.
+ /// Thrown when is .
+ public static async Task ToBytesAsync(
+ this Stream stream,
+ CancellationToken cancellationToken = default)
+ {
+ if (stream is null)
+ {
+ throw new ArgumentNullException(nameof(stream));
+ }
+
+ if (stream.CanSeek)
+ {
+ stream.Position = 0;
+ }
+
+ using var ms = new MemoryStream();
+ await stream.CopyToAsync(ms, 81920, cancellationToken).ConfigureAwait(false);
+ return ms.ToArray();
+ }
+
+ ///
+ /// Asynchronously reads all content from the stream and converts it to a string using UTF-8 encoding.
+ ///
+ /// The stream to read from. The stream position will be reset to 0 before reading if the stream supports seeking.
+ /// A token to cancel the asynchronous operation.
+ /// A that represents the asynchronous operation, containing a string with all text content from the stream.
+ /// Thrown when is .
+ public static async Task ToStringDataAsync(
+ this Stream stream,
+ CancellationToken cancellationToken = default)
+ {
+ if (stream is null)
+ {
+ throw new ArgumentNullException(nameof(stream));
+ }
+
+ if (stream.CanSeek)
+ {
+ stream.Position = 0;
+ }
+
+ using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: -1, leaveOpen: true);
+#if NET7_0_OR_GREATER
+ return await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
+#else
+ cancellationToken.ThrowIfCancellationRequested();
+ return await reader.ReadToEndAsync().ConfigureAwait(false);
+#endif
}
}
\ No newline at end of file
diff --git a/src/Atc/Extensions/StringBuilderExtensions.cs b/src/Atc/Extensions/StringBuilderExtensions.cs
index 510f7bac..e8c11313 100644
--- a/src/Atc/Extensions/StringBuilderExtensions.cs
+++ b/src/Atc/Extensions/StringBuilderExtensions.cs
@@ -53,7 +53,7 @@ public static void Append(
if (indentSpaces < 0)
{
- throw new ArgumentOutOfRangeException(nameof(value));
+ throw new ArgumentOutOfRangeException(nameof(indentSpaces));
}
if (value is null)
@@ -61,7 +61,8 @@ public static void Append(
throw new ArgumentNullException(nameof(value));
}
- sb.Append(value.PadLeft(value.Length + indentSpaces));
+ sb.Append(' ', indentSpaces);
+ sb.Append(value);
}
///
@@ -111,7 +112,7 @@ public static void AppendLine(
if (indentSpaces < 0)
{
- throw new ArgumentOutOfRangeException(nameof(value));
+ throw new ArgumentOutOfRangeException(nameof(indentSpaces));
}
if (value is null)
@@ -119,7 +120,8 @@ public static void AppendLine(
throw new ArgumentNullException(nameof(value));
}
- sb.AppendLine(value.PadLeft(value.Length + indentSpaces));
+ sb.Append(' ', indentSpaces);
+ sb.AppendLine(value);
}
///
diff --git a/src/Atc/Extensions/StringExtensions.cs b/src/Atc/Extensions/StringExtensions.cs
index 7e045a15..edb2a711 100644
--- a/src/Atc/Extensions/StringExtensions.cs
+++ b/src/Atc/Extensions/StringExtensions.cs
@@ -938,27 +938,31 @@ public static string XmlEncode(this string xml)
}
return xml
- .Replace("&", "&", StringComparison.Ordinal)
+ .Replace("&", "&", StringComparison.Ordinal)
.Replace("'", "'", StringComparison.Ordinal)
.Replace("<", "<", StringComparison.Ordinal)
.Replace(">", ">", StringComparison.Ordinal)
- .Replace("\"", """, StringComparison.Ordinal);
+ .Replace("\"", """, StringComparison.Ordinal);
}
///
- /// Decodes an XML string by unescaping special character entities (&, ', <, >, ").
+ /// Decodes an XML string by unescaping special character entities (&, ', <, >, ").
///
/// The XML string to decode.
/// The decoded XML string with special character entities replaced.
+ ///
+ /// The ampersand entity (& ) is decoded last so that already-decoded content
+ /// is not re-interpreted, keeping / a faithful round-trip.
+ ///
public static string XmlDecode(this string xml)
=> string.IsNullOrEmpty(xml)
? xml
: xml
- .Replace("&", "&", StringComparison.Ordinal)
.Replace("'", "'", StringComparison.Ordinal)
.Replace("<", "<", StringComparison.Ordinal)
.Replace(">", ">", StringComparison.Ordinal)
- .Replace(""", "\"", StringComparison.Ordinal);
+ .Replace(""", "\"", StringComparison.Ordinal)
+ .Replace("&", "&", StringComparison.Ordinal);
///
/// Sorts letters in the string alphabetically.
@@ -2276,13 +2280,13 @@ private static string NormalizeAccentsHelper(
case LetterAccentType.Grave:
value = value
.Replace("à", "a", StringComparison.Ordinal)
- .Replace("è ", "e", StringComparison.Ordinal)
- .Replace("ì ", "i", StringComparison.Ordinal)
+ .Replace("è", "e", StringComparison.Ordinal)
+ .Replace("ì", "i", StringComparison.Ordinal)
.Replace("ò", "o", StringComparison.Ordinal)
.Replace("ù", "u", StringComparison.Ordinal)
.Replace("à", "a", StringComparison.Ordinal)
- .Replace("è ", "e", StringComparison.Ordinal)
- .Replace("ì ", "i", StringComparison.Ordinal)
+ .Replace("è", "e", StringComparison.Ordinal)
+ .Replace("ì", "i", StringComparison.Ordinal)
.Replace("ò", "o", StringComparison.Ordinal)
.Replace("ù", "u", StringComparison.Ordinal);
break;
@@ -2402,7 +2406,7 @@ private static string NormalizeAccentsHelper(
.Replace("Ä", "A", StringComparison.Ordinal)
.Replace("Ë", "E", StringComparison.Ordinal)
.Replace("Ï", "I", StringComparison.Ordinal)
- .Replace("Ö,", "O", StringComparison.Ordinal)
+ .Replace("Ö", "O", StringComparison.Ordinal)
.Replace("Ü", "U", StringComparison.Ordinal)
.Replace("", "Y", StringComparison.Ordinal);
break;
diff --git a/src/Atc/Extensions/StringHasIsExtensions.cs b/src/Atc/Extensions/StringHasIsExtensions.cs
index 4fc1cf2d..ffb86e51 100644
--- a/src/Atc/Extensions/StringHasIsExtensions.cs
+++ b/src/Atc/Extensions/StringHasIsExtensions.cs
@@ -17,11 +17,13 @@ public static class StringHasIsExtensions
private static readonly Lazy RxNumeric = new(() => new Regex("[^0-9]", RegexOptions.Singleline | RegexOptions.Compiled, TimeSpan.FromSeconds(1)));
#endif
- private static readonly Lazy RxEmailAddress = new(() => new Regex(@"^(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9]+@((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,6}$", RegexOptions.Singleline | RegexOptions.Compiled, TimeSpan.FromSeconds(1)));
+ private static readonly Lazy RxEmailAddress = new(() => new Regex(@"^(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9]+@((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,63}$", RegexOptions.Singleline | RegexOptions.Compiled, TimeSpan.FromSeconds(1)));
+ private static readonly Lazy RxMacAddress = new(() => new Regex(@"^(?:(?:[0-9A-Fa-f]{2}[:\-]){5}[0-9A-Fa-f]{2}|[0-9A-Fa-f]{12}|[0-9A-Fa-f]{4}\.[0-9A-Fa-f]{4}\.[0-9A-Fa-f]{4})$", RegexOptions.Singleline | RegexOptions.Compiled, TimeSpan.FromSeconds(1)));
private static readonly Lazy RxGuid = new(() => new Regex(@"^(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}$", RegexOptions.Singleline | RegexOptions.Compiled, TimeSpan.FromSeconds(1)));
private static readonly Lazy RxKey = new(() => new Regex(@"^([a-zA-Z]+[a-zA-Z0-9_]+$)", RegexOptions.Singleline | RegexOptions.Compiled, TimeSpan.FromSeconds(1)));
private static readonly Lazy RxHtmlTags = new(() => new Regex(@"<[^>]+>", RegexOptions.Multiline | RegexOptions.Compiled, TimeSpan.FromSeconds(5)));
private static readonly Lazy RxSingleWord = new(() => new Regex(@"^((?!-)+)([a-zA-Z_-]+$).*((? RxHostName = new(() => new Regex(@"^(?=.{1,253}\.?$)(?!-)[A-Za-z0-9-]{1,63}(?
/// Determines whether [has HTML tags] [the specified value].
@@ -586,4 +588,78 @@ public static bool IsUriHttpOrHttps(this string value)
/// if the value is a valid opc.tcp:// URI; otherwise, .
public static bool IsUriOpcTcp(this string value)
=> UriAttribute.IsValidOpcTcp(value);
+
+ ///
+ /// Determines whether the specified value is a syntactically valid DNS host name (RFC 1123).
+ ///
+ /// The string to validate.
+ /// if the value is a valid host name; otherwise, .
+ ///
+ /// Accepts single-label names (e.g. localhost ) and an optional trailing dot (e.g. example.com. ).
+ /// Each label is 1-63 ASCII alphanumeric/hyphen characters and may not start or end with a hyphen;
+ /// the total length is limited to 253 characters. Underscores and raw Unicode (non-punycode IDN) are not allowed.
+ /// This is a purely syntactic check and does not perform any DNS resolution.
+ ///
+ public static bool IsHostName(this string value)
+ => !string.IsNullOrEmpty(value) &&
+ RxHostName.Value.IsMatch(value);
+
+ ///
+ /// Determines whether the specified value is a valid IPv4 address.
+ ///
+ /// The string to validate.
+ /// if the value is a valid IPv4 address; otherwise, .
+ public static bool IsIPv4Address(this string value)
+ => !string.IsNullOrEmpty(value) &&
+ IPAddress.TryParse(value, out var address) &&
+ address.AddressFamily == AddressFamily.InterNetwork &&
+ value.Split('.').Length == 4;
+
+ ///
+ /// Determines whether the specified value is a valid IPv6 address.
+ ///
+ /// The string to validate.
+ /// if the value is a valid IPv6 address; otherwise, .
+ public static bool IsIPv6Address(this string value)
+ => !string.IsNullOrEmpty(value) &&
+ IPAddress.TryParse(value, out var address) &&
+ address.AddressFamily == AddressFamily.InterNetworkV6;
+
+ ///
+ /// Determines whether the specified value is a valid IPv4 or IPv6 address.
+ ///
+ /// The string to validate.
+ /// if the value is a valid IP address; otherwise, .
+ public static bool IsIPAddress(this string value)
+ => value.IsIPv4Address() ||
+ value.IsIPv6Address();
+
+ ///
+ /// Determines whether the specified value is a valid TCP/UDP port number.
+ ///
+ /// The string to validate.
+ ///
+ /// if the value parses to an integer in the range 1–65535; otherwise, .
+ ///
+ public static bool IsPort(this string value)
+ => !string.IsNullOrEmpty(value) &&
+ int.TryParse(value, NumberStyles.None, GlobalizationConstants.EnglishCultureInfo, out var port) &&
+ port is >= 1 and <= 65535;
+
+ ///
+ /// Determines whether the specified value is a valid MAC address.
+ ///
+ /// The string to validate.
+ ///
+ /// if the value is a valid 48-bit MAC address; otherwise, .
+ ///
+ ///
+ /// Accepts the three most common notations: colon-separated (AA:BB:CC:DD:EE:FF ),
+ /// hyphen-separated (AA-BB-CC-DD-EE-FF ), and dot-separated Cisco style (AABB.CCDD.EEFF ),
+ /// as well as the compact twelve-hex-digit form (AABBCCDDEEFF ).
+ /// The check is case-insensitive.
+ ///
+ public static bool IsMacAddress(this string value)
+ => !string.IsNullOrEmpty(value) &&
+ RxMacAddress.Value.IsMatch(value);
}
\ No newline at end of file
diff --git a/src/Atc/Extensions/TaskExtensions.cs b/src/Atc/Extensions/TaskExtensions.cs
index c4cd2d86..0b5e0ec8 100644
--- a/src/Atc/Extensions/TaskExtensions.cs
+++ b/src/Atc/Extensions/TaskExtensions.cs
@@ -65,7 +65,10 @@ public static void StartAndWaitAllThrottled(
foreach (var task in tasks)
{
// Increment the number of tasks currently running and wait if too many are running.
- throttler.Wait(timeoutInMilliseconds, cancellationToken);
+ if (!throttler.Wait(timeoutInMilliseconds, cancellationToken))
+ {
+ throw new TimeoutException($"Timed out after {timeoutInMilliseconds} ms waiting for a task slot to become available.");
+ }
cancellationToken.ThrowIfCancellationRequested();
task.Start();
diff --git a/src/Atc/Extensions/TypeExtensions.cs b/src/Atc/Extensions/TypeExtensions.cs
index d3fea70a..ea6e0513 100644
--- a/src/Atc/Extensions/TypeExtensions.cs
+++ b/src/Atc/Extensions/TypeExtensions.cs
@@ -156,7 +156,7 @@ public static bool IsInheritedFrom(
inheritTypeFullName = inheritTypeFullName.Substring(0, inheritTypeFullName.IndexOf(GenericSign, StringComparison.Ordinal));
}
- return string.Equals(baseTypeFullName, inheritTypeFullName, StringComparison.OrdinalIgnoreCase) || type.BaseType.IsInheritedFrom(inheritType);
+ return string.Equals(baseTypeFullName, inheritTypeFullName, StringComparison.Ordinal) || type.BaseType.IsInheritedFrom(inheritType);
}
///
@@ -572,7 +572,7 @@ public static string BeautifyName(
.ToArray();
for (var i = 0; i < sa.Length; i++)
{
- sa[0] = "T";
+ sa[i] = "T";
}
genericArguments = string.Join(", ", sa);
diff --git a/src/Atc/Factories/AsyncEnumerableFactory.cs b/src/Atc/Factories/AsyncEnumerableFactory.cs
index 9b11e788..456b08d1 100644
--- a/src/Atc/Factories/AsyncEnumerableFactory.cs
+++ b/src/Atc/Factories/AsyncEnumerableFactory.cs
@@ -10,10 +10,18 @@ public static class AsyncEnumerableFactory
///
/// The type of the elements in the sequence.
/// An empty .
- public static async IAsyncEnumerable Empty()
+ public static IAsyncEnumerable Empty()
+ => EmptyAsyncEnumerable.Instance;
+
+ private static class EmptyAsyncEnumerable
{
- await Task.CompletedTask;
- yield break;
+ internal static readonly IAsyncEnumerable Instance = CreateEmpty();
+
+ private static async IAsyncEnumerable CreateEmpty()
+ {
+ await Task.CompletedTask.ConfigureAwait(false);
+ yield break;
+ }
}
///
@@ -21,10 +29,108 @@ public static async IAsyncEnumerable Empty()
///
/// The type of the item.
/// The item to convert.
+ /// A token to cancel the asynchronous iteration.
/// An containing the single item.
- public static async IAsyncEnumerable FromSingleItem(T item)
+ public static async IAsyncEnumerable FromSingleItem(
+ T item,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
+ cancellationToken.ThrowIfCancellationRequested();
yield return item;
await Task.CompletedTask;
}
+
+ ///
+ /// Wraps an array of items as an , yielding each item in order.
+ ///
+ /// The type of the elements.
+ /// The items to yield.
+ /// A token to cancel the asynchronous iteration; checked before each item is yielded.
+ /// An that yields each element of .
+ /// Thrown when is .
+ public static IAsyncEnumerable FromItems(
+ T[] items,
+ CancellationToken cancellationToken = default)
+ {
+ if (items is null)
+ {
+ throw new ArgumentNullException(nameof(items));
+ }
+
+ return FromItemsCore(items, cancellationToken);
+ }
+
+ ///
+ /// Wraps an as an , yielding each element in order.
+ ///
+ /// The type of the elements.
+ /// The synchronous sequence to wrap.
+ /// A token to cancel the asynchronous iteration; checked before each element is yielded.
+ /// An that yields each element of .
+ /// Thrown when is .
+ public static IAsyncEnumerable FromEnumerable(
+ IEnumerable source,
+ CancellationToken cancellationToken = default)
+ {
+ if (source is null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ return FromEnumerableCore(source, cancellationToken);
+ }
+
+ ///
+ /// Creates an that awaits the specified task and yields its result as a single element.
+ ///
+ /// The type of the task result.
+ /// The task whose result will be yielded.
+ /// A token to cancel the asynchronous operation before the task is awaited.
+ /// An that yields the single result of .
+ /// Thrown when is .
+ public static IAsyncEnumerable FromTask(
+ Task task,
+ CancellationToken cancellationToken = default)
+ {
+ if (task is null)
+ {
+ throw new ArgumentNullException(nameof(task));
+ }
+
+ return FromTaskCore(task, cancellationToken);
+ }
+
+ private static async IAsyncEnumerable FromItemsCore(
+ T[] items,
+ [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ foreach (var item in items)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ yield return item;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ private static async IAsyncEnumerable FromEnumerableCore(
+ IEnumerable source,
+ [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ foreach (var item in source)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ yield return item;
+ }
+
+ await Task.CompletedTask;
+ }
+
+ private static async IAsyncEnumerable FromTaskCore(
+ Task task,
+ [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ yield return await task.ConfigureAwait(false);
+ }
}
\ No newline at end of file
diff --git a/src/Atc/Formatters/StringCaseFormatter.cs b/src/Atc/Formatters/StringCaseFormatter.cs
index a57f77e7..f4f8ae18 100644
--- a/src/Atc/Formatters/StringCaseFormatter.cs
+++ b/src/Atc/Formatters/StringCaseFormatter.cs
@@ -48,7 +48,7 @@ namespace Atc;
public sealed class StringCaseFormatter : IFormatProvider, ICustomFormatter
{
///
- /// Static using .
+ /// Static using .
///
public static readonly StringCaseFormatter Default = new();
@@ -87,25 +87,25 @@ public string Format(
return format switch
{
- "U" => str.ToUpper(CultureInfo.CurrentCulture),
- "Ul" => str.ToUpper(CultureInfo.CurrentCulture).EnsureFirstCharacterToLower(),
+ "U" => str.ToUpper(CultureInfo.InvariantCulture),
+ "Ul" => str.ToUpper(CultureInfo.InvariantCulture).EnsureFirstCharacterToLower(),
"u" => str.EnsureFirstCharacterToUpper(),
- "L" => str.ToLower(CultureInfo.CurrentCulture),
- "Lu" => str.ToLower(CultureInfo.CurrentCulture).EnsureFirstCharacterToUpper(),
+ "L" => str.ToLower(CultureInfo.InvariantCulture),
+ "Lu" => str.ToLower(CultureInfo.InvariantCulture).EnsureFirstCharacterToUpper(),
"l" => str.EnsureFirstCharacterToLower(),
- "U." => str.ToUpper(CultureInfo.CurrentCulture).EnsureEndsWithDot(),
- "Ul." => str.ToUpper(CultureInfo.CurrentCulture).EnsureFirstCharacterToLower().EnsureEndsWithDot(),
+ "U." => str.ToUpper(CultureInfo.InvariantCulture).EnsureEndsWithDot(),
+ "Ul." => str.ToUpper(CultureInfo.InvariantCulture).EnsureFirstCharacterToLower().EnsureEndsWithDot(),
"u." => str.EnsureFirstCharacterToUpper().EnsureEndsWithDot(),
- "L." => str.ToLower(CultureInfo.CurrentCulture).EnsureEndsWithDot(),
- "Lu." => str.ToLower(CultureInfo.CurrentCulture).EnsureFirstCharacterToUpper().EnsureEndsWithDot(),
+ "L." => str.ToLower(CultureInfo.InvariantCulture).EnsureEndsWithDot(),
+ "Lu." => str.ToLower(CultureInfo.InvariantCulture).EnsureFirstCharacterToUpper().EnsureEndsWithDot(),
"l." => str.EnsureFirstCharacterToLower().EnsureEndsWithDot(),
- "U:" => str.ToUpper(CultureInfo.CurrentCulture).EnsureEndsWithColon(),
- "Ul:" => str.ToUpper(CultureInfo.CurrentCulture).EnsureFirstCharacterToLower().EnsureEndsWithColon(),
+ "U:" => str.ToUpper(CultureInfo.InvariantCulture).EnsureEndsWithColon(),
+ "Ul:" => str.ToUpper(CultureInfo.InvariantCulture).EnsureFirstCharacterToLower().EnsureEndsWithColon(),
"u:" => str.EnsureFirstCharacterToUpper().EnsureEndsWithColon(),
- "L:" => str.ToLower(CultureInfo.CurrentCulture).EnsureEndsWithColon(),
- "Lu:" => str.ToLower(CultureInfo.CurrentCulture).EnsureFirstCharacterToUpper().EnsureEndsWithColon(),
+ "L:" => str.ToLower(CultureInfo.InvariantCulture).EnsureEndsWithColon(),
+ "Lu:" => str.ToLower(CultureInfo.InvariantCulture).EnsureFirstCharacterToUpper().EnsureEndsWithColon(),
"l:" => str.EnsureFirstCharacterToLower().EnsureEndsWithColon(),
_ => str,
diff --git a/src/Atc/GlobalizationConstants.cs b/src/Atc/GlobalizationConstants.cs
index 5fd98600..55c8d330 100644
--- a/src/Atc/GlobalizationConstants.cs
+++ b/src/Atc/GlobalizationConstants.cs
@@ -13,10 +13,10 @@ public static class GlobalizationConstants
///
/// EnglishCultureInfo.
///
- public static readonly CultureInfo EnglishCultureInfo = new("en-US");
+ public static readonly CultureInfo EnglishCultureInfo = CultureInfo.ReadOnly(new CultureInfo("en-US"));
///
/// DanishCultureInfo.
///
- public static readonly CultureInfo DanishCultureInfo = new("da-DK");
+ public static readonly CultureInfo DanishCultureInfo = CultureInfo.ReadOnly(new CultureInfo("da-DK"));
}
\ No newline at end of file
diff --git a/src/Atc/Helpers/ArticleNumberHelper.cs b/src/Atc/Helpers/ArticleNumberHelper.cs
index ac8945b0..bc018c75 100644
--- a/src/Atc/Helpers/ArticleNumberHelper.cs
+++ b/src/Atc/Helpers/ArticleNumberHelper.cs
@@ -6,16 +6,28 @@ namespace Atc.Helpers;
///
/// https://en.wikipedia.org/wiki/International_Article_Number.
///
-public static class ArticleNumberHelper
+public static partial class ArticleNumberHelper
{
- private static readonly Lazy AsinRegex = new(
+#if NET7_0_OR_GREATER
+ [GeneratedRegex(@"^B\d{2}\w{7}|\d{9}(X|\d)$", RegexOptions.None, matchTimeoutMilliseconds: 250)]
+ private static partial Regex GetAsinRegex();
+
+ [GeneratedRegex(@"^\d{4}-\d{3}[\dxX]{1}$", RegexOptions.None, matchTimeoutMilliseconds: 250)]
+ private static partial Regex GetIssnRegex();
+#else
+ private static readonly Lazy AsinRegexLazy = new(
() => new Regex(@"^B\d{2}\w{7}|\d{9}(X|\d)$", RegexOptions.Compiled, TimeSpan.FromMilliseconds(250)),
LazyThreadSafetyMode.ExecutionAndPublication);
- private static readonly Lazy IssnRegex = new(
+ private static readonly Lazy IssnRegexLazy = new(
() => new Regex(@"^\d{4}-\d{3}[\dxX]{1}$", RegexOptions.Compiled, TimeSpan.FromMilliseconds(250)),
LazyThreadSafetyMode.ExecutionAndPublication);
+ private static Regex GetAsinRegex() => AsinRegexLazy.Value;
+
+ private static Regex GetIssnRegex() => IssnRegexLazy.Value;
+#endif
+
///
/// Get ArticleNumberType.
///
@@ -70,7 +82,7 @@ public static bool IsValidAsin(string asin)
return false;
}
- return AsinRegex.Value.IsMatch(asin);
+ return GetAsinRegex().IsMatch(asin);
}
///
@@ -190,7 +202,7 @@ public static bool IsValidIssn(string code)
return false;
}
- return IssnRegex.Value.IsMatch(code);
+ return GetIssnRegex().IsMatch(code);
}
///
diff --git a/src/Atc/Helpers/AssemblyHelper.cs b/src/Atc/Helpers/AssemblyHelper.cs
index 69ebb561..f3f4557b 100644
--- a/src/Atc/Helpers/AssemblyHelper.cs
+++ b/src/Atc/Helpers/AssemblyHelper.cs
@@ -105,7 +105,7 @@ public static byte[] ReadAsBytes(FileInfo assemblyFile)
if (!assemblyFile.Extension.Equals(".dll", StringComparison.OrdinalIgnoreCase) &&
!assemblyFile.Extension.Equals(".exe", StringComparison.OrdinalIgnoreCase) &&
- !(assemblyFile.Name.StartsWith('~') && assemblyFile.Name.EndsWith(".tmp", StringComparison.Ordinal)))
+ !(assemblyFile.Name.StartsWith("~", StringComparison.Ordinal) && assemblyFile.Name.EndsWith(".tmp", StringComparison.Ordinal)))
{
throw new IOException("File is not a dll or a executable file");
}
diff --git a/src/Atc/Helpers/CSharpTypeHelper.cs b/src/Atc/Helpers/CSharpTypeHelper.cs
index 482ea7e3..65fdb00c 100644
--- a/src/Atc/Helpers/CSharpTypeHelper.cs
+++ b/src/Atc/Helpers/CSharpTypeHelper.cs
@@ -80,7 +80,7 @@ public static bool IsExtendedValueType(string typeName)
/// True if the type is nullable.
public static bool IsNullable(string typeName)
=> !string.IsNullOrEmpty(typeName) &&
- typeName.EndsWith('?');
+ typeName.EndsWith("?", StringComparison.Ordinal);
///
/// Gets the base type by removing the nullable marker (?).
diff --git a/src/Atc/Helpers/CultureHelper.cs b/src/Atc/Helpers/CultureHelper.cs
index dbc0542f..9d47a464 100644
--- a/src/Atc/Helpers/CultureHelper.cs
+++ b/src/Atc/Helpers/CultureHelper.cs
@@ -32,7 +32,8 @@ public static List GetCultures()
allCultures ??= new Dictionary>();
- allCultures[Thread.CurrentThread.CurrentUICulture.LCID] = new List();
+ var lcid = Thread.CurrentThread.CurrentUICulture.LCID;
+ var result = new List();
var culturesFromPlatform = GetCultureInfoFromPlatform();
foreach (var cultureInfo in culturesFromPlatform)
{
@@ -57,13 +58,14 @@ public static List GetCultures()
culture.CountryDisplayName = TryTranslateCountryEnglishName(culture.CountryEnglishName);
culture.LanguageDisplayName = TryTranslateLanguageEnglishName(culture.LanguageEnglishName);
- if (!allCultures[Thread.CurrentThread.CurrentUICulture.LCID].Contains(culture))
+ if (!result.Contains(culture))
{
- allCultures[Thread.CurrentThread.CurrentUICulture.LCID].Add(culture);
+ result.Add(culture);
}
}
- return allCultures[Thread.CurrentThread.CurrentUICulture.LCID];
+ allCultures[lcid] = result;
+ return result;
}
}
@@ -131,10 +133,17 @@ public static List GetCultures(
Thread.CurrentThread.CurrentUICulture = new CultureInfo(displayLanguageLcid);
}
- var cultures = GetCultures();
- if (backupCultureInfo is not null)
+ List cultures;
+ try
{
- Thread.CurrentThread.CurrentUICulture = backupCultureInfo;
+ cultures = GetCultures();
+ }
+ finally
+ {
+ if (backupCultureInfo is not null)
+ {
+ Thread.CurrentThread.CurrentUICulture = backupCultureInfo;
+ }
}
var data = new List();
@@ -615,11 +624,19 @@ public static Dictionary GetCountryNames(
Thread.CurrentThread.CurrentUICulture = new CultureInfo(displayLanguageLcid);
}
- var cultures = GetCultures();
- var data = DataFactory.CreateKeyValueDictionaryOfIntString(dropDownFirstItemType);
- if (backupCultureInfo is not null)
+ List cultures;
+ Dictionary data;
+ try
{
- Thread.CurrentThread.CurrentUICulture = backupCultureInfo;
+ cultures = GetCultures();
+ data = DataFactory.CreateKeyValueDictionaryOfIntString(dropDownFirstItemType);
+ }
+ finally
+ {
+ if (backupCultureInfo is not null)
+ {
+ Thread.CurrentThread.CurrentUICulture = backupCultureInfo;
+ }
}
var countryDisplayNameCount = new Dictionary(StringComparer.Ordinal);
@@ -750,11 +767,19 @@ public static Dictionary GetLanguageNames(
Thread.CurrentThread.CurrentUICulture = new CultureInfo(displayLanguageLcid);
}
- var cultures = GetCultures();
- var data = DataFactory.CreateKeyValueDictionaryOfIntString(dropDownFirstItemType);
- if (backupCultureInfo is not null)
+ List cultures;
+ Dictionary data;
+ try
{
- Thread.CurrentThread.CurrentUICulture = backupCultureInfo;
+ cultures = GetCultures();
+ data = DataFactory.CreateKeyValueDictionaryOfIntString(dropDownFirstItemType);
+ }
+ finally
+ {
+ if (backupCultureInfo is not null)
+ {
+ Thread.CurrentThread.CurrentUICulture = backupCultureInfo;
+ }
}
var languageDisplayNameCount = new Dictionary(StringComparer.Ordinal);
@@ -810,19 +835,25 @@ public static List GetCultureLcidsWhereCountryIsNotTranslated(
Thread.CurrentThread.CurrentUICulture = new CultureInfo(displayLanguageLcid);
}
- var culturesFromPlatform = GetCultureInfoFromPlatform();
- var data = (
- from cultureInfo
- in culturesFromPlatform
- let countryEnglishName = ExtractCountryEnglishName(cultureInfo)
- let countryDisplayName = TryTranslateCountryEnglishName(countryEnglishName, useValueAsDefault: false)
- where countryDisplayName is null
- select cultureInfo.LCID)
- .ToList();
-
- if (backupCultureInfo is not null)
+ List data;
+ try
{
- Thread.CurrentThread.CurrentUICulture = backupCultureInfo;
+ var culturesFromPlatform = GetCultureInfoFromPlatform();
+ data = (
+ from cultureInfo
+ in culturesFromPlatform
+ let countryEnglishName = ExtractCountryEnglishName(cultureInfo)
+ let countryDisplayName = TryTranslateCountryEnglishName(countryEnglishName, useValueAsDefault: false)
+ where countryDisplayName is null
+ select cultureInfo.LCID)
+ .ToList();
+ }
+ finally
+ {
+ if (backupCultureInfo is not null)
+ {
+ Thread.CurrentThread.CurrentUICulture = backupCultureInfo;
+ }
}
if (includeOnlyLcids is null || includeOnlyLcids.Count <= 0)
@@ -997,9 +1028,7 @@ private static string TryTranslateLanguageEnglishName(
private static ResourceManager GetResourceManagerForResource(
string resource)
{
- var assembly = AppDomain.CurrentDomain
- .GetAssemblies()
- .Single(x => x.FullName!.StartsWith(ResourceBaseName + ", Version", StringComparison.Ordinal));
+ var assembly = typeof(CultureHelper).Assembly;
return new ResourceManager($"{ResourceBaseName}.Resources.{resource}", assembly);
}
}
\ No newline at end of file
diff --git a/src/Atc/Helpers/DateTimeHelper.cs b/src/Atc/Helpers/DateTimeHelper.cs
index c5c03278..fd76f94d 100644
--- a/src/Atc/Helpers/DateTimeHelper.cs
+++ b/src/Atc/Helpers/DateTimeHelper.cs
@@ -10,8 +10,8 @@ public static class DateTimeHelper
private const int MaxTimeLengthFor12Hours = 8;
///
- /// Tries to parse a string representation of a DateTime using
- /// the current UI culture's date and time format.
+ /// Tries to parse a string representation of a DateTime using the current UI culture's date and time format.
+ /// Use this variant when parsing input from a user interface.
///
/// The string to parse.
///
@@ -21,12 +21,12 @@ public static class DateTimeHelper
///
/// if the parsing was successful; otherwise, .
///
- public static bool TryParseUsingCurrentUiCulture(
+ public static bool TryParseUi(
string value,
out DateTime result)
{
result = default;
- if (!TryParseUsingSpecificCulture(value, Thread.CurrentThread.CurrentUICulture, out var res))
+ if (!TryParseUsingSpecificCulture(value, CultureInfo.CurrentUICulture, out var res))
{
return false;
}
@@ -78,8 +78,8 @@ public static bool TryParseUsingSpecificCulture(
}
///
- /// Tries to parse a string representation of a short date using
- /// the current UI culture's date format.
+ /// Tries to parse a string representation of a short date using the current UI culture's date format.
+ /// Use this variant when parsing input from a user interface.
///
/// The string to parse.
///
@@ -89,12 +89,12 @@ public static bool TryParseUsingSpecificCulture(
///
/// if the parsing was successful; otherwise, .
///
- public static bool TryParseShortDateUsingCurrentUiCulture(
+ public static bool TryParseShortDateUi(
string value,
out DateTime result)
{
result = default;
- if (!TryParseShortDateUsingSpecificCulture(value, Thread.CurrentThread.CurrentUICulture, out var res))
+ if (!TryParseShortDateUsingSpecificCulture(value, CultureInfo.CurrentUICulture, out var res))
{
return false;
}
@@ -146,8 +146,8 @@ public static bool TryParseShortDateUsingSpecificCulture(
}
///
- /// Tries to parse a string representation of a short time using the
- /// current UI culture's time format (12-hour or 24-hour).
+ /// Tries to parse a string representation of a short time using the current UI culture's time format (12-hour or 24-hour).
+ /// Use this variant when parsing input from a user interface.
///
/// The string to parse.
///
@@ -157,12 +157,12 @@ public static bool TryParseShortDateUsingSpecificCulture(
///
/// if the parsing was successful; otherwise, .
///
- public static bool TryParseShortTimeUsingCurrentUiCulture(
+ public static bool TryParseShortTimeUi(
string value,
out DateTime result)
{
result = default;
- if (!TryParseShortTimeUsingSpecificCulture(value, Thread.CurrentThread.CurrentUICulture, out var res))
+ if (!TryParseShortTimeUsingSpecificCulture(value, CultureInfo.CurrentUICulture, out var res))
{
return false;
}
@@ -221,8 +221,8 @@ public static bool TryParseShortTimeUsingSpecificCulture(
}
///
- /// Tries to parse a string representation of a short UTC time using the
- /// current UI culture's time format (12-hour or 24-hour).
+ /// Tries to parse a string representation of a short UTC time using the current UI culture's time format (12-hour or 24-hour).
+ /// Use this variant when parsing input from a user interface.
///
/// The string to parse.
///
@@ -232,12 +232,12 @@ public static bool TryParseShortTimeUsingSpecificCulture(
///
/// if the parsing was successful; otherwise, .
///
- public static bool TryParseShortTimeUsingCurrentUiCultureUtc(
+ public static bool TryParseShortTimeUiUtc(
string value,
out DateTime result)
{
result = default;
- if (!TryParseShortTimeUsingSpecificCultureUtc(value, Thread.CurrentThread.CurrentUICulture, out var res))
+ if (!TryParseShortTimeUsingSpecificCultureUtc(value, CultureInfo.CurrentUICulture, out var res))
{
return false;
}
diff --git a/src/Atc/Helpers/DateTimeOffsetHelper.cs b/src/Atc/Helpers/DateTimeOffsetHelper.cs
index 90a6fc31..d4d15fb8 100644
--- a/src/Atc/Helpers/DateTimeOffsetHelper.cs
+++ b/src/Atc/Helpers/DateTimeOffsetHelper.cs
@@ -10,8 +10,8 @@ public static class DateTimeOffsetHelper
private const int MaxTimeLengthFor12Hours = 8;
///
- /// Tries to parse a string representation of a DateTimeOffset using
- /// the current UI culture's date and time format.
+ /// Tries to parse a string representation of a DateTimeOffset using the current UI culture's date and time format.
+ /// Use this variant when parsing input from a user interface.
///
/// The string to parse.
///
@@ -21,10 +21,40 @@ public static class DateTimeOffsetHelper
///
/// if the parsing was successful; otherwise, .
///
- public static bool TryParseUsingCurrentUiCulture(
+ public static bool TryParseUi(
string value,
out DateTimeOffset result)
{
+ result = default;
+ if (!TryParseUsingSpecificCulture(value, CultureInfo.CurrentUICulture, out var res))
+ {
+ return false;
+ }
+
+ result = res;
+ return true;
+ }
+
+ ///
+ /// Tries to parse a string representation of a DateTimeOffset using a specific culture's date and time format.
+ ///
+ /// The string to parse.
+ /// The culture info to use for parsing.
+ ///
+ /// When this method returns, contains the parsed DateTimeOffset ,
+ /// if the parse operation was successful; otherwise, contains the default DateTimeOffset .
+ ///
+ ///
+ /// if the parsing was successful; otherwise, .
+ ///
+ /// Thrown when is null.
+ public static bool TryParseUsingSpecificCulture(
+ string value,
+ CultureInfo cultureInfo,
+ out DateTimeOffset result)
+ {
+ ArgumentNullException.ThrowIfNull(cultureInfo);
+
result = default;
if (string.IsNullOrWhiteSpace(value) ||
value.Length < DateLength)
@@ -34,7 +64,7 @@ public static bool TryParseUsingCurrentUiCulture(
if (!DateTimeOffset.TryParse(
value,
- Thread.CurrentThread.CurrentUICulture.DateTimeFormat,
+ cultureInfo.DateTimeFormat,
DateTimeStyles.None,
out var res))
{
@@ -46,8 +76,8 @@ public static bool TryParseUsingCurrentUiCulture(
}
///
- /// Tries to parse a string representation of a short date using
- /// the current UI culture's date format.
+ /// Tries to parse a string representation of a short date using the current UI culture's date format.
+ /// Use this variant when parsing input from a user interface.
///
/// The string to parse.
///
@@ -57,10 +87,40 @@ public static bool TryParseUsingCurrentUiCulture(
///
/// if the parsing was successful; otherwise, .
///
- public static bool TryParseShortDateUsingCurrentUiCulture(
+ public static bool TryParseShortDateUi(
string value,
out DateTimeOffset result)
{
+ result = default;
+ if (!TryParseShortDateUsingSpecificCulture(value, CultureInfo.CurrentUICulture, out var res))
+ {
+ return false;
+ }
+
+ result = res;
+ return true;
+ }
+
+ ///
+ /// Tries to parse a string representation of a short date using a specific culture's date format.
+ ///
+ /// The string to parse.
+ /// The culture info to use for parsing.
+ ///
+ /// When this method returns, contains the parsed DateTimeOffset ,
+ /// if the parse operation was successful; otherwise, contains the default DateTimeOffset .
+ ///
+ ///
+ /// if the parsing was successful; otherwise, .
+ ///
+ /// Thrown when is null.
+ public static bool TryParseShortDateUsingSpecificCulture(
+ string value,
+ CultureInfo cultureInfo,
+ out DateTimeOffset result)
+ {
+ ArgumentNullException.ThrowIfNull(cultureInfo);
+
result = default;
if (string.IsNullOrWhiteSpace(value) ||
value.Length > DateLength)
@@ -70,7 +130,7 @@ public static bool TryParseShortDateUsingCurrentUiCulture(
if (!DateTimeOffset.TryParse(
value,
- Thread.CurrentThread.CurrentUICulture.DateTimeFormat,
+ cultureInfo.DateTimeFormat,
DateTimeStyles.None,
out var res))
{
@@ -82,8 +142,8 @@ public static bool TryParseShortDateUsingCurrentUiCulture(
}
///
- /// Tries to parse a string representation of a short time using the
- /// current UI culture's time format (12-hour or 24-hour).
+ /// Tries to parse a string representation of a short time using the current UI culture's time format (12-hour or 24-hour).
+ /// Use this variant when parsing input from a user interface.
///
/// The string to parse.
///
@@ -93,14 +153,44 @@ public static bool TryParseShortDateUsingCurrentUiCulture(
///
/// if the parsing was successful; otherwise, .
///
- public static bool TryParseShortTimeUsingCurrentUiCulture(
+ public static bool TryParseShortTimeUi(
string value,
out DateTimeOffset result)
{
+ result = default;
+ if (!TryParseShortTimeUsingSpecificCulture(value, CultureInfo.CurrentUICulture, out var res))
+ {
+ return false;
+ }
+
+ result = res;
+ return true;
+ }
+
+ ///
+ /// Tries to parse a string representation of a short time using a specific culture's time format (12-hour or 24-hour).
+ ///
+ /// The string to parse.
+ /// The culture info to use for parsing.
+ ///
+ /// When this method returns, contains the parsed DateTimeOffset ,
+ /// if the parse operation was successful; otherwise, contains the default DateTimeOffset .
+ ///
+ ///
+ /// if the parsing was successful; otherwise, .
+ ///
+ /// Thrown when is null.
+ public static bool TryParseShortTimeUsingSpecificCulture(
+ string value,
+ CultureInfo cultureInfo,
+ out DateTimeOffset result)
+ {
+ ArgumentNullException.ThrowIfNull(cultureInfo);
+
result = default;
- var use24Hours = !(Thread.CurrentThread.CurrentUICulture.DateTimeFormat.ShortTimePattern.StartsWith("h:", StringComparison.Ordinal) ||
- Thread.CurrentThread.CurrentUICulture.DateTimeFormat.ShortTimePattern.StartsWith("h.", StringComparison.Ordinal));
+ var use24Hours = !(cultureInfo.DateTimeFormat.ShortTimePattern.StartsWith("h:", StringComparison.Ordinal) ||
+ cultureInfo.DateTimeFormat.ShortTimePattern.StartsWith("h.", StringComparison.Ordinal));
var maxLength = use24Hours ? MaxTimeLengthFor24Hours : MaxTimeLengthFor12Hours;
@@ -110,10 +200,10 @@ public static bool TryParseShortTimeUsingCurrentUiCulture(
return false;
}
- var dateTimeOffsetValue = $"{DateTimeOffset.Now.ToShortDateStringUsingCurrentUiCulture()} {value}";
+ var dateTimeOffsetValue = $"{DateTimeOffset.Now.ToShortDateStringUsingSpecificCulture(cultureInfo)} {value}";
if (!DateTimeOffset.TryParse(
dateTimeOffsetValue,
- Thread.CurrentThread.CurrentUICulture.DateTimeFormat,
+ cultureInfo.DateTimeFormat,
DateTimeStyles.None,
out var res))
{
@@ -125,8 +215,8 @@ public static bool TryParseShortTimeUsingCurrentUiCulture(
}
///
- /// Tries to parse a string representation of a short UTC time using the
- /// current UI culture's time format (12-hour or 24-hour).
+ /// Tries to parse a string representation of a short UTC time using the current UI culture's time format (12-hour or 24-hour).
+ /// Use this variant when parsing input from a user interface.
///
/// The string to parse.
///
@@ -136,14 +226,44 @@ public static bool TryParseShortTimeUsingCurrentUiCulture(
///
/// if the parsing was successful; otherwise, .
///
- public static bool TryParseShortTimeUsingCurrentUiCultureUtc(
+ public static bool TryParseShortTimeUiUtc(
string value,
out DateTimeOffset result)
{
+ result = default;
+ if (!TryParseShortTimeUsingSpecificCultureUtc(value, CultureInfo.CurrentUICulture, out var res))
+ {
+ return false;
+ }
+
+ result = res;
+ return true;
+ }
+
+ ///
+ /// Tries to parse a string representation of a short UTC time using a specific culture's time format (12-hour or 24-hour).
+ ///
+ /// The string to parse.
+ /// The culture info to use for parsing.
+ ///
+ /// When this method returns, contains the parsed DateTimeOffset in UTC,
+ /// if the parse operation was successful; otherwise, contains the default DateTimeOffset .
+ ///
+ ///
+ /// if the parsing was successful; otherwise, .
+ ///
+ /// Thrown when is null.
+ public static bool TryParseShortTimeUsingSpecificCultureUtc(
+ string value,
+ CultureInfo cultureInfo,
+ out DateTimeOffset result)
+ {
+ ArgumentNullException.ThrowIfNull(cultureInfo);
+
result = default;
- var use24Hours = !(Thread.CurrentThread.CurrentUICulture.DateTimeFormat.ShortTimePattern.StartsWith("h:", StringComparison.Ordinal) ||
- Thread.CurrentThread.CurrentUICulture.DateTimeFormat.ShortTimePattern.StartsWith("h.", StringComparison.Ordinal));
+ var use24Hours = !(cultureInfo.DateTimeFormat.ShortTimePattern.StartsWith("h:", StringComparison.Ordinal) ||
+ cultureInfo.DateTimeFormat.ShortTimePattern.StartsWith("h.", StringComparison.Ordinal));
var maxLength = use24Hours ? MaxTimeLengthFor24Hours : MaxTimeLengthFor12Hours;
@@ -153,10 +273,10 @@ public static bool TryParseShortTimeUsingCurrentUiCultureUtc(
return false;
}
- var dateTimeOffsetValue = $"{DateTimeOffset.UtcNow.ToShortDateStringUsingCurrentUiCulture()} {value}";
+ var dateTimeOffsetValue = $"{DateTimeOffset.UtcNow.ToShortDateStringUsingSpecificCulture(cultureInfo)} {value}";
if (!DateTimeOffset.TryParse(
dateTimeOffsetValue,
- Thread.CurrentThread.CurrentUICulture.DateTimeFormat,
+ cultureInfo.DateTimeFormat,
DateTimeStyles.None,
out var res))
{
diff --git a/src/Atc/Helpers/Enums/EnumHelper.cs b/src/Atc/Helpers/Enums/EnumHelper.cs
index 1d4125fe..dc727ae7 100644
--- a/src/Atc/Helpers/Enums/EnumHelper.cs
+++ b/src/Atc/Helpers/Enums/EnumHelper.cs
@@ -212,14 +212,14 @@ public static Dictionary ConvertEnumToDictionary(
if (dropDownFirstItemType == DropDownFirstItemType.None)
{
- if (!list.ContainsKey((int)objEnumValue))
+ if (!list.ContainsKey(Convert.ToInt32(objEnumValue, CultureInfo.InvariantCulture)))
{
- list.Add((int)objEnumValue, value);
+ list.Add(Convert.ToInt32(objEnumValue, CultureInfo.InvariantCulture), value);
}
}
- else if (!list.ContainsKey((int)objEnumValue))
+ else if (!list.ContainsKey(Convert.ToInt32(objEnumValue, CultureInfo.InvariantCulture)))
{
- list.Add((int)objEnumValue, value);
+ list.Add(Convert.ToInt32(objEnumValue, CultureInfo.InvariantCulture), value);
}
}
@@ -636,7 +636,7 @@ private static bool ShouldEnumValueBeSkipped(
bool byFlagIncludeBase,
bool byFlagIncludeCombined)
{
- if (!includeDefault && (int)objEnumValue == 0)
+ if (!includeDefault && Convert.ToInt64(objEnumValue, CultureInfo.InvariantCulture) == 0L)
{
return true;
}
@@ -646,7 +646,7 @@ private static bool ShouldEnumValueBeSkipped(
return false;
}
- var n = (int)objEnumValue;
+ var n = Convert.ToInt64(objEnumValue, CultureInfo.InvariantCulture);
if (!byFlagIncludeBase && n.IsBinarySequence())
{
return true;
@@ -662,6 +662,6 @@ private static bool ShouldEnumValueBeSkipped(
return false;
}
- return !includeDefault || (int)objEnumValue != 0;
+ return !includeDefault || Convert.ToInt64(objEnumValue, CultureInfo.InvariantCulture) != 0L;
}
}
\ No newline at end of file
diff --git a/src/Atc/Helpers/MathHelper.cs b/src/Atc/Helpers/MathHelper.cs
index 833ef9c4..fc09cbeb 100644
--- a/src/Atc/Helpers/MathHelper.cs
+++ b/src/Atc/Helpers/MathHelper.cs
@@ -207,9 +207,7 @@ public static int Min(int[] values)
throw new ArgumentNullException(nameof(values));
}
- return values
- .Concat(new[] { int.MaxValue })
- .Min();
+ return values.DefaultIfEmpty(int.MaxValue).Min();
}
///
@@ -223,9 +221,7 @@ public static int Min(List