C# Interfaces and Contracts: Ask for a Capability, Choose the Implementation
An interface describes what an object can do without choosing its concrete class. Follow a small preview contract from declaration to implementation, then use the same consumer with different objects.
What you will understand
- Explain an interface as a capability contract rather than a concrete object.
- Read required method and getter declarations.
- Implement a contract explicitly on a class with public members.
- Use interface references, arrays and parameters without subtype switches.
- Supply a consumer dependency through a normal constructor.
- Distinguish compiler-checked signatures from written behavioral rules.
- Recognize multiple contracts and explicit interface implementation.
- Choose between an interface, abstract base and composition using the responsibility.
An interface says what a caller can ask for
Imagine a page that needs a message preview. It does not need to know every detail of the object creating that preview. It only needs two things: a channel name and a method that turns a supplied message into preview text. An interface names that small capability.
IMessagePreview declares Channel and Format. Think of it as a promise that an implementing object can answer those requests. The declaration does not create a formatter, store its message or send anything. A concrete class such as EmailPreview supplies the actual implementation.
In IMessagePreview preview = new EmailPreview(), the left side tells the compiler which contract the caller will use. The right side creates the object that fulfills it. There is one object, and preview holds a reference to it. This is the same identity idea you learned with base-class references.
The first program reads Channel and calls Format. Follow each use back to the public property and method in EmailPreview. Notice that no override keyword is needed for this ordinary interface implementation: the class is fulfilling an interface declaration, not overriding a virtual base-class method.
Program.cs · Read the promise, then find the implementation
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Email
Email preview: Lesson ready
IMessagePreview promises a readable Channel and a Format method accepting and returning string. EmailPreview explicitly lists that interface and provides both members publicly. The interface variable refers to the concrete object; it does not create another object. Formatting is a local preview, not delivery.
Read the declaration and implementation side by side
interface introduces a contract type. Prefixing its name with I, as in IMessagePreview, is a naming convention rather than a special compiler requirement. The method declaration string Format(string message); specifies a name, input type and result type, ending with a semicolon because this basic contract supplies no body.
string Channel { get; } requires a readable property. A consumer can read it through the interface but cannot assume a setter exists. The class may compute the channel or store it internally; the caller should not need to know how it is represented.
class EmailPreview : IMessagePreview explicitly states that the class implements the interface. Its ordinary implicit implementations are public and match the required members. A concrete class missing Channel, or supplying a private Format instead, does not fulfill this contract and fails compilation.
These examples use required instance members without default bodies. Modern C# also supports default interface implementations and advanced static abstract contracts. You do not need those features to understand this lesson, and their existence is why we do not claim that interfaces can never contain implementation code.
Program.cs · The class must implement every required member
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Compilation fails: IncompletePreview does not implement IMessagePreview.Channel.Format alone does not fulfill the complete contract. The required Channel getter is missing. These beginner interface declarations have no default implementations, so every listed member must have a suitable implementation in a concrete type.
Program.cs · An ordinary implicit implementation must be public
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Compilation fails: a non-public Format method cannot implement the interface member implicitly.The contract exposes Format to interface callers. A private method of the same signature does not provide that ordinary public implementation. Make the intended method public, or use the explicit implementation syntax introduced later in this lesson.
Use an interface variable without confusing it with an instance
You cannot create new IMessagePreview(). The interface is not a concrete class with an instance constructor. Choose a concrete implementation and assign its reference to the interface variable. The consumer can then use only the members declared by that interface.
C# does not infer this interface relationship merely because a class happens to have Channel and Format members. LookAlike intentionally has similar members but omits : IMessagePreview, so assignment fails. A matching shape is not the same as a declared implemented contract here.
Changing from a concrete reference to an interface reference does not copy the object or erase its other capabilities. It selects the view used by that expression. If a concrete type has extra members, a caller using IMessagePreview should normally avoid depending on them; otherwise the supposedly interchangeable design is still tied to one implementation.
Program.cs · An interface is not a concrete object to instantiate
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Compilation fails: an instance of the interface IMessagePreview cannot be created.Choose a class that implements the contract, such as EmailPreview. An interface variable is useful for referring to that instance, but the interface declaration does not supply a constructible object with instance storage.
Program.cs · Matching method names alone does not implement an interface
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Compilation fails: LookAlike cannot implicitly convert to IMessagePreview.The methods happen to match, but the class does not declare : IMessagePreview. C# requires that declared type relationship here; it does not infer it just from similarly shaped members. List the interface deliberately and meet its contract.
Depend on the capability, then supply a concrete choice
Show(IMessagePreview preview) accepts the capability its work needs. Main can supply EmailPreview or SmsPreview. The helper calls Format on the supplied object instead of branching on concrete class names. Replacing the implementation should not require editing the consumer.
The same contract works in an array. Every element supports the requested members, so one foreach loop can render all previews. Output order follows the array order; the implementation behind each reference supplies its own channel and formatting. An empty array should produce no lines.
A consumer can also receive and store an interface in its constructor. PreviewCard does this and rejects a null dependency immediately. The caller chooses the concrete formatter, so the card does not create a fixed one internally. This is often called dependency injection, but the example is just normal constructor arguments, a field and delegation.
An interface does not choose an implementation automatically. Somewhere, such as Main, concrete objects still need to be created and connected. A dependency-injection framework can automate some wiring in larger applications, but it is not required for the language feature or the principle.
Program.cs · Use the same caller with another implementation
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Email preview: Welcome
SMS preview: Welcome
Show asks only for the IMessagePreview capability. Changing the supplied concrete object changes formatting without rewriting Show. Neither class needs to inherit from a custom common base. Each implementation follows the same stated message validation and trimming policy.
Program.cs · One interface array supports one simple processing loop
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Email | Email preview: Ready
SMS | SMS preview: Ready
Each array element can be used through the same two-member contract. The loop does not inspect class names or cast to email and SMS types. Array order controls line order, while each actual object supplies its implementation.
Program.cs · Give a consumer its dependency through the constructor
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
[SMS] SMS preview: Practice today
PreviewCard stores the interface it needs rather than constructing a fixed EmailPreview internally. The caller chooses the implementation and supplies it through the constructor. This is constructor dependency injection in its simplest form; no framework or container is needed. A null dependency is rejected immediately.
A contract includes meaning that the compiler cannot infer
The compiler checks that a required member exists with a compatible declaration. It cannot infer every promise about the returned text. A method that always returns TODO can satisfy the string signature while failing the feature. Syntax correctness and useful behavior need separate verification.
For our email and SMS preview pair, the written policy is simple: reject null, empty or whitespace-only messages; otherwise trim outside whitespace and preserve the remaining message after the channel prefix. Both implementations follow it so the consumer can rely on it. This differs from the prior polymorphism lesson’s simpler interpolation examples that allowed empty text.
Throw an argument exception before calling Trim on missing input. Main can handle a rejected message at an appropriate boundary; the lab execution tests catch expected failures to inspect them. A real delivery service would have further concerns such as recipients, retries and provider errors, none of which this local preview contract claims to implement.
The guided repair demonstrates another behavioral failure: accepting an interface argument and then overwriting it with a new fixed implementation. No keyword prevents that mistake. A focused test that supplies SmsPreview exposes whether the consumer truly respects its dependency.
Small interfaces can describe different capabilities on one object
A class can implement several interfaces, separated by commas. CombinedPreview can format a local message and describe itself through two small contracts. Two interface variables still refer to that same instance, as ReferenceEquals confirms.
This is not multiple inheritance of class instance state. A C# class has one direct base class but can implement multiple interfaces. Interfaces do not declare instance fields or instance constructors. Each implementing class decides where its data lives and how to establish valid state.
Keep contracts focused. A consumer that only renders a preview should not be forced to depend on unrelated file-writing or account-management operations. Multiple small capabilities are useful when responsibilities actually differ; creating an interface for every class without a caller need adds names without necessarily improving the design.
Explicit implementation is a second syntax option. IMessagePreview.Format names the contract on the member itself and does not use a public modifier. Callers access that member through the interface view. It can separate same-named contracts or keep a concrete public surface small, but it is still available to code that can use the interface and is not a secrecy mechanism.
Program.cs · One object can fulfill more than one small contract
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Local preview: Ready
A local demonstration formatter
True
CombinedPreview lists two interfaces separated by a comma. Both variables refer to the same instance through different capability views. This small local formatter only interpolates fixed input; it intentionally makes no claim to use the stricter email/SMS validation policy. Multiple interfaces do not mean multiple base classes.
Program.cs · An explicit member is accessed through its interface
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Explicit
Explicit preview: Ready
The member names include IMessagePreview. and have no public modifier. These explicit implementations are callable through the interface view, not as concrete.Format(). This can separate capability surfaces or resolve same-named contracts. The class name does not make these methods secret from someone who can use the interface.
Choose an interface for a capability and an abstract class for shared state
In the previous lesson, PracticeReport shared learner state, constructor validation and a rendering workflow. An abstract base class fit those related objects because they genuinely reused that base implementation and initialization. An interface cannot directly provide per-instance fields or an instance constructor for them.
IMessagePreview instead describes the capability a consumer needs without choosing one custom base class. Different types can fulfill it even if they already belong to different class hierarchies. A class can also derive from an abstract base and implement interfaces when both relationships are useful.
Ask two questions: what must the caller be able to request, and what implementation state must related types share? The first can suggest an interface; the second can suggest an abstract base. Composition can connect consumers and implementations regardless of that choice. These tools work together rather than forming a universal ranking of better and worse.
Do not add a framework, type switch or downcast just because the interface hides concrete details. That smaller view is often the point. If a consumer constantly needs extra subtype-specific members, reconsider whether its contract expresses the real responsibility.
Verify every implementation through the same caller
Test both preview implementations through IMessagePreview references. Use an ordinary message, a message with outside spaces, empty text, whitespace-only text and null. Both should preserve the same policy, with only the channel-specific prefix changing.
Change the message so hard-coded reference text cannot accidentally pass. Reverse the array to check output order and use an empty array to check that the consumer adds no fake previews. For PreviewCard, supply each implementation and verify that a null dependency is rejected during construction.
Also inspect the compilation boundaries: an incomplete implementation, non-public implicit method and undeclared look-alike type must fail where demonstrated. A concrete class with explicit members should be used through its interface view. These tests teach different rules; one successful reference program cannot prove all of them.
The lab completes the SMS behavior while leaving the consumer unchanged. It produces local text, not a sent email, SMS, stored message or provider integration. Next we move to enums, where small sets of named states make a different kind of contract easier to understand.
Guided repair: respect the supplied implementation
Show receives SmsPreview but prints Email preview because it replaces the supplied argument with a new EmailPreview. Repair the consumer so it respects the capability object selected by its caller.
Program.cs · A consumer can accidentally ignore its supplied capability
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Email preview: Ready
Show accepts an IMessagePreview but immediately replaces it with a new EmailPreview. It compiles yet ignores the caller’s choice. An interface parameter is useful only when the consumer actually delegates to the supplied implementation.
Repair checkpoints
- Remove preview = new EmailPreview() from Show.
- Keep the IMessagePreview parameter and call its Format method.
- Leave concrete selection at the call site.
- Verify Show(new SmsPreview()) produces SMS preview: Ready.
- Do not replace the mistake with a type switch or a forced cast.
Reveal the repair and explanation
Program.cs · Use the dependency that the caller supplied
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
SMS preview: Ready
The consumer now respects its argument and works with either implementation. It does not need a type switch or a downcast. The interface contract is the connection between the consumer and the selected object.
Independent lab: interchangeable preview implementations
Complete two interchangeable preview implementations under one behavioral policy. Keep the interface and array consumer unchanged, then verify that every accepted message survives formatting.
Acceptance checklist
- Preserve IMessagePreview with its Channel getter and Format method.
- Keep each class explicitly declaring that it implements the interface.
- Keep required ordinary members public.
- Reject null, empty and whitespace-only messages with ArgumentException identifying message.
- Trim outside spaces and return the appropriate channel prefix followed by the remaining message.
- Replace the SMS placeholder with its actual message-based result.
- Leave the shared IMessagePreview[] loop unchanged; it must not branch on subtype names.
- Treat all output as local preview text with no message delivery.
Program.cs · Interchangeable preview implementations starter
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Email preview: Your practice is ready
SMS preview: TODO
The SMS type fulfills the required C# signatures but returns a placeholder. Finish the behavior so the shared consumer can trust both implementations to preserve a valid message and trim its outside spaces. Do not change the loop to special-case SMS.
Show hints
- The SMS method already validates its input; replace only the placeholder return.
- Use message.Trim() rather than a hard-coded lesson sentence.
- An interface guarantees required members, while tests verify the written formatting policy.
Reveal the reference solution
Program.cs · Interchangeable preview implementations reference solution
Predict the result, edit Program.cs, then run your program.
Standard input · optional
Select Run code to compile and execute this program.
Email preview: Your practice is ready
SMS preview: Your practice is ready
Both concrete classes provide the promised readable channel and formatted message. They reject missing input before trimming it. The interface describes their capability, while their implementations enforce the behavioral policy. This example returns local strings only; no provider, recipient or delivery operation exists.
Changed-input checks
- Both implementations preserve Welcome after the correct prefix.
- Outside spaces are trimmed, while spaces inside a valid message remain.
- Empty, whitespace-only and null messages are rejected for message.
- Reversing the array reverses the output order.
- An empty interface array prints no lines.
- PreviewCard works with either implementation and rejects a null dependency.
Recap: capability, implementation and consumer
Identify the required members, the concrete implementation and the caller that selects it. Verify the behavioral policy as well as the signatures.
interface IMessagePreview- Declare the capability available to the consumer.
string Channel { get; }- Require readable channel information without promising a setter.
string Format(string message);- Require the basic formatting operation and its result type.
class EmailPreview : IMessagePreview- Declare that the class fulfills the interface.
public implementation- Ordinary implicit members that satisfy the basic contract.
constructor dependency- Receive the chosen implementation instead of constructing a fixed one inside the consumer.
multiple interfaces- Expose separate capabilities of the same object.
explicit interface member- Expose a particular implementation through its interface view.
Frequently asked questions
Is an interface a class with no code?
No. It defines a contract and cannot be directly instantiated. Modern C# can give some interface members default bodies, but the basic required members here leave their implementation to classes.
Why does the implementing method use public rather than override?
These ordinary implicit implementations fulfill interface declarations. They are not overrides of a virtual base-class method. Explicit interface implementation uses another syntax shown in the lesson.
Does having the right method name automatically implement the interface?
No. The class must declare the implemented interface and fulfill its members. A similarly shaped unrelated class does not automatically convert to that interface.
Do I need a dependency-injection framework?
No. Passing an interface implementation through a constructor is ordinary C#. A framework can help wire larger applications, but is not needed for this example.
Can one class use an abstract base and interfaces?
Yes. A class can have one direct base class and implement multiple interfaces. Shared state and capability contracts can serve different purposes.
Does the compiler prove every implementation behaves correctly?
No. It checks the required declarations, not every business rule. Tests must verify message preservation, validation and whether the consumer respects its supplied dependency.
Does an explicit implementation hide a secret?
No. It changes the member-access surface: callers use the interface view. Code able to use that interface can still invoke it.
Can you explain the interface contract?
Explain capability contracts, implementations, dependency selection and shared consumers. Read the explanation after checking your answers.
Next step and references
An interface describes a capability without choosing its concrete object. Implement the required members, supply the object at the boundary and keep the consumer focused on its contract. Tests verify the behavioral meaning that signatures alone cannot prove.
Next is C# Enums and Named States. You will represent a small set of states with clear names and validate values entering from outside the program.
Official references
Original SovranCode explanations, examples and exercises. Technical details were checked against these references.
Last updated: .