Open-closed principle is about program entities should be opened for extension, but closed for changing. We should create extension points where we will insert abstraction classes and interfaces. You will have possibility add new classes without changing current classes. Also you can delete old irrelevant classes entirely.

Let’s speak about this principle at example. We have ECM-system, that contains documents.

    public class Doc
    {
        public string Num { get; set; }
        public string Date { get; set; }
        public DocTypes DocType { get; set; }
    }

Also system has document types.

    public enum DocTypes
    {
        In,
        Out
    }

Let’s create report subsystem, that can give information depends on document type.

    public class PrintGenerator
    {
        public string CreateForm(Doc doc)
        {
            switch (doc.DocType)
            {
                case DocTypes.In:
                    return $"Входящий {doc.Num} от {doc.Date}";
                case DocTypes.Out:
                    return $"Исходящий {doc.Num} от {doc.Date}";
                default:
                    return "";
            }
        }
    }

If you want to add functionality, for example, order printing, you should change class PrintGenerator. And at that moment you can break current realization.

What we can do? We should have possibility to change logic without changing current classes, but with adding new components.

We’ll create interface for report form generator and one form for each document type.

    public interface IPrintGenerator
    {
        public string CreateForm(Doc doc);
    }

    public class PrintGeneratorIn : IPrintGenerator
    {
        public string CreateForm(Doc doc)
        {
            return $"Входящий {doc.Num} от {doc.Date}";
        }
    }

    public class PrintGeneratorOut : IPrintGenerator
    {
        public string CreateForm(Doc doc)
        {
            return $"Исходящий {doc.Num} от {doc.Date}";
        }
    }

    public class PrintGeneratorPr:IPrintGenerator
    {
        public string CreateForm(Doc doc)
        {
            return $"Приказ {doc.Num} от {doc.Date}";
        }
    }

Let’s create abstract document class. Using inheritance we receive incoming, outcoming and order document types.

    abstract public class Doc
    {
        public string Num { get; set; }
        public string Date { get; set; }
        abstract public IPrintGenerator PrintGenerator { get; set; }
    }
    public class DocIn:Doc
    {
        public override IPrintGenerator PrintGenerator { get; set; } = new PrintGeneratorIn();
    }
    public class DocOut : Doc
    {
        public override IPrintGenerator PrintGenerator { get; set; } = new PrintGeneratorOut();
    }
    public class DocPr : Doc
    {
        public override IPrintGenerator PrintGenerator { get; set; } = new PrintGeneratorPr();
    }

So when we decide to add new document types or printing forms, we can extend our structure without changing current sources.

        static void Main(string[] args)
        {
            var docList = new List<Doc>();
            docList.Add(new DocIn() { Num = "В-1", Date = "2022-01-01"});
            docList.Add(new DocOut() { Num = "И-1", Date = "2022-01-01"});
            docList.Add(new DocPr() { Num = "Пр-1", Date = "2022-01-01" });

            foreach(var doc in docList)
                Console.WriteLine(doc.PrintGenerator.CreateForm(doc));
        }