Course contentsShow
← C#
Lesson 165 of 1,9082. Object-Oriented Design in C#Free lesson

Managing Interface Versioning

You will handle breaking changes in public interfaces while maintaining backward compatibility for dependent systems.

In this lesson, you will learn how to evolve your public interfaces to include new functionality without breaking the code that already relies on your existing library.

The Problem with Versioning

When you publish an interface, such as IDataProcessor, other developers will implement it in their own systems. If you later add a new method to that interface, every single developer who implemented IDataProcessor will suddenly find their code broken, as their classes no longer "fulfill the contract" of your updated interface. This is known as a breaking change.

Solving Versioning with Interface Aggregation

Instead of modifying an existing interface, the cleanest way to handle versioning is to keep the original interface untouched and create a new, "versioned" interface that inherits from the old one. This technique is often referred to as Interface Aggregation.

Imagine you have a legacy interface:

public interface IDataProcessor {
    void Process(string data);
}

If you need to add a feature to support batch processing, do not change IDataProcessor. Instead, create a new interface that inherits from the original:

public interface IDataProcessorV2 : IDataProcessor {
    void ProcessBatch(IEnumerable<string> data);
}

Supporting Multiple Versions

By using this approach, existing systems can continue to use IDataProcessor without any modifications. When you want to roll out the new features, you can check if a component supports the newer version using the is operator or a cast:

public void HandleProcessor(IDataProcessor processor) {
    // Old code still works perfectly fine here
    processor.Process("Data");

    // Check if the processor supports the new version
    if (processor is IDataProcessorV2 advancedProcessor) {
        advancedProcessor.ProcessBatch(new[] { "Data1", "Data2" });
    }
}

This pattern allows your framework to grow over time while ensuring that dependent systems aren't forced to break just because you added a new capability. You provide the evolution as an opt-in experience rather than a mandatory overhaul.

Key Takeaway: Never modify an existing public interface; instead, create a new interface that inherits from the original to ensure backward compatibility.