Background and motivation
IParsable<TSelf>, ISpanParsable<TSelf>, and IUtf8SpanParsable<TSelf> model parsing as an intrinsic capability of the target type.
That works well when the target type owns its textual representation:
TSelf.Parse(value, provider);
TSelf.TryParse(value, provider, out result);
However, some parsing scenarios require an external parsing strategy instead of a static member on the target type. This is similar to the relationship between IComparable<T> and IComparer<T>:
IComparable<T> // intrinsic comparison capability
IComparer<T> // external comparison strategy
The corresponding parsing relationship would be:
IParsable<TSelf> // intrinsic parsing capability
IParser<T> // external parsing strategy
External parser strategies are useful when the target type:
- is defined in a third-party library;
- cannot be modified by the application;
- does not define
Parse or TryParse methods;
- exposes parsing through a different pattern;
- needs multiple independent parsing strategies;
- should not own application-specific parsing rules.
There is currently no simple strongly-typed BCL strategy interface for parsing values externally.
API Proposal
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
namespace System
{
/// <summary>Defines a mechanism for parsing a string to a value.</summary>
/// <typeparam name="T">The type of value to parse.</typeparam>
public interface IParser<T>
{
/// <summary>Parses a string into a value.</summary>
/// <param name="s">The string to parse.</param>
/// <param name="provider">An object that provides culture-specific formatting information about <paramref name="s" />.</param>
/// <returns>The result of parsing <paramref name="s" />.</returns>
/// <exception cref="ArgumentNullException"><paramref name="s" /> is <c>null</c>.</exception>
/// <exception cref="FormatException"><paramref name="s" /> is not in the correct format.</exception>
/// <exception cref="OverflowException"><paramref name="s" /> is not representable by <typeparamref name="T" />.</exception>
T Parse(string s, IFormatProvider? provider);
/// <summary>Tries to parse a string into a value.</summary>
/// <param name="s">The string to parse.</param>
/// <param name="provider">An object that provides culture-specific formatting information about <paramref name="s" />.</param>
/// <param name="result">On return, contains the result of successfully parsing <paramref name="s" /> or an undefined value on failure.</param>
/// <returns><c>true</c> if <paramref name="s" /> was successfully parsed; otherwise, <c>false</c>.</returns>
bool TryParse(
[NotNullWhen(true)] string? s,
IFormatProvider? provider,
[MaybeNullWhen(returnValue: false)] out T result);
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
namespace System
{
/// <summary>Defines a mechanism for parsing a span of characters to a value.</summary>
/// <typeparam name="T">The type of value to parse.</typeparam>
public interface ISpanParser<T> : IParser<T>
{
/// <summary>Parses a span of characters into a value.</summary>
/// <param name="s">The span of characters to parse.</param>
/// <param name="provider">An object that provides culture-specific formatting information about <paramref name="s" />.</param>
/// <returns>The result of parsing <paramref name="s" />.</returns>
/// <exception cref="FormatException"><paramref name="s" /> is not in the correct format.</exception>
/// <exception cref="OverflowException"><paramref name="s" /> is not representable by <typeparamref name="T" />.</exception>
T Parse(ReadOnlySpan<char> s, IFormatProvider? provider);
/// <summary>Tries to parse a span of characters into a value.</summary>
/// <param name="s">The span of characters to parse.</param>
/// <param name="provider">An object that provides culture-specific formatting information about <paramref name="s" />.</param>
/// <param name="result">On return, contains the result of successfully parsing <paramref name="s" /> or an undefined value on failure.</param>
/// <returns><c>true</c> if <paramref name="s" /> was successfully parsed; otherwise, <c>false</c>.</returns>
bool TryParse(
ReadOnlySpan<char> s,
IFormatProvider? provider,
[MaybeNullWhen(returnValue: false)] out T result);
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
namespace System
{
/// <summary>Defines a mechanism for parsing a span of UTF-8 characters to a value.</summary>
/// <typeparam name="T">The type of value to parse.</typeparam>
public interface IUtf8SpanParser<T>
{
/// <summary>Parses a span of UTF-8 characters into a value.</summary>
/// <param name="utf8Text">The span of UTF-8 characters to parse.</param>
/// <param name="provider">An object that provides culture-specific formatting information about <paramref name="utf8Text" />.</param>
/// <returns>The result of parsing <paramref name="utf8Text" />.</returns>
/// <exception cref="FormatException"><paramref name="utf8Text" /> is not in the correct format.</exception>
/// <exception cref="OverflowException"><paramref name="utf8Text" /> is not representable by <typeparamref name="T" />.</exception>
T Parse(ReadOnlySpan<byte> utf8Text, IFormatProvider? provider);
/// <summary>Tries to parse a span of UTF-8 characters into a value.</summary>
/// <param name="utf8Text">The span of UTF-8 characters to parse.</param>
/// <param name="provider">An object that provides culture-specific formatting information about <paramref name="utf8Text" />.</param>
/// <param name="result">On return, contains the result of successfully parsing <paramref name="utf8Text" /> or an undefined value on failure.</param>
/// <returns><c>true</c> if <paramref name="utf8Text" /> was successfully parsed; otherwise, <c>false</c>.</returns>
bool TryParse(
ReadOnlySpan<byte> utf8Text,
IFormatProvider? provider,
[MaybeNullWhen(returnValue: false)] out T result);
}
}
API Usage
Parsing with an external strategy
using System;
public static class ParserExtensions
{
public static T ParseWith<T>(
this IParser<T> parser,
string value,
IFormatProvider? provider = null)
{
ArgumentNullException.ThrowIfNull(parser);
return parser.Parse(value, provider);
}
public static bool TryParseWith<T>(
this IParser<T> parser,
string? value,
IFormatProvider? provider,
out T result)
{
ArgumentNullException.ThrowIfNull(parser);
return parser.TryParse(value, provider, out result);
}
}
Generic code that does not require the target type to implement IParsable<TSelf>
using System;
public static class ConversionPipeline
{
public static T Convert<T>(
string value,
IParser<T> parser,
IFormatProvider? provider = null)
{
ArgumentNullException.ThrowIfNull(parser);
return parser.Parse(value, provider);
}
}
Example type
The target type does not implement IParsable<TSelf> and does not define static parsing members.
public readonly record struct Money(decimal Amount, string Currency);
Example external parser
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
public sealed class MoneyParser : ISpanParser<Money>
{
public Money Parse(string s, IFormatProvider? provider)
{
ArgumentNullException.ThrowIfNull(s);
return Parse(s.AsSpan(), provider);
}
public bool TryParse(
[NotNullWhen(true)] string? s,
IFormatProvider? provider,
out Money result)
{
if (s is null)
{
result = default;
return false;
}
return TryParse(s.AsSpan(), provider, out result);
}
public Money Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
{
if (!TryParse(s, provider, out Money result))
{
throw new FormatException();
}
return result;
}
public bool TryParse(
ReadOnlySpan<char> s,
IFormatProvider? provider,
out Money result)
{
int separatorIndex = s.IndexOf(' ');
if (separatorIndex <= 0 || separatorIndex == s.Length - 1)
{
result = default;
return false;
}
ReadOnlySpan<char> currencyText = s[..separatorIndex];
ReadOnlySpan<char> amountText = s[(separatorIndex + 1)..];
if (currencyText.Length == 3 &&
decimal.TryParse(amountText, NumberStyles.Number, provider, out decimal amount))
{
result = new Money(amount, currencyText.ToString().ToUpperInvariant());
return true;
}
result = default;
return false;
}
}
Usage:
IParser<Money> parser = new MoneyParser();
Money value = parser.Parse("USD 123.45", CultureInfo.InvariantCulture);
Design notes
Relationship to IParsable<TSelf>
IParser<T> does not replace IParsable<TSelf>.
IParsable<TSelf> models an intrinsic capability of the target type. IParser<T> models an external strategy object. Both are useful in different scenarios, just as both IComparable<T> and IComparer<T> are useful.
Why no variance?
IParser<T> returns T and also uses T in an out T result parameter. This makes generic variance undesirable or invalid for the proposed shape.
Why does IUtf8SpanParser<T> not inherit ISpanParser<T>?
UTF-8 parsing can be useful independently from UTF-16 parsing. Libraries that operate directly on UTF-8 payloads may not want a UTF-16 parsing dependency in their generic constraints.
A parser can implement both interfaces when appropriate:
public sealed class SomeParser<T> :
ISpanParser<T>,
IUtf8SpanParser<T>
{
}
Why no exact parsing in the initial proposal?
Exact parsing with a format string is a related but separate capability.
A future extension could introduce:
IFormatParser<T>
IFormatSpanParser<T>
IUtf8FormatSpanParser<T>
The initial proposal focuses on the minimal external parsing strategy equivalent of IParsable<TSelf>.
Alternative Designs
Keep using IParsable<TSelf>
One option is to require target types to implement IParsable<TSelf>.
This works when the target type can be modified and when parsing behavior naturally belongs to the type itself. It does not work well for third-party types, sealed types, legacy types, or multiple independent parsing strategies.
Use delegates
Another option is to use delegates such as:
Func<string, IFormatProvider?, T>
or custom delegate types.
This is lightweight, but it does not provide a standard shape for TryParse, span parsing, UTF-8 parsing, or reusable parser objects.
Use TypeConverter
TypeConverter can represent external conversion logic for some scenarios.
However, it is object-based, reflection-oriented, and not designed as a high-performance strongly-typed parsing strategy for generic code.
Use reflection or convention-based parsing
A library can discover static Parse and TryParse methods by reflection.
This avoids new interfaces, but it loses compile-time constraints, has more complicated failure modes, and is less consistent with the existing static abstract parsing interfaces.
Include exact parsing in the initial API
The proposal could include IFormatParser<T> and related span/UTF-8 interfaces from the start.
This would make the API more complete, but it would also increase the initial surface area. Exact parsing can be considered as a separate follow-up proposal.
Risks
Increased public API surface area
The proposal adds new public interfaces to System. This increases the long-term API surface area and should be justified by concrete generic programming scenarios.
Possible overlap with existing APIs
The proposal overlaps conceptually with IParsable<TSelf>, TypeConverter, and convention-based parsing patterns. The distinction is that IParser<T> is a strongly-typed external strategy interface.
Generic libraries may over-constrain APIs
Libraries may start requiring IParser<T> where a delegate would be sufficient. This is a general risk with capability interfaces.
Ambiguity around ownership of parsing rules
External parsers allow parsing behavior to be supplied outside the target type. This is useful, but it can also lead to multiple competing parser implementations for the same type.
Performance depends on implementations
The interfaces allow allocation-free span parsing, but naive implementations may allocate when converting spans to strings. Implementations should avoid unnecessary allocations in span and UTF-8 paths.
Compatibility commitment
Once added, interface names, method signatures, nullability annotations, and inheritance shape become long-term public contracts.
Open questions
-
Should the interfaces live in System, or in a more specific namespace?
-
Should the initial proposal include only IParser<T>, or should it also include ISpanParser<T> and IUtf8SpanParser<T>?
-
Should exact parsing interfaces such as IFormatParser<T> be included now or left for a future proposal?
-
Should IUtf8SpanParser<T> inherit from IParser<T>, or remain independent?
-
Should any built-in parser implementations be provided, or should this proposal only define interfaces?
Background and motivation
IParsable<TSelf>,ISpanParsable<TSelf>, andIUtf8SpanParsable<TSelf>model parsing as an intrinsic capability of the target type.That works well when the target type owns its textual representation:
However, some parsing scenarios require an external parsing strategy instead of a static member on the target type. This is similar to the relationship between
IComparable<T>andIComparer<T>:The corresponding parsing relationship would be:
External parser strategies are useful when the target type:
ParseorTryParsemethods;There is currently no simple strongly-typed BCL strategy interface for parsing values externally.
API Proposal
API Usage
Parsing with an external strategy
Generic code that does not require the target type to implement
IParsable<TSelf>Example type
The target type does not implement
IParsable<TSelf>and does not define static parsing members.Example external parser
Usage:
Design notes
Relationship to
IParsable<TSelf>IParser<T>does not replaceIParsable<TSelf>.IParsable<TSelf>models an intrinsic capability of the target type.IParser<T>models an external strategy object. Both are useful in different scenarios, just as bothIComparable<T>andIComparer<T>are useful.Why no variance?
IParser<T>returnsTand also usesTin anout T resultparameter. This makes generic variance undesirable or invalid for the proposed shape.Why does
IUtf8SpanParser<T>not inheritISpanParser<T>?UTF-8 parsing can be useful independently from UTF-16 parsing. Libraries that operate directly on UTF-8 payloads may not want a UTF-16 parsing dependency in their generic constraints.
A parser can implement both interfaces when appropriate:
Why no exact parsing in the initial proposal?
Exact parsing with a format string is a related but separate capability.
A future extension could introduce:
The initial proposal focuses on the minimal external parsing strategy equivalent of
IParsable<TSelf>.Alternative Designs
Keep using
IParsable<TSelf>One option is to require target types to implement
IParsable<TSelf>.This works when the target type can be modified and when parsing behavior naturally belongs to the type itself. It does not work well for third-party types, sealed types, legacy types, or multiple independent parsing strategies.
Use delegates
Another option is to use delegates such as:
or custom delegate types.
This is lightweight, but it does not provide a standard shape for
TryParse, span parsing, UTF-8 parsing, or reusable parser objects.Use
TypeConverterTypeConvertercan represent external conversion logic for some scenarios.However, it is object-based, reflection-oriented, and not designed as a high-performance strongly-typed parsing strategy for generic code.
Use reflection or convention-based parsing
A library can discover static
ParseandTryParsemethods by reflection.This avoids new interfaces, but it loses compile-time constraints, has more complicated failure modes, and is less consistent with the existing static abstract parsing interfaces.
Include exact parsing in the initial API
The proposal could include
IFormatParser<T>and related span/UTF-8 interfaces from the start.This would make the API more complete, but it would also increase the initial surface area. Exact parsing can be considered as a separate follow-up proposal.
Risks
Increased public API surface area
The proposal adds new public interfaces to
System. This increases the long-term API surface area and should be justified by concrete generic programming scenarios.Possible overlap with existing APIs
The proposal overlaps conceptually with
IParsable<TSelf>,TypeConverter, and convention-based parsing patterns. The distinction is thatIParser<T>is a strongly-typed external strategy interface.Generic libraries may over-constrain APIs
Libraries may start requiring
IParser<T>where a delegate would be sufficient. This is a general risk with capability interfaces.Ambiguity around ownership of parsing rules
External parsers allow parsing behavior to be supplied outside the target type. This is useful, but it can also lead to multiple competing parser implementations for the same type.
Performance depends on implementations
The interfaces allow allocation-free span parsing, but naive implementations may allocate when converting spans to strings. Implementations should avoid unnecessary allocations in span and UTF-8 paths.
Compatibility commitment
Once added, interface names, method signatures, nullability annotations, and inheritance shape become long-term public contracts.
Open questions
Should the interfaces live in
System, or in a more specific namespace?Should the initial proposal include only
IParser<T>, or should it also includeISpanParser<T>andIUtf8SpanParser<T>?Should exact parsing interfaces such as
IFormatParser<T>be included now or left for a future proposal?Should
IUtf8SpanParser<T>inherit fromIParser<T>, or remain independent?Should any built-in parser implementations be provided, or should this proposal only define interfaces?