Custom Validation for nested model in .net core

1.3k Views Asked by At

I am trying to validate a nested model using custom validation. But the problem is AttributeAdapterBase.AddValidation function is never called on nested model. However it works well with simple class property

Custom required validation attribute:

    public interface IId
    {
        long Id { get; set; }
    }

    public class Select2RequiredAttribute : RequiredAttribute
    {
        public Select2RequiredAttribute(string errorMessage = "") : base()
        {
            ErrorMessage = errorMessage;
        }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            Type t = value.GetType();

            if (typeof(IId).IsAssignableFrom(t))
            {
                if ((value as IId).Id == 0)
                {
                    return new ValidationResult(ErrorMessage);
                }
            }
            else
            {
                return new ValidationResult(ErrorMessage);
            }

            return ValidationResult.Success;
        }
}

Attribute adapter base:

public class Select2RequiredAttributeAdapter : AttributeAdapterBase<Select2RequiredAttribute>
{
    public Select2RequiredAttributeAdapter(Select2RequiredAttribute attribute, IStringLocalizer stringLocalizer) : base(attribute, stringLocalizer)
    {
    }

    public override void AddValidation(ClientModelValidationContext context)
    {
        MergeAttribute(context.Attributes, "data-val", "true");
        MergeAttribute(context.Attributes, "data-val-select2-required", GetErrorMessage(context));
    }

    public override string GetErrorMessage(ModelValidationContextBase validationContext)
    {
        return Attribute.ErrorMessage ?? GetErrorMessage(validationContext.ModelMetadata, validationContext.ModelMetadata.GetDisplayName());
    }
}

Adapter provider:

public class Select2RequiredAdapterProvider : IValidationAttributeAdapterProvider
{
    private readonly IValidationAttributeAdapterProvider _baseProvider = new ValidationAttributeAdapterProvider();

    public IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer)
    {
        if (attribute is Select2RequiredAttribute)
        {
            return new Select2RequiredAttributeAdapter(attribute as Select2RequiredAttribute, stringLocalizer);
        }
        else
        {
            return _baseProvider.GetAttributeAdapter(attribute, stringLocalizer);
        }
    }
}

Startup.cs:

services.AddSingleton<IValidationAttributeAdapterProvider, Select2RequiredAdapterProvider>();

Model classes:

public interface IBaseBriefViewModel : IId
{
    string Name { get; set; }
}

public class BaseBriefViewModel : IBaseBriefViewModel
{
    public virtual long Id { get; set; }
    public string Name { get; set; }
}

public class UserViewModel 
{
    public long Id { get; set; }
    public string Name { get; set; }
    [Select2Required("Branch is required.")]
    public BaseBriefViewModel Branch { get; set; }
}

Branch select 2 partial view:

@model DataLibrary.ViewModels.BriefViewModels.BaseBriefViewModel
@{
    var elementId = ViewData["ElementId"] != null && !string.IsNullOrEmpty(ViewData["ElementId"].ToString()) ? ViewData["ElementId"].ToString() : "branch-id";
}

<div class="form-group">
    <label>Branch: <span class="text-danger"></span></label>
    <div class="row">
        <div class="@select2Class">
            @Html.DropDownListFor(model => model.Id, new List<SelectListItem>() {
           new SelectListItem()
           {
               Value = (Model!=null&&Model.Id>0)?Model.Id.ToString():"",
               Text = (Model!=null&&Model.Id>0)?Model.Name:"",
               Selected = (Model!=null&&Model.Id>0)?true:false,
           }}, new { @id = elementId, @class = "form-control disable-field"})
            @Html.ValidationMessageFor(model => model.Id, "", new { @class = "text-danger" })
        </div>
    </div>
</div>

<script>
    $(function () {
        var id = "#" + "@elementId";
        var url = '/Branch/GetBranchsForSelect2';
        var dataArray = function (params) {
            params.page = params.page || 1;
            return {
                prefix: params.term,
                pageSize: pageSize,
                pageNumber: params.page,
            };
        };
        Select2AutoCompleteAjax(id, url, dataArray, pageSize, "---Branch---");
    });
</script>

All this code works well for server side. But for better user experience I want to show error before submitting form. How can I achieve this? I want to use this BaseBriefViewModel for a lot of Select2 in the project. So hard coding a static error message is not a good idea. What I really want to do is pass a error message from parent object. Like Branch is required in this specific case. Maybe in some other class I might pass Product is required

Any direction will be appreciated

1

There are 1 best solutions below

0
aleksander_si On

At the moment this is not supported - but support is in planned. See dotnet github issue:

https://github.com/dotnet/runtime/issues/36093