简体   繁体   中英

Fluent Validation C# Unique Id in collection

I have an entity that has a nested collection as a property which I will receive from a front-end application.

I just want to make sure that each item in this collection(ICollection ChildClassCollection) has a unique Id within the model I have received.

I am using FluentValidation and would like to add this validation using it too for consistency.

It's something very simple I couldn`t find an elegant way to solve..

An example:

public class ParentClass
{
    public string Name { get; set; }

    public ICollection<ChildClass> ChildClassCollection { get; set; }
}

public class ChildClass
{
    public int Id { get; set; }

    public string Name { get; set; }
}

Use HashSet. https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1?view=netframework-4.7.2

public class ParentClass
{
    public string Name { get; set; }

    public HashSet<ChildClass> ChildClassCollection { get; set; }
}

public class ChildClass
{
    public int Id { get; set; }

    public string Name { get; set; }
}

Do you want to use only fluentValidator to achieve this without any relationship ?

If so, you can custom a validator,then use some logic to confirm whether there is a duplicate value

in database,like this:

public class ChildClassValidator : AbstractValidator<ChildClass>
{
    private readonly MyDbContext _context;
    public ChildClassValidator(MyDbContext context)
    {
        _context = context;
        RuleFor(x => x.Id).NotEmpty().WithMessage("ID is required.").Must(IsUnique).WithMessage("parent have more than one ids");
    }

    private bool IsUnique(int id)
    {
        var model = _context.ParentClasses.GroupBy(x => x.Id)
           .Where(g => g.Count() > 1)
           .Select(y => y.Key)
           .ToList();  //judge whether parentclass has duplicate id
        if (model==null) 
            return true;
        else return false;
    }
}

Here is what I ended up with: PRetty cleand, plus, when it encounters issue, it will exit

           this.RuleFor(or => or.ChildClassCollection)
               .Must(this.IsDistinct)
               .WithMessage("There are more than one entity with the same Id");

        public bool IsDistinct(List<UpdateRoleDTO> elements)
        {
            var encounteredIds = new HashSet<int>();

            foreach (var element in elements)
            {
                if (!encounteredIds.Contains(element.Id))
                {
                   encounteredIds.Add(element.Id);
                }
                else
                {
                    return false;
                }
            }

            return true;
       }

I use mine custom helper, based by ansver: Check a property is unique in list in FluentValidation . Because the helper is much more convenient to use than each time to describe the rules. Such a helper can be easily chained with other validators like IsEmpty .

Code:

public static class FluentValidationHelper
{
    public static IRuleBuilder<T, IEnumerable<TSource>> Unique<T, TSource, TResult>(
        this IRuleBuilder<T, IEnumerable<TSource>> ruleBuilder,
        Func<TSource, TResult> selector, string? message = null)
    {
        if (selector == null)
            throw new ArgumentNullException(nameof(selector), "Cannot pass a null selector.");

        ruleBuilder
            .Must(x =>
            {
                var array = x.Select(selector).ToArray();
                return array.Count() == array.Distinct().Count();
            })
            .WithMessage(message ?? "Elements are not unique.");
        return ruleBuilder;
    }
}

Usage:

For example we want to choose unique id

RuleFor(_ => _.ChildClassCollection!)
    .Unique(_ => _.Id);

Or want choose unique id and name

RuleFor(_ => _.ChildClassCollection!)
    .Unique(_ => new { _.Id, _.Name });

Update

I optimization performance using code from answer YoannaKostova

public static class FluentValidationHelper
{
    
    public static bool IsDistinct<TSource, TResult>(this IEnumerable<TSource> elements, Func<TSource, TResult> selector)
    {
        var hashSet = new HashSet<TResult>();
        foreach (var element in elements.Select(selector))
        {
            if (!hashSet.Contains(element))
                hashSet.Add(element);
            else
                return false;
        }
        return true;
    }


    public static IRuleBuilder<T, IEnumerable<TSource>> Unique<T, TSource, TResult>(
        this IRuleBuilder<T, IEnumerable<TSource>> ruleBuilder,
        Func<TSource, TResult> selector, string? message = null)
    {
        if (selector == null)
            throw new ArgumentNullException(nameof(selector), "Cannot pass a null selector.");

        ruleBuilder
            .Must(x => x.IsDistinct(selector))
            .WithMessage(message ?? "Elements are not unique.");
        return ruleBuilder;
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM