Skip to content

[API Proposal]: Add external parser strategy interfaces #129774

Description

@OleksandrTsvirkun

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

  1. Should the interfaces live in System, or in a more specific namespace?

  2. Should the initial proposal include only IParser<T>, or should it also include ISpanParser<T> and IUtf8SpanParser<T>?

  3. Should exact parsing interfaces such as IFormatParser<T> be included now or left for a future proposal?

  4. Should IUtf8SpanParser<T> inherit from IParser<T>, or remain independent?

  5. Should any built-in parser implementations be provided, or should this proposal only define interfaces?

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-System.RuntimeuntriagedNew issue has not been triaged by the area owner

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions