📄 core/src/MessageOptions.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.FileProviders.Physical;
using Microsoft.Extensions.Options;

namespace Cfa;

public sealed class MessageOptions
{
    [Required]
    [ValidateEnumeratedItems]
    public List<MessageRoute> MessageRoutes { get; set; } = [];

    public static MessageOptions LoadJson(string? path = null, bool optional = false, string? localPath = null)
    {
        var userPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cfa.json");
        return LoadLayeredJson(
            path,
            optional,
            userPath,
            localPath ?? Path.Combine(Environment.CurrentDirectory, ".cfa.json")
        );
    }

    internal static MessageOptions LoadLayeredJson(
        string? path,
        bool optional,
        string userPath,
        string? localPath = null
    )
    {
        var files = new List<PhysicalFileProvider>();
        try
        {
            using var configuration = new ConfigurationManager();
            AddFile(userPath, true);
            if (localPath is not null)
                AddFile(localPath, true);
            if (path is not null)
                AddFile(path, optional);
            var services = new ServiceCollection();
            services.AddOptions<MessageOptions>().Bind(configuration).Validate<MessageOptionsValidator>();
            using var provider = services.BuildServiceProvider();
            return provider.GetRequiredService<IOptions<MessageOptions>>().Value;

            void AddFile(string file, bool isOptional)
            {
                var fullPath = Path.GetFullPath(file);
                var directory = Path.GetDirectoryName(fullPath)!;
                if (isOptional && !Directory.Exists(directory))
                    return;
                var source = new PhysicalFileProvider(directory, ExclusionFilters.None);
                files.Add(source);
                configuration.AddJsonFile(source, Path.GetFileName(fullPath), isOptional, reloadOnChange: false);
            }
        }
        finally
        {
            foreach (var source in files)
                source.Dispose();
        }
    }
}

public sealed class MessageRoute
{
    [Required]
    [ValidateObjectMembers]
    public PublisherRule Publisher { get; set; } = new();

    [Required]
#pragma warning disable IL2026 // T[] implements ICollection; MinLength does not need reflection.
    [MinLength(1)]
#pragma warning restore IL2026
    [ValidateEnumeratedItems]
    public RegistrationRule[] Registrations { get; set; } = [];
}

public sealed class PublisherRule
{
    [Required]
    [ValidateObjectMembers]
    public MethodSelector Method { get; set; } = new();

    [Range(0, int.MaxValue)]
    public int MessageArgument { get; set; }
}

public sealed class RegistrationRule
{
    [Required]
    [ValidateObjectMembers]
    public MethodSelector Method { get; set; } = new();

    [Range(0, int.MaxValue)]
    public int MessageTypeArgument { get; set; }

    [Range(0, int.MaxValue)]
    public int HandlerArgument { get; set; }
}

public sealed class MethodSelector
{
    [Required]
    public string Type { get; set; } = "";

    [Required]
    public string Name { get; set; } = "";

    [Range(0, int.MaxValue)]
    public int? GenericArity { get; set; }

    [Range(0, int.MaxValue)]
    public int? ParameterCount { get; set; }
}

[OptionsValidator]
internal partial class MessageOptionsValidator : IValidateOptions<MessageOptions>;