【发布时间】:2020-11-21 17:00:53
【问题描述】:
在我的 ASP.NET Core Web 应用程序中,如果另一个字段具有特定值,则我的类具有一些必需的字段。例如,我有一个课程Person,其中包含有关就业的字段,例如职位、雇主名称和工作开始日期。仅当枚举字段 Person.EmployementStatus 等于 Employed 时,才需要这些字段。为了做到这一点,我创建了我自己的RequiredIfAttribute,如果它有一个默认值并且父属性等于一个条件值,它会将选定的属性设置为无效。对于Person 类,字段JobTitle、Employer 和WorkStartingDate 具有[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<string>(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