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

Validation reuse #327

Open
glen-84 opened this issue Jan 10, 2023 · 8 comments
Open

Validation reuse #327

glen-84 opened this issue Jan 10, 2023 · 8 comments
Labels
enhancement New feature or request

Comments

@glen-84
Copy link

glen-84 commented Jan 10, 2023

Describe the feature

It's currently possible to do something like this:

[ValueObject]
public readonly partial struct ArticleId
{
    private static Validation Validate(int value) => IdValidator.IsValid(value);
}

[ValueObject<short>]
public readonly partial struct LocaleId
{
    private static Validation Validate(short value) => IdValidator.IsValid(value);
}

// Define once.
public static class IdValidator
{
    public static Validation IsValid(long value)
    {
        return value > 0
            ? Validation.Ok
            : Validation.Invalid("ID must be a positive integer.");
    }
}

But that line of code seems unnecessary. What about a parameter to specify the type of a static validation class?

[ValueObject(validator: typeof(IdValidator))]
public readonly partial struct ArticleId { }

A generic overload could also work, but if I'm not mistaken it would require you to always specify the underlying type.

[ValueObject<int, IdValidator>]
public readonly partial struct ArticleId { }

I think that would be worth it though.

@glen-84 glen-84 added the enhancement New feature or request label Jan 10, 2023
@SteveDunn
Copy link
Owner

That's a great idea - thanks @glen-84 ! I'll take a look at implementing that soon.

@glen-84
Copy link
Author

glen-84 commented Dec 14, 2023

Is this still planned? It could be useful.

One thing to note is that the Validate method (if one exists) should still be called, even when you specify a validator on the attribute itself, to allow for additional validation of a specific value object.

It might also make sense for the call to the Validator class to pass the value object type, like:

IdValidator.IsValid(typeof(ArticleId), value);

// and/or

IdValidator.IsValid<ArticleId>(value);

So that the validator can access information about the value object type, or access its (static abstract) properties.

Ultimately, I'm looking for a way to "bake" some validation into a value object. For example, given the following value objects:

public sealed class StringValueAttribute : ValueObjectAttribute<string>
{
    public StringValueAttribute(Conversions conversions = Conversions.None) : base(conversions) { }
}

[StringValue]
public sealed partial class EmailAddress
{
    public static int MinimumLength { get; } = 3;
    public static int MaximumLength { get; } = 60;
}

[StringValue]
public sealed partial class Username
{
    public static int MinimumLength { get; } = 2;
    public static int MaximumLength { get; } = 20;
}

... a way to validate these objects while making use of their properties, without having to manually call a validation method within Validate.

@SteveDunn
Copy link
Owner

Thanks for the feedback @glen-84 - sorry that this feature has taken so long to implement. I haven't had much time lately.

Re. your suggestion above: are you saying that the validator type somehow 'inspects' the type for known properties such as the length properties, and generates the associated code? I'm thinking, as I write this, that maybe an AutoValidated attribute would be useful, and would work something like this:

  • has the type got a Vogen attribute?
  • if so, has it got an AutoValidated attribute?
  • if so, has it got any of the known properties that control validation

... and this makes me think of FluentValidation.

I think there's definitely an opportunity to improve validation, but I'm also thinking that it might be better as a completely separate nuget package and just have Vogen provide 'hooks' for this new package...

@glen-84
Copy link
Author

glen-84 commented Dec 17, 2023

Re. your suggestion above: are you saying that the validator type somehow 'inspects' the type for known properties such as the length properties, and generates the associated code?

Nope. The validator class would be manually written. Something like this:

public interface IStringValue
{
    public static abstract int MinimumLength { get; }
    public static abstract int MaximumLength { get; }
}

public static class MyStringValidator
{
    public static Validation IsValid<TValueObject>(string value) where TValueObject : IStringValue
    {
        var minimumLength = TValueObject.MinimumLength;
        var maximumLength = TValueObject.MaximumLength;

        // validation here ...
    }
}

public sealed class StringValueAttribute : ValueObjectAttribute<string>
{
    public StringValueAttribute(
        Conversions conversions = Conversions.None)
        : base(conversions, validator: typeof(MyStringValidator)) { }
}

[StringValue]
public sealed partial class EmailAddress : IStringValue
{
    public static int MinimumLength { get; } = 3;
    public static int MaximumLength { get; } = 60;
}

[StringValue]
public sealed partial class Username : IStringValue
{
    public static int MinimumLength { get; } = 2;
    public static int MaximumLength { get; } = 20;
}

I don't know if static abstract properties are the best solution, but it's just an idea.

@marcwittke
Copy link

I was looking for this exact feature right now. When it comes to strings the main use would be validation of lenghts, requiredness, and maybe some functional validation (EmailAddress, IpAddress come to mind)

would it be an idea to mimic the attributes from System.ComponentModel.DataAnnotations and have similar ones for value types (on classes and structs, since the original ones can only be applied to properties)

[StringValue]
[MinLength(2)]
[MaxLength(20)]
public sealed partial class Username 
{
}

@SteveDunn
Copy link
Owner

It certainly looks like this would be useful. I'll see if I can add it early next year.

@MrSanchez
Copy link

This feature would be quite nice indeed.
I use ValueObjects for various things including for my entity IDs and for each I want to validate that the ID string is parsable to a Mongo ObjectId format (I also have other IDs that check if it's Guid-parsable, etc). Given that the system has 20+ entities the following code is repeated 20+ times which is unfortunate:

    [ValueObject<string>(toPrimitiveCasting: CastOperator.Implicit)] // legacy support
    public partial class MyEntityNameId
    {
        private static Validation Validate(string id) => MongoObjectIdValidator.IsObjectId(id);
    }

Would be nice if this could be reduced to:

    [MongoIdValueObject] // custom attribute that wraps/inherits from ValueObjectAttribute<string> and sets toPrimitiveCasting and defines a custom validator like typeof(MongoObjectIdValidator)
    public partial class MyEntityNameId { } 

Even for VOs that don't have custom validators it would be valuable to support custom attributes like [StringValueObject] that allows one to skip the toPrimitiveCasting default setting that I for example have for all the IDs, but not all VOs.

@glen-84
Copy link
Author

glen-84 commented Jan 5, 2025

@MrSanchez

Even for VOs that don't have custom validators it would be valuable to support custom attributes like [StringValueObject] that allows one to skip the toPrimitiveCasting default setting that I for example have for all the IDs, but not all VOs.

Doesn't this already work?

public sealed class StringValueObjectAttribute()
    : ValueObjectAttribute<string>(toPrimitiveCasting: CastOperator.Implicit);

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

No branches or pull requests

4 participants