35

I'm using CodeFirst Entitty framework 5. I have a class representing a user.

public class User
{
    [Key]
    public int UserId { get; set; }

    [Url]
    [DataType(DataType.Url)]
    [Required(AllowEmptyStrings= true)]
    public string WebSite { get; set; }

    [Phone]
    [DataType(DataType.PhoneNumber)]
    [Required(AllowEmptyStrings = true)]
    public string Phone { get; set; }

    [Phone]
    [DataType(DataType.PhoneNumber)]
    [Required(AllowEmptyStrings = true)]
    public string Fax { get; set; }
}

I like the validation mechanics for Phone and Url attributes a lot, but unfortunately validation fails when fields marked with these attributes are empty strings which I actually want to allow. [Required(AllowEmptyStrings = true)] doesn't seem to work with Phone or Url attributes. The same seems to apply to some other DataAnnotations attributes like EmailAddress.

Is there a way to allow empty strings for fields marked with such attributes?

Nu-hin
  • 691
  • 1
  • 7
  • 12

5 Answers5

45

Use following two data annotations:

[Required(AllowEmptyStrings = true)]
[DisplayFormat(ConvertEmptyStringToNull = false)]
Jonesopolis
  • 25,034
  • 12
  • 68
  • 112
user5791468
  • 451
  • 4
  • 3
25

Validation attributes like [Phone] and [EmailAddress] will check any non-null string values. Because the string type is inherently nullable, empty strings passed to the ModelBinder are read as null, which passes the validation check.

When you add the [Required] attribute, the string becomes effectively non-nullable. (If using Code First, EF will script a non-nullable database column.) The ModelBinder will now interpret a blank value as String.Empty - which will fail the attribute validation check.

So there is no way to allow empty strings with validation attributes, but you can allow null strings. All you need to do is remove the [Required] attribute. Blank values will be null and non-blank values will be validated.

In my case, I am importing records from a CSV file, and had this problem because I was skipping the normal ModelBinder. If you are doing something unusual like this, be sure to include a manual check before saving to your data model:

Email = (record.Email == String.Empty) ? null : record.Email
Neil Laslett
  • 2,019
  • 22
  • 22
  • I wasn't using Model Binder. I was writing data importer and the source data contains empty emails and phones. I guess I should just convert empty strings from source to nulls then. Thanks for the explanation! – Nu-hin Dec 20 '13 at 19:18
  • 1
    @Nu-hin. Your DBA will thank you in the future if you get in a habbit of cleaning up ambiguous pointless values like empty strings and replacing them with null. Null is undefined or unknown. A empty string phone number is undefined or unknown. Always fun to do work on a db years later and find a mix of null, empty strings, whitespace strings, and the always helpful "NO PHONE NUMBER" in the phone number field. – Gerald Davis May 13 '15 at 15:50
  • This seems pretty unhelpful. Yes in *some* situations, the absence of a value means "not known", but "Definitely empty" means something quite different from "Not known". So there are cases where a empty string is not a "pointless value". – Ian Griffiths Sep 24 '15 at 12:52
  • This did not work for me with the DomainAttribute but did work with the EmailAddressAttribute! – daniel.caspers Oct 04 '16 at 22:13
  • 1
    This does not seem to work anymore with `System.ComponentModel.DataAnnotations.EmailAddressAttribute` as empty strings are failing validation, even though the field is not required – Mord Zuber Dec 05 '17 at 10:42
  • @MordZuber Interesting. Two options: either `EmailAddressAttribute` has been updated to validate `null` strings or the ModelBinder is setting your blank incoming value to `string.Empty` instead of `null`. Can you remove the validation and check the actual value set by ModelBinder? – Neil Laslett Dec 06 '17 at 19:03
  • My case was string.Empty being sent over the wire. Since the validator only checks for null, it was failing, so I just wrote a new wrapper attribute – Mord Zuber Dec 06 '17 at 19:05
  • `EmailAddressAttribute` definitely still passes null values. https://referencesource.microsoft.com/#System.ComponentModel.DataAnnotations/DataAnnotations/EmailAddressAttribute.cs I wonder why it was setting it to empty? – Neil Laslett Dec 06 '17 at 19:06
  • Good reference: https://stackoverflow.com/questions/3641723/why-do-i-get-null-instead-of-empty-string-when-receiving-post-request-in-from-ra – Neil Laslett Dec 06 '17 at 19:10
2

I did something similar below.

  [JsonProperty("phone")]
  [NullablePhone]
  public string Phone { get; set; }

/// <summary>
/// Validation attribute for phone numbers.
/// </summary>
/// <seealso cref="ValidationAttribute" />
public class NullablePhoneAttribute : ValidationAttribute
{
    /// <summary>
    /// Returns true if phone is empty or valid.
    /// </summary>
    /// <param name="value">The value of the object to validate.</param>
    /// <returns>
    ///   <see langword="true" /> if the specified value is valid; otherwise, <see langword="false" />.
    /// </returns>
    public override bool IsValid(object value)
    {
        if (value == null)
        {
            return true;
        }
        if (string.IsNullOrEmpty(value.ToString()))
        {
            return true;
        }
        PhoneAttribute phone = new PhoneAttribute();
        return phone.IsValid(value);
    }

}
Teoman shipahi
  • 47,454
  • 15
  • 134
  • 158
0

Only way I could have made this work was to take source code of .Net and adjusted it myself to allow nullable values for email attribute.

Code can be found here: https://referencesource.microsoft.com/#System.ComponentModel.DataAnnotations/DataAnnotations/EmailAddressAttribute.cs

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public sealed class NullableEmailAddressAttribute : DataTypeAttribute
{

    // This attribute provides server-side email validation equivalent to jquery validate,
    // and therefore shares the same regular expression.  See unit tests for examples.
    private static Regex _regex = CreateRegEx();

    public NullableEmailAddressAttribute()
        : base(DataType.EmailAddress)
    {

        // DevDiv 468241: set DefaultErrorMessage not ErrorMessage, allowing user to set
        // ErrorMessageResourceType and ErrorMessageResourceName to use localized messages.
        // DefaultErrorMessage = DataAnnotationsResources.EmailAddressAttribute_Invalid;
    }

    public override bool IsValid(object value)
    {
        if (value == null)
            return true;

        string valueAsString = value as string;

        if (string.IsNullOrEmpty(valueAsString))
            return true;

        // Use RegEx implementation if it has been created, otherwise use a non RegEx version.
        if (_regex != null)
        {
            return valueAsString != null && _regex.Match(valueAsString).Length > 0;
        }
        else
        {
            int atCount = 0;

            foreach (char c in valueAsString)
            {
                if (c == '@')
                {
                    atCount++;
                }
            }

            return (valueAsString != null
            && atCount == 1
            && valueAsString[0] != '@'
            && valueAsString[valueAsString.Length - 1] != '@');
        }
    }

    private static Regex CreateRegEx()
    {
        // We only need to create the RegEx if this switch is enabled.
        //if (AppSettings.DisableRegEx)
        //{
        //    return null;
        //}

        const string pattern = @"^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$";
        const RegexOptions options = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture;

        // Set explicit regex match timeout, sufficient enough for email parsing
        // Unless the global REGEX_DEFAULT_MATCH_TIMEOUT is already set
        TimeSpan matchTimeout = TimeSpan.FromSeconds(2);

        try
        {
            if (AppDomain.CurrentDomain.GetData("REGEX_DEFAULT_MATCH_TIMEOUT") == null)
            {
                return new Regex(pattern, options, matchTimeout);
            }
        }
        catch
        {
            // Fallback on error
        }

        // Legacy fallback (without explicit match timeout)
        return new Regex(pattern, options);
    }
}

Usage is like so:

    [NullableEmailAddress]
    public string DigitalInvoiceEmail { get; set; }
Kadaj
  • 615
  • 3
  • 13
  • 31
0

I've just come accros this very issue in .NET 6 and realized that - when nullable reference types are enabled - the builtin phone attribute causes validation to fail when the textbox is empty. In order to allow empty value & null for the phone attribute in that case, simply mark the string property for the phone number as nullable.

[Display(Name = "Business phone (optional)")]
public string? PhoneNumber { get; set; }

or dissable nullable reference type handling for the page model class by prepending

#nullable disable
Sven Mawby
  • 645
  • 6
  • 16