paint-brush
Entity Framework 8: trucos sobre clases parciales que debes conocerpor@markpelf
Nueva Historia

Entity Framework 8: trucos sobre clases parciales que debes conocer

por Mark Pelf17m2025/02/19
Read on Terminal Reader

Demasiado Largo; Para Leer

En el enfoque EF 8 – Database First, las clases EF generadas no se pueden ampliar directamente con funcionalidad adicional. Para solucionar esto, podemos aprovechar clases C# parciales. Este artículo presenta trucos útiles para ampliar la funcionalidad en un entorno EF/ASP.NET.
featured image - Entity Framework 8: trucos sobre clases parciales que debes conocer
Mark Pelf HackerNoon profile picture

Este artículo demuestra varias técnicas para usar clases C# parciales para abordar problemas comunes en EF 8/ASP.NET8.


Resumen: En el enfoque EF 8 – Database First, las clases EF generadas no se pueden ampliar directamente con funcionalidad adicional, ya que se sobrescriben cuando se regenera el modelo. Para solucionar esto, podemos aprovechar clases C# parciales. Este artículo presenta trucos útiles para ampliar la funcionalidad en un entorno EF/ASP.NET.

1 Entity Framework Core: enfoque de base de datos primero

En mi proyecto C#/ASP.NET 8 MVC , utilizo el enfoque Entity Framework Core Database First . Esto es necesario porque varias aplicaciones de diferentes tecnologías dependen de la misma base de datos de SQL Server. Para generar un modelo a partir de la base de datos, utilizo EFCorePowerTools.


El problema surge cuando el modelo EF generado debe actualizarse para reflejar los cambios en el esquema de la base de datos. Dado que las clases de entidad EF generadas se sobrescribirán, se perderán todas las modificaciones que se les hayan realizado. Esto es un problema porque, a veces, necesitamos ampliar las entidades EF para usarlas dentro de la aplicación.


Para resolver este problema, recurro a trucos que involucran clases parciales de C# , que se explicarán a continuación.

2 Una situación típica en una aplicación C#/EF/ASP.NET

En una aplicación típica de C#/EF/ASP.NET 8 MVC, la situación podría parecerse al siguiente código:

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

Aquí, Customer-partial-class-1 es una clase generada por EF (a través de ingeniería inversa a partir de la base de datos). Incluye algunos atributos de validación que corresponden a las restricciones de la base de datos. Esta clase se utiliza normalmente en una clase de modelo (por ejemplo, CustomerEdit_ViewModel ), donde los atributos de validación se procesan durante una acción o un método (por ejemplo, CustomerEdit ).

Truco 1: Utilizar clases parciales para agregar propiedades personalizadas

Si necesitamos agregar propiedades a la clase generada, no podemos modificar directamente Customer-partial-class-1 , ya que al hacerlo se sobrescribirían los cambios. En su lugar, podemos crear Customer-partial-class-2 y agregar allí nuestras propiedades personalizadas. Para evitar que EF incluya estas propiedades en el modelo, debemos usar el atributo [NotMapped] .


He aquí un ejemplo:

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

El código, con comentarios detallados, debería explicarse por sí solo.

Truco 2: utilice clases parciales para agregar atributos de validación personalizados

A veces, los atributos de validación generados automáticamente en EF no son suficientes y necesitamos agregar reglas de validación personalizadas. Nuevamente, las clases parciales vienen al rescate. A continuación, se muestra un ejemplo:

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

Como puede ver, modificamos Customer-partial-class-2 y agregamos una clase Customer_MetaData . El objetivo aquí es agregar una validación adicional para la propiedad NAME, que requiere un mínimo de 3 caracteres.


En ASP.NET MVC, podemos usar ModelState para validar y recuperar mensajes de error. Tenga en cuenta que también tuvimos que usar el método de utilidad ValidationUtil.ValidateModelForCustomRules para activar manualmente la validación personalizada.


El código y los comentarios deben proporcionar todos los detalles necesarios para comprender esta técnica.

5 Conclusión

En mi aplicación C#/EF/ASP.NET 8 MVC, utilizo las técnicas anteriores con clases C# parciales para ampliar la funcionalidad de las clases generadas por EF. Estos trucos simplifican los escenarios comunes, lo que hace que el código sea más conciso y más fácil de mantener.