0

I'm writing an ASP.NET MVC 4 web app. I'm using DbContext.ValidateEntity to check whether or not about-to-be-added entities pass some checks. Checks like whether or not their custom Name property is unique, or other properties pass my custom logic.

My unit tests have spotted some behavior that I didn't expect, though. I'm using the repository pattern and I have a global DbContext that I use and pass into my repository functions for retrieving EF model entities from the database.

Up front, the problem is that inside of ValidateEntity, when looking at an entity that is being added, I query for all pre-existing entities and make sure that a particular field meets passes some uniqueness checks of mine. But, in the pre-existing items that I query, I already see the item being added.

So for example, if no entities exist in the database and I am creating the first one, I will see it in ValidateEntity if I query for all existing entities.

I thought that SaveChanges called ValidateEntity for all entities in the collection, prior to submitting them to the database?

Ryan
  • 7,733
  • 10
  • 61
  • 106
  • Hmm. It looks like I cannot re-use the same `DbContext` instance (`this`), inside of `ValidateEntity` to look up other entities, otherwise it pulls in the to-be-added entities. – Ryan Oct 08 '13 at 23:01
  • You can either use another context to query or still use the same context but add AsNoTracking() on the query to avoid the results from being merged into the context. – divega Oct 09 '13 at 03:57
  • The about to be added entity will have a different id to the one in the database...as will an entity being updated to have the same name as another. http://stackoverflow.com/a/16647237/150342 – Colin Oct 09 '13 at 04:45
  • 1
    FYI, this doesn't solve your problem, but *NEVER* use a global data context in a web application. By "global" I assume you mean it's static, or effectively static. If that is the case, then you have serious problems when multiple users access your site at the same time. The problem is that your you could be adding objects to the context in one thread, but in another, it's calling save changes when you are not yet finished. This is very bad. – Erik Funkenbusch Oct 09 '13 at 05:37

3 Answers3

1

You shouldn't ever have to "check for uniqueness" in a database. The database is designed to do that for you already. Databases have something called Unique constraints that you can apply, and if your insert violates those constraints, an exception will be thrown. All you have to do is catch that exception and deal with it. Adding a bunch of code to do things the database does for you is redundant and slow, not to mention error prone as you're finding out.

Erik Funkenbusch
  • 92,674
  • 28
  • 195
  • 291
  • The built-in IdentityDbContext that comes with ASP.NET Identity actually does use the ValidateEntity method to do a uniqueness check on usernames, even though there is no such restraint on the database. – Casey Apr 19 '14 at 03:31
  • @emodendroket - this is because entity framework does not support unique constraints explicitly (you can't add such constraints via ef) But you can add unique constraints in your own database and/or migration scripts. MS does it this way so that it's more "packageable" – Erik Funkenbusch Apr 19 '14 at 03:59
  • Ah. I was wondering why it was done that way myself. It seems like it would be pretty small to add that data annotation but they just got indices so maybe soon. – Casey Apr 20 '14 at 04:05
  • As an update to my own comment, you can now use data annotations to specify unique columns. – Casey Jul 11 '14 at 18:15
0

Maybe you could use a custom validation attribute instead of using ValidateEntity? Here's an exemple of a custom attribute that check if the value is a decimal :

public class MustBeDecimalAttribute : ValidationAttribute, IClientValidatable
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            if (Equals(value, null))
                return ValidationResult.Success;

            using (new CultureSubstitution(CultureInfo.InvariantCulture))
            {
                if (string.IsNullOrWhiteSpace(value.ToString()))
                    return ValidationResult.Success;

                return !value.ToString().ToDecimal().HasValue
                           ? new ValidationResult(FormatErrorMessage(validationContext.DisplayName))
                           : ValidationResult.Success;
            }
        }

        public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
        {
            var rule = new ModelClientValidationRule
                           {
                               ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
                               ValidationType = "mustbedecimal"
                           };

            yield return rule;
        }

But don't implement IClientValidatable since you need to query the db. When you have your attribute built, just add it to the Model like this :

[MustBeDecimal]
public decimal? RegularPrice { get; set; }
VinnyG
  • 6,883
  • 7
  • 58
  • 76
0

The problem ended up being with my DbContext. The DbContext should not be global because it's not at all thread safe. Thank you to Mystere Man for pointing that out.

Ryan
  • 7,733
  • 10
  • 61
  • 106