Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use (string message) constructor when exception customization is an exception type #58

Open
truegoodwill opened this issue Jun 10, 2022 · 2 comments

Comments

@truegoodwill
Copy link
Contributor

truegoodwill commented Jun 10, 2022

Currently, when an exception type is specified, the exception will be created using the parameterless constructor.

It would be nice if the exception message could be set using the (string message) constructor.

Here's one example of how to achieve that. It does so without any change in configuration so it would be a possibly unexpected change in behaviour for some users. It doesn't break any of the existing unit tests however.

I'd like some feedback on how you see the issue before actually making a pull request for it:

throw exceptionCustomizations.Customization.Match(
    message => new ArgumentException(message: message ?? generalMessage, paramName: paramName),
    type => Create(type, Combine(generalMessage, paramName)),
    func => func(),
    func => func(paramName));

static string Combine(string message, string paramName) => $"{message} (Parameter '{paramName}')";

static Exception Create(Type exceptionType, string message)
{
    if (!Constructors.TryGetValue(exceptionType, out var constructor))
    {
        foreach (var constructorInfo in exceptionType.GetConstructors(Instance | Public | NonPublic))
        {
            var parameters = constructorInfo.GetParameters();
            if (parameters.Length == 1 && parameters[0].ParameterType == typeof(string))
            {
                constructor = message => (Exception)constructorInfo.Invoke(new[] { message })!;
                Constructors[exceptionType] = constructor;
                return constructor(message);
            }
        }

        Constructors[exceptionType] = null!;
    }

    return constructor?.Invoke(message) ?? (Exception)Activator.CreateInstance(type)!;
}

private static readonly Dictionary<Type, Func<string?, Exception>?> Constructors = new();
@truegoodwill
Copy link
Contributor Author

truegoodwill commented Jun 10, 2022

I'd also really like to see a Func<string, string, Exception> choice added to the ExceptionCustomizations OneOf monad that takes both paramName and message as parameters.

For bonus points, I'd also like to be able to add a object? State property to the ExceptionCustomizations struct that would allow an additional callback choice used to create exceptions: Func<string, string, object?, Exception>. It helps avoid allocations due to closures in exception creation methods that need some additional state information.

In fact, I need these features and will be putting them in a fork unless we're able to discuss them here and hopefully have them put in the main library in a way that aligns with your vision.

Thank you, and nice to meet you, and great work on the library.

@truegoodwill
Copy link
Contributor Author

truegoodwill commented Jun 10, 2022

Sample usage:

public interface ICommand
{
}

public sealed record MyCommand(string Value1, string Value2) : ICommand
{
}

public sealed class CommandRejectedException : Exception
{
    public ICommand? Command { get; }

    public CommandRejectedException(ICommand? command, string? reason)
        : base($"'{command?.GetType().Name ?? "Null"}' command was rejected. {reason}")
    {
        this.Command = command;
    }
}

public static class CommandValidationExtensions
{
    public static Validatable<TCommand> ThrowCommandRejected<TCommand>(this TCommand command, [CallerArgumentExpression("command")] string? paramName = "")
        where TCommand : notnull, ICommand
    {
        var exceptionThrower = (paramName, message, state) => new CommandRejectedException((ICommand)state, $"{message} (Parameter '{paramName}')");
        return new Validatable<TCommand>(command, paramName!, exceptionThrower).WithState(command);
    }
}

[TestMethod]
public void MyCommandTest()
{
    var command = new MyCommand("Hello", "World");

    var action = () => command.ThrowCommandRejected().IfLongerThan(static x => x.Value2, 4);

    action.Should()
        .ThrowExactly<CommandRejectedException>()
        .WithMessage(@"'MyCommand' command was rejected. String should not be longer than 4 characters. (Parameter 'command: static x => x.Value2')")
        .Which.Command.Should().BeSameAs(command);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant