データベースから検証する必要がある非常に単純なモデルがあります
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
であり、あまり多くのリフレクションを使用していない限り、許容範囲です。
クライアント側の検証なし:クライアント側の検証に関して回答している人には、そのような検証は必要ありません。私の検証のほとんどはサーバー側であり、同じことが必要です。クライアント側の検証メソッドで答えないでください。
追伸データベースから属性ベースの検証を行う方法のヒントを誰かに教えてもらえれば、非常にすばらしいでしょう。
より複雑で一般的な解決策については、この回答の途中から添付されている[〜#〜] 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
アクションでエラーが発生することがあります。
[〜#〜] edit [〜#〜]ここでは、より一般的で詳細な回答を示します。
作成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" };
}
}
_
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; }
}
_
次のように出力-
[〜#〜] edit [〜#〜]このソリューションをより一般的にするための詳細な説明。
プロパティEmail
があり、以下の検証を行う必要があるとします-
その場合、IEmailCommand
から継承したIValidatorCommand
を作成できます。次に、IEmailFormatCommand
からIEmailLengthCommand
、IEmailUniqueCommand
およびIEmailCommand
を継承します。
私たちのValidatorFactory
は、_Dictionary<Type, IValidatorCommand> Commands
_に3つすべてのコマンド実装のプールを保持します。
Email
プロパティを3つのコマンドで装飾する代わりに、IEmailCommand
で装飾できます。
この場合、ValidatorFactory.GetCommand()
メソッドを変更する必要があります。毎回1つのコマンドを返すのではなく、特定のタイプに一致するすべてのコマンドを返す必要があります。したがって、基本的にはその署名はList<IValidatorCommand> GetCommand(Type validatetype)
である必要があります。
プロパティに関連付けられたすべてのコマンドを取得できるので、コマンドをループして、検証結果をValidatorAttribute
で取得できます。
私は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。
私は過去にこれをやったことがあり、それは私のために働きました:
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
}
}
カスタム検証 を使用する必要があると思います
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;
}
データベースに存在する値のみを持つことができるフィールドのサーバー側検証のための非常にシンプルなソリューションを提供します。まず、検証属性が必要です。
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);
}
これが私の試みです-
まず、プロパティに対して実行する必要がある検証を識別するために、列挙型を識別子として持つことができます。
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];
}
}
システムの設計と規則に基づいて、実際の実装は若干異なる場合がありますが、高レベルでは問題に適切に対処する必要があります。それが役に立てば幸い
モデル内:
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);
あなたが本当にデータベースから検証したい場合は、ここにあなたが従うことができるいくつかのテクニックがあります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