提问者:小点点

EF核心-如何避免自定义RequiredAttribute将数据库列设置为不可为空


在我的ASP.NET核心web应用程序中,我与某些字段进行了类,如果另一个字段具有特定值,则这些字段是必需的。例如,我有一个类person,其中包含与雇用有关的字段,如职务,雇主名称和工作开始日期。只有当枚举字段Person.EmploymentStatus等于Employed时,才需要这些字段。为了做到这一点,我创建了自己的RequireDifAttribute,如果选择的属性具有默认值,并且父属性等于条件值,则它将在其中设置该属性无效。对于Person类,字段JobTitleEmployerWorkStartingDate具有[RequiredIf]属性,其中父属性为EmploymentStatus,条件值为Employed。下面是person模型类:

[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; }
}

以下是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;
    }
}

这适用于ASP.NET核心web应用程序。如果用户在表单中选择“Employed”,而将其余字段保留为空,则在UI中会显示一条错误消息,类似于“当Employed Status为Employed时,需要职称”。

但是,这些字段在数据库中应该是可以为空的。如果用户处于“失业”或“自营职业”状态,则数据库中的“雇主”等字段应为空值。问题是,当我使用add-migrationPowerShell脚本添加迁移时,它将那些字段设置为不可为空。迁移是这样的:

...
            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);
                });
...

像Job Title这样的字段的参数“nullable”等于false,而我需要它们等于truejobtitle=table.column(maxlength:128,nullable:false)。当字段为空且雇用状态为失业或自营职业时,这会使应用程序在SqlException下崩溃。它指出“无法将值NULL插入到表'RequiredCascadingAttributeTestContext-EF4BFD77-387D-4CB1-B197-58F1999C04C7.DBO.Person‘中的列'E雇主’中;列不允许NULL.insert失败。语句已终止。”

我知道我可以更改迁移的代码,但是我有很多字段使用自定义的[RequiredIf]属性。每次添加新迁移时,都会显示一组Alter column语句,使字段不可为NULL。那么,如何使EF Core避免在迁移中将具有[RequiredIf]属性的字段设置为非空值呢?我真的找不到办法来完成这件事。

谢了。


共1个答案

匿名用户

对于RequiredIfAttribute,您需要实现ValidationAttribute而不是RequiredAtrribute,因为对于EF,它已经有了一些您不想使用的行为(在本例中,将字段设置为不可为空)。

因此它应该看起来像

public class RequiredIfAttribute : ValidationAttribute

在将更改保存到实际数据库之前,会调用验证,这样就可以在那里检查它,您已经在代码中这样做了