ASP.NET MVC 4,EF5,模型中的唯一属性 - 最佳实践?

Posted

技术标签:

【中文标题】ASP.NET MVC 4,EF5,模型中的唯一属性 - 最佳实践?【英文标题】:ASP.NET MVC 4, EF5, Unique property in model - best practice? 【发布时间】:2013-05-16 17:29:55 【问题描述】:

ASP.NET MVC 4、EF5、代码优先、SQL Server 2012 Express

在模型中强制执行唯一值的最佳做法是什么?我有一个地方类,它有一个“url”属性,每个地方都应该是唯一的。

public class Place

      [ScaffoldColumn(false)]
      public virtual int PlaceID  get; set; 

      [DisplayName("Date Added")]
      public virtual DateTime DateAdded  get; set; 

      [Required(ErrorMessage = "Place Name is required")]
      [StringLength(100)]
      public virtual string Name  get; set; 

      public virtual string URL  get; set; 
;

为什么不能在其上放置一个 [唯一] 数据注释?

我已经看到 1 或 2 次关于此的讨论,但没有谈到最佳实践。使用 Code First,您能以某种方式告诉数据库在数据库中的字段上设置唯一约束吗?

什么是最简单的方法 - 什么是最佳实践?

【问题讨论】:

我现在使用(并推荐)Dapper.net 和数据注释的价值 - 喜欢它。 我不再首先使用代码 - 这似乎是一个已经死掉的想法。 【参考方案1】:

尽管听起来很疯狂,但如今的最佳做法是使用内置验证,而是使用 FluentValidation。然后代码将非常易于阅读和超级可维护,因为验证将在单独的类上进行管理,这意味着更少的意大利面条代码。

您要达到的目标的伪示例。

[Validator(typeof(PlaceValidator))]
class Place

    public int Id  get; set; 
    public DateTime DateAdded  get; set; 
    public string Name  get; set; 
    public string Url  get; set; 


public class PlaceValidator : AbstractValidator<Place>

    public PlaceValidator()
    
        RuleFor(x => x.Name).NotEmpty().WithMessage("Place Name is required").Length(0, 100);
        RuleFor(x => x.Url).Must(BeUniqueUrl).WithMessage("Url already exists");
    

    private bool BeUniqueUrl(string url)
    
        return new DataContext().Places.FirstOrDefault(x => x.Url == url) == null
    

【讨论】:

有趣。所以我只需要将它用于任何唯一属性 - 其他所有内容都可以保持原样,传统的 Code First 对吗?这是否涵盖所有基础、竞争条件、数据库上 100% 可靠的唯一性等? 我还需要安装fluentvalidation nuget包吗? 这个例子可以毫无问题地使用 POCO,我在我所有的项目中都使用它。如您所见,它将检查TryValidateModel() 上的数据库中的唯一性,之后您通常会调用_db.SaveChanges();,因此基本上没有任何延迟。是的,你当然需要 nuget 包。 谢谢。我注意到您已将“名称”添加为不为空。你能不能只在类定义中放一个 [Required] 数据注释?或者这会取代所有数据注释/完全阻止它们工作 - 无论是对于这个模型还是任何模型? 谁的最佳实践?也有人可能会争辩说,这是为了获得最小收益而需要承担的大量代码【参考方案2】:

此链接可能会有所帮助: https://github.com/fatihBulbul/UniqueAttribute

[Table("TestModels")]
public class TestModel


    [Key]
    public int Id  get; set; 

    [Display(Name = "Some", Description = "desc")]
    [Unique(ErrorMessage = "This already exist !!")]
    public string SomeThing  get; set; 

【讨论】:

【参考方案3】:

唯一的方法是在生成迁移后更新迁移(假设您正在使用它们),以便对列实施唯一约束。

public override void Up() 
  // create table
  CreateTable("dbo.MyTable", ...;
  Sql("ALTER TABLE MyTable ADD CONSTRAINT U_MyUniqueColumn UNIQUE(MyUniqueColumn)");

public override void Down() 
  Sql("ALTER TABLE MyTable DROP CONSTRAINT U_MyUniqueColumn");

不过,最难的一点是在访问数据库之前在代码级别强制执行约束。为此,您可能需要一个包含唯一值完整列表的存储库,并确保新实体不会通过工厂方法违反它。

// Repository for illustration only
public class Repo 
  SortedList<string, Entity1> uniqueKey1 = ...; // assuming a unique string column 
  public Entity1 NewEntity1(string keyValue) 
    if (uniqueKey1.ContainsKey(keyValue) throw new ArgumentException ... ;
    return new Entity1  MyUniqueKeyValue = keyValue ;
  

参考资料:

Repository - Fowler(Repository 原出处) Repostory - MSDN Tutorial: Repository in MVC (www.asp.net) Singleton in C# - SO

脚注:

有很多要求 [Unique] 在代码中优先,但看起来它甚至没有制作版本 6:http://entityframework.codeplex.com/wikipage?title=Roadmap

您可以尝试在这里投票:http://data.uservoice.com/forums/72025-entity-framework-feature-suggestions/suggestions/1050579-unique-constraint-i-e-candidate-key-support

【讨论】:

我从未使用过存储库或工厂 - 任何链接/教程推荐?我想我也可以“手动”验证新地点的用户输入以确保 URL 是唯一的?如果验证存在缺陷,是否可以在对象级别强制执行 - 比如在 SQL Server 中设置唯一约束(也许你的建议涵盖了这一点?)似乎有更好的解决方案,因为这是一个常见的要求? 注意 - 这是一个规模相对较小的应用程序,目前只有一个开发人员 - 所以一个简单/快速的解决方案会很好。我还需要为用户强制执行唯一字段 - 例如在他们的 Twitter 句柄或电子邮件中。 @user2254951。使用提供的代码和链接进行编辑。你需要一个公共的地方来检查唯一性,因此我的单例建议因为它会在 IIS 中的 appdomain 的持续时间内存在于内存中,你只需要在第一次查询时播种它,但这很容易。 @user2254951。小型应用程序可能会一直等到您写入数据库,但创建工厂方法相对容易,并且可以节省额外的代码,以便在您保存时从重复和异常中恢复。 谢谢 - 会看的。我还发现了在 Seed 方法中直接向数据库添加 UNIQUE 约束的建议 - 首先破坏代码的纯度,但应该有效?!【参考方案4】:

您可以在将数据保存到数据库表之前在代码级别进行此检查。

您可以尝试在您的视图模型上使用 Remote 数据注释来进行异步验证,以使 UI 响应更快。

public class CreatePlaceVM

  [Required]
  public string PlaceName  set;get;

  [Required]
  [Remote("IsExist", "Place", ErrorMessage = "URL exist!")
  public virtual string URL  get; set; 

确保您的Placecontroller 中有一个IsExists 操作方法,它接受URL 参数并再次检查您的表格并返回true 或false。

这个msdn link 有一个示例程序来展示如何实现远程属性来进行即时验证。

另外,如果您使用的是存储过程(出于某种原因),您可以在 INSERT 查询之前进行 EXISTS 检查。

【讨论】:

这看起来很酷 - 我是否必须实现其他任何东西才能使其工作?如果另一个用户同时执行相同的查询怎么办 - 有什么方法可以防止更深/数据库级别的重复? 执行两次(一次使用上述方式(指示用户更改重复值),然后在保存之前的 HttpPost 操作方法中执行第二次(检查数据库以查看是否有该项目存在与否)。除非你有一些巨大的性能问题(我猜你没有),否则你不会有任何问题 是的,这样的检查应该在与插入新 URL 的代码的事务中,以确保没有竞争条件。打开事务,是否存在?,插入,提交【参考方案5】:

我解决了在您的验证流程中启用构造函数注入的一般问题,集成到正常的 DataAnnotations 机制中,而无需借助 this answer 中的框架,从而可以编写:

class MyModel 

    ...
    [Required, StringLength(42)]
    [ValidatorService(typeof(MyDiDependentValidator), ErrorMessage = "It's simply unacceptable")]
    public string MyProperty  get; set; 
    ....


public class MyDiDependentValidator : Validator<MyModel>

    readonly IUnitOfWork _iLoveWrappingStuff;

    public MyDiDependentValidator(IUnitOfWork iLoveWrappingStuff)
    
        _iLoveWrappingStuff = iLoveWrappingStuff;
    

    protected override bool IsValid(MyModel instance, object value)
    
        var attempted = (string)value;
        return _iLoveWrappingStuff.SaysCanHazCheez(instance, attempted);
    

使用一些辅助类(看那里),你可以把它连接起来,例如在 ASP.NET MVC 中就像在 Global.asax 中一样:-

DataAnnotationsModelValidatorProvider.RegisterAdapterFactory(
    typeof(ValidatorServiceAttribute),
    (metadata, context, attribute) =>
        new DataAnnotationsModelValidatorEx(metadata, context, attribute, true));

【讨论】:

【参考方案6】:

在我的 ASP.NET Razor 页面项目中遇到了类似的问题。创建自定义 UniqueDataAttribute 不起作用,因为在编辑时,如果您不更改唯一字段,它将引发错误。

我需要唯一的书名。我就是这样解决的:

    我通过 EF Core 迁移向数据库中的字段添加了唯一约束。在 ApplicationDbContext 类中添加以下内容,然后运行迁移。

代码:

protected override void OnModelCreating(ModelBuilder builder)
        
            builder.Entity<Book>()
                .HasIndex(u => u.Name)
                .IsUnique();
        
    接下来,创建了如下的辅助/扩展方法。

代码:

        // Validate uniqueness of Name field in database.
        // If validation is done on existing record, pass the id of the record.
        // Else, if validating new record Name, then id is set to dummy key integer -1
        public static bool UniqueNameInDb(this string data, ApplicationDbContext db, int id = -1)
        
            var duplicateData = from o in db.Book
                                where o.Name == data && o.Id != id
                                select o;
            if(duplicateData.Any())
            
                return false;
            
            return true;
        
    
    然后在 OnPost() 方法的 Create and Edit page model 中使用如下。

创建模型:

public async Task<IActionResult> OnPost()
        
            if(ModelState.IsValid)
            
                if (!Book.Name.UniqueNameInDb(_db)) //<--Uniqueness validation
                
                    ModelState.AddModelError("Book.Name", "Name already exist"); //<-- Add error to the ModelState, that would be displayed in view.
                    return Page();
                

                await _db.Book.AddAsync(Book);
                await _db.SaveChangesAsync();

                return RedirectToPage("Index");

            
            else
            
                return Page();
            
        

编辑模型:

public async Task<IActionResult> OnPost()
        
            if(ModelState.IsValid)
            
                var bookFromDb = await _db.Book.FindAsync(Book.Id);
                if (!Book.Name.UniqueNameInDb(_db, Book.Id)) //<--Uniqueness validation
                
                    ModelState.AddModelError("Book.Name", "Name already exist"); //<-- Add error to the ModelState, that would be displayed in view.
                    return Page();
                
                bookFromDb.Name = Book.Name;
                bookFromDb.Author = Book.Author;

                await _db.SaveChangesAsync();

                return RedirectToPage("Index");
            

            return Page();
        

PS:您的 Razor 视图应该在表单中设置模型验证以捕获并显示错误。

<div class="text-danger" asp-validation-summary="ModelOnly"></div>

及以下针对该字段的验证。

<span asp-validation-for="Book.Name" class="text-danger"></span>

【讨论】:

【参考方案7】:

好吧,这很简单,但是如果这有效与否,请不客气。只需在添加新用户之前检查电子邮件是否已存在即可。

if (!db.Users.Any(x => x.Email == data.Email))
 // your code for adding
else
 // send a viewbag to the view
 //  ViewBag.error = "Email Already Exist";

【讨论】:

以上是关于ASP.NET MVC 4,EF5,模型中的唯一属性 - 最佳实践?的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET MVC 4、Internet 应用程序模板、SimpleMembership - 帐户模型 - 强制执行唯一的用户名和电子邮件

asp.net mvc 4,模型绑定改变了线程?

将 DbDataReader 的结果转换为 ASP.NET MVC 4 中的数据库模型,来自使用 ADO.NET 的存储过程 [重复]

Asp.net MVC 4 / 5 中的分页

带有通用提供程序的 ASP.NET MVC 4 中的错误

ASP.NET MVC 4 模型验证