Как я могу использовать пользовательские атрибуты проверки на дочерних моделях сущности БД?

Резюме:

Я хочу, чтобы валидатор аннотации данных ссылался на другое свойство в том же классе (TitleAuthorAndPublishingConfiguration).

Однако, DB.SaveChanges() не вызывает этот класс напрямую. Скорее, он вызывается родителем этого класса (WebsiteConfiguration).

Поэтому validationContext.ObjectType возвращает WebsiteConfiguration, и я не могу ссылаться на свойства TitleAuthorAndPublishingConfiguration в валидаторе аннотации данных.


WebsiteConfiguration.cs

public class WebsiteConfiguration
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ID { get; set; }

    public TitleAuthorAndPublishingConfiguration TitleAuthorAndPublishing { get; set; }

    public BookChaptersAndSectionsConfiguration BookChaptersAndSections { get; set; }

    public SocialMediaLoginsConfiguration SocialMediaLogins { get; set; }

    public TagGroupsConfiguration TagGroups { get; set; }
}

public class TitleAuthorAndPublishingConfiguration 
{
    public string BookTitle { get; set; }

    public bool IsPublished { get; set; }

    // how do I access a property of current model when calling DB.SaveChanges() on parent?
    [RequiredIfOtherFieldIsEnabled("IsPublished")]
    public string Publisher { get; set; }
}

// ... and other sub models...

ApplicationDbContext.cs

DbSet<WebsiteConfiguration> WebsiteConfiguration {get;set;}

Пример кода обновления

    public void SeedWebsiteConfiguration()
    {
        var titleAuthorAndPublishingConfiguration = new TitleAuthorAndPublishingConfiguration()
        {
            // seed values
        };
        var bookChaptersAndSectionsConfiguration = new BookChaptersAndSectionsConfiguration()
        {
            // seed values
        };
        var socialMediaLoginConfiguration = new SocialMediaLoginsConfiguration()
        {
            // seed values
        };
        var tagGroupsConfiguration = new TagGroupsConfiguration()
        {
            // seed values
        };
        var websiteConfiguration = new WebsiteConfiguration()
        {
            TitleAuthorAndPublishing = titleAuthorAndPublishingConfiguration,
            BookChaptersAndSections = bookChaptersAndSectionsConfiguration,
            SocialMediaLogins = socialMediaLoginConfiguration,
            TagGroups = tagGroupsConfiguration
        };
        DB.WebsiteConfiguration.Add(websiteConfiguration);
        DB.SaveChanges();
    }

Код Validator

public class RequiredIfOtherFieldIsEnabledAttribute : ValidationAttribute
{
    private string _ifWhatIsEnabled { get; set; }


    public RequiredIfOtherFieldIsEnabledAttribute(string IfWhatIsEnabled)
    {
        _ifWhatIsEnabled = IfWhatIsEnabled;
    }

    protected override ValidationResult IsValid(object currentPropertyValue, ValidationContext validationContext)
    {
        var isEnabledProperty = validationContext.ObjectType.GetProperty(_ifWhatIsEnabled);
        if (isEnabledProperty == null)
        {
            return new ValidationResult(
                string.Format("Unknown property: {0}", _ifWhatIsEnabled)
            );
        }
        var isEnabledPropertyValue = (bool)isEnabledProperty.GetValue(validationContext.ObjectInstance, null);

        if (isEnabledPropertyValue == true)
        {
            if (String.IsNullOrEmpty(currentPropertyValue.ToString()))
            {
                return new ValidationResult(String.Format("This field is required if {0} is enabled", isEnabledProperty));
            }
        }
        return ValidationResult.Success;
    }
}

Вопросы

  • Есть ли способ получить доступ к свойствам дочерней модели из validationContext?

  • Я ошибаюсь в своем подходе? Есть ли лучший способ хранить несколько моделей как часть более крупной модели в одной таблице БД?

Я надеялся не иметь нескольких таблиц конфигурации и вызовов в БД. (В этом примере есть 4 модели для детей, но в следующем приложении их может быть 10+.)

Настройка выше соответствует моим потребностям во многих отношениях. Но я не хочу отказываться от функциональности DataAnnotations на дополнительных моделях!


Бонусный вопрос

Я столкнулся с несколькими сообщениями вроде этого: Как я могу проверить валидатор Data Annotations, чтобы также проверить сложные дочерние свойства?

Но это 4 года, и мне интересно, изменилось ли с тех пор что-то.

Я пытаюсь сделать что-то, что в принципе невозможно (или, по крайней мере, очень сложно)?

Ответ 1

Я пытаюсь сделать что-то, что в принципе невозможно (или, по крайней мере, очень сложно)?

Нет, существует очень простое решение, которое идеально интегрируется с каркасом и технологиями с использованием DataAnnotations.

Вы можете создать пользовательский ValidationAttribute, который вызывается EF Validation и вызывает Validator.TryValidateObject внутри. Таким образом, когда CustomValidation.IsValid вызывается EF, вы запускаете проверку дочернего комплексного объекта вручную и т.д. Для всего графа объектов. В качестве бонуса вы можете собрать все ошибки благодаря CompositeValidationResult.

то есть.

using System;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;

public class Program
{
    public static void Main() {
   var person = new Person {
      Address = new Address {
         City = "SmallVille",
         State = "TX",
         Zip = new ZipCode()
      },
      Name = "Kent"
   };

   var context = new ValidationContext(person, null, null);
   var results = new List<ValidationResult>();

   Validator.TryValidateObject(person, context, results, true);

   PrintResults(results, 0);

   Console.ReadKey();
}

private static void PrintResults(IEnumerable<ValidationResult> results, Int32 indentationLevel) {
   foreach (var validationResult in results) {
      Console.WriteLine(validationResult.ErrorMessage);
      Console.WriteLine();

      if (validationResult is CompositeValidationResult) {
         PrintResults(((CompositeValidationResult)validationResult).Results, indentationLevel + 1);
      }
   }
}

}

public class ValidateObjectAttribute: ValidationAttribute {
   protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
      var results = new List<ValidationResult>();
      var context = new ValidationContext(value, null, null);

      Validator.TryValidateObject(value, context, results, true);

      if (results.Count != 0) {
         var compositeResults = new CompositeValidationResult(String.Format("Validation for {0} failed!", validationContext.DisplayName));
         results.ForEach(compositeResults.AddResult);

         return compositeResults;
      }

      return ValidationResult.Success;
   }
}

public class CompositeValidationResult: ValidationResult {
   private readonly List<ValidationResult> _results = new List<ValidationResult>();

   public IEnumerable<ValidationResult> Results {
      get {
         return _results;
      }
   }

   public CompositeValidationResult(string errorMessage) : base(errorMessage) {}
   public CompositeValidationResult(string errorMessage, IEnumerable<string> memberNames) : base(errorMessage, memberNames) {}
   protected CompositeValidationResult(ValidationResult validationResult) : base(validationResult) {}

   public void AddResult(ValidationResult validationResult) {
      _results.Add(validationResult);
   }
}

public class Person {
  [Required]
  public String Name { get; set; }

  [Required, ValidateObject]
  public Address Address { get; set; }
}

public class Address {
  [Required]
  public String Street1 { get; set; }

  public String Street2 { get; set; }

  [Required]
  public String City { get; set; }

  [Required]
  public String State { get; set; }

  [Required, ValidateObject]
  public ZipCode Zip { get; set; }
}

public class ZipCode {
  [Required]
  public String PrimaryCode { get; set; }

  public String SubCode { get; set; }
}