24

ASP.NET MVC 4, EF5, Code First, SQL Server 2012 Express

What is best practice to enforce a unique value in a model? I have a places class that has a 'url' property that should be unique for every place.

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

Why isn't there just a [Unique] data annotation you can place on it?

I have seen 1 or 2 discussions on this, but no talk of best practice. Using Code First can you somehow tell the database to set a unique constraint on the field in the database?

What is easiest way - and what is best practice?

niico
  • 11,206
  • 23
  • 78
  • 161

8 Answers8

36

As crazy as it might sound the best practice nowadays is to not use built-in validation and instead use FluentValidation. Then the code will be very easy to read and super-maintainable since validation will be managed on separate class meaning less spaghetti code.

Pseudo-example of what you are trying to achieve.

[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
    }
}
Stan
  • 25,744
  • 53
  • 164
  • 242
  • Interesting. So I will only need to use this for any Unique properties - everything else can remain as is, conventional Code First right? Does this cover all bases, race conditions, 100% reliable uniqueness on the database etc? – niico May 21 '13 at 21:23
  • do I also need to install the fluentvalidation nuget package? – niico May 21 '13 at 21:27
  • This example will work with POCO with no problem, I use this on all my projects. As you can see it will check for uniquness in database on `TryValidateModel()` after which you usually call `_db.SaveChanges();` so there basically is no latency whatsoever. Yes, of course you will need nuget package. – Stan May 22 '13 at 09:20
  • Thanks. I notice you've added 'Name' to not be empty. Can you not just put a [Required] data annotation on there in the class definition instead? Or will this replace all data annotations / stop them from working at all - either for this or any models? – niico May 22 '13 at 12:53
  • @user2254951 Yes you can, however I think it's best to use only one method of validation and not to mix them both together. What's the point of adding `[Required]` if you can just do `NotEmpty()`. – Stan May 22 '13 at 13:22
  • Thanks. Well [Required] is just a lot more concise? Also it works with validation - will this work with unobtrusive jquery validation? Should I just take it off any model with a unique requirement - or stop using data annotation completely? – niico May 22 '13 at 17:58
  • FluentValidation can be integrated fully with MVC3/MVC4 infrastructure very easy. All you have to do is add `FluentValidationModelValidatorProvider.Configure();` in your `Global.asax.cs`. `NotEmpty()` is pretty concise IMO. I would def stop using data annotations and leave my model validation completely separate (in the same file ofc). – Stan May 22 '13 at 18:14
  • Sorry finally got round to implementing this. Works well - except it isn't showing validation errors, just a blank page (rather than redirecting to an action). How should I show errors? thx – niico Jun 16 '13 at 16:03
  • @niico I suppose you can just start a new question on this matter, I don't really understand what do you mean without code samples. – Stan Jun 16 '13 at 16:30
  • Nice answer, but, How we implement it when we edit the model but url doesnt change? The validation said "Url already exists" – Stefan Luv Oct 04 '14 at 20:51
  • @StefanLuv You can get the whole object with `Must((x, y) => {x.Id, x.Url, etc...});` Then you can just check if id is the same then ignore, if id is different then throw validation error. – Stan Oct 05 '14 at 12:27
  • You are my master @Steve , thanks, im trying to learn .NET and dudes like you helps a lot. – Stefan Luv Oct 06 '14 at 03:11
  • 3
    Best Practice according to who? It might also be argued that this is a massive boatload of code to take on for minimal gain – Ruben Bartelink Feb 04 '16 at 14:39
7

This link might help: 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; }
}
Ruchi
  • 1,238
  • 11
  • 32
5

The only way is to update your migration once you generate it, assuming you are using them, so that it enforces a unique constraint on the column.

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

The hard bit, though, is enforcing the constraint at the code level before you get to the database. For that you might need a repository that contains the complete list of unique values and makes sure that new entities don't violate that through a factory method.

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

References:

Footnote:

There are a lot of requests for [Unique] in code first, but it looks like it isn't even making version 6: http://entityframework.codeplex.com/wikipage?title=Roadmap

You could try voting for it here: http://data.uservoice.com/forums/72025-entity-framework-feature-suggestions/suggestions/1050579-unique-constraint-i-e-candidate-key-support

Community
  • 1
  • 1
Andy Brown
  • 18,961
  • 3
  • 52
  • 62
  • I've never used a repository or factory - any link / tutorial recommendations? I presume I could also 'manually' validate user input for new Places to ensure URL is unique? Can this be enforced at the object level in case the validation is flawed - like setting a unique constraint in SQL Server (maybe your suggestion covers that?) Seems there could be a better solution to this as it's a common requirement? – niico May 21 '13 at 20:19
  • Note - this is a relatively small scale app with a single developer for now - so a simple / quick solution would be good. I also need to enforce unique field for users - say in their Twitter handle or email. – niico May 21 '13 at 20:21
  • @user2254951. Edit with Code and links provided. You'll need a common place to check for uniqueness, hence my singleton suggestion as it will live in memory for the duration of your appdomain in IIS, you'll just have to seed it when you first query, but that's easy enough. – Andy Brown May 21 '13 at 20:39
  • @user2254951. Small scale apps could maybe get away with waiting until you write to the database, but it's relatively easy to create the factory method and it will save you extra code to recover from duplicates and exceptions when you save. – Andy Brown May 21 '13 at 20:40
  • Thanks - will look. I have also found suggestions for directly adding a UNIQUE constraint to the database in the Seed method - breaks purity of code first, but should be effective?! – niico May 21 '13 at 20:41
  • @user2254951. Same deal if you do it in Seed, but if you are using migrations, then its better to do it there. – Andy Brown May 21 '13 at 20:48
3

You may do this checking in the code level before saving the data to the Database tables.

You can try using the Remote data annotation on your viewmodel to do an asynchronous validation to make the UI more responsive.

public class CreatePlaceVM
{
  [Required]
  public string PlaceName { set;get;}

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

Make sure you have an IsExists action method in your Placecontroller which accepts a URL paramtere and check it againist your table and return true or false.

This msdn link has a sample program to show how to implement Remote attribute to do instant validation.

Also, If you are using a Stored procedure (For some reason), you can do an EXISTS check there before the INSERT query.

Shyju
  • 214,206
  • 104
  • 411
  • 497
  • This looks cool - do I have to implement anything else to make it work? What if another user performs the same query at the same time - is there any way to also prevent duplicates at a deeper / database level, say? – niico May 21 '13 at 20:27
  • Do it twice (once with the above way (to indicate the user to change the duplicated value) and second in your HttpPost action method just before you save ( Check the db to see whether you have the item exist or not). You won't have any problem unless you have some huge performance issues ( which i guess you don't have) – Shyju May 21 '13 at 20:36
  • Yeh, a check like this should be in a transaction with the code that inserts the new URL, to ensure no race conditions. open transaction, does exist?, insert, commit – AaronLS May 21 '13 at 20:57
2

I solved the general problem of enabling constructor injection in your Validation flow, integrating into the normal DataAnnotations mechanism without resorting to frameworks in this answer, enabling one to write:

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

With some helper classes (look over there), you wire it up e.g. in ASP.NET MVC like so in the Global.asax :-

DataAnnotationsModelValidatorProvider.RegisterAdapterFactory(
    typeof(ValidatorServiceAttribute),
    (metadata, context, attribute) =>
        new DataAnnotationsModelValidatorEx(metadata, context, attribute, true));
Community
  • 1
  • 1
Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249
0

Faced similar issue in my ASP.NET Razor Page Project. Creating custom UniqueDataAttribute didn't work, because on Edit, it would throw an error if you're not changing unique field.

I needed unique Book Name. This is how I resolved:

  1. I added unique constraint to the field in database via EF Core migrations. Added following in ApplicationDbContext class and then ran migration.

Code:

protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.Entity<Book>()
                .HasIndex(u => u.Name)
                .IsUnique();
        }
  1. Next, created helper/extension method as follows.

Code:

        // 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;
        }
    }
  1. Then used it in Create and Edit page model in OnPost() method as follows.

Create 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();
            }
        }

Edit Model:

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: Your Razor view should've Model validation set on in the form to capture and display the error.

i.e,

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

and below validation against the field.

<span asp-validation-for="Book.Name" class="text-danger"></span>
Meer
  • 678
  • 6
  • 12
0

well it's simple but idk if this is efficient or not. Just check before adding a new user whether the email already exists or not.

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";
irzum shahid
  • 181
  • 2
  • 12
0

To achieve Unique for multiple Columns

            modelBuilder.Entity<DataClass>()
            .HasIndex(u => new
            {
                u.col1,
                u.col2
            })
            .IsUnique();
Wowo Ot
  • 1,362
  • 13
  • 21