【问题标题】:Predefine entity values on creation在创建时预定义实体值
【发布时间】:2013-08-31 10:31:59
【问题描述】:

我有以下通过一对多关系相关的模型类。这些类通过 Code First 方法持久化到 SQL Server 数据库中:

public class Topic
{
    [Key]
    public int Id { get; set; }

    [InverseProperty("Topic")]
    public virtual IList<Chapter> Chapters { get; set; }

    //some other properties...
}

public class Chapter : IValidatableObject
{
    [Key]
    public int Id { get; set; }

    [Required]
    public string Key { get; set }

    public virtual Topic Topic { get; set; }

    //some other properties...
}

每个Topic 包含一堆Chapters。每个Chapter 都有一个Key,在其Topic 中必须是唯一的。

我试图用以下方法验证这一点:

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{  
    var chaptersWithSameKey = Topic.Chapters.Where(t => t.Key == Key);
    foreach (var item in chaptersWithSameKey)
    {
        if (item.Id != Id)
        {
            yield return new ValidationResult("The key must be unique.", new string[] { "Key" });
            break;
        }
    }            
}

但是,在发布到创建或编辑操作后进行验证时,Topic 始终为 null。这似乎是合理的,因为视图不包含有关Topic 的信息。但是,我可以在控制器中提取主题,因为主题的 id 是 URL 的一部分。

我的第一次尝试是在控制器中的 Post Create 操作的开头设置主题:

[HttpPost]
public ActionResult Create(int topicId, Chapter chapter)
{
    var topic = db.Topics.Find(topicId);
    if (topic == null)
        return HttpNotFound();
    chapter.Topic = topic;
    if(ModelState.IsValid)
        ...
}

然而,本章的Validate 方法在控制器执行任何操作之前被调用。所以,本章的主题又是null

另一种方法是通过以下方式告诉创建视图它属于哪个主题:

[HttpGet]
public ActionResult Create(int topicId)
{
    var topic = ...
    var newChapter = new Chapter() { Topic = topic };
    return View(newChapter);
}

并在视图中设置一个隐藏字段:

@Html.HiddenFor(model => model.Topic)
@Html.HiddenFor(model => model.Topic.Id)

第一个像以前一样给出null 主题。这看起来很自然,因为渲染的隐藏字段的值只是主题的ToString() 结果。

第二个似乎试图验证主题,但由于缺少属性而失败。当Topic 的只读属性试图评估另一个null 属性时,实际原因是NullReferenceException。我完全不知道为什么要访问只读属性。调用堆栈有一些Validate... 方法。

那么对于上述情况,最好的解决方案是什么?我正在尝试在模型中进行验证,但缺少一些可以在控制器中检索的必要值。

我可以为此任务创建一个包含int TopicId 而不是Topic Topic 的视图模型。但是我必须将每个属性和注释复制到视图模型或通过继承来完成。第一种方法似乎效率很低。

所以到目前为止,继承方法可能是最好的选择。但是有没有其他选项不需要引入额外的类型?

【问题讨论】:

  • 如果Topic 为空,为什么不向Topic.Chapters.Where 抛出NullReferenceException
  • @haim770 是的,当然。我认为这很明显。

标签: asp.net .net validation asp.net-mvc-4 entity-framework-5


【解决方案1】:

这是我的最终解决方案,稍微改编了 haim 的回答。我不完全同意他的原因是我想在创建(或编辑)实体时强制执行模型层约束。提到的约束(唯一键)是模型层约束,我不明白为什么应该将它移到视图中。

我做了以下事情。 Chapter 保持不变(包括验证属性和自定义验证方法)。我创建了一个视图模型,它继承了Chapter's 属性和行为,并添加了用于标识主题的TopicId 属性。此外,它会覆盖Topic 属性并从数据库中获取主题。

[NotMapped]
public class ChapterViewModel : Chapter
{
    public int TopicId { get; set; }
    public override Topic Topic
    {
        get
        {
            return DbContext.Topics.Find(TopicId);
        }
    }

    private MyDbContext ctx;
    public MyDbContext DbContext
    {
        private get { if (ctx == null) ctx = new CadenzaDbContext(); return ctx; }
        set { ctx = value; }
    }

    public ChapterViewModel() { }
    public ChapterViewModel(Chapter c)
    {
        Id = c.Id;
        TopicId = c.Topic == null ? -1 : c.Topic.Id;
        Key = c.Key;
    }
    public Chapter ToPlainChapter(MyDbContext db)
    {
        DbContext = db;
        return new Chapter()
        {
            Id = Id,
            Topic = Topic,
            Key = Key,
            Name = Name
        };
    }
}

它可以像原来的Chapter一样使用。在查看或访问 DbContext 中的章节跟踪条目时,可能需要与 Chapter 进行特殊转换。

上述方法的优点是我不必将每个属性都复制到视图模型中。此外,现有的验证规则也适用于视图模型,这在我的情况下是所需的行为。任何显示属性只能应用一次,而不是在每个视图模型中。

【讨论】:

    【解决方案2】:

    首先,您必须意识到验证(然后是您的Validate() 方法)是由ModelBinder 在您的Action 执行之前尽早执行的。

    其次,我认为您的主要问题是您没有使用ViewModel,而是将您的Entity/Model 返回到视图并返回到控制器。

    您的视图通常与模型/实体本身具有不同的职责和关注点(就像您的情况一样)。不同的数据结构、不同的验证规则,最重要的是,您可以调整 ViewModel 对象以适应确切的页面/视图需求。

    您当前的Validate() 方法似乎适合数据层验证需求,而不是您的视图验证需求。

    试试这个:

    public class CreateChapterViewModel : IValidatableObject
    {
        public int Id { get; set; }  // possible not needed for 'Create' flow
        public string Key { get; set }
    
        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {  
             // validation logic that applies to Chapter creation only, for example:
             // if (this.Key == null) ...
        }
    }
    

    然后在你的Action:

    [HttpPost]
    public ActionResult Create(int topicId, CreateChapterViewModel chapter)
    {
         ...
    }
    

    总而言之,不要试图强迫你的实体在你的视图上,他们通常有不同的需求,用 ViewModels 给他们提供,让他们也发送 ViewModels。

    这种方法的权衡是您必须将实体映射到 ViewModel 并返回,要么创建自己的映射器,要么使用 AutoMapper 之类的东西。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-01-04
      • 2012-05-10
      相关资源
      最近更新 更多