来自数据库的 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;
假设我们的 Repository
和 UnitOfWork
以下列方式定义 -
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
,我需要在其中进行以下验证 -
-
格式
长度
独特
在这种情况下,我们可以创建继承自 IValidatorCommand
的 IEmailCommand
。然后从IEmailCommand
继承IEmailFormatCommand
、IEmailLengthCommand
和IEmailUniqueCommand
。
我们的ValidatorFactory
将在Dictionary<Type, IValidatorCommand> Commands
中保存所有三个命令实现的池。
现在我们可以使用IEmailCommand
来装饰我们的Email
属性,而不是使用三个命令来装饰它。
在这种情况下,我们的ValidatorFactory.GetCommand()
方法需要更改。它应该返回特定类型的所有匹配命令,而不是每次都返回一个命令。所以基本上它的签名应该是List<IValidatorCommand> 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 模型验证的主要内容,如果未能解决你的问题,请参考以下文章