Table of Contents

Source Generator

Foreword

There is nothing inherently bad about reflection and compiled expressions.

For 99% of applications, they work perfectly and are straightforward to set up with minimal configuration. However, reflection and compiled expressions are incompatible with AOT compilation and trimming, so an alternative API is provided. Aside from the start-up cost, performance differences between reflection and source generation in FlameCsv are not significant (see: benchmarks). 1

Type Map Generator

FlameCsv includes a source generator that creates code for binding .NET types to/from CSV headers. This enables writing trimmable code without any reflection or dynamic code generation — a necessity for Native AOT applications.

To use the source generator, create a partial class and apply CsvTypeMapAttribute<T, TValue>:

[CsvTypeMap<char, User>]
partial class UserTypeMap;

// example usage
IEnumerable<User> users = CsvReader.Read(csv, UserTypeMap.Default);

The source generator implements CsvTypeMap<T, TValue> on the target type, and generates a static Default-property that can be used to get a thread-safe singleton instance of the type map.

Source generation offers many advantages, such as improved performance due to no reflection and improved JIT optimization, and easier development as you can debug the code as if you wrote it yourself. On the other hand, while great care has been given to the performance of the incremental source generator to keep the impact as small as possible, it still has some effect on compile-times and CPU use during development.

Configuration

The source generator uses attribute configuration to determine the generated code. All features of the reflection-based API are supported.

The code generation can be configured with these properties on CsvTypeMapAttribute<T, TValue>:

  • IgnoreUnmatched: Allows unmatched CSV headers to be ignored when reading. Useful when you only need a subset of fields, e.g., reading 3rd party data with unwanted metadata.
  • ThrowOnDuplicate: Configures the typemap to throw if the same member is matched multiple times. This can happen with duplicate headers or when using header aliases.
  • NoCaching: Disables internal caching. By default, bindings with the same typemap and options instances are cached since they produce identical output. The caching is lightweight, and uses weak references and periodic trimming.
  • SupportsAssemblyAttributes: Configures the source generator to scan the assembly for configuration attributes. By default, only attributes directly on the type and its members are scanned.
[CsvTypeMap<char, User>(SupportsAssemblyAttributes = true)]
partial class UserTypeMap;
Note

These properties on the attribute affect only the generated Default instance. If you create a typemap manually with new(), you need to configure it separately.

Limitations

Due to how generic constraints and CsvConverterFactory<T> work, converter factories are not supported by the source generator. However, since the type is known at compile time, you can add the required converters explicitly beforehand.

Example

class User : IActive
{
    [CsvHeader("id", "user_id")]
    public int Id { get; set; }

    [CsvOrder(999)]
    public string? Name { get; set; }

    public bool IsAdmin { get; set; }

    [CsvRequired]
    bool IActive.IsActive { get; set; }
}

interface IActive
{
    bool IsActive { get; set; }
}

[CsvTypeMap<char, User>]
partial class UserTypeMap;
// <auto-generated>
// Generated by FlameCsv.SourceGen v0.1.0
// </auto-generated>

// <global namespace>

partial class UserTypeMap : global::FlameCsv.Binding.CsvTypeMap<char, global::User>
{
    /// <summary>
    /// Returns a thread-safe instance of the typemap with default options.
    /// </summary>
    /// <remarks>
    /// Unmatched headers cause an exception.<br/>
    /// Duplicate headers are ignored.<br/>
    /// De/materializer caching is enabled.
    /// </remarks>
    public static global::UserTypeMap Default { get; } = new global::UserTypeMap()
    {
        IgnoreUnmatched = false,
        ThrowOnDuplicate = false,
        NoCaching = false,
    };

    private const int @s__Id_IActive_IsActive = 1;
    private const int @s__Id_Id = 2;
    private const int @s__Id_IsAdmin = 3;
    private const int @s__Id_Name = 4;
    private const int @s__MinId = 1;
    private const int @s__MaxId = 4;

    protected override global::FlameCsv.Reading.IMaterializer<char, global::User> BindForReading(scoped global::System.ReadOnlySpan<string> headers, global::FlameCsv.CsvOptions<char> options)
    {
        TypeMapMaterializer materializer = new TypeMapMaterializer(headers.Length);

        global::System.Collections.Generic.IEqualityComparer<string> comparer = options.Comparer;

        for (int index = 0; index < headers.Length; index++)
        {
            string name = headers[index];


            if ((materializer.@s__Converter_IActive_IsActive is null || this.ThrowOnDuplicate) &&
                comparer.Equals(name, "IsActive"))
            {
                if (materializer.@s__Converter_IActive_IsActive is null)
                {
                    materializer.@s__Converter_IActive_IsActive = options.Aot.GetConverter<bool>();
                    materializer.Targets[index] = @s__Id_IActive_IsActive;
                    continue;
                }

                if (this.ThrowOnDuplicate)
                {
                    base.ThrowDuplicate("IsActive", name, headers);
                }
            }

            if ((materializer.@s__Converter_Id is null || this.ThrowOnDuplicate) &&
                comparer.Equals(name, "user_id"))
            {
                if (materializer.@s__Converter_Id is null)
                {
                    materializer.@s__Converter_Id = options.Aot.GetConverter<int>();
                    materializer.Targets[index] = @s__Id_Id;
                    continue;
                }

                if (this.ThrowOnDuplicate)
                {
                    base.ThrowDuplicate("Id", name, headers);
                }
            }

            if ((materializer.@s__Converter_IsAdmin is null || this.ThrowOnDuplicate) &&
                comparer.Equals(name, "IsAdmin"))
            {
                if (materializer.@s__Converter_IsAdmin is null)
                {
                    materializer.@s__Converter_IsAdmin = options.Aot.GetConverter<bool>();
                    materializer.Targets[index] = @s__Id_IsAdmin;
                    continue;
                }

                if (this.ThrowOnDuplicate)
                {
                    base.ThrowDuplicate("IsAdmin", name, headers);
                }
            }

            if ((materializer.@s__Converter_Name is null || this.ThrowOnDuplicate) &&
                comparer.Equals(name, "Name")) // Explicit order: 999
            {
                if (materializer.@s__Converter_Name is null)
                {
                    materializer.@s__Converter_Name = options.Aot.GetConverter<string>();
                    materializer.Targets[index] = @s__Id_Name;
                    continue;
                }

                if (this.ThrowOnDuplicate)
                {
                    base.ThrowDuplicate("Name", name, headers);
                }
            }

            if (this.IgnoreUnmatched)
            {
                materializer.Targets[index] = -1;
            }
            else
            {
                base.ThrowUnmatched(name, index);
            }
        }

        if (!global::System.MemoryExtensions.ContainsAnyInRange(materializer.Targets, @s__MinId, @s__MaxId))
        {
            base.ThrowNoFieldsBound(headers);
        }
        if (materializer.@s__Converter_IActive_IsActive is null)
        {
            base.ThrowRequiredNotRead(GetMissingRequiredFields(materializer), headers);
        }

        return materializer;
    }

    protected override global::FlameCsv.Reading.IMaterializer<char, global::User> BindForReading(global::FlameCsv.CsvOptions<char> options)
    {
        throw new global::System.NotSupportedException("Index binding is not yet supported for the source generator.");
    }

    private static System.Collections.Generic.IEnumerable<string> GetMissingRequiredFields(TypeMapMaterializer materializer)
    {
        if (materializer.@s__Converter_IActive_IsActive is null) yield return "IActive_IsActive";
    }

    private struct ParseState
    {
        public bool IActive_IsActive;
        public int Id;
        public bool IsAdmin;
        public string Name;
    }

    [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
    internal sealed class TypeMapMaterializer : global::FlameCsv.Reading.IMaterializer<char, global::User>
    {
        public global::FlameCsv.CsvConverter<char, bool> @s__Converter_IActive_IsActive;
        public global::FlameCsv.CsvConverter<char, int> @s__Converter_Id;
        public global::FlameCsv.CsvConverter<char, bool> @s__Converter_IsAdmin;
        public global::FlameCsv.CsvConverter<char, string> @s__Converter_Name;

        public readonly int[] Targets;

        public TypeMapMaterializer(int length)
        {
            Targets = new int[length];
        }

        public global::User Parse<TReader>(ref TReader reader) where TReader : global::FlameCsv.Reading.ICsvRecordFields<char>, allows ref struct
        {
            int[] targets = Targets;

            if (targets.Length != reader.FieldCount)
            {
                global::FlameCsv.Exceptions.CsvReadException.ThrowForInvalidFieldCount(expected: targets.Length, actual: reader.FieldCount);
            }

#if RELEASE
            global::System.Runtime.CompilerServices.Unsafe.SkipInit(out ParseState state);
#else
            ParseState state = default;
#endif
            for (int target = 0; target < targets.Length; target++)
            {
                global::System.ReadOnlySpan<char> @field = reader[target];

                bool result = targets[target] switch
                {
                    @s__Id_IActive_IsActive => @s__Converter_IActive_IsActive.TryParse(@field, out state.IActive_IsActive),
                    @s__Id_Id => @s__Converter_Id.TryParse(@field, out state.Id),
                    @s__Id_IsAdmin => @s__Converter_IsAdmin.TryParse(@field, out state.IsAdmin),
                    @s__Id_Name => @s__Converter_Name.TryParse(@field, out state.Name),
                    0 => ThrowForInvalidTarget(target), // Should never happen
                    _ => true, // Ignored fields have target set to -1
                };

                if (!result)
                {
                    ThrowForFailedParse(@field, target);
                }
            }

            // Required fields are guaranteed to be non-null.
            // Optional fields are null-checked to only write a value when one was read.
            global::User obj = new global::User();
            if (@s__Converter_Name is not null) obj.Name = state.Name;
            if (@s__Converter_Id is not null) obj.Id = state.Id;
            if (@s__Converter_IsAdmin is not null) obj.IsAdmin = state.IsAdmin;
            ((global::IActive)obj).IsActive = state.IActive_IsActive;
            return obj;
        }

        [global::System.Diagnostics.CodeAnalysis.DoesNotReturn]
        [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
        private static bool ThrowForInvalidTarget(int target) => throw new global::System.Diagnostics.UnreachableException($"Converter {target} was uninitialized");

        [global::System.Diagnostics.CodeAnalysis.DoesNotReturn]
        [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
        private void ThrowForFailedParse(scoped global::System.ReadOnlySpan<char> field, int target)
        {
            if (target == @s__Id_IActive_IsActive) global::FlameCsv.Exceptions.CsvParseException.Throw(@field, @s__Converter_IActive_IsActive, "IsActive");
            if (target == @s__Id_Id) global::FlameCsv.Exceptions.CsvParseException.Throw(@field, @s__Converter_Id, "Id");
            if (target == @s__Id_IsAdmin) global::FlameCsv.Exceptions.CsvParseException.Throw(@field, @s__Converter_IsAdmin, "IsAdmin");
            if (target == @s__Id_Name) global::FlameCsv.Exceptions.CsvParseException.Throw(@field, @s__Converter_Name, "Name");
            throw new global::System.Diagnostics.UnreachableException("Invalid target: " + target.ToString());
        }
    }

    protected override global::FlameCsv.Writing.IDematerializer<char, global::User> BindForWriting(global::FlameCsv.CsvOptions<char> options)
    {
        return new Dematerializer
        {
            @s__Converter_IActive_IsActive = options.Aot.GetConverter<bool>(),
            @s__Converter_Id = options.Aot.GetConverter<int>(),
            @s__Converter_IsAdmin = options.Aot.GetConverter<bool>(),
            @s__Converter_Name = options.Aot.GetConverter<string>(),
        };
    }

    [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
    internal sealed class Dematerializer : global::FlameCsv.Writing.IDematerializer<char, global::User>
    {
        public int FieldCount => 4;

        public required global::FlameCsv.CsvConverter<char, bool> @s__Converter_IActive_IsActive { get; init; }
        public required global::FlameCsv.CsvConverter<char, int> @s__Converter_Id { get; init; }
        public required global::FlameCsv.CsvConverter<char, bool> @s__Converter_IsAdmin { get; init; }
        public required global::FlameCsv.CsvConverter<char, string> @s__Converter_Name { get; init; }

        public void Write(ref readonly global::FlameCsv.Writing.CsvFieldWriter<char> writer, global::User obj)
        {
            writer.WriteField(@s__Converter_IActive_IsActive, ((global::IActive)obj).IsActive);
            writer.WriteDelimiter();
            writer.WriteField(@s__Converter_Id, obj.Id);
            writer.WriteDelimiter();
            writer.WriteField(@s__Converter_IsAdmin, obj.IsAdmin);
            writer.WriteDelimiter();
            writer.WriteField(@s__Converter_Name, obj.Name);
        }

        public void WriteHeader(ref readonly global::FlameCsv.Writing.CsvFieldWriter<char> writer)
        {
            writer.WriteRaw("IsActive");
            writer.WriteDelimiter();
            writer.WriteRaw("id");
            writer.WriteDelimiter();
            writer.WriteRaw("IsAdmin");
            writer.WriteDelimiter();
            writer.WriteRaw("Name");
        }
    }
}

Enum Converter Generator

CsvEnumConverterAttribute<T, TEnum> can be used to generate extremely performant enum converters that are essentially hyper-optimized hand-written implementations specific to the enum. Simply apply the attribute to a partial class. The generated converter supports parsing and formatting numbers, enum names, EnumMemberAttribute, and case-insensitive parsing of both UTF-16 and UTF-8 data. The generator correctly handles oddities such as surrogates and non-ASCII data, such as emojis.

[CsvEnumConverter<char, DayOfWeek>]
partial class DayOfWeekConverter;

Limitations

  • Hex-formatted values are not supported
  • Custom enum names must not start with a digit or a minus
  • Custom names cannot be empty, or the same as another enum's name or custom name

Example

Below is an example of the code generated by the source generator at the time of writing. The actual generated code may differ in later versions:

// <auto-generated>
// Generated by FlameCsv.SourceGen v0.1.0
// </auto-generated>
#nullable enable

using System;
using __Unsafe = global::System.Runtime.CompilerServices.Unsafe;
using __MemoryMarshal = global::System.Runtime.InteropServices.MemoryMarshal;

namespace FlameCsv.Tests.Converters;

partial class EnumGeneratorCharTests {
[global::System.CodeDom.Compiler.GeneratedCode("FlameCsv.SourceGen", "0.1.0")]
partial class DayOfWeekConverter : global::FlameCsv.CsvConverter<char, global::System.DayOfWeek>
{
    private static WriteNumberImpl WriteNumberStrategy { get; } = new();
    private static WriteStringImpl WriteStringStrategy { get; } = new();
    private static ReadOrdinalImpl OrdinalStrategy { get; } = new();
    private static ReadIgnoreCaseImpl IgnoreCaseStrategy { get; } = new();

    private readonly ParseStrategy _parseStrategy;
    private readonly FormatStrategy _formatStrategy;
    private readonly bool _allowUndefinedValues;
    private readonly bool _ignoreCase;
    private readonly string? _format;

    public DayOfWeekConverter(CsvOptions<char> options)
    {
        global::System.ArgumentNullException.ThrowIfNull(options);
        _allowUndefinedValues = options.AllowUndefinedEnumValues;
        _ignoreCase = options.IgnoreEnumCase;
        _format = options.GetFormat(typeof(global::System.DayOfWeek), options.EnumFormat);
        _parseStrategy = _ignoreCase ? IgnoreCaseStrategy : OrdinalStrategy;
        _formatStrategy = _format switch
        {
            null or "g" or "G" => WriteStringStrategy,
            "d" or "D" => WriteNumberStrategy,
            "x" or "X" => throw new global::System.NotImplementedException("Hex format not supported"),
            { } configuredFormat => throw new global::System.NotSupportedException("Invalid enum format specified: " + configuredFormat)
        };
    }

    public override bool TryParse(global::System.ReadOnlySpan<char> source, out global::System.DayOfWeek value)
    {
        if (source.IsEmpty)
        {
            __Unsafe.SkipInit(out value);
            return false;
        }

        ref char first = ref __MemoryMarshal.GetReference(source);

        // Enum is small and contiguous from 0, try to use fast path
        if (source.Length == 1)
        {
            value = (global::System.DayOfWeek)(uint)(first - '0');
            return value < (global::System.DayOfWeek)7;
        }
        else if (_parseStrategy.TryParse(source, out value))
        {
            return true;
        }

        // not a known value
        return global::System.Enum.TryParse(source, _ignoreCase, out value)
            && (_allowUndefinedValues || IsDefined(value));
    }

    public override bool TryFormat(global::System.Span<char> destination, global::System.DayOfWeek value, out int charsWritten)
    {
        if (destination.IsEmpty)
        {
            __Unsafe.SkipInit(out charsWritten);
            return false;
        }

        if (_formatStrategy.TryFormat(destination, value, out charsWritten))
        {
            return true;
        }

        return ((global::System.ISpanFormattable)value).TryFormat(destination, out charsWritten, _format, provider: null);
    }

    /// <summary>
    /// Determines whether a specified value is defined for the enumeration.
    /// </summary>
    [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
    public static bool IsDefined(global::System.DayOfWeek value)
    {
        return (uint)value < 7U;
    }

    [global::System.CodeDom.Compiler.GeneratedCode("FlameCsv.SourceGen", "0.1.0")]
    private abstract class ParseStrategy
    {
        public abstract bool TryParse(global::System.ReadOnlySpan<char> source, out global::System.DayOfWeek value);
    }

    [global::System.CodeDom.Compiler.GeneratedCode("FlameCsv.SourceGen", "0.1.0")]
    private abstract class FormatStrategy
    {
        public abstract bool TryFormat(global::System.Span<char> destination, global::System.DayOfWeek value, out int charsWritten);
    }

    [global::System.CodeDom.Compiler.GeneratedCode("FlameCsv.SourceGen", "0.1.0")]
    private sealed class ReadOrdinalImpl : ParseStrategy
    {
        public override bool TryParse(global::System.ReadOnlySpan<char> source, out global::System.DayOfWeek value)
        {
            ref char first = ref __MemoryMarshal.GetReference(source);

            switch (source.Length)
            {
                case 6:
                {
                    switch (first)
                    {
                        case 'S':
                        {
                            if (source.EndsWith("unday", global::System.StringComparison.Ordinal))
                            {
                                value = global::System.DayOfWeek.Sunday;
                                return true;
                            }
                            break;
                        }
                        case 'M':
                        {
                            if (source.EndsWith("onday", global::System.StringComparison.Ordinal))
                            {
                                value = global::System.DayOfWeek.Monday;
                                return true;
                            }
                            break;
                        }
                        case 'F':
                        {
                            if (source.EndsWith("riday", global::System.StringComparison.Ordinal))
                            {
                                value = global::System.DayOfWeek.Friday;
                                return true;
                            }
                            break;
                        }
                    }
                    break;
                }
                case 7:
                {
                    if (source.Equals("Tuesday", global::System.StringComparison.Ordinal))
                    {
                        value = global::System.DayOfWeek.Tuesday;
                        return true;
                    }
                    break;
                }
                case 9:
                {
                    if (source.Equals("Wednesday", global::System.StringComparison.Ordinal))
                    {
                        value = global::System.DayOfWeek.Wednesday;
                        return true;
                    }
                    break;
                }
                case 8:
                {
                    switch (first)
                    {
                        case 'T':
                        {
                            if (source.EndsWith("hursday", global::System.StringComparison.Ordinal))
                            {
                                value = global::System.DayOfWeek.Thursday;
                                return true;
                            }
                            break;
                        }
                        case 'S':
                        {
                            if (source.EndsWith("aturday", global::System.StringComparison.Ordinal))
                            {
                                value = global::System.DayOfWeek.Saturday;
                                return true;
                            }
                            break;
                        }
                    }
                    break;
                }
                default:
                    break;
            }

            __Unsafe.SkipInit(out value);
            return false;
        }
    }

    [global::System.CodeDom.Compiler.GeneratedCode("FlameCsv.SourceGen", "0.1.0")]
    private sealed class ReadIgnoreCaseImpl : ParseStrategy
    {
        public override bool TryParse(global::System.ReadOnlySpan<char> source, out global::System.DayOfWeek value)
        {
            ref char first = ref __MemoryMarshal.GetReference(source);

            switch (source.Length)
            {
                case 6:
                {
                    switch ((char)(first | 0x20))
                    {
                        case 's':
                        {
                            if (source.EndsWith("unday", global::System.StringComparison.OrdinalIgnoreCase))
                            {
                                value = global::System.DayOfWeek.Sunday;
                                return true;
                            }
                            break;
                        }
                        case 'm':
                        {
                            if (source.EndsWith("onday", global::System.StringComparison.OrdinalIgnoreCase))
                            {
                                value = global::System.DayOfWeek.Monday;
                                return true;
                            }
                            break;
                        }
                        case 'f':
                        {
                            if (source.EndsWith("riday", global::System.StringComparison.OrdinalIgnoreCase))
                            {
                                value = global::System.DayOfWeek.Friday;
                                return true;
                            }
                            break;
                        }
                    }
                    break;
                }
                case 7:
                {
                    if (source.Equals("Tuesday", global::System.StringComparison.OrdinalIgnoreCase))
                    {
                        value = global::System.DayOfWeek.Tuesday;
                        return true;
                    }
                    break;
                }
                case 9:
                {
                    if (source.Equals("Wednesday", global::System.StringComparison.OrdinalIgnoreCase))
                    {
                        value = global::System.DayOfWeek.Wednesday;
                        return true;
                    }
                    break;
                }
                case 8:
                {
                    switch ((char)(first | 0x20))
                    {
                        case 't':
                        {
                            if (source.EndsWith("hursday", global::System.StringComparison.OrdinalIgnoreCase))
                            {
                                value = global::System.DayOfWeek.Thursday;
                                return true;
                            }
                            break;
                        }
                        case 's':
                        {
                            if (source.EndsWith("aturday", global::System.StringComparison.OrdinalIgnoreCase))
                            {
                                value = global::System.DayOfWeek.Saturday;
                                return true;
                            }
                            break;
                        }
                    }
                    break;
                }
                default:
                    break;
            }

            __Unsafe.SkipInit(out value);
            return false;
        }
    }

    [global::System.CodeDom.Compiler.GeneratedCode("FlameCsv.SourceGen", "0.1.0")]
    private sealed class WriteNumberImpl : FormatStrategy
    {
        public override bool TryFormat(global::System.Span<char> destination, global::System.DayOfWeek value, out int charsWritten)
        {
            __Unsafe.SkipInit(out charsWritten);

            ref char dst = ref __MemoryMarshal.GetReference(destination);

            if ((uint)value < 7)
            {
                dst = (char)('0' + value);
                charsWritten = 1;
                return true;
            }

            return false;
        }
    }

    [global::System.CodeDom.Compiler.GeneratedCode("FlameCsv.SourceGen", "0.1.0")]
    private sealed class WriteStringImpl : FormatStrategy
    {
        public override bool TryFormat(global::System.Span<char> destination, global::System.DayOfWeek value, out int charsWritten)
        {
            __Unsafe.SkipInit(out charsWritten);

            ref char dst = ref __MemoryMarshal.GetReference(destination);

            switch (value)
            {
                case global::System.DayOfWeek.Sunday:
                {
                    if ("Sunday".TryCopyTo(destination))
                    {
                        charsWritten = 6;
                        return true;
                    }
                    break;
                }
                case global::System.DayOfWeek.Monday:
                {
                    if ("Monday".TryCopyTo(destination))
                    {
                        charsWritten = 6;
                        return true;
                    }
                    break;
                }
                case global::System.DayOfWeek.Tuesday:
                {
                    if ("Tuesday".TryCopyTo(destination))
                    {
                        charsWritten = 7;
                        return true;
                    }
                    break;
                }
                case global::System.DayOfWeek.Wednesday:
                {
                    if ("Wednesday".TryCopyTo(destination))
                    {
                        charsWritten = 9;
                        return true;
                    }
                    break;
                }
                case global::System.DayOfWeek.Thursday:
                {
                    if ("Thursday".TryCopyTo(destination))
                    {
                        charsWritten = 8;
                        return true;
                    }
                    break;
                }
                case global::System.DayOfWeek.Friday:
                {
                    if ("Friday".TryCopyTo(destination))
                    {
                        charsWritten = 6;
                        return true;
                    }
                    break;
                }
                case global::System.DayOfWeek.Saturday:
                {
                    if ("Saturday".TryCopyTo(destination))
                    {
                        charsWritten = 8;
                        return true;
                    }
                    break;
                }
            }

            return false;
        }
    }
}
} // EnumGeneratorCharTests