0

Using ASP.NET Core 2.2 Razor Pages, I'm exploring binding radio buttons and dropdownlists to the page model.

Plenty of people are asking about client-side validation to "get it to work".

My question is: when I look at this code. Is there any server-side check being done by the binding engine?

@foreach (var gender in Model.Genders)
{
    <input type="radio" asp-for="Gender" value="@gender" id="Gender@(gender)" /> @gender
}

@Html.DropDownListFor(x => x.Country, new List<SelectListItem>
{
    new SelectListItem() {Text = "Canada", Value="CA"},
    new SelectListItem() {Text = "USA", Value="US"},
    new SelectListItem() {Text = "Mexico", Value="MX"}
})  

What's preventing someone from posting gender "bababa" and country "xxx", which could cause undefined behaviors in my code and database?

I'd be surprised if the above code is doing such validation (correct me if I'm wrong), and I couldn't find posts asking about that because everyone is asking about client-side validation.

What's the recommend approach here?

Etienne Charland
  • 3,424
  • 5
  • 28
  • 58

2 Answers2

0

Server side and client side validations are important, you always need implement server side validations, maybe your client validations could be omited but never the server side validations, the code that you posted not perform any server side validation

Zach dev
  • 1,610
  • 8
  • 15
  • That's what I'm saying. Found tons of people talking about radio buttons and dropdownlist but NONE has any server-side validation whatsoever! What's the right approach? CustomValidator validating against a list? Then there's the problem I can't pass a dynamic list or object to an attribute, must be static data. Every single person using a radio or dropdown "should" have the solution to this, right? Or there's a LOT of vulnerable sites out there. – Etienne Charland May 17 '19 at 03:12
  • Found this for Enum validation which covers some radio button cases. Will have to keep looking for the dropdownlist. https://stackoverflow.com/questions/26425242/validating-enum-values-within-c-sharp-mvc-partial-validation-occurs-how-to-ch – Etienne Charland May 17 '19 at 09:24
0

Came up with my own elegant solution since I found nothing out there.

With the helper class below, I'll declare my model with this

[BindProperty]
public InputList Gender { get; set; } = new InputList(new[] { "Man", "Woman" });

[BindProperty]
public InputList Country { get; set; } = new InputList(new NameValueCollection()
{
    { "", "--Select--" },
    { "CA", "Canada" },
    { "US", "USA" },
    { "MX", "Mexico" }
});

Insert radio buttons and a dropdown list on my page

@foreach (var item in Model.Gender.ListItems)
{
    <input type="radio" asp-for="Gender.Value" value="@item.Value" id="Gender@(item.Value)" /><label for="Gender@(item.Value)" style="padding-right:15px;"> @item.Text </label>
}
<span asp-validation-for="Gender" class="text-danger"></span>

@Html.DropDownListFor(x => x.Country.Value, Model.Country.ListItems)
<span asp-validation-for="Country" class="text-danger"></span>

And voilà! Validation works both on the client-side and server-side, ensuring posted value is valid.

Of course, can move "Man" and "Woman" into constants, and can move the list of countries into a separate class that generates it once for the whole application.

Here's the InputList helper class.

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace EmergenceGuardian.WebsiteTools.Web
{
    /// <summary>
    /// Represents a list of items to display as radio buttons or drop down list that can be bound to a web page and validated.
    /// </summary>
    [InputListValidation]
    public class InputList
    {
        /// <summary>
        /// Initializes a new instance of InputList with specified list of items that will be used for both the value and text.
        /// </summary>
        /// <param name="values">A list of string values reprenting valid values.</param>
        /// <param name="required">Whether this field is required.</param>
        public InputList(IEnumerable<string> values, bool required = true)
        {
            Required = required;
            foreach (var item in values)
            {
                ListItems.Add(new SelectListItem(item, item));
            }
        }

        /// <summary>
        /// Initializes a new instance of InputList with specified list of SelectListItem objects.
        /// </summary>
        /// <param name="values">A list of SelectListItem objects representing display text and valid values.</param>
        /// <param name="required">Whether this field is required.</param>
        public InputList(IEnumerable<SelectListItem> values, bool required = true)
        {
            Required = required;
            ListItems.AddRange(values);
        }

        /// <summary>
        /// Initializes a new instance of InputList with a NameValueCollection allowing quick collection initializer.
        /// </summary>
        /// <param name="values">The NameValueCollection containing display texts and valid values.</param>
        /// <param name="required">Whether this field is required.</param>
        public InputList(NameValueCollection values, bool required = true)
        {
            Required = required;
            foreach (var key in values.AllKeys)
            {
                ListItems.Add(new SelectListItem(values[key], key));
            }
        }

        /// <summary>
        /// Gets or sets whether this field is required.
        /// </summary>
        public bool Required { get; set; }
        /// <summary>
        /// Gets or sets the list of display text and valid values, used for display and validation.
        /// </summary>
        public List<SelectListItem> ListItems { get; set; } = new List<SelectListItem>();
        /// <summary>
        /// Gets or sets the user input value. This value can be bound to the UI and validated by InputListValidation.
        /// </summary>
        public string Value { get; set; }
    }

    /// <summary>
    /// Validates an InputList class to ensure Value is contained in ListItems.
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    sealed public class InputListValidationAttribute : ValidationAttribute
    {
        private const string DefaultErrorMessage = "Selected value is invalid.";
        private const string DefaultRequiredErrorMessage = "The {0} field is required.";

        public InputListValidationAttribute()
        {
        }

        /// <summary>
        /// Validates whether InputList.Value contains a valid value.
        /// </summary>
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            var input = value as InputList;
            if (input != null)
            {
                if (string.IsNullOrEmpty(input.Value))
                {
                    if (input.Required)
                    {
                        return new ValidationResult(string.Format(ErrorMessage ?? DefaultRequiredErrorMessage, validationContext.MemberName));
                    }
                }
                else if (input.ListItems?.Any(x => x.Value == input.Value) == false)
                {
                    return new ValidationResult(ErrorMessage ?? DefaultErrorMessage);
                }

            }
            return ValidationResult.Success;
        }
    }
}
Etienne Charland
  • 3,424
  • 5
  • 28
  • 58
  • give to model access to the persistence layer by his own is not the best practice. Works? yes of course but there are other ways, you can explore and implement something more smooth and easily to mantein with FluentValidation – Zach dev May 17 '19 at 14:19
  • FluentValidation can handle the validation part. For a dropdownlist of countries, I still need to pass a list of SelectListItem, and I don't want to repeat that list every time I use it. I don't think FluentValidation will help with that. – Etienne Charland May 17 '19 at 15:52
  • FluentValidator doesn't easily give me access to the class containing the list of countries, I haven't found a method "IsInList", and so far only complicates things without giving any clear solution. But then there are also issues with my code above: you could bind to InputList.Required and override the validation rules! Then there's this bug I bumped into. Overall, 2 days into this and I still haven't found a satisfying solution. https://github.com/aspnet/AspNetCore/issues/10330 – Etienne Charland May 18 '19 at 08:51