Reputation: 1070
In my ASP.NET Core web application, I have classes with some fields that are required if another field has a specific value. For example, I have a class Person
with fields regarding employment, such as job title, employer name, and work starting date. These fields are required only if the enum field Person.EmployementStatus
is equal to Employed
. In order to do this, I created my own RequiredIfAttribute
where it will set the selected property to be invalid if it has a default value and the parent property is equal to a conditional value. In the case of the Person
class, the fields JobTitle
, Employer
, and WorkStartingDate
have the [RequiredIf]
attribute where the parent property is EmployementStatus
and the conditional value is Employed
. Here's the person model class:
[DisplayColumn(nameof(PersonName))]
public class Person
{
[Key]
[Display(Name = "ID")]
public int PersonId { get; set; }
[Required]
[StringLength(128)]
[Display(Name = "Person Name", ShortName = "Name")]
public string PersonName { get; set; }
[Required]
[Display(Name = "Employment Status")]
public PersonEmploymentStatus EmploymentStatus { get; set; }
// This field is required if employment status equals employed
[RequiredIf(nameof(EmploymentStatus), PersonEmploymentStatus.Employed)]
[Display(Name = "Job Title")]
[StringLength(128)]
public string JobTitle { get; set; }
// This field is required if employment status equals employed
[RequiredIf(nameof(EmploymentStatus), PersonEmploymentStatus.Employed)]
[Display(Name = "Employer")]
[StringLength(128)]
public string Employer { get; set; }
// This field is required if employment status equals employed
[RequiredIf(nameof(EmploymentStatus), PersonEmploymentStatus.Employed)]
[Display(Name = "Work Starting Date")]
public DateTime? WorkStartingDate { get; set; }
}
Here's the definition of the RequiredIfAttribute
:
using System;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
/// <summary>
/// Required attribute that depends on a specific value of another property in the same instance
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class RequiredIfAttribute : RequiredAttribute
{
// Value of the property that will make the selected property to be required
private object _propertyConditionalValue;
/// <summary>
/// Name of the property to check its value
/// </summary>
public string ParentPropertyName { get; set; }
/// <summary>
/// Initializes attribute with the parent property and value to check if the selected property is populated or not
/// </summary>
/// <param name="propertyName">Name of the parent property</param>
/// <param name="propertyConditionalValue">Value to check if the parent property is equal to this</param>
public RequiredIfAttribute(string propertyName, object propertyConditionalValue) =>
(ParentPropertyName, _propertyConditionalValue) = (propertyName, propertyConditionalValue);
/// <inheritdoc />
protected override ValidationResult IsValid(object value, ValidationContext context)
{
// Get the parent property
PropertyInfo parentProp = context.ObjectType.GetProperty(ParentPropertyName);
// Get the value of the parent property
object parentPropertyValue = parentProp?.GetValue(context.ObjectInstance);
// Check if the value of the parent property is equal to the conditional value that will require
// the selected property to be populated, and if the selected property is not populated, then return invalid result
if (_propertyConditionalValue.Equals(parentPropertyValue) && value == default)
{
// Display name of the parent property
string parentPropDisplayName = parentProp.Name;
// Try to get the display attribute from the parent property, if it has any
DisplayAttribute displayAttribute = parentProp.GetCustomAttribute<DisplayAttribute>();
if (displayAttribute != null)
{
// Use the name from the display attribute instead
parentPropDisplayName = displayAttribute.Name ?? displayAttribute.ShortName ?? parentPropDisplayName;
}
// Calculate error message
string errorMessage = $"When {parentPropDisplayName} is {_propertyConditionalValue}, {context.DisplayName} is required.";
// Return invalid result
return new ValidationResult(errorMessage);
}
// Otherwise, return a valid result
return ValidationResult.Success;
}
}
This works on the ASP.NET Core web application. If the user selects "Employed" in the form and leaves the rest of the fields empty, an error message displays in the UI like "When Employment Status is Employed, Job Title is required".
However, these fields should be nullable in the database. In case the user in unemployed or self-employed, fields like the employer should have a null value in the database. The problem is that when I add a migration using the Add-Migration
PowerShell script, it sets those fields as non-nullables. This is what the migration looks like:
...
migrationBuilder.CreateTable(
name: "Person",
columns: table => new
{
PersonId = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
PersonName = table.Column<string>(maxLength: 128, nullable: false),
EmploymentStatus = table.Column<byte>(nullable: false),
JobTitle = table.Column<string>(maxLength: 128, nullable: false),
Employer = table.Column<string>(maxLength: 128, nullable: false),
WorkStartingDate = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Person", x => x.PersonId);
});
...
Fields like the Job Title have the parameter "nullable" equal to false when I need them to be equal to true JobTitle = table.Column<string>(maxLength: 128, nullable: false)
. This makes the application crash with a SqlException when the fields are null and the employment status is unemployed or self-employed. It says "Cannot insert the value NULL into column 'Employer', table 'RequiredCascadingAttributeTestContext-ef4bfd77-387d-4cb1-b197-58f1999c04c7.dbo.Person'; column does not allow nulls. INSERT fails.
The statement has been terminated."
I know I can just change the code of the migration, but I have a lot of fields that are using the custom [RequiredIf]
attribute. And every time I add a new migration, a bunch of Alter column statements show up to make the fields non-nullables. So, How can I make EF Core avoid setting fields with the [RequiredIf]
attribute to non-nullables in the migrations? I can't really find a way to accomplish this.
Thanks.
Upvotes: 0
Views: 1035
Reputation: 253
With RequiredIfAttribute you need to implement ValidationAttribute rather than RequiredAtrribute since for EF that has already some behaviour which you do not want to use (in this case setting the field as non-nullable).
Therefore it should look like
public class RequiredIfAttribute : ValidationAttribute
Validations are being called before saving changes to actual database so it is possible to check it there, which you're already doing in your code
Upvotes: 1