web-dev-qa-db-fra.com

Validation de modèle MVC à partir de la base de données

J'ai un modèle très simple, qui doit être validé à partir de la base de données

public class UserAddress
{
    public string CityCode {get;set;}
}

CityCode peut avoir des valeurs qui ne sont disponibles que dans ma table de base de données.

Je sais que je peux faire quelque chose comme.

[HttpPost]
public ActionResult Address(UserAddress model)
{
    var connection = ; // create connection
    var cityRepository = new CityRepository(connection);

    if (!cityRepository.IsValidCityCode(model.CityCode))
    {
        // Added Model error
    }
}

Cela semble très WET étant donné que je dois utiliser ce modèle beaucoup de fois et ajouter la même logique à chaque endroit me donne l’impression que je n’utilise pas correctement MVC Architecture.

Alors, quel est le meilleur modèle pour valider le modèle de la base de données? 

NOTE: La plupart des validations sont des recherches sur un seul champ à partir de la base de données, une autre validation peut inclure une combinaison de champs. Mais pour le moment, je suis satisfait de la validation de la recherche sur un seul champ, tant qu'elle est DRY et n'utilise pas trop de réflexion, elle est acceptable.

PAS DE VALIDATION CÔTÉ CLIENT: À quiconque répond en termes de validation côté client, je n'ai pas besoin d'une telle validation, la plupart de mes validations sont effectuées côté serveur, et j'ai besoin de la même chose, veuillez pas répondre avec les méthodes de validation côté client.


P.S. Si quelqu'un peut me donner un indice sur la manière de valider une base de données à partir d'attributs, il sera extrêmement reconnaissant.

22
PaRiMaL RaJ

Veuillez vérifier leEDITfigurant en pièce jointe au milieu de cette réponse, pour une solution plus élaborée et générique.


Voici ma solution pour effectuer une validation basée sur un attribut simple. Créer un attribut - 

public class Unique : ValidationAttribute
{
    public Type ObjectType { get; private set; }
    public Unique(Type type)
    {
        ObjectType = type;
    }
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (ObjectType == typeof(Email))
        {
            // Here goes the code for creating DbContext, For testing I created List<string>
            // DbContext db = new DbContext();
            var emails = new List<string>();
            emails.Add("[email protected]");
            emails.Add("[email protected]");

            var email = emails.FirstOrDefault(u => u.Contains(((Email)value).EmailId));

            if (String.IsNullOrEmpty(email))
                return ValidationResult.Success;
            else
                return new ValidationResult("Mail already exists");
        }

        return new ValidationResult("Generic Validation Fail");
    }
}

J'ai créé un modèle simple pour tester - 

public class Person
{
    [Required]
    [Unique(typeof(Email))]
    public Email PersonEmail { get; set; }
    [Required]
    public GenderType Gender { get; set; }
}

public class Email
{
    public string EmailId { get; set; }
}

Ensuite, j'ai créé la vue suivante - 

@model WebApplication1.Controllers.Person
@using WebApplication1.Controllers;

<script src="~/Scripts/jquery-1.10.2.min.js"></script>
<script src="~/Scripts/jquery.validate.min.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>

@using (Html.BeginForm("CreatePersonPost", "Sale"))
{
    @Html.EditorFor(m => m.PersonEmail)

    @Html.RadioButtonFor(m => m.Gender, GenderType.Male) @GenderType.Male.ToString()
    @Html.RadioButtonFor(m => m.Gender, GenderType.Female) @GenderType.Female.ToString()
    @Html.ValidationMessageFor(m => m.Gender)

    <input type="submit" value="click" />
}

Maintenant, lorsque j'entre le même email - [email protected] et que je clique sur le bouton Soumettre, des erreurs peuvent survenir dans mon action POST, comme indiqué ci-dessous. 

enter image description here


EDITVoici une réponse plus générique et détaillée.


Créer IValidatorCommand

public interface IValidatorCommand
{
    object Input { get; set; }
    CustomValidationResult Execute();
}

public class CustomValidationResult
{
    public bool IsValid { get; set; }
    public string ErrorMessage { get; set; }
}

Supposons que nous ayons notre Repository et UnitOfWork définis de la manière suivante -

public interface IRepository<TEntity> where TEntity : class
{
    List<TEntity> GetAll();
    TEntity FindById(object id);
    TEntity FindByName(object name);
}

public interface IUnitOfWork
{
    void Dispose();
    void Save();
    IRepository<TEntity> Repository<TEntity>() where TEntity : class;
} 

Créons maintenant notre propre Validator Commands

public interface IUniqueEmailCommand : IValidatorCommand { }

public interface IEmailFormatCommand : IValidatorCommand { }

public class UniqueEmail : IUniqueEmailCommand
{
    private readonly IUnitOfWork _unitOfWork;
    public UniqueEmail(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    public object Input { get; set; }

    public CustomValidationResult Execute()
    {
        // Access Repository from Unit Of work here and perform your validation based on Input
        return new CustomValidationResult { IsValid = false, ErrorMessage = "Email not unique" };
    }
}

public class EmailFormat : IEmailFormatCommand
{
    private readonly IUnitOfWork _unitOfWork;
    public EmailFormat(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    public object Input { get; set; }

    public CustomValidationResult Execute()
    {
        // Access Repository from Unit Of work here and perform your validation based on Input
        return new CustomValidationResult { IsValid = false, ErrorMessage = "Email format not matched" };
    }
}

Créez notre Validator Factory qui nous donnera une commande particulière basée sur Type.

public interface IValidatorFactory
{
    Dictionary<Type,IValidatorCommand> Commands { get; }
}

public class ValidatorFactory : IValidatorFactory
{
    private static Dictionary<Type,IValidatorCommand> _commands = new Dictionary<Type, IValidatorCommand>();

    public ValidatorFactory() { }        

    public Dictionary<Type, IValidatorCommand> Commands
    {
        get
        {
            return _commands;
        }
    }

    private static void LoadCommand()
    {
        // Here we need to use little Dependency Injection principles and
        // populate our implementations from a XML File dynamically
        // at runtime. For demo, I am passing null in place of UnitOfWork
        _commands.Add(typeof(IUniqueEmailCommand), new UniqueEmail(null));
        _commands.Add(typeof(IEmailFormatCommand), new EmailFormat(null));
    }

    public static IValidatorCommand GetCommand(Type validatetype)
    {
        if (_commands.Count == 0)
            LoadCommand();            

        var command = _commands.FirstOrDefault(p => p.Key == validatetype);
        return command.Value ?? null;
    }
}

Et l'attribut de validation rénové - 

public class MyValidateAttribute : ValidationAttribute
{
    public Type ValidateType { get; private set; }
    private IValidatorCommand _command;
    public MyValidateAttribute(Type type)
    {
        ValidateType = type;            
    }
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        _command = ValidatorFactory.GetCommand(ValidateType);
        _command.Input = value;
        var result = _command.Execute();

        if (result.IsValid)
            return ValidationResult.Success;
        else
            return new ValidationResult(result.ErrorMessage);
    }
}

Enfin, nous pouvons utiliser notre attribut comme suit - 

public class Person
{
    [Required]
    [MyValidate(typeof(IUniqueEmailCommand))]
    public string Email { get; set; }
    [Required]
    public GenderType Gender { get; set; }
}

Sortie comme suit - 

enter image description here


EDITExplication détaillée pour rendre cette solution plus générique.


Disons que j'ai une propriété Email où je dois faire les validations suivantes - 

  1. Format
  2. Longueur
  3. Unique

Dans ce cas, nous pouvons créer IEmailCommand hérité de IValidatorCommand. Puis héritez de IEmailFormatCommand, IEmailLengthCommand et IEmailUniqueCommand de IEmailCommand.

Notre ValidatorFactory tiendra le pool des trois implémentations de commande dans Dictionary<Type, IValidatorCommand> Commands.

Au lieu de décorer notre propriété Email avec trois commandes, nous pouvons le décorer avec IEmailCommand.

Dans ce cas, notre méthode ValidatorFactory.GetCommand() doit être modifiée. Au lieu de renvoyer une commande à chaque fois, il devrait renvoyer toutes les commandes correspondantes pour un type particulier. Donc, fondamentalement, sa signature devrait être List<IValidatorCommand> GetCommand(Type validatetype).

Maintenant que nous pouvons obtenir toutes les commandes associées à une propriété, nous pouvons les mettre en boucle et obtenir les résultats de la validation dans notre ValidatorAttribute.

27
ramiramilu

J'aurais utilisé RemoteValidation. J'ai trouvé cela plus simple pour des scénarios tels que les validations par rapport à la base de données.

Décorez votre propriété avec l'attribut Remote - 

[Remote("IsCityCodeValid","controller")]
public string CityCode { get; set; }

Maintenant, "IsCityCodeValid" sera une méthode d'action, qui retournera JsonResult et prendra le nom de propriété que vous voulez valider en tant que paramètre et "contrôleur" est le nom du contrôleur dans lequel votre méthode sera placée. Assurez-vous que le nom du paramètre est identique au nom de la propriété.

Faites vos validations dans la méthode, et si elle est valide, renvoyez json true et false sinon. Simple et rapide!

    public JsonResult IsCityCodeValid(string CityCode)
    {
        //Do you DB validations here
        if (!cityRepository.IsValidCityCode(cityCode))
        {
            //Invalid
           return Json(false, JsonRequestBehavior.AllowGet);
        }
        else
        {            
            //Valid
            return Json(true, JsonRequestBehavior.AllowGet);
        }
    }

Et vous avez terminé !! Le framework MVC se chargera du reste.

Et bien sûr, en fonction de vos besoins, vous pouvez utiliser différentes surcharges d'attributs distants. Vous pouvez également inclure d'autres propriétés dépendantes, en définissant un message d'erreur personnalisé, etc. Vous pouvez même transmettre la classe de modèle en tant que paramètre à la méthode d'action de résultat Json Réf. MSDN.

2
Yogi

Je l'ai fait par le passé et cela a fonctionné pour moi:

public interface IValidation
{
    void AddError(string key, string errorMessage);
    bool IsValid { get; }
}

public class MVCValidation : IValidation
{
    private ModelStateDictionary _modelStateDictionary;

    public MVCValidation(ModelStateDictionary modelStateDictionary)
    {
        _modelStateDictionary = modelStateDictionary;
    }
    public void AddError(string key, string errorMessage)
    {
        _modelStateDictionary.AddModelError(key, errorMessage);
    }
    public bool IsValid
    {
        get
        {
            return _modelStateDictionary.IsValid;
        }
    }
}

Au niveau de votre couche de gestion, procédez comme suit:

public class UserBLL
{
    private IValidation _validator;
    private CityRepository _cityRepository;
    public class UserBLL(IValidation validator, CityRepository cityRep)
    {
        _validator = validator;
        _cityRepository = cityRep;
    }
    //other stuff...
    public bool IsCityCodeValid(CityCode cityCode)
    {
        if (!cityRepository.IsValidCityCode(model.CityCode))
        {
            _validator.AddError("Error", "Message.");
        }
        return _validator.IsValid;
    }
}

Et maintenant, au niveau du contrôleur, utilisez votre IoC favori pour enregistrer et créer une instance du this.ModelState dans votre UserBLL:

public class MyController
{
    private UserBLL _userBll;
    public MyController(UserBLL userBll)
    {
        _userBll = userBll;
    }
    [HttpPost]
    public ActionResult Address(UserAddress model)
    {
        if(userBll.IsCityCodeValid(model.CityCode))
        {
            //do whatever
        }
        return View();//modelState already has errors in it so it will display in the view
    }
}
1
SOfanatic

Je pense que vous devriez utiliser un validation personnalisée

public class UserAddress
{
    [CustomValidation(typeof(UserAddress), "ValidateCityCode")]
    public string CityCode {get;set;}
}
public static ValidationResult ValidateCityCode(string pNewName, ValidationContext pValidationContext)
{
    bool IsNotValid = true // should implement here the database validation logic
    if (IsNotValid)
        return new ValidationResult("CityCode not recognized", new List<string> { "CityCode" });
    return ValidationResult.Success;
}
1
liviu mamelluc

J'offrirais une solution très simple pour la validation côté serveur de champs qui ne peuvent avoir que des valeurs existantes dans la base de données. Tout d'abord, nous aurons besoin d'un attribut de validation:

public class ExistAttribute : ValidationAttribute
{
    //we can inject another error message or use one from resources
    //aint doing it here to keep it simple
    private const string DefaultErrorMessage = "The value has invalid value";

    //use it for validation purpose
    private readonly ExistRepository _repository;

    private readonly string _tableName;
    private readonly string _field;

    /// <summary>
    /// constructor
    /// </summary>
    /// <param name="tableName">Lookup table</param>
    /// <param name="field">Lookup field</param>
    public ExistAttribute(string tableName, string field) : this(tableName, field, DependencyResolver.Current.GetService<ExistRepository>())
    {
    }

    /// <summary>
    /// same thing
    /// </summary>
    /// <param name="tableName"></param>
    /// <param name="field"></param>
    /// <param name="repository">but we also inject validation repository here</param>
    public ExistAttribute(string tableName, string field, ExistRepository repository) : base(DefaultErrorMessage)
    {
        _tableName = tableName;
        _field = field;
        _repository = repository;

    }

    /// <summary>
    /// checking for existing object
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    public override bool IsValid(object value)
    {
        return _repository.Exists(_tableName, _field, value);
    }
}

Le référentiel de validation lui-même semble assez simple:

public class ExistRepository : Repository
{
    public ExistRepository(string connectionString) : base(connectionString)
    {
    }

    public bool Exists(string tableName, string fieldName, object value)
    {
        //just check if value exists
        var query = string.Format("SELECT TOP 1 1 FROM {0} l WHERE {1} = @value", tableName, fieldName);
        var parameters = new DynamicParameters();
        parameters.Add("@value", value);

        //i use dapper here, and "GetConnection" is inherited from base repository
        var result = GetConnection(c => c.ExecuteScalar<int>(query, parameters, commandType: CommandType.Text)) > 0;
        return result;
    }
}

Voici la classe Repository de base:

public class Repository
{
    private readonly string _connectionString;

    public Repository(string connectionString)
    {
        _connectionString = connectionString;
    }

    protected T GetConnection<T>(Func<IDbConnection, T> getData)
    {
        var connectionString = _connectionString;
        using (var connection = new SqlConnection(connectionString))
        {
            connection.Open();
            return getData(connection);
        }
    }
}

Et maintenant, ce que vous devez faire dans model est de marquer vos champs avec ExistAttribute, en spécifiant le nom de la table et le nom du champ pour la recherche:

public class UserAddress
{
    [Exist("dbo.Cities", "city_id")]
    public int CityCode { get; set; }

    [Exist("dbo.Countries", "country_id")]
    public int CountryCode { get; set; }
}

Action du contrôleur:

    [HttpPost]
    public ActionResult UserAddress(UserAddress model)
    {
        if (ModelState.IsValid) //you'll get false here if CityCode or ContryCode don't exist in Db
        {
            //do stuff
        }
        return View("UserAddress", model);
    }
1
Sergio

Voici ma tentative - 

Pour commencer, pour identifier la validation que nous devons effectuer sur la propriété, nous pouvons avoir un enum comme identifiant.

public enum ValidationType
{
    City,
    //Add more for different validations
}

Définissez ensuite notre attribut de validation personnalisé comme suit: le type enum est déclaré comme paramètre d’attribut - 

public class ValidateLookupAttribute : ValidationAttribute
{
    //Use this to identify what validation needs to be performed
    public ValidationType ValidationType { get; private set; }
    public ValidateLookupAttribute(ValidationType validationType)
    {
        ValidationType = validationType;
    }
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        //Use the validation factory to get the validator associated
        //with the validator type
        ValidatorFactory validatorFactory = new ValidatorFactory();
        var Validator = validatorFactory.GetValidator(ValidationType);

        //Execute the validator
        bool isValid = Validator.Validate(value);

        //Validation is successful, return ValidationResult.Succes
        if (isValid)
            return ValidationResult.Success;

        else //Return validation error
            return new ValidationResult(Validator.ErrorMessage);
    }
}

Pour aller plus loin si vous devez ajouter plus de validations, la classe d'attribut n'a pas besoin d'être modifiée.

Et maintenant, décorez simplement votre propriété avec cet attribut

    [ValidateLookup(ValidationType.City)]
    public int CityId { get; set; }

Voici les autres éléments de la solution

Interface du validateur. Tous les validateurs implémenteront cette interface. Il a juste une méthode pour valider l'objet entrant et un message d'erreur spécifique au validateur lorsque la validation échoue.

public interface IValidator
{
    bool Validate(object value);

    string ErrorMessage { get; set; }
}

Classe CityValidator (Bien sûr, vous pouvez améliorer cette classe avec l’utilisation de DI, etc., c’est juste pour des raisons de référence).

 public class CityValidator : IValidator
{
    public bool Validate(object value)
    {
        //Validate your city here
        var connection = ; // create connection
        var cityRepository = new CityRepository(connection);

        if (!cityRepository.IsValidCityCode((int)value))
        {
            // Added Model error
            this.ErrorMessage = "City already exists";
        }
        return true;
    }

    public ErrorMessage { get; set; }
}

Validator Factory, il s’agit de fournir le validateur correct associé au type de validation.

public class ValidatorFactory
{
    private Dictionary<ValidationType, IValidator> validators = new Dictionary<ValidationType, IValidator>();

    public ValidatorFactory()
    {
        validators.Add(ValidationType.City, new CityValidator());
    }
    public IValidator GetValidator(ValidationType validationType)
    {
        return this.validators[validationType];
    }
}

En fonction de la conception de votre système et des conventions, l'implémentation réelle peut varier légèrement, mais à un niveau élevé, elle devrait résoudre les problèmes correctement. J'espère que cela pourra aider

0
Yogi
  • Salut .. Je pense que cela est utile pour votre question. 
  • J'utilise cette méthode pour
  • appelant une seule fonction dans divers endroits. Je vais expliquer en détail Ci-dessous.

Dans le modèle:

public class UserAddress
{
    public string CityCode {get;set;}
}

Dans le contrôleur: Première création d'une fonction unique pour validation pour une connexion unique

 public dynamic GetCity(string cityCode)
        {
           var connection = ; // create connection
           var cityRepository = new CityRepository(connection);

           if (!cityRepository.IsValidCityCode(model.CityCode))
           {
               // Added Model error
           }
           return(error);
        }

Appel de fonction depuis un autre contrôleur comme:

var error = controllername.GetCity(citycode);

Autre méthode pour plusieurs connexions

 public dynamic GetCity(string cityCode,string connection)
            {

               var cityRepository = new CityRepository(connection);

               if (!cityRepository.IsValidCityCode(model.CityCode))
               {
                   // Added Model error
               }
               return(error);
            }

Appel de fonction depuis un autre contrôleur comme:

var error = controllername.GetCity(citycode,connection);      
0

si vous voulez vraiment valider à partir d'une base de données, voici quelques techniques que vous pouvez suivre 1.Utilisez System.ComponentModel.DataAnnotations, ajoutez une référence à la classe 

public int StudentID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50)]
public string FirstName { get; set; }
public Nullable<System.DateTime> EnrollmentDate { get; set; }
[StringLength(50)]
 public string MiddleName { get; set; }

ici la longueur de la chaîne est définie i.e 50 et la date/heure peut être nullable, etc Base de données EF en premier avec ASP.NET MVC: amélioration de la validation des données

0
rohit poudel