来自数据库的 MVC 模型验证

Posted

技术标签:

【中文标题】来自数据库的 MVC 模型验证【英文标题】:MVC Model Validation From Database 【发布时间】:2015-09-15 14:24:46 【问题描述】:

我有一个非常简单的模型,需要从数据库中进行验证

public class UserAddress

    public string CityCode get;set;

CityCode 可以具有仅在我的数据库表中可用的值。

我知道我可以做类似的事情。

[HttpPost]
public ActionResult Address(UserAddress model)

    var connection = ; // create connection
    var cityRepository = new CityRepository(connection);

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

这似乎非常WET,因为我必须在很多地方使用这个模型,并且在每个地方添加相同的逻辑似乎我没有正确使用 MVC 架构。

那么,从数据库验证模型的最佳模式是什么?

注意: 大多数验证是从数据库中查找单个字段,其他验证可能包括字段组合。但是现在我对单字段查找验证很满意,只要它是 DRY 并且没有使用过多的反射,这是可以接受的。

没有客户端验证: 对于任何在客户端验证方面回答的人,我不需要任何此类验证,我的大部分验证都是服务器端的,我需要同样的,请不要用客户端验证方法回答。


附:如果有人能给我提示如何从数据库中进行基于属性的验证,那将是非常棒的。

【问题讨论】:

您还有其他想要执行的数据库验证示例吗?您是否需要比较许多查找?试图画出一些共性,看看我们能有多通用。 @mattytommo - 其中大部分是查找!大多数是单字段查找,其他验证可能包括字段组合。但是现在我对单字段查找验证很满意,只要它是干燥的并且不使用太多反射是可以接受的。 不幸的是,这是查找的常见问题,关于它们是否应该放在数据库中,或者它们是否应该作为枚举或类似的东西放在应用程序中存在争议。根据您的设置,您可以创建一个通用验证属性[ValidLookupValue],它接受type(T) 并验证该值是否在选定的查找中(您可能必须让您的查找实现接口或基类才能实现这一点)。 你应该在你的 MVC 项目中有你的属性,但它会与你的业务逻辑层(或者类似的东西,如果你有的话)通信,然后它会调用数据库。至于枚举,这取决于你如何构建它,有些人每个查找类型都有一个表,所以在这种情况下,它可以转换为每个查找类型的枚举。 这更像是一个 SoC 问题。为了使这个干,你希望你的数据库调用被包装和分层。 【参考方案1】:

请检查此答案中间附加的EDIT,以获得更详细和通用的解决方案。


以下是我进行简单的基于属性的验证的解决方案。创建一个属性 -

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("ra@ra.com");
            emails.Add("ve@ve.com");

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

我创建了一个简单的模型来测试 -

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; 

然后我创建了以下视图 -

@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" />

现在,当我输入相同的电子邮件 - ra@ra.com 并单击提交按钮时,我的 POST 操作中会出现错误,如下所示。


编辑这里有更通用和详细的答案。


创建IValidatorCommand -

public interface IValidatorCommand

    object Input  get; set; 
    CustomValidationResult Execute();


public class CustomValidationResult

    public bool IsValid  get; set; 
    public string ErrorMessage  get; set; 

假设我们的 RepositoryUnitOfWork 以下列方式定义 -

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;
 

现在让我们创建自己的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" ;
    

创建我们的Validator Factory,它将根据类型为我们提供特定命令。

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;
    

以及翻新的验证属性 -

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

最后我们可以使用我们的属性如下 -

public class Person

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

输出如下 -


编辑详细说明,使此解决方案更通用。


假设我有一个属性 Email,我需要在其中进行以下验证 -

    格式 长度 独特

在这种情况下,我们可以创建继承自 IValidatorCommandIEmailCommand。然后从IEmailCommand继承IEmailFormatCommandIEmailLengthCommandIEmailUniqueCommand

我们的ValidatorFactory 将在Dictionary&lt;Type, IValidatorCommand&gt; Commands 中保存所有三个命令实现的池。

现在我们可以使用IEmailCommand 来装饰我们的Email 属性,而不是使用三个命令来装饰它。

在这种情况下,我们的ValidatorFactory.GetCommand() 方法需要更改。它应该返回特定类型的所有匹配命令,而不是每次都返回一个命令。所以基本上它的签名应该是List&lt;IValidatorCommand&gt; GetCommand(Type validatetype)

现在我们可以获取与属性关联的所有命令,我们可以循环命令并在 ValidatorAttribute 中获取验证结果。

【讨论】:

似乎足够好。但是如何将CityRepository 提供给UniqueValidationAttribute 呢? UniqueValidationAttribute 中创建RepositoryType 属性,并以与设置ObjectType 相同的方式设置它。 @PaRiMaLRaJ 为什么这个答案不适合你? @OmriAharon - 这是最接近可接受的答案,但仍在寻找更好的答案! @PaRiMaLRaJ,请使用更通用的解决方案检查更新的答案。【参考方案2】:

我会使用RemoteValidation。对于数据库验证等场景,我发现这是最简单的。

用 Remote 属性装饰你的财产 -

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

现在,“IsCityCodeValid”将是一个操作方法,它将返回 JsonResult 并将您要验证的属性名称作为参数,而“控制器”是您的方法将被放置在其中的控制器的名称。确保参数名称与属性名称相同。

在方法中进行验证,如果有效则返回 json true,否则返回 false。简单快捷!

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

你就完成了!!。 MVC 框架将负责其余的工作。

当然,根据您的要求,您可以使用不同的远程属性重载。您还可以包含其他依赖属性,定义自定义错误消息等。您甚至可以将模型类作为参数传递给 Json 结果操作方法 MSDN Ref.

【讨论】:

谢谢,从来不知道这个,我可以在不同的上下文中使用它,但不是在这个问题的上下文中。 @please read my question update Note #2,没有客户端验证,如果可能的话删除你的答案 感谢@PaRiMaLRaJ。我想我会保留这个答案以供参考。现在我对您的问题更加清楚,发布了另一个解决方案。希望有帮助:)【参考方案3】:

我认为你应该使用custom validation

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;

【讨论】:

虽然您走在正确的轨道上,但这不是通用的,因此 OP 必须为他的每种数据库查找类型构建其中一个,因此不是所需的 DRY 方法 :) 基于此我认为构建通用解决方案很容易,因为您可以访问需要验证的对象的类型 typeof(UserAddress) @liviumamelluc 我仍然看不到它是干的,这几乎没有额外的复杂性。 如果查找不像城市那样变化很大,您可以将它们添加到缓存组件中,该组件将在应用程序启动时初始化,然后根据该缓存验证它们 如果查找不像城市那样变化很大,您可以将它们添加到缓存组件中,该组件将在应用程序启动时初始化,然后根据该缓存验证它们【参考方案4】:

如果你真的想从数据库中验证,这里有一些你可以遵循的技术 1.使用System.ComponentModel.DataAnnotations添加对类的引用

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; 

这里定义了字符串长度,即 50 和 datetime 可以为空等 EF Database First with ASP.NET MVC: Enhancing Data Validation

【讨论】:

我想你以另一种方式理解了我的问题。 请详细说明或简要说明【参考方案5】:

我过去做过这个,它对我有用:

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;
        
    

在您的业务层级别执行以下操作:

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;
    

现在在控制器级别用户您最喜欢的 IoC 注册和this.ModelState 的实例到您的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
    

【讨论】:

【参考方案6】:

我会提供一个非常简单的解决方案,用于服务器端验证只能具有数据库中存在的值的字段。首先我们需要一个验证属性:

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

验证存储库本身看起来也很简单:

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;
    

这是基础Repository 类:

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

现在,你需要在模型中做的是用ExistAttribute标记你的字段,指定表名和字段名进行查找:

public class UserAddress

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

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

控制器动作:

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

【讨论】:

【参考方案7】:

这是我的尝试 -

首先,要确定我们需要对属性执行什么验证,我们可以使用枚举作为标识符。

public enum ValidationType

    City,
    //Add more for different validations

接下来定义我们的自定义验证属性如下,其中枚举类型被声明为属性参数 -

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

如果您需要添加更多验证,则更进一步,不需要更改属性类。

现在只需使用此属性装饰您的财产

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

这是解决方案的其他连接部分-

验证器接口。所有验证器都将实现此接口。它只有一个方法来验证传入的对象和验证失败时验证器特定的错误消息。

public interface IValidator

    bool Validate(object value);

    string ErrorMessage  get; set; 

CityValidator 类(当然你可以使用 DI 等来改进这个类,它只是用于参考目的)。

 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,负责提供与验证类型相关的正确验证器

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

根据您的系统设计和约定,实际实现可能会略有不同,但在高层次上应该可以很好地解决问题。希望有帮助

【讨论】:

【参考方案8】: 嗨..我认为这对您的问题很有用。 我使用这种方法 在各个位置调用单个函数。我会详细解释 下面。

在模型中:

public class UserAddress

    public string CityCode get;set;

在控制器中: 首先创建单个函数以验证单个连接

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

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

来自另一个控制器的函数调用,例如:

var error = controllername.GetCity(citycode);

多连接的其他方法

 public dynamic GetCity(string cityCode,string connection)
            

               var cityRepository = new CityRepository(connection);

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

来自另一个控制器的函数调用,例如:

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

【讨论】:

【参考方案9】:

Andrew Lock 对此有一个优雅的解决方案。通过创建自定义验证属性,然后从验证上下文中获取您的外部服务。

public class CustomValidationAttribute : ValidationAttribute

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    
        var service = (IExternalService) validationContext
                         .GetService(typeof(IExternalService));

        // ... validation logic
    

这里有更多细节

https://andrewlock.net/injecting-services-into-validationattributes-in-asp-net-core/

【讨论】:

以上是关于来自数据库的 MVC 模型验证的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET MVC 4 模型验证

ASP.NET MVC 4 模型验证

来自 spring mvc 的 LDAP 身份验证

如何在没有模型的情况下验证 MVC3 中的强类型视图

MVC 架构中的数据和表单验证

来自 JQuery 填充字段的值未绑定到 MVC 模型