来自数据库的MVC模型验证

时间:2015-06-29 07:44:19

标签: c# .net asp.net-mvc validation asp.net-mvc-4

我有一个非常简单的模型,需要从数据库

进行验证
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并且没有使用过多的反射就可以接受。

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

P.S。如果任何人可以给我提示如何从数据库进行基于属性的验证,那将是非常好的。

9 个答案:

答案 0 :(得分:28)

请查看本答案中间附带的编辑,以获得更详细和通用的解决方案。

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

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操作可能会出错,如下所示。

enter image description here

编辑以下是更通用和详细的答案。

创建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,它将为我们提供基于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;
    }
}

重新安装的验证属性 -

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

输出如下 -

enter image description here

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

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

  1. 格式
  2. 长度
  3. 唯一
  4. 在这种情况下,我们可以创建从IEmailCommand继承的IValidatorCommand。然后从IEmailFormatCommand继承IEmailLengthCommandIEmailUniqueCommandIEmailCommand

    我们的ValidatorFactory将保留Dictionary<Type, IValidatorCommand> Commands中所有三个命令实现的池。

    现在我们可以使用Email来装饰我们的IEmailCommand属性,而不是使用ValidatorFactory.GetCommand()来装饰它们。

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

    现在我们可以获取与属性相关的所有命令,我们可以循环命令并在ObservableCollection<string> Tags中获取验证结果。

答案 1 :(得分:2)

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

使用远程属性 -

装饰您的属性
[Remote("IsCityCodeValid","controller")]
public string CityCode { get; set; }

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

在方法中进行验证,如果有效,则返回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.

答案 2 :(得分:1)

我认为你应该使用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;
}

答案 3 :(得分:1)

我过去做过这件事,对我有用:

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的{​​{1}}注册到您的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
    }
}

答案 4 :(得分:1)

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

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

答案 5 :(得分:0)

如果你真的想从数据库验证这里有一些你可以遵循的技术 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,日期时间可以为空 EF Database First with ASP.NET MVC: Enhancing Data Validation

答案 6 :(得分:0)

这是我的尝试 -

首先,要确定我们需要对属性执行哪些验证,我们可以将枚举作为标识符。

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 Class(当然你可以使用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];
    }
}

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

答案 7 :(得分:0)

  • 嗨..我认为这对你的问题有用。
  • 我使用该方法
  • 在各种位置调用单个函数。我会详细解释一下 下方。

在模型中:

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

答案 8 :(得分:0)

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/