paint-brush
Entity Framework 8 — трюки с частичным классом, о которых вам следует знатьк@markpelf
Новая история

Entity Framework 8 — трюки с частичным классом, о которых вам следует знать

к Mark Pelf17m2025/02/19
Read on Terminal Reader

Слишком долго; Читать

В подходе EF 8 – Database First сгенерированные классы EF не могут быть напрямую расширены дополнительной функциональностью. Чтобы преодолеть это, мы можем использовать частичные классы C#. В этой статье представлены полезные приемы для расширения функциональности в среде EF/ASP.NET.
featured image - Entity Framework 8 — трюки с частичным классом, о которых вам следует знать
Mark Pelf HackerNoon profile picture

В этой статье демонстрируется несколько методов использования частичных классов C# для решения распространенных проблем в EF 8/ASP.NET8.


Аннотация: В подходе EF 8 – Database First сгенерированные классы EF не могут быть напрямую расширены дополнительной функциональностью, поскольку они перезаписываются при повторной генерации модели. Чтобы преодолеть это, мы можем использовать частичные классы C#. В этой статье представлены полезные приемы для расширения функциональности в среде EF/ASP.NET.

1 Entity Framework Core – подход, ориентированный на базу данных

В моем проекте C#/ASP.NET 8 MVC я использую подход Entity Framework Core Database First . Это необходимо, поскольку несколько приложений в разных технологиях используют одну и ту же базу данных SQL Server. Для генерации модели из базы данных я использую EFCorePowerTools.


Проблема возникает, когда сгенерированную модель EF необходимо обновить, чтобы отразить изменения в схеме базы данных. Поскольку сгенерированные классы сущностей EF будут перезаписаны, любые внесенные в них изменения будут потеряны. Это проблема, поскольку иногда нам нужно расширить сущности EF для использования в приложении.


Для решения этой проблемы я прибегаю к приемам с использованием частичных классов C# , которые будут описаны ниже.

2 Типичная ситуация в приложении C#/EF/ASP.NET

В типичном приложении 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 ).

3. Прием 1. Использование частичных классов для добавления пользовательских свойств

Если нам нужно добавить свойства в сгенерированный класс, мы не можем напрямую изменить 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); } }

Код с подробными комментариями должен быть понятен без дополнительных пояснений.

4. Прием 2. Использование частичных классов для добавления пользовательских атрибутов проверки

Иногда автоматически сгенерированные атрибуты валидации в 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 для ручного запуска пользовательской проверки.


Код и комментарии должны содержать всю необходимую информацию для понимания этой методики.

5 Заключение

В моем приложении C#/EF/ASP.NET 8 MVC я использую вышеописанные приемы с частичными классами C# для расширения функциональности классов, сгенерированных EF. Эти приемы упрощают общие сценарии, делая код более лаконичным и простым в обслуживании.