web-dev-qa-db-ja.com

データベースからのMVCモデルの検証

データベースから検証する必要がある非常に単純なモデルがあります

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であり、あまり多くのリフレクションを使用していない限り、許容範囲です。

クライアント側の検証なし:クライアント側の検証に関して回答している人には、そのような検証は必要ありません。私の検証のほとんどはサーバー側であり、同じことが必要です。クライアント側の検証メソッドで答えないでください。


追伸データベースから属性ベースの検証を行う方法のヒントを誰かに教えてもらえれば、非常にすばらしいでしょう。

23
Parimal Raj

より複雑で一般的な解決策については、この回答の途中から添付されている[〜#〜] 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("[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");
    }
}
_

テストする簡単なモデルを作成しました-

_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" />
}
_

次に、同じ電子メール-_[email protected]_を入力して[送信]ボタンをクリックすると、以下に示すように、POSTアクションでエラーが発生することがあります。

enter image description here


[〜#〜] edit [〜#〜]ここでは、より一般的で詳細な回答を示します。


作成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" };
    }
}
_

Typeに基づいて特定のコマンドを提供する_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; }
}
_

次のように出力-

enter image description here


[〜#〜] edit [〜#〜]このソリューションをより一般的にするための詳細な説明。


プロパティEmailがあり、以下の検証を行う必要があるとします-

  1. フォーマット
  2. 長さ
  3. ユニークな

その場合、IEmailCommandから継承したIValidatorCommandを作成できます。次に、IEmailFormatCommandからIEmailLengthCommandIEmailUniqueCommandおよびIEmailCommandを継承します。

私たちのValidatorFactoryは、_Dictionary<Type, IValidatorCommand> Commands_に3つすべてのコマンド実装のプールを保持します。

Emailプロパティを3つのコマンドで装飾する代わりに、IEmailCommandで装飾できます。

この場合、ValidatorFactory.GetCommand()メソッドを変更する必要があります。毎回1つのコマンドを返すのではなく、特定のタイプに一致するすべてのコマンドを返す必要があります。したがって、基本的にはその署名はList<IValidatorCommand> GetCommand(Type validatetype)である必要があります。

プロパティに関連付けられたすべてのコマンドを取得できるので、コマンドをループして、検証結果をValidatorAttributeで取得できます。

30
ramiramilu

私は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。

4
Yogi

私は過去にこれをやったことがあり、それは私のために働きました:

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
    }
}
1
SOfanatic

カスタム検証 を使用する必要があると思います

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

データベースに存在する値のみを持つことができるフィールドのサーバー側検証のための非常にシンプルなソリューションを提供します。まず、検証属性が必要です。

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

これが私の試みです-

まず、プロパティに対して実行する必要がある検証を識別するために、列挙型を識別子として持つことができます。

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

バリデーターファクトリー、これは検証タイプに関連付けられた正しいバリデーターを提供する責任があります

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

システムの設計と規則に基づいて、実際の実装は若干異なる場合がありますが、高レベルでは問題に適切に対処する必要があります。それが役に立てば幸い

0
Yogi
  • こんにちは。これはあなたの質問に役立つと思います。
  • 私はその方法を使用します
  • さまざまな場所で単一の関数を呼び出す。以下に詳しく説明します。

モデル内:

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

In Controller:最初に、単一の接続を検証するための単一の関数を作成します

 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);      
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やdatetimeはnull可能などです EF Database First with ASP.NET MVC:Enhancing Data Validation

0
rohit poudel