Explain the four pillars of OOP, with an example of each from your project.
The most-asked CS question in the index: 74 reports, from Infosys, Deloitte, EPAM, Cognizant, Capgemini, HCLTech and Luxoft.
Say this first · about 30 seconds
OOP builds code from : each object holds its data and the methods that keep that data right. I'll show all four on BMW FS , where a and a are both a .
Abstraction means showing what an object can do and hiding how. For example, our calls on any contract and never sees how each kind works it out.
Encapsulation means an object guards its own data. For example, has a private setter, so the only way to change it is , which refuses a negative amount.
Inheritance means a class reuses another class's code and adds its own. For example, and inherit and from , and adds a service fee.
Polymorphism means one call runs different code depending on the real object. For example, the loops a list of contracts and calls : a loan returns its monthly amount, a lease adds its fee.
What I add: I keep inheritance short, a parent and its children, and mark classes so nobody inherits from them by accident. When only one behaviour differs, I pass in a small object behind an interface instead of making a subclass.
Make it yours · BMW FS go-live checks (invented example)
Situation: at BMW FS a deal must pass checks before it goes live. One rule applies to every product: the term must be above 0 months. Each product also has its own rule: an Operating Lease, where the car goes back at the end, must state its return conditions.
What I did: one abstract class, , with one child class per product. The shared rule is written once in (inheritance). The problems sit in a private list that outside code can only read (encapsulation). Each child overrides with its own rule (polymorphism).
Result: picks the class with one if, then uses only and its method (abstraction). is , so no child can replace it for callers using the type, and none can skip the shared rule.
What I'd say: all four pillars in one small feature. A base method that always runs the shared rule, then calls the step each child writes, is the template method pattern.
The code, simplified
Invented to show the shape, in C#. Tap Next to walk deal D-1042, an Operating Lease, through its check.
BMW.sln · where these 4 files sit
Checks/
Diagram · step 1Deal D-1042 is an Operating Lease, so builds an .
GoLiveServicecalls the check only through DealCheck
DealCheckinherited code
OperatingLeaseCheckthe object's class
new OperatingLeaseCheck()
GoLiveService.cs · line 3 · 1 of 5Starts the check. The deals controller calls it with deal D-1042, loaded from the database: Operating Lease, 36 months, no return conditions.
Fix later in your repoExample written for this card: invented, not from a real codebase.
Say it out loud, then check
Show the 30-second answer
OOP builds code from : each object holds its data and the methods that keep that data right. I'll show all four on BMW FS , where a and a are both a .
Abstraction means showing what an object can do and hiding how. For example, our calls on any contract and never sees how each kind works it out.
Encapsulation means an object guards its own data. For example, has a private setter, so the only way to change it is , which refuses a negative amount.
Inheritance means a class reuses another class's code and adds its own. For example, and inherit and from , and adds a service fee.
Polymorphism means one call runs different code depending on the real object. For example, the loops a list of contracts and calls : a loan returns its monthly amount, a lease adds its fee.
What I add: I keep inheritance short, a parent and its children, and mark classes so nobody inherits from them by accident. When only one behaviour differs, I pass in a small object behind an interface instead of making a subclass.
Check your answer: tick what you said
You said 0 of 8
Then add your example: BMW FS go-live checks (invented example)
Situation: at BMW FS a deal must pass checks before it goes live. One rule applies to every product: the term must be above 0 months. Each product also has its own rule: an Operating Lease, where the car goes back at the end, must state its return conditions.
What I did: one abstract class, , with one child class per product. The shared rule is written once in (inheritance). The problems sit in a private list that outside code can only read (encapsulation). Each child overrides with its own rule (polymorphism).
Result: picks the class with one if, then uses only and its method (abstraction). is , so no child can replace it for callers using the type, and none can skip the shared rule.
What I'd say: all four pillars in one small feature. A base method that always runs the shared rule, then calls the step each child writes, is the template method pattern.
See it
Billing runif Kind is "Lease", add fee
Old Contractone class + a Kind text
Kind"Loan" or "Lease"
Balanceanyone can set it
ServiceFeeleases only
1/10Before: one class for every kind. A Kind text says if it is a loan or a lease.Statements and letters each copy that if. Billing's copy forgets the fee: lease bills are 60 short.Fix: one class per kind. holds the shared part; and hold their own.Abstraction: is . It says every contract has , not how.Inheritance: and and . Written once.Encapsulation: has a . refuses -500.The gets a of . Inside: one real Loan, one real Lease.Polymorphism: it calls c.AmountDue(). This object is a Loan, so Loan's runs.Same line, next object: a Lease. Lease's override runs: 400 + 60 = 460. No if anywhere.Abstraction says what. Encapsulation guards data. Inheritance shares code. Polymorphism picks it.
In plain words
A is BMW FS's deal with a customer for a car. A loan: the customer pays monthly and keeps the car. A lease: the customer pays monthly plus a , and gives the car back at the end.
Abstraction is a car's pedal. You press it; you don't need to know the engine. The asks without knowing each kind's rule.
Encapsulation is a bank. You can't walk into the vault; you hand the teller a slip and the teller checks it. is the vault, is the teller.
Inheritance is a family recipe. The child gets everything the parent has, then adds its own. A lease is a contract, plus a service fee.
Polymorphism means many shapes. One question, "what do you owe this month?", and each kind of contract answers its own way.
The senior answer · about 2 minutes
The four, in my words
Abstraction: the caller only knows what an object can do, not how it does it. Our just calls on each contract and asks an , the class that loads contracts from the database, for the list. It doesn't know the pricing rules or which database sits behind it.
Encapsulation: the object protects its own data. has a , so outside code can read it but not set it. The only way to change it is , which checks the amount first. And the throws if the balance is zero or negative, so a contract can never even be created in a broken state.
Inheritance: I use it only when the child really is a kind of the parent and they share fields and methods. A lease really is a contract: it has a and takes payments the same way, so it gets those from instead of copying them.
Polymorphism: the loops a mixed list and calls the same method on every item, and each object runs its own version. So adding a new kind of contract means adding a class, not adding another if somewhere.
Where inheritance hurts
When you change the parent, every child changes with it, even the ones you never looked at. That's called the fragile base class problem.
When a child can't really do what the parent promises. We had a car subscription paid by card, with no balance. Someone made it inherit , its threw an error, and the nightly payment job stopped. That breaks Liskov's rule: a child must work anywhere the parent works. Full story under Production bugs.
When the chain gets deep, a child of a child of a child, nobody can tell which version of a method actually runs.
How I choose
First I ask: is it really a kind of this, and can it keep every promise the parent makes? If yes, and they share data, I use an , a parent you can't build on its own, like . That's is-a.
If they only share an ability, like being billed, I use an , like .
If one behaviour changes on its own, like late fees that differ per client, I use , which is has-a: the contract holds a late-fee rule and calls it, so I don't need a subclass for every kind and client pair. The composition page draws it step by step.
By default I mark classes , so nobody can inherit from them by accident, and I keep inheritance to two levels: a parent and its children.
In production
Encapsulation matters in production, not just in theory. A public list once let a preview screen put a fake late fee on a real bill (see Production bugs).
I also keep one test that loops every kind of contract through a variable and checks each answer. We added it after the BMW FS incident further down: a child class wrote on a method, so its version was skipped. That test would have caught it before release.
All four in one day
One day at BMW FS, in time order. Each step names the pillar doing the work.
Billing runnightly
Loan8,000
Lease12,000
APIpays
get due contracts: 2
1/7Abstraction. 02:00: the gets its due contracts. It only sees .Polymorphism. It calls on the first one. It's a Loan, so Loan's version runs: 400.Polymorphism. Same line, next one: a Lease. Lease's version adds its fee: 400 + 60 = 460.Inheritance. 09:15: the customer pays 460. isn't in Lease; it comes from .Encapsulation. Only can change . It checks the amount, then 12,000 → 11,540.Encapsulation. A bug sends -500. refuses it, and the balance stays 11,540.Recap, in time order. The note shows what each pillar did today.
The story
Before
Before the classes, every BMW FS contract was one class with a Kind text, and each feature asked it with a switch (a block that picks code by a value). This is how that breaks when a new kind joins.
Billinghas a switch
switch (c.Kind)
{
case "Loan":
return c.MonthlyAmount;
case "Lease":
return c.MonthlyAmount
+ c.ServiceFee;
}
1/5A switch picks code by a value. Old billing worked out each bill with one, on the Kind text.The statement and the letter need the amount too, so each got its own copy of that switch.Navistar Financial's truck leases join, with a mileage fee. Every copy needs a new case.Two copies are updated. The statement is missed: it shows one amount, the bill charges another.With classes, the truck lease is one new class. The three features call , unchanged.
Why it had to change
A rule copied into many switch blocks gets missed in one of them. Nothing fails at build time, and nobody notices until a customer's bill and statement disagree.
How it works now
Each kind becomes a class that overrides . The billing loop is foreach (Contract c in due) c.AmountDue();. .NET picks the code from the object, while the app runs. Step by step (the is on its page):
Billingthe loop
ctype: Contract
Objectthe real one
Its classLease or Loan
has AmountDue? yes
1/5At build time the compiler sees only Contract c. Contract has , so the line builds.While the app runs, c holds just a (an address). .NET follows it to the object.Every object carries its real type. This one says Lease, so .NET looks in Lease's methods.Lease's took the place of Contract's bodyless one, so it runs: 400 + 60.Next turn of the loop, c points at a Loan object. Same line, Loan's code: 500.
What happens next
In the real API the classes sit at the bottom of every request. The one place the kind is still checked is where a database row becomes an object. EF Core is told once, at start-up, that Kind "Loan" means a and "Lease" a ; then it builds the right class for every row.
POST /contracts/42/paymentsamount: 200
ContractsControllerreads the request
PaymentServicedoes the job
1/6A payment arrives as an HTTP request. The controller reads it and hands it to the service.The service asks the repository for contract 42. It knows the , not the database.EF Core reads row 42 from one table. Its Kind column says Lease.So EF Core builds a object. The kind is checked here, once, and never again.The service calls contract.ApplyPayment(200). Contract's check passes and the balance drops. writes it back. The loads due contracts the same way: a mixed list.
What we gained
One place per kind's rules. guarded by the class that owns it. A new kind is one new class plus its tests. And a fake contract in a test is one line.
In the lending API
The shared part every contract inherits. The comments mark the pillars; polymorphism happens when the calls , as in the diagram.
Only the key lines are shown; Deep walks the whole file.
Lending.sln · where this file sits
src/Domain/Contracts/
⋯ 5 lines in Deep⋯ 16 lines in Deep⋯ 2 lines in Deep
Contract.cs · line 4 · 1 of 4: nobody can write new Contract(...). Every real contract is a Loan or a Lease, so a plain Contract would be a bill with no rule. .
The code, file by file
The contracts in the lending API: one abstract base, two kinds, the that uses them, and where a payment enters from HTTP. Tap Next to walk the diagram with the code.
Lending.sln · where these 6 files sit
src/
Domain/Contracts/
Application/
Billing/
Contracts/
Api/Controllers/
Contract.cs · line 1 · 1 of 9The folder for business rules. These classes know nothing about HTTP or the database, so a test can build them in one line.
What they ask next
The follow-ups interviews really ask. Answer out loud, then tap to check.
ReportedWhat is the difference between abstraction and encapsulation?
Abstraction is about what callers see: they call and never see each kind's rule. Encapsulation is about who may change the data: changes only through . One shapes the outside, the other guards the inside.
// Abstraction: what, not howpublic abstract decimal AmountDue();
// Encapsulation: only I change itpublicdecimal Balance
{ get; privateset; }
ReportedWhat is compile-time vs run-time polymorphism?
Compile time is overloading: same name, different parameters, and the compiler picks one when you build. Run time is overriding: a or method replaced with , and .NET picks by the object's real type while the app runs.
// compile time: the parameters pick
Bill(Contract c)
Bill(Contract c, DateOnly day)
// run time: the object picks
c.AmountDue();
ReportedAbstract class or interface: when do you choose each?
Abstract class when the types really are one family and share data and code: every contract has a . Interface when unrelated types share only an ability: a contract and a subscription can both be billed. A class gets one base class but many interfaces. ,.
Answer, then open the next level. That is the order an interviewer digs in.
Reporteda candidate was asked Intervieweran interviewer asks it
ReportedWhat is the difference between abstraction and encapsulation?
Abstraction is about what callers see: they call and never see each kind's rule. Encapsulation is about who may change the data: changes only through . One shapes the outside, the other guards the inside.
// Abstraction: what, not howpublic abstract decimal AmountDue();
// Encapsulation: only I change itpublicdecimal Balance
{ get; privateset; }
No. If a getter hands out a or an object with public setters, callers change your data without asking. Hand out a read-only view () and add through one checked method.
Why not…Is a security boundary?
No. Reflection is .NET code that looks inside a class while the app runs (); it can call even a private setter, as below. stops mistakes, not attackers. Real protection is the API checking that the caller may pay into contract 42 at all.
var p = typeof(Contract)
.GetProperty(nameof(Contract.Balance));
p!.SetValue(loan, 0m); // runs: 0 owed
ReportedWhat is compile-time vs run-time polymorphism?
Compile time is overloading: same name, different parameters, and the compiler picks one when you build. Run time is overriding: a or method replaced with , and .NET picks by the object's real type while the app runs.
// compile time: the parameters pick
Bill(Contract c)
Bill(Contract c, DateOnly day)
// run time: the object picks
c.AmountDue();
Explain this lineWhat happens if you use instead of ?
has two jobs. new Lease(...) builds an object. Written on a method, means "a different method that happens to share the name": it hides the parent's method instead of replacing it. A call through a variable then runs Contract's version. That is how BMW FS lease bills lost their service fee.
How does .NET know which override to run?
Every object carries its real type. Each type has a method table, a list of its methods; an override takes the parent's place in the child's list. A call looks up that one : no type check, no if..
InterviewerWhy not call a virtual method from a constructor?
The base constructor runs first, but the override it calls belongs to the child, whose constructor has not run yet. The child's fields set there are still null or 0. Analyzer rule CA2214 warns about it. The first live-coding task below is this exact trap.
ReportedAbstract class or interface: when do you choose each?
Abstract class when the types really are one family and share data and code: every contract has a . Interface when unrelated types share only an ability: a contract and a subscription can both be billed. A class gets one base class but many interfaces. ,.
ReportedWhen does inheritance hurt? What do you use instead?
Three signs: the child is not truly a kind of the parent, the tree gets deep, or one behaviour varies on its own. Then I use composition: the class holds a part and calls it, instead of being a subclass. Late fees differ by client, so takes an ; no UsLoan,NzLoan,UsLease,NzLease.
publicinterfaceILateFeeRule
{
decimal Fee(int daysLate);
}
// in Contract: has a rule, is not oneprotected Contract(decimal balance,
decimal monthlyAmount, ILateFeeRule fees)
UsLoan
NzLoan
UsLease
NzLease
1/4Late fees differ by client: BMW FS (US) one way, UDC Finance (NZ) another. Subclass per mix?Add Australia and a third kind: 3 × 3 = 9 classes, each with a copy of fee code.Composition: holds a fee rule and asks it. It has a rule; it is not a rule.Each client's rule is one small class that plugs in. 3 kinds + 3 rules, not 3 × 3.
A child must work anywhere its parent is expected. If code that loops contracts breaks on one kind, that kind should not be a contract. The usual sign is an override that throws .
Why seal classes by default?
An open class is a promise to every future child: change me carefully. says this class was not designed as a parent, so nobody builds on it by accident. .
Why not…So is inheritance bad?
No. When the types really are one family and every child keeps every promise, one abstract base is the simplest code, like . I keep it to two levels and use composition for what varies on its own.
ReportedWhy does C# not allow multiple class inheritance?
The diamond problem: if two parents both have , which one runs, and whose data do you get? C# allows one base class and many interfaces instead.
Two interfaces have the same default method. What happens?
Since C# 8 an interface may give a method a body (a default). If two interfaces give different defaults and the class writes none, the variable's type picks: ((IA)x).Describe() runs that interface's. If both defaults replace the same method of a parent interface, the build fails (CS8705) until the class writes its own.
How would you test a class hierarchy?
One test that runs against every kind, proving each child keeps the parent's promises (a payment lowers the balance, the amount due is above 0). Then a few tests per child for its own rule. A new kind that fails the shared test should not inherit.
// AllKinds: one Loan, one Lease, ...// [Theory] runs once per item
[Theory, MemberData(nameof(AllKinds))]
publicvoid Amount_due_is_positive(
Contract c)
=> Assert.True(c.AmountDue() > 0);
Production bugs, and how to tell them
Each one: what you see, why, the fix in code, and the line to say.
A preview's fake late fee ends up on the real bill
Customer complaint, BMW FS call centre:
"I paid on 28 Sep, on time.
My October bill has a $25 late fee."
(no exception, no error log)
Cause
had grown a list (extra amounts like late fees), shown as a public . A call-centre preview added a pretend late fee to the real lease to work out a total. The same request then saved a call note, so saved the fake fee too.
Fix
Keep the list private, expose a read-only view, add charges only through with a check, and work out previews in a local variable. A test previews a lease and expects its charges to be unchanged.
Diagram · step 1 hands out its real list. Anyone holding the lease can add to it.
Lease objectloaded by EF Core
Charges: public List
Before · Lease.cs · line 3 · 1 of 1Get-only is not read-only. Callers can't swap the list, but they can , or what is inside.
Say it
Encapsulation failed, not the maths. The class handed out its real list, so a screen that only wanted to look changed the contract, and the save made it permanent. We made the list private with a read-only view and one checked method to add to it. The old code no longer compiles, and a test proves a preview changes nothing.
A subclass that throws stops the nightly payment job
System.NotSupportedException: Subscriptions have no balance
at Subscription.ApplyPayment(Decimal amount)
at PaymentJob.RunAsync(CancellationToken ct)
Posted 1,912 of 6,140 payments. Job failed.
Cause
inherited from only to reuse billing. says every contract can take ; a subscription can't, so its override threw, and the loop over contracts stopped at the first subscription.
Fix
stops inheriting . What the two really share, a monthly bill, becomes the interface, which both implement. Subscription card payments get their own job. One test runs Contract's rules against every kind.
Diagram · step 2To reuse billing, someone writes Subscription : Contract. Its can only throw.
Payment joba new product
Before · Subscription.cs · line 1 · 1 of 2The wrong question was asked: "can I reuse Contract's code?" The right one: "is a subscription really a contract with a balance?" No.
Say it
We used inheritance for reuse, not for a real "is a". A subscription has no balance, so its override threw, and every loop that trusted Contract broke. That is the Liskov rule: a child must work anywhere its parent is expected. We moved the shared part to an interface and added one test that every kind of contract must pass.
Production incident: Lease bills sent without the service fee
P2 · serious, has a workaroundBMW FScame with a deployBilling run in the servicing API
Composed, not a real event at BMW FS. The mechanism is real C#: `new` on a method hides the parent's version, so a call through the parent type runs the parent's code. The counts and amounts are composed. This is the earlier design: later `AmountDue()` became `abstract`, as on the card.
The story in 5 lines
On 1 Oct the made 4,200 lease bills, each 60 short: the monthly fee was missing. BMW FS's billing lead spotted it in the daily income report.
No errors anywhere. Every bill was a believable number, just too low.
Release 7.3 added a to , so the could loop one list of contracts. already had its own ; the build warned, and was added to clear the warning.
hides instead of replacing, and the calls through , so every lease ran Contract's version.
Fix: , a test that bills every kind through , and a review rule that on a method needs a written reason.
Who's who (4 people)
You
.NET developer on the vendor team. Finds the cause, writes the fix.
Tech lead
Vendor senior. Reviews every change, runs the postmortem.
L2 pod
Vendor support. First look, runs the log queries.
BMW FS billing lead
Owns customer bills. Decides how the missed fees are collected.
Tap a step. The map lights the place where it happens.
Detect: 4,200 lease bills, all 60 short. Nothing threw; the average lease bill fell from 460 to 400.
Wed 1 Oct, 19:30 IST (10:00 ET)BMW FS billing lead, then the L2 pod
What: income for 1 Oct shows $0. Last month it was about $252,000. The billing lead raises a ticket.
Why it hid: nothing failed. The finished, every bill was a valid amount, and no alert watches amounts.
How we saw it: runs the query below. Loan bills look normal; the average lease bill fell by exactly 60, the fee.
App Insights: average bill by kind
customEvents
| where timestamp > ago(40d)
| where name == "BillCreated"
| extend kind = tostring(
customDimensions.Kind),
amount = todouble(
customDimensions.Amount)
| summarize avg(amount)
by kind, bin(timestamp, 1d)
In theory
An alert on the average bill per kind: page someone if it moves more than 5% between runs.
In practice
No alert. A person reading a report found it the same evening, before bills were emailed.
Try it yourself · 10 minutes, on your Mac
dotnet new console -n HideLab. Write with public virtual decimal AmountDue() => 400m;.
Write Lease : Contract with public new decimal AmountDue() => 460m;.
Contract c = new Lease(); then print c.AmountDue(): 400. Print ((Lease)c).AmountDue(): 460.
Change to and run again: both print 460.
You should see
With , the variable's type picks the method. With , the object does.
The senior move
"When a refactor makes a method virtual, I search every child that already has that name. The compiler only warns, and one keyword silences it."
SourcesSourced: method hiding, vs and warning CS0114 (C# docs, "Knowing when to use override and new keywords"). Composed: BMW FS's systems, dates, counts, amounts, names, ticket and numbers, and the (standard columns).
Traps
Don't sayEncapsulation means making fields private.SayPrivate data plus methods that keep it valid, and never handing out a list or object callers can change.
Don't sayAbstraction and encapsulation are the same thing.SayAbstraction decides what callers see. Encapsulation decides who can change the data.
Don't sayInheritance is for reusing code.SayInheritance is for a real "is a". For reuse alone I use composition, so a parent's change can't break unrelated classes.
Don't sayOverloading is run-time polymorphism.SayOverloading is picked by the compiler. Overriding is picked while the app runs, by the object's real type.
Traps
What should you say instead? Tap to check.
Don't sayEncapsulation means making fields private.
Say instead
Private data plus methods that keep it valid, and never handing out a list or object callers can change.
Don't sayAbstraction and encapsulation are the same thing.
Say instead
Abstraction decides what callers see. Encapsulation decides who can change the data.
Don't sayInheritance is for reusing code.
Say instead
Inheritance is for a real "is a". For reuse alone I use composition, so a parent's change can't break unrelated classes.
Don't sayOverloading is run-time polymorphism.
Say instead
Overloading is picked by the compiler. Overriding is picked while the app runs, by the object's real type.
Live coding
Easy first, then harder. Set a timer and type it before you look.
Task 1
Question index: Musteasy · 20 minutes
Model contracts in C#: an abstract with a guarded balance, and and each overriding . Prove in a test that a bad payment is refused and changes nothing.
Given.cs · line 1 · 1 of 4Top-level lines run first. The classes are below.
Stuck? Hints
Which constructor runs first when you write new Lease()?
Print is virtual. Whose Print runs inside the base constructor?
Kind uses . What type is the variable c?
Show a solution
Lending.sln · where this file sits
Output · line 1 · 1 of 3Base constructor → virtual Print → Lease's Print, but _name is not set yet.
What the interviewer checks
Line 1 is null: the base constructor calls Lease's override before has set _name.
Line 2 is : the object is a Lease, so the override runs.
Line 3 is base Kind: hides, and c is typed .
Senior extra: with string? _name = "Lease"; (a field initializer) line 1 prints , because in C# field initializers run before the base constructor. Analyzer rule CA2214 warns about the virtual call.
Task 3
Question index: Mustmedium · 25 minutes
A picks the amount with a switch on Kind (given above). Turn it into classes, then add Navistar's (adds a mileage fee) without editing .
PaymentCalculator.cs · line 7 · 1 of 1Every new kind lands here, and in every other switch like it. is one database row with every kind's columns.
Stuck? Hints
What do both branches share? That goes in the base class.
Each branch becomes one override.
Where must the Kind text still be read? Only where a row becomes an object.
Show a solution
Lending.sln · where these 2 files sit
src/
Domain/Contracts/
Infrastructure/Contracts/
Diagram · step 3Fix: one class per kind. holds the shared part; and hold their own.
Billing runif Kind is "Lease", add fee
Old Contractone class + a Kind text
Kind"Loan" or "Lease"
Balanceanyone can set it
ServiceFeeleases only
TruckLease.cs · line 6 · 1 of 2The only new data.
What the interviewer checks
An abstract base with , one class per kind.
The switch leaves billing; one factory maps Kind to a class when a row is loaded (EF Core can do this for you).
is one new file; and its tests are untouched.
You can say when a switch is fine: it sits in one place and the set of kinds never grows.
Senior extra: the factory is the one switch left, so a new kind is one line there; a test that loads every Kind value guards it.
With AI tools
Interviewers now ask how you check AI-written code. Copilot often writes public List<T> Items { get; set; } and a new subclass for each variation, because both are common in the code it learned from. Its quick fix for warning CS0114 ("this method hides the parent's") can be "add the keyword", which silences the warning and keeps the bug. In review I look for three things: lists and setters that leak, children that throw , and where was meant.