【问题标题】:EF Core - How to avoid a custom RequiredAttribute to set a database column to be not nullableEF Core - 如何避免自定义RequiredAttribute 将数据库列设置为不可为空
【发布时间】:2020-11-21 17:00:53
【问题描述】:

在我的 ASP.NET Core Web 应用程序中,如果另一个字段具有特定值,则我的类具有一些必需的字段。例如,我有一个课程Person,其中包含有关就业的字段,例如职位、雇主名称和工作开始日期。仅当枚举字段 Person.EmployementStatus 等于 Employed 时,才需要这些字段。为了做到这一点,我创建了我自己的RequiredIfAttribute,如果它有一个默认值并且父属性等于一个条件值,它会将选定的属性设置为无效。对于Person 类,字段JobTitleEmployerWorkStartingDate 具有[RequiredIf] 属性,其中父属性为EmployementStatus,条件值为Employed。这是人模型类:

[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 Core Web 应用程序。如果用户在表单中选择“已就业”并将其余字段留空,则 UI 中会显示一条错误消息,例如“当就业状态为已就业时,需要职位”。

但是,这些字段在数据库中应该可以为空。如果用户处于失业或自雇状态,像雇主这样的字段在数据库中应该有一个空值。问题是当我使用Add-Migration PowerShell 脚本添加迁移时,它会将这些字段设置为不可为空的。这是迁移的样子:

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

当我需要它们等于 true JobTitle = table.Column&lt;string&gt;(maxLength: 128, nullable: false) 时,像 Job Title 这样的字段的参数“nullable”等于 false。当字段为空且就业状态为失业或自雇人士时,这会使应用程序崩溃并引发 SqlException。它说“无法将值 NULL 插入 'Employer' 列,表 'RequiredCascadingAttributeTestContext-ef4bfd77-387d-4cb1-b197-58f1999c04c7.dbo.Person';列不允许空值。插入失败。 声明已终止。”

我知道我可以只更改迁移代码,但我有很多字段使用自定义 [RequiredIf] 属性。每次我添加一个新的迁移时,都会出现一堆 Alter 列语句以使字段不可为空。那么,如何使 EF Core 避免在迁移中将具有 [RequiredIf] 属性的字段设置为不可为空的值?我真的找不到实现此目的的方法。

谢谢。

【问题讨论】:

  • RequiredIfAttribute => 在这里您可能想要实现 ValidationAttribute 而不是 RequiredAtrribute 因为您只想验证而不是让 EF 基于它生成一些东西。 ValidateAttribute 应该在保存之前调用
  • @AndrejDobeš 太棒了,我没想到。有用!非常感谢。
  • 太好了,那我就把它作为答案
  • 尽管可行,但您可能需要考虑使用一组不同的模型,并且在您的公共 api 中使用您的数据库模型。然后,您将所需的 if 放在您的视图模型上,并使它们远离您的数据模型并在两者之间进行映射。然后,您只需使用 EF 关心的内容来装饰您的数据模型。此外,现在您也不必担心过度发布攻击,因为您自己处理映射。我很惊讶没有人提到在您的 api 界面中避免使用 EF 数据模型

标签: c# entity-framework asp.net-core entity-framework-core


【解决方案1】:

使用RequiredIfAttribute,您需要实现ValidationAttribute 而不是RequiredAtrribute,因为EF 已经有一些您不想使用的行为(在这种情况下,将字段设置为不可为空)。

因此它应该看起来像

public class RequiredIfAttribute : ValidationAttribute

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

【讨论】:

    猜你喜欢
    • 2017-07-16
    • 1970-01-01
    • 1970-01-01
    • 2022-11-04
    • 2021-04-17
    • 1970-01-01
    • 1970-01-01
    • 2019-01-25
    • 1970-01-01
    相关资源
    最近更新 更多