この記事では、部分的な C# クラスを使用して EF 8/ASP.NET8 の一般的な問題に対処するためのいくつかの手法について説明します。
概要: EF 8 - データベース ファースト アプローチでは、生成された EF クラスはモデルの再生成時に上書きされるため、追加機能で直接拡張することはできません。これを克服するには、部分的な C# クラスを活用できます。この記事では、EF/ASP.NET 環境で機能を拡張するための便利なコツを紹介します。
私のC#/ASP.NET 8 MVCプロジェクトでは、 Entity Framework Core Database Firstアプローチを使用しています。これは、さまざまなテクノロジの複数のアプリケーションが同じ SQL Server データベースに依存しているために必要です。データベースからモデルを生成するには、EFCorePowerTools を使用します。
問題は、生成された EF モデルを更新してデータベース スキーマの変更を反映する必要がある場合に発生します。生成された EF エンティティ クラスは上書きされるため、それらに加えられた変更はすべて失われます。アプリケーション内で使用するために EF エンティティを拡張する必要がある場合があるため、これが問題となります。
この問題を解決するために、私は部分的な C# クラスを使用するトリックに頼っています。これについては以下で説明します。
一般的な C#/EF/ASP.NET 8 MVC アプリケーションでは、状況は次のコードのようになります。
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //this is typical class generated by EF Core Power Tools //in EF-Database-First approach // <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated> [PrimaryKey("Id")] public partial class Customer { //Customer-partial-class-1 [Key] [StringLength(15)] public string? Id { get; set; } [StringLength(15)] public string? NAME { get; set; } public short? Language { get; set; } } //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //this is typical model calls for ASP.NET MVC public class CustomerEdit_ViewModel { //model public string? Id { get; set; } = null; //view model // this is our Customer Entity from EF Core public Customer? Customer1 { get; set; } = null; //this is flag for submit button public bool IsSubmit { get; set; } = false; } //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //this is typical controller for ASP.NET MVC public class CustomersController : Controller { // some Controller code here public async Task<ActionResult> CustomerEdit(CustomerEdit_ViewModel model) { if (model.IsSubmit) { // Model validation is done during model binding // we have to check if model is valid if (ModelState.IsValid) { if (model.Customer1 != null) { //we update existing customer in database //redirect to the list of customers } } else { // we go for presentation of validation errors ModelState.AddModelError("", "PleaseCorrectAllErrors"); } } else { ModelState.Clear(); //go for presentation of original data if (model.Id != null) { //get Customer by Id from database } } return View("CustomerEdit", model); } }
ここで、 Customer-partial-class-1は、EF によって生成されたクラスです (データベースからのリバース エンジニアリング経由)。これには、データベース制約に対応するいくつかの検証属性が含まれます。このクラスは通常、モデル クラス (例: CustomerEdit_ViewModel ) で使用され、検証属性はアクションまたはメソッド (例: CustomerEdit ) 中に処理されます。
生成されたクラスにプロパティを追加する必要がある場合、 Customer-partial-class-1を直接変更することはできません。変更すると、変更が上書きされてしまうためです。代わりに、 Customer-partial-class-2を作成し、そこにカスタム プロパティを追加できます。EF がこれらのプロパティをモデルに含めないようにするには、 [NotMapped] 属性を使用する必要があります。
次に例を示します。
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //this is typical class generated by EF Core Power Tools //in EF-Database-First approach // <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated> [PrimaryKey("Id")] public partial class Customer { //Customer-partial-class-1 [Key] [StringLength(15)] public string? Id { get; set; } [StringLength(15)] public string? NAME { get; set; } public short? Language { get; set; } } //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //this is our partial class that extends our generated class //and is not overwritten by EF Core Power Tools //we use it to add some additional properties public partial class Customer { //Customer-partial-class-2 [NotMapped] public int NumberOfCreditCards { get; set; } = 0; [NotMapped] public string? LanguageString { get { string? result = "Unknown"; if (Language == 1) { result = "English"; } else if (Language == 2) { result = "German"; } return result; } } } //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //this is typical model calls for ASP.NET MVC public class CustomerEdit_ViewModel { //model public string? Id { get; set; } = null; //view model // this is our Customer Entity from EF Core public Customer? Customer1 { get; set; } = null; //this is flag for submit button public bool IsSubmit { get; set; } = false; } //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //this is typical controller for ASP.NET MVC public class CustomersController : Controller { // some Controller code here //............. //this is typical action for editing customer public async Task<ActionResult> CustomerEdit(CustomerEdit_ViewModel model) { if (model.IsSubmit) { // Model validation is done during model binding // we have to check if model is valid if (ModelState.IsValid) { if (model.Customer1 != null) { //we update existing customer in database //redirect to the list of customers } } else { // we go for presentation of validation errors ModelState.AddModelError("", "PleaseCorrectAllErrors"); } } else { ModelState.Clear(); //go for presentation of original data if (model.Id != null) { //get Customer by Id from database } } return View("CustomerEdit", model); } }
詳細なコメントが付いたコードは、説明を必要とせずに理解できるはずです。
場合によっては、EF で自動的に生成される検証属性では不十分なため、カスタム検証ルールを追加する必要があります。この場合も、部分クラスが役に立ちます。以下に例を示します。
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //this is typical class generated by EF Core Power Tools //in EF-Database-First approach // <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated> [PrimaryKey("Id")] public partial class Customer { //Customer-partial-class-1 [Key] [StringLength(15)] public string? Id { get; set; } [StringLength(15)] public string? NAME { get; set; } public short? Language { get; set; } } //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //this is our partial class that extends our generated class //and is not overwritten by EF Core Power Tools //we use it to add some additional properties //and do some additional validation [MetadataType(typeof(Customer_MetaData))] public partial class Customer { //Customer-partial-class-2 public Customer() { TypeDescriptor.AddProviderTransparent( new AssociatedMetadataTypeTypeDescriptionProvider( typeof(Customer), typeof(Customer_MetaData)), typeof(Customer)); } [NotMapped] public int NumberOfCreditCards { get; set; } = 0; [NotMapped] public string? LanguageString { get { string? result = "Unknown"; if (Language == 1) { result = "English"; } else if (Language == 2) { result = "German"; } return result; } } } //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //this is our metadata class for our partial class //purpose of this class is to add validation attributes to our partial class //in addition to those that are already in generated class public class Customer_MetaData { //main trick here is that we are adding more validation attributes //in addition to those in generated class [Required] [MinLength(5)] public string? Id { get; set; } = string.Empty; //main trick here is that we are adding more validation attributes //in addition to those in generated class [Required] [MinLength(3)] public string? NAME { get; set; } = string.Empty; } //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //this is typical model calls for ASP.NET MVC public class CustomerEdit_ViewModel { //model public string? Id { get; set; } = null; //view model // this is our Customer Entity from EF Core public Customer? Customer1 { get; set; } = null; //this is flag for submit button public bool IsSubmit { get; set; } = false; } //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //this is utility class for validation public class ValidationUtil { /// <summary> /// Validates the specified model object against custom validation rules. /// </summary> /// <param name="model">The model object to validate.</param> /// <param name="modelState">The ModelStateDictionary to store validation errors.</param> /// <param name="logger">The logger to log errors.</param> /// <param name="prefix">An optional prefix for error keys in the ModelStateDictionary.</param> /// <returns>True if validation is successful; otherwise, false.</returns> /// <exception cref="ArgumentNullException">Thrown when the model is null.</exception> public static bool ValidateModelForCustomRules( object model, ModelStateDictionary modelState, ILogger? logger, string? prefix = null) { bool validationSuccessful = false; try { if (model == null) { throw new ArgumentNullException(nameof(model)); } else { var validationContext = new ValidationContext(model); var validationResults = new List<ValidationResult>(); Validator.TryValidateObject(model, validationContext, validationResults, true); foreach (var result in validationResults) { foreach (var memberName in result.MemberNames) { string key = string.IsNullOrEmpty(prefix) ? memberName : $"{prefix}.{memberName}"; modelState.AddModelError(key, result.ErrorMessage ?? "Error"); } } //Go recursively into depth for all properties of the model object that are objects themselves //we must go manually recursively into depth because API Validator.TryValidateObject does validation //only for class properties on first level foreach (var property in model.GetType().GetProperties()) { if (property.PropertyType.IsClass && property.PropertyType != typeof(string)) { var propertyValue = property.GetValue(model); if (propertyValue != null) { validationSuccessful &= ValidateModelForCustomRules(propertyValue, modelState, logger, property.Name); } } } } } catch (Exception ex) { string methodName = $"Type: {System.Reflection.MethodBase.GetCurrentMethod()?.DeclaringType?.FullName}, " + $"Method: ValidateModel; "; logger?.LogError(ex, methodName); validationSuccessful = false; } return validationSuccessful; } } //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ //this is typical controller for ASP.NET MVC public class CustomersController : Controller { // some Controller code here //............. //this is typical action for editing customer public async Task<ActionResult> CustomerEdit(CustomerEdit_ViewModel model) { if (model.IsSubmit) { // Model validation is done during model binding but we need more //validating for custom validation rules ValidationUtil.ValidateModelForCustomRules(model, ModelState, null); // we have to check if model is valid if (ModelState.IsValid) { if (model.Customer1 != null) { //we update existing customer in database //redirect to the list of customers } } else { // we go for presentation of validation errors ModelState.AddModelError("", "PleaseCorrectAllErrors"); } } else { ModelState.Clear(); //go for presentation of original data if (model.Id != null) { //get Customer by Id from database } } return View("CustomerEdit", model); } }
ご覧のとおり、 Customer-partial-class-2を変更し、 Customer_MetaDataクラスを追加しました。ここでの目的は、NAME プロパティに追加の検証を追加し、最低 3 文字を要求することです。
ASP.NET MVC では、 ModelStateを使用してエラー メッセージを検証および取得できます。カスタム検証を手動でトリガーするには、ユーティリティ メソッドValidationUtil.ValidateModelForCustomRulesも使用する必要がありました。
コードとコメントには、このテクニックを理解するために必要なすべての詳細が記載されているはずです。
私の C#/EF/ASP.NET 8 MVC アプリケーションでは、部分的な C# クラスで上記の手法を使用して、EF で生成されたクラスの機能を拡張しています。これらのトリックにより、一般的なシナリオが簡素化され、コードがより簡潔になり、保守が容易になります。