NeuroAgent

C# Property Hiding: Private Setters in Inheritance

Learn why hiding inherited properties with private setters violates LSP in C#. Discover better alternatives for controlling access to inherited properties while maintaining object-oriented principles.

Is hiding an inherited property with a private setter a good practice in object-oriented programming?

I’m working with inheritance and want to prevent uncontrolled modification of inherited properties. Is declaring a new private setter on an inherited property considered acceptable, or should this approach be avoided?

csharp
public class Item 
{
    private float price;

    public float Price
    {
        get => price;
        protected set => price = value;
    }
}

public class RegulatedItem : Item 
{
    public new float Price 
    {
        get => base.Price;
        private set => base.Price = value;
    }

    public void ApplyPromotion(float percent)
    {
        if (holiday)
            // prevent abusive price manipulation
        else
            Price *= percent;
    }
}

What are the best practices for controlling access to inherited properties in C#?

NeuroAgent

Using a private setter to hide an inherited property is generally not considered good practice in object-oriented programming as it violates the Liskov Substitution Principle and creates design inconsistencies. While it technically prevents external modification, it breaks the contract established by the base class and can lead to unexpected behavior in polymorphic scenarios.

Contents

Understanding Property Hiding in C#

In C#, when you use the new keyword to declare a property with the same name as an inherited property, you’re hiding rather than overriding the base class member. This creates a completely separate property that exists only in the derived class, while the base class property remains accessible through the base reference.

csharp
public class Item 
{
    private float price;

    public float Price
    {
        get => price;
        protected set => price = value;
    }
}

public class RegulatedItem : Item 
{
    public new float Price // Hides the base Price property
    {
        get => base.Price;
        private set => base.Price = value;
    }
}

Key Differences Between Hiding and Overriding

Aspect Hiding (new) Overriding (virtual/override)
Inheritance Creates new member Replaces base member
Polymorphism Not preserved Preserved
Type system Separate declarations Unified interface
Base class access Still accessible through base Replaced in derived instances

The Microsoft documentation explains that “typically, you restrict the accessibility of the set accessor, while keeping the get accessor publicly accessible” source.


Liskov Substitution Principle Concerns

The Liskov Substitution Principle (LSP) is a fundamental SOLID principle that states: “objects of a derived class should be able to replace objects of the base class without affecting the correctness of the program” source.

Violations Caused by Property Hiding

Using a private setter to hide inherited properties creates several LSP violations:

  1. Broken Substitution Contract: A RegulatedItem cannot truly be used where an Item is expected because they have different property interfaces.

  2. Unexpected Behavior: Code expecting an Item might not work correctly with a RegulatedItem due to the hidden property.

  3. Type Confusion: The same property name behaves differently in different contexts, creating cognitive overhead for developers.

As one source explains: “To quote the Liskov substitution principle, you should be able to place BrokenEgg wherever you can put an Egg. This is most likely not the case. If it is part of your Egg model that viability can be set then BrokenEgg is not an egg.” source

Polymorphism Issues

csharp
List<Item> items = new List<Item> { new Item(), new RegulatedItem() };
foreach (var item in items)
{
    // This works for Item but fails for RegulatedItem's hidden property
    item.Price = 10.0f; // Compile error for RegulatedItem
}

Alternative Access Control Approaches

Instead of hiding properties with private setters, consider these more maintainable alternatives:

1. Protected Setters (Recommended)

csharp
public class Item 
{
    private float price;

    public float Price
    {
        get => price;
        protected set => price = value; // Allow derived classes to modify
    }
}

public class RegulatedItem : Item 
{
    public void ApplyPromotion(float percent)
    {
        if (holiday)
            // Add validation logic here
            Price *= percent; // Direct access to protected setter
    }
}

This approach maintains inheritance integrity while still allowing controlled modification.

2. Internal (Friend) Setters

csharp
public class Item 
{
    private float price;

    public float Price
    {
        get => price;
        internal set => price = value; // Only same assembly can modify
    }
}

This is useful when you need assembly-level access control without inheritance concerns.

3. Validation Through Methods

csharp
public class RegulatedItem : Item 
{
    public bool ApplyPromotion(float percent)
    {
        if (holiday && percent > MAX_PROMOTION)
            return false; // Prevent abusive price manipulation
            
        SetPrice(Price * percent);
        return true;
    }
    
    protected virtual void SetPrice(float newPrice)
    {
        base.Price = newPrice;
    }
}

Best Practices for Inherited Property Management

1. Favor Protected Over Private Modifiers

As one Stack Overflow answer explains: “Whenever I’ve needed to change the access level of a setter, I’ve generally changed it to either Protected (only this class and derived classes can change the value) or Friend (only members of my assembly can change the value).” source

2. Consider Composition Over Inheritance

For complex scenarios where properties need different behavior, composition often provides better flexibility:

csharp
public class Item 
{
    private float price;
    private IPricingStrategy pricingStrategy;

    public float Price 
    {
        get => price;
        set 
        {
            if (pricingStrategy.CanSetPrice(value))
                price = value;
        }
    }
}

public interface IPricingStrategy
{
    bool CanSetPrice(float newPrice);
}

public class RegulatedPricingStrategy : IPricingStrategy
{
    public bool CanSetPrice(float newPrice)
    {
        return newPrice <= MAX_PRICE;
    }
}

3. Use Interfaces for Contracts

Define interfaces that specify the expected behavior:

csharp
public interface IPricedItem
{
    float Price { get; }
}

public interface IPromotableItem : IPricedItem
{
    bool ApplyPromotion(float percent);
}

When to Consider Property Hiding

While generally discouraged, there are rare scenarios where property hiding might be appropriate:

1. Legacy Code Migration

When gradually refactoring legacy code to avoid breaking existing consumers.

2. API Evolution

For public APIs where you need to maintain backward compatibility while changing internal behavior.

3. Specialized Contexts

In very specific domain contexts where the semantic meaning of the property changes significantly in the derived class.

Even in these cases, consider adding documentation and potentially deprecation warnings to guide users toward the new approach.


Practical Implementation Examples

Example 1: Safe Property Modification

csharp
public abstract class Product
{
    protected decimal _price;

    public decimal Price
    {
        get => _price;
        protected set => _price = Math.Max(0, value); // Ensure non-negative
    }
}

public class ElectronicsProduct : Product
{
    public void SetPrice(decimal newPrice)
    {
        if (newPrice > MAX_ELECTRONICS_PRICE)
            throw new InvalidOperationException("Price exceeds maximum for electronics");
            
        Price = newPrice;
    }
}

Example 2: Validation Through Properties

csharp
public class ValidatedItem : Item
{
    public new float Price
    {
        get => base.Price;
        set
        {
            if (value < 0)
                throw new ArgumentException("Price cannot be negative");
            if (holiday && value > MAX_HOLIDAY_PRICE)
                throw new InvalidOperationException("Price exceeds holiday limit");
                
            base.Price = value;
        }
    }
}

Example 3: Event-Based Modification

csharp
public class ObservableItem : Item
{
    public event EventHandler PriceChanged;

    public new float Price
    {
        get => base.Price;
        protected set
        {
            var oldValue = base.Price;
            base.Price = value;
            OnPriceChanged(oldValue, value);
        }
    }

    protected virtual void OnPriceChanged(float oldValue, float newValue)
    {
        PriceChanged?.Invoke(this, new PriceChangedEventArgs(oldValue, newValue));
    }
}

Conclusion

Hiding inherited properties with private setters should generally be avoided in favor of more maintainable approaches that preserve the Liskov Substitution Principle. Key takeaways include:

  1. Use protected setters instead of private setters when derived classes need to modify inherited properties
  2. Consider composition for complex property behavior changes rather than inheritance
  3. Leverage interfaces to define clear contracts for property access
  4. Validate through methods when complex logic is needed for property modification
  5. Document design decisions to help future developers understand the reasoning behind access control choices

The Microsoft documentation emphasizes that “it’s generally a bad programming style to change the state of the object by using the get accessor,” but the same principle applies to setters - they should maintain the expected contract established by the base class source.

For your specific scenario, consider using a protected setter with validation logic or a dedicated method for price modifications rather than hiding the property entirely.

Sources

  1. Microsoft Learn - Restricting Accessor Accessibility
  2. Microsoft Learn - Using Properties
  3. Stack Overflow - How can one type access a private setter of another type’s property?
  4. Reddit r/csharp - Can I make a property have a private setter in a sub class?
  5. Software Engineering Stack Exchange - .NET Properties - Use Private Set or ReadOnly Property?
  6. ByteHide - Liskov Substitution Principle in C#
  7. Dot Net Tutorials - Liskov Substitution Principle Examples in C#
  8. Tutorialsteacher - SOLID: Liskov Substitution Principle
  9. Code Maze - SOLID Principles in C# - Liskov Substitution Principle
  10. C# Corner - SOLID Principles In C# - Liskov Substitution Principle