Entity Framework - Validation of Foreign Keys

c# entity-framework entity-framework-6

Question

Having the following TestClass:

[Table("xyz")]
public partial class TestClass{
    [Key]
    public int key {get; set;}
    [ForeignKey("key")]
    public virtual ICollection<ExternalClass> externalClasses{get; set;}

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        ... 
    }
}

How do I need to set that class up to match my following requirements:

  • I want to test the model with Entity Framework unit testing
  • In production, the externalClasses shouldn't get saved (should already be in database)
  • The EF must assure, that each external class exists in DB. If not, throw exception.

My first thought was to set the foreign objects null and request the database within the validate method to check if the foreign-key(s) exist. But this approach doesn't work that good with Unit Testing and in addition to that in my opinion it's not that clean to have database requests within a model.

Does anyone have an idea how to handle that on a clean way using the EF?

1
1
7/31/2018 11:00:12 AM

Accepted Answer

in my opinion it's not that clean to have database requests within a model

I tend to agree, but then for validation I tend to make an exception. Why?

When it's decided that validation be done by IValidatableObject it should be the only point of validation as much as possible (by which I mean that the Validate method is not the best place for applying more complex validations involving other entities).

It would be very inconvenient to have one part of the "simple" validation rules in the entity itself and another part in --well, wherever. That's the problem: it can be anywhere. And, hence, it can be skipped when saving the entity through another code path.

Therefore I tend to make the context that executes the validation available to the validationContext. This is done in an override of the context's ValidateEntity method that is called by SaveChanges (when context.Configuration.ValidateOnSaveEnabled is true):

protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items)
{
    items.Add("context", this);
    return base.ValidateEntity(entityEntry, items);
}

Now it's possible to get a handle to the context inside the Validate method:

var context = (MyContext)validationContext.Items["context"];

...and it can be used to execute database queries.

This should be handled with care though. It's good to adhere to three rules:

  1. Don't run queries that change the content of the change tracker, that is, don't query full entities, just projections. Validation is carried out while a number of tracked entities is waiting to be saved to the database. It's better not to change anything in this collection of tracked entities.
  2. Don't run heavy queries.
  3. Don't do this when the application routinely saves large amounts of the entity in one unit of work. (In that case using IValidatableObject isn't the best idea anyway).

With this in place it's possible to run a validation like:

var ids = externalClasses.Select(c => c.ID).ToList();
if (context.ExternalClasses.Any(c => !ids.Contains(c.ID))
{
    yield return new ValidationResult("Some external classes don't exist", 
        new[] { nameof(externalClasses) });
}

This executes a relatively light query that only returns a boolean value and that doesn't attach new entities to the change tracker.

1
7/31/2018 2:09:22 PM

Popular Answer

Until now I solved the problem as follows:

I created the entity class as follows (simplyfied the example by having a single foreign-object instead of an ICollection):

[Table("xyz")]
public partial class TestClass{
    [Key]
    public int key {get; set;}
    [ForeignKey("key")]
    [Required]
    public virtual ExternalClass externalClass{get; set;}



    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        ... 
    }
}

Then in my ServiceClass where I'm creating the new record:

public void InsertNewRecord(TestClass newClass){
    newClass.externalClass = context.Where(e => e.ID = newClass.externalClass.Id).First();
    context.Add(newClass);
    context.SaveChanges();        
}

In case that the externalClass (foreign) couldn't be found, newClass.externalClass is set null and following an validation error is thrown because of the [Required] Annotation. This has the additional advantage, that EF doesn't try to save the foreign-object externalClass because it now recognizes that this entry still exists.



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow