paint-brush
Entity Framework 8 – Partial Classes Tricks You Should Know Aboutby@markpelf
254 reads New Story

Entity Framework 8 – Partial Classes Tricks You Should Know About

by Mark PelfFebruary 19th, 2025
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

In EF 8 – Database First approach the generated EF classes cannot be directly extended with additional functionality. To overcome this, we can leverage partial C# classes. This article presents useful tricks for extending functionality in an EF/ASP.NET environment.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Entity Framework 8 – Partial Classes Tricks You Should Know About
Mark Pelf HackerNoon profile picture

This article demonstrates several techniques for using partial C# classes to address common issues in EF 8/ASP.NET8.


Abstract: In EF 8 – Database First approach the generated EF classes cannot be directly extended with additional functionality since they are overwritten when the model is regenerated. To overcome this, we can leverage partial C# classes. This article presents useful tricks for extending functionality in an EF/ASP.NET environment.

1 Entity Framework Core – Database First Approach

In my C#/ASP.NET 8 MVC project, I am using the Entity Framework Core Database First approach. This is necessary because several applications across different technologies rely on the same SQL Server database. To generate a model from the database, I use EFCorePowerTools.


The issue arises when the generated EF model needs to be updated to reflect changes to the database schema. Since the generated EF entity classes will be overwritten, any modifications made to them are lost. This is a problem because, at times, we need to extend the EF entities for use within the application.


To solve this issue, I rely on tricks involving partial C# classes, which will be explained below.

2 A Typical Situation in C#/EF/ASP.NET app

In a typical C#/EF/ASP.NET 8 MVC application, the situation might look like the following code:

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

Here, Customer-partial-class-1 is a class generated by EF (via reverse engineering from the database). It includes some validation attributes that correspond to database constraints. This class is usually used in a model class (e.g., CustomerEdit_ViewModel), where the validation attributes are processed during an action or method (e.g., CustomerEdit).

3 Trick 1 – Use Partial Classes to Add Custom Properties

If we need to add properties to the generated class, we can't directly modify Customer-partial-class-1, because doing so would lead to the changes being overwritten. Instead, we can create Customer-partial-class-2 and add our custom properties there. To prevent EF from including these properties in the model, we must use the [NotMapped] attribute.


Here is an example:


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

The code, with detailed comments, should be self-explanatory.

4 Trick 2 - Use Partial Classes to Add Custom Validation Attributes

Sometimes, the automatically generated validation attributes in EF are not sufficient, and we need to add custom validation rules. Again, partial classes come to the rescue. Below is an example:


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

As you can see, we modified Customer-partial-class-2 and added a Customer_MetaData class. The purpose here is to add additional validation for the NAME property, requiring a minimum of 3 characters.


In ASP.NET MVC, we can use ModelState to validate and retrieve error messages. Note that we also had to use the utility method ValidationUtil.ValidateModelForCustomRules to manually trigger custom validation.


The code and comments should provide all the necessary details for understanding this technique.

5 Conclusion

In my C#/EF/ASP.NET 8 MVC application, I use the above techniques with partial C# classes to extend the functionality of EF-generated classes. These tricks simplify common scenarios, making the code more concise and easier to maintain.