EFCore Tutorial P5:Backing Fields and Shadow Properties

mohamed Tayel - Sep 15 - - Dev Community

In this article, we’ll explore how to implement Backing Fields and Shadow Properties in Entity Framework Core (EF Core), enhancing your control over data and metadata management. We will also walk through how to seed data into your database, handle migrations, and test both valid and invalid cases for modifying inventory quantities. By the end, you will have a robust project structure that ensures both data integrity and efficient metadata tracking.


1. Backing Fields in EF Core

Backing Fields are private fields that store data for public properties, giving you fine-grained control over how sensitive data is accessed and modified. This is especially useful for properties like Quantity in an Inventory class, where we need to enforce rules, such as preventing negative quantities.

Inventory Class with Backing Field for Quantity

Here’s how you can apply a backing field for the Quantity property:

public class Inventory
{
    private int _quantity; // Backing field for Quantity

    public int Id { get; set; }
    public int ProductId { get; set; }
    public Product Product { get; set; }

    public int Quantity
    {
        get => _quantity;
        set
        {
            if (value < 0)
                throw new ArgumentException("Quantity cannot be negative.");
            _quantity = value;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • The _quantity backing field ensures that the Quantity property cannot be set to a negative value, protecting your business logic and data integrity.

2. Refactor into Services and Configuration

To maintain a clean and scalable architecture, we’ll refactor the business logic into a Services folder and manage entity configuration in an EntityConfiguration folder. This keeps your code organized and maintainable as your application grows.

Folder Structure

/Services
    /ProductService.cs
    /InventoryService.cs
/EntityConfiguration
    /ProductConfiguration.cs
    /InventoryConfiguration.cs

Enter fullscreen mode Exit fullscreen mode

Folder Structure


3. Product and Inventory Configuration with Data Seeding

We’ll use the HasData() method to seed initial data for both Product and Inventory entities. When the database is created, this data will be automatically inserted, making it easier to test the application.

ProductConfiguration Class

public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
    public void Configure(EntityTypeBuilder<Product> builder)
    {
        builder.Property(p => p.Price).HasColumnType("decimal(18,2)");

        builder.HasOne(p => p.Inventory)
               .WithOne(i => i.Product)
               .HasForeignKey<Inventory>(i => i.ProductId);

        // Seed data for Products
         // Seed data for Products
 builder.HasData(
 new Product { Id = 3, Name = "Tablet", Price = 299.99M, CategoryId = 1 },
 new Product { Id = 4, Name = "Smartwatch", Price = 199.99M, CategoryId = 1 },
 new Product { Id = 5, Name = "Desktop", Price = 1200.00M, CategoryId = 1 },
 new Product { Id = 6, Name = "Monitor", Price = 300.00M, CategoryId = 1 }
 );
    }
}
Enter fullscreen mode Exit fullscreen mode

InventoryConfiguration Class

public class InventoryConfiguration : IEntityTypeConfiguration<Inventory>
{
    public void Configure(EntityTypeBuilder<Inventory> builder)
    {
        builder.Property(i => i.Quantity).IsRequired();



        // Seed data for Inventory
        builder.HasData(
            new Inventory { Id = 1, ProductId = 1, Quantity = 50 },
            new Inventory { Id = 2, ProductId = 2, Quantity = 100 }
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

By using HasData(), we ensure that the database is seeded with initial values for Product and Inventory.


4. Running Migrations and Updating the Database

Before using the seeded data, you need to run migrations and update the database. This ensures that the schema and data are applied correctly.

Steps to Migrate and Update the Database

  1. Apply Entity Configurations: Ensure that InventoryConfiguration is applied in the AppDbContext by adding this line:
   modelBuilder.ApplyConfiguration(new InventoryConfiguration());
Enter fullscreen mode Exit fullscreen mode
  1. Add Migrations: In the Package Manager Console or CLI, run the following command to add a migration:
   Add-Migration SeedProductAndInventory 
Enter fullscreen mode Exit fullscreen mode
  1. Update the Database: Apply the migration and seed the data by running:
   Update-Database
Enter fullscreen mode Exit fullscreen mode

This will create the necessary database schema and insert the initial Product and Inventory data.


5. Testing the Backing Fields (Implemented in Sections 1-4)

Sure! To ensure we properly test the Backing Field logic in the InventoryService, let’s first add the necessary logic in the InventoryService class. Once that’s done, we’ll update the Program.cs file to reflect the changes and test the functionality.

InventoryService with Backing Field Logic

We need to ensure the InventoryService handles the validation logic based on the backing field defined in the Inventory class. Specifically, the service should catch any exceptions triggered by trying to set a negative quantity.

Here’s how you can implement the logic in InventoryService:

public class InventoryService
{
    private readonly AppDbContext _context;

    public InventoryService(AppDbContext context)
    {
        _context = context;
    }

    // Retrieve inventory by ProductId
    public Inventory GetInventoryByProduct(int productId)
    {
        return _context.Inventories.FirstOrDefault(i => i.ProductId == productId);
    }

    // Update the quantity of inventory for a specific product
    public void UpdateQuantity(int productId, int quantity)
    {
        var inventory = _context.Inventories.FirstOrDefault(i => i.ProductId == productId);
        if (inventory != null)
        {
            try
            {
                inventory.Quantity = quantity;  // Backing field logic will validate this
                _context.SaveChanges();
            }
            catch (ArgumentException ex)
            {
                // Log the exception or throw a custom error if needed
                throw new ArgumentException(ex.Message);
            }
        }
        else
        {
            throw new InvalidOperationException("Inventory not found.");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • GetInventoryByProduct: This method fetches the inventory for a given product based on ProductId.
  • UpdateQuantity: It attempts to set a new quantity, which is validated by the backing field in the Inventory class. If the quantity is invalid (less than 0), an ArgumentException is caught and re-thrown.

Updating Program.cs

With the InventoryService logic in place, we can now update Program.cs to test both valid and invalid quantity updates.

using System;

class Program
{
    static void Main(string[] args)
    {
        using (var context = new AppDbContext())
        {
            var productService = new ProductService(context);

            // Create a product
            productService.CreateProduct("Laptop", 999.99M, "Electronics");

            // Read all products
            var products = productService.GetAllProducts();
            foreach (var product in products)
            {
                Console.WriteLine($"Product: {product.Name}, Category: {product.Category.Name}, Price: {product.Price}");
            }

            // Update product price
            var productIdToUpdate = products[0].Id;
            productService.UpdateProductPrice(productIdToUpdate, 899.99M);


        }
        CallInventory();

    }

    public static void CallInventory()
    {
        using (var context = new AppDbContext())
        {
            var inventoryService = new InventoryService(context);

            // Access seeded inventory data for Product 3
            var inventory = inventoryService.GetInventoryByProduct(3);
            Console.WriteLine($"Product 3 has {inventory.Quantity} items in stock.");

            // Update quantity with a valid value (greater than 0)
            inventoryService.UpdateQuantity(3, 150);
            Console.WriteLine("Quantity updated to 150.");

            // Try updating with an invalid quantity (less than 0)
            try
            {
                inventoryService.UpdateQuantity(3, -50);
            }
            catch (ArgumentException ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • We start by retrieving the inventory for ProductId = 3 and displaying its current quantity.
  • Next, we update the quantity with a valid value (150), and it will succeed.
  • Finally, we attempt to update the quantity with an invalid value (-50), which should trigger the backing field’s validation logic and throw an exception, which we catch and display.

With these changes, your InventoryService now handles backing field validation, and you can test it in the Program.cs file to ensure the logic behaves as expected.


6. Introducing Shadow Properties

Now that we’ve tested the backing field, let’s move on to Shadow Properties. Shadow properties exist in the EF Core model but are not explicitly defined in the entity classes. They are useful for tracking metadata like timestamps or additional information without modifying the domain model.

We’ll apply shadow properties to both the Product and Inventory entities to track when they were last updated.

InventoryConfiguration with Shadow Property

public class InventoryConfiguration : IEntityTypeConfiguration<Inventory>
{
    public void Configure(EntityTypeBuilder<Inventory> builder)
    {
        builder.Property(i => i.Quantity).IsRequired();

        // Add shadow property for tracking last updated time
        builder.Property<DateTime>("LastUpdated");

        // Seed data for Inventory
        builder.HasData(
            new Inventory { Id = 1, ProductId = 1, Quantity = 50 },
            new Inventory { Id = 2, ProductId = 2, Quantity = 100 }
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

ProductConfiguration with Shadow Property

public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
    public void Configure(EntityTypeBuilder<Product> builder)
    {
        builder.Property(p => p.Price).HasColumnType("decimal(18,2)");

        // Add shadow property for tracking last updated time
        builder.Property<DateTime>("LastUpdated");

        builder.HasOne(p => p.Inventory)
               .WithOne(i => i.Product)
               .HasForeignKey<Inventory>(i => i.ProductId);

        // Seed data for Products
        builder.HasData(
            new Product { Id = 1, Name = "Laptop", Price = 999.99M, CategoryId = 1 },
            new Product { Id = 2, Name = "Smartphone", Price = 499.99M, CategoryId = 1 }
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Add Migrations:

In the Package Manager Console or CLI, run the following command to add a migration:

   Add-Migration AddLastUpdateFiled
Enter fullscreen mode Exit fullscreen mode

Update the Database:

Apply the migration and seed the data by running:

   Update-Database
Enter fullscreen mode Exit fullscreen mode

7. Using Shadow Properties in InventoryService

To use the shadow property LastUpdated, we’ll modify InventoryService to update and retrieve the shadow property when inventory data is changed.

public class InventoryService
{
    private readonly AppDbContext _context;

    public InventoryService(AppDbContext context)
    {
        _context = context;
    }

    public void UpdateQuantity(int productId, int quantity)
    {
        var inventory = _context.Inventories.FirstOrDefault(i => i.ProductId == productId);
        if (inventory != null)
        {
            inventory.Quantity = quantity;

            // Update the LastUpdated shadow property
            _context.Entry(inventory).Property("LastUpdated").CurrentValue = DateTime.Now;

            _context.SaveChanges();
        }
        else
        {
            throw new InvalidOperationException("Inventory not found.");
        }
    }

    public DateTime? GetLastUpdated(int productId)
    {
        var inventory = _context.Inventories.FirstOrDefault(i => i.ProductId == productId);
        if (inventory != null)
        {
            // Retrieve the LastUpdated shadow property
            return _context.Entry(inventory).Property("LastUpdated").CurrentValue as DateTime?;
        }

        return null;
    }
}
Enter fullscreen mode Exit fullscreen mode

8. Testing Shadow Properties in Program.cs

Let’s update Program.cs to test the shadow properties in both the Product and Inventory entities.

using ProductData;
using ProductData.Services;
using System;

class Program
{
    static void Main(string[] args)
    {
        using (var context = new AppDbContext())
        {
            var productService = new ProductService(context);

            // Create a product
            productService.CreateProduct("Laptop", 999.99M, "Electronics");

            // Read all products
            var products = productService.GetAllProducts();
            foreach (var product in products)
            {
                Console.WriteLine($"Product: {product.Name}, Category: {product.Category.Name}, Price: {product.Price}");
            }

            // Update product price
            var productIdToUpdate = products[0].Id;
            productService.UpdateProductPrice(productIdToUpdate, 899.99M);


        }
        CallInventory();

    }

    public static void CallInventory()
    {
        using (var context = new AppDbContext())
        {
            var inventoryService = new InventoryService(context);

            // Access seeded inventory data for Product 1
            var inventory = inventoryService.GetInventoryByProduct(3);
            Console.WriteLine($"Product 3 has {inventory.Quantity} items in stock.");

            // Update quantity with a valid value (greater than 0)
            inventoryService.UpdateQuantity(3, 150);
            Console.WriteLine("Quantity updated to 150.");
            var lastUpdated = inventoryService.GetLastUpdated(3);
            Console.WriteLine($"Product 1 Last Updated: {lastUpdated}");
            // Try updating with an invalid quantity (less than 0)
            try
            {
                inventoryService.UpdateQuantity(3, -50);
            }
            catch (ArgumentException ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Expected Output:

  • You will see the updated timestamp for ProductId = 3, confirming that the shadow property is being updated correctly.

Conclusion

By testing the Backing Fields in section 5 and introducing Shadow Properties in section 6, we now have complete control over both data and metadata in our application. Backing fields ensure valid data input, while shadow properties provide metadata tracking without modifying the core domain models. With the implementation of both techniques, your EF Core solution is more robust and maintainable.
Source Code EFCoreDemo

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player