
Introduction
In C#,interfaces are powerful for designing flexible and reusable code. An interface defines a contract that classes or structs can implement, specifying what methods, properties, events, or indexers they must provide without dictating how they should do so. This abstraction allows developers to build loosely coupled systems where components can interact seamlessly, regardless of their underlying implementations. By adhering to interfaces, you can simplify testing, enhance code maintainability, and support polymorphism, making it easier to extend and modify applications as requirements evolve.
Basic example
In a team of developers, each developer may have their own naming conventions which for multiple projects leads to inconsistencies. This makes it difficult to move from project to project along with writing generic code.
In the following classes each has an identifier that in two cases is constant while one is not along with if in code there is a need to get at the identifier additional code is required.
publicclassPerson{publicintPersonIdentifier{get;set;}publicstringFirstName{get;set;}publicstringLastName{get;set;}publicDateOnlyBirthDate{get;set;}publicGenderGender{get;set;}publicLanguageLanguage{get;set;}}publicclassCustomer{publicintCustomerIdentifier{get;set;}publicstringFirstName{get;set;}publicstringLastName{get;set;}publicstringAccountNumber{get;set;}}publicclassProduct{publicintProdId{get;set;}publicstringName{get;set;}publicdecimalPrice{get;set;}}
To remedy the identifier issue, the following interface and code reviews provide consistency.
/// <summary>/// Represents an entity with a unique identifier./// </summary>/// <remarks>/// This interface is implemented by classes that require an identifier property./// It is commonly used to standardize the identification of entities across the application./// </remarks>publicinterfaceIIdentity{publicintId{get;set;}}
Implementing the interface, for each class, Id points to the original identifier.
publicclassPerson:IIdentity{publicintId{get=>PersonIdentifier;set=>PersonIdentifier=value;}publicintPersonIdentifier{get;set;}publicstringFirstName{get;set;}publicstringLastName{get;set;}publicDateOnlyBirthDate{get;set;}publicGenderGender{get;set;}publicLanguageLanguage{get;set;}}publicclassCustomer:IIdentity{publicintId{get=>CustomerIdentifier;set=>CustomerIdentifier=value;}publicintCustomerIdentifier{get;set;}publicstringFirstName{get;set;}publicstringLastName{get;set;}publicstringAccountNumber{get;set;}}publicclassProduct:IIdentity{publicintId{get=>ProdId;set=>ProdId=value;}publicintProdId{get;set;}publicstringName{get;set;}publicdecimalPrice{get;set;}}
To test this out, the classes above are used which implement IIdentity and a Category class which does not implement IIdentity.
internalpartialclassProgram{privatestaticvoidMain(string[]args){Personperson=new(){PersonIdentifier=1,FirstName="John",LastName="Doe",BirthDate=newDateOnly(1980,1,1)};if(personisIIdentityp){AnsiConsole.MarkupLine($"[cyan]Id[/] "+$"{p.Id,-3}[cyan]PersonIdentifier[/] "+$"{person.PersonIdentifier}");}else{AnsiConsole.MarkupLine("[red]Person is not an IIdentity[/]");}Customercustomer=new(){CustomerIdentifier=2,FirstName="Jane",LastName="Doe",AccountNumber="1234567890"};if(customerisIIdentityc){AnsiConsole.MarkupLine($"[cyan]Id[/] "+$"{c.Id,-3}[cyan]CustomerIdentifier[/] "+$"{customer.CustomerIdentifier}");}else{AnsiConsole.MarkupLine("[red]Customer is not an IIdentity[/]");}Productproduct=new(){ProdId=3,Name="Widget",Price=9.99m};if(productisIIdentityprod){AnsiConsole.MarkupLine($"[cyan]Id[/] "+$"{prod.Id,-3}[cyan]ProdId[/] "+$"{product.ProdId}");}else{AnsiConsole.MarkupLine("[red]Product is not an IIdentity[/]");}Categorycategory=new(){CategoryId=4,Name="Widgets"};if(categoryisIIdentitycat){AnsiConsole.MarkupLine($"[cyan]Id[/] "+$"{cat.Id,-3}[cyan]CategoryId[/] "+$"{category.CategoryId}");}else{AnsiConsole.MarkupLine("[red]Category is not an IIdentity[/]");}Console.ReadLine();}}
Note person, customer and product display Id from IIdentity while category shows that the class does not implement IIdentity usingtype checking with pattern matching.
Multiple interfaces
Using Person and Customer classes, each had a FirstName, LastName but not BirthDate, Gender or Language.
The following interfaces ensures each class which implements this interface has FirstName and LastName as with the first example for id, a developer might use First_Name and Last_Name or totally different naming convention. In this case they can implement FirstName and LastName to point to their property names.
Since each class is assumed to be used to store some type of person, their birth date, gender and spoken language are required. Keep in mind these properties may not be necessary, and that birth date could also be a date time or even a date time offset depending on business requirements such as a doctor recording a birth date in a hospital.
/// <summary>/// Represents a human entity with basic personal attributes./// </summary>/// <remarks>/// This interface defines the essential properties that describe a human,/// such as their name, birthdate, gender, and preferred language./// It is intended to be implemented by classes that model human-related entities./// </remarks>publicinterfaceIHuman{publicstringFirstName{get;set;}publicstringLastName{get;set;}publicDateOnlyBirthDate{get;set;}publicGenderGender{get;set;}publicLanguageLanguage{get;set;}}
Revised classes
publicclassPerson:IIdentity,IHuman{publicintId{get=>PersonIdentifier;set=>PersonIdentifier=value;}publicintPersonIdentifier{get;set;}publicstringFirstName{get;set;}publicstringLastName{get;set;}publicDateOnlyBirthDate{get;set;}publicGenderGender{get;set;}publicLanguageLanguage{get;set;}}publicclassCustomer:IIdentity,IHuman{publicintId{get=>CustomerIdentifier;set=>CustomerIdentifier=value;}publicintCustomerIdentifier{get;set;}publicstringFirstName{get;set;}publicstringLastName{get;set;}publicDateOnlyBirthDate{get;set;}publicGenderGender{get;set;}publicLanguageLanguage{get;set;}publicstringAccountNumber{get;set;}}
Example which uses dual pattern matching rather and condition && condition as one would do in earlier frameworks to determine, in this case if Person implements IIdentity and IHuman.
privatestaticvoidImplementsIdentifyAndHuman(){PersonnewPerson=new(){PersonIdentifier=5,FirstName="Tatiana",LastName="Mikhaylov",BirthDate=newDateOnly(1990,5,15),Gender=Gender.Female,Language=Language.Russian};if(newPersonisIIdentitypandIHumanh){AnsiConsole.MarkupLine($"[cyan]Id[/]{p.Id,-3}[cyan] "+$"First[/]{h.FirstName} [cyan] "+$"Last[/]{h.LastName} [cyan] "+$"Gender[/]{h.Gender} [cyan] "+$"Language[/]{h.Language} [cyan] "+$"Birth[/]{h.BirthDate}");}else{AnsiConsole.MarkupLine("[red]newPerson does not use IIdentity/IHuman");}}
INotifyPropertyChanged
INotifyPropertyChanged notifies clients that a property value has changed which is used often in Windows Forms projects.
In the following sample.
- The Person class (Person.cs) is divided into two files, the main class file contains properties.
- The class file PersonNotify.cs contains code which is required by INotifyPropertyChanged.
- field - Field backed property (currently in preview) is used rather than separate private fields for each property which is cleaner than conventual fields.
PersonNofiy.cs
/// <summary>/// Implements <see cref="INotifyPropertyChanged"/> interfaces./// </summary>publicpartialclassPerson{publiceventPropertyChangedEventHandler?PropertyChanged;protectedvoidOnPropertyChanged(stringpropertyName)=>PropertyChanged?.Invoke(this,new(propertyName));protectedboolSetField<T>(refTfield,Tvalue,[CallerMemberName]stringpropertyName=""){if(EqualityComparer<T>.Default.Equals(field,value))returnfalse;field=value;OnPropertyChanged(propertyName);returntrue;}}
Person.cs
/// <summary>/// Represents a person with identifiable and human-related attributes./// </summary>/// <remarks>/// This class implements the <see cref="IIdentity"/> and/// <see cref="IHuman"/> interfaces, providing properties/// for unique identification and personal details such as name, birthdate, gender,/// and language. It also supports property change notifications through/// <see cref="INotifyPropertyChanged"/>./// </remarks>publicpartialclassPerson:IIdentity,IHuman,INotifyPropertyChanged{publicintId{get=>PersonIdentifier;set=>PersonIdentifier=value;}publicintPersonIdentifier{get;set;}publicstringFirstName{get=>field.TrimEnd();set=>SetField(reffield,value,nameof(FirstName));}publicstringLastName{get=>field.TrimEnd();set=>SetField(reffield,value,nameof(LastName));}publicDateOnlyBirthDate{get;set=>SetField(reffield,value,nameof(BirthDate));}publicGenderGender{get;set=>SetField(reffield,value,nameof(Gender));}publicLanguageLanguage{get;set=>SetField(reffield,value,nameof(Language));}}
Example
For this example, the code has been modified to log notifications to a file using SeriLog NuGet package.
PersonNotify.cs
Note the logging only occurs while running in Microsoft Visual Studio or Microsoft Visual Studio Code.
publicpartialclassPerson{publiceventPropertyChangedEventHandler?PropertyChanged;protectedvoidOnPropertyChanged(stringpropertyName)=>PropertyChanged?.Invoke(this,new(propertyName));protectedboolSetField<T>(refTfield,Tvalue,[CallerMemberName]stringpropertyName=""){if(Debugger.IsAttached)Log.Information("Property: {P}",propertyName);if(EqualityComparer<T>.Default.Equals(field,value))returnfalse;field=value;if(Debugger.IsAttached)Log.Information(" Value: {V}",value);OnPropertyChanged(propertyName);returntrue;}}
Method in Program.cs
privatestaticvoidLogAndModifyPerson(){Log.Information($"Creating new person\n{newstring('-',80)}");PersonnewPerson=new(){PersonIdentifier=5,FirstName="Tatiana",LastName="Mikhaylov",BirthDate=newDateOnly(1990,5,15),Gender=Gender.Female,Language=Language.Russian};Log.Information($"Modifying person\n{newstring('-',80)}");newPerson.FirstName="Jane";newPerson.Language=Language.English;AnsiConsole.MarkupLine("[cyan]See log file[/]");}
Log file after running the code inLogAndModifyPerson.
[2025-01-01 15:49:28.381 [Information] Creating new person--------------------------------------------------------------------------------[2025-01-01 15:49:28.403 [Information] Property: "FirstName"[2025-01-01 15:49:28.404 [Information] Value: "Tatiana"[2025-01-01 15:49:28.405 [Information] Property: "LastName"[2025-01-01 15:49:28.405 [Information] Value: "Mikhaylov"[2025-01-01 15:49:28.405 [Information] Property: "BirthDate"[2025-01-01 15:49:28.405 [Information] Value: 05/15/1990[2025-01-01 15:49:28.406 [Information] Property: "Gender"[2025-01-01 15:49:28.407 [Information] Property: "Language"[2025-01-01 15:49:28.407 [Information] Value: Russian[2025-01-01 15:49:28.408 [Information] Modifying person--------------------------------------------------------------------------------[2025-01-01 15:49:28.408 [Information] Property: "FirstName"[2025-01-01 15:49:28.408 [Information] Value: "Jane"[2025-01-01 15:49:28.408 [Information] Property: "Language"[2025-01-01 15:49:28.408 [Information] Value: English
Dependency Injection
Using interfaces for Dependency Injection (DI) promotes flexibility, maintainability, and testability in software design. By programming to an interface rather than a concrete implementation, developers can easily swap out dependencies without altering the core application logic, adhering to the Dependency Inversion Principle.
This decoupling enables easier unit testing, as mock or stub implementations of the interfaces can be injected into tests, isolating the unit under test. Furthermore, using interfaces makes the codebase more extensible, allowing new implementations to be added without modifying existing code. It also enhances readability and enforces a clear contract for the behavior of dependencies, resulting in more robust and scalable systems.
Many third party libraries uses DI for many of the reasons listed above.
The following usesFluentValidation.AspNetCore NuGet package for validating a entry read from a SQL-Server localDb database using Microsoft EF Core 9.
Note: Data is added to the database in the DbContext.
IValidator is registered as service for the following validator.
publicclassPersonValidator:AbstractValidator<Person>{publicPersonValidator(){RuleFor(x=>x.FirstName).NotEmpty();RuleFor(x=>x.LastName).NotEmpty();RuleFor(x=>x.EmailAddress).NotEmpty().EmailAddress();}}publicpartialclassPerson{publicintPersonId{get;set;}[Display(Name="First")]publicstringFirstName{get;set;}[Display(Name="Last")]publicstringLastName{get;set;}[Display(Name="Email")]publicstringEmailAddress{get;set;}}
Registration is done in Program.cs
publicclassProgram{publicstaticvoidMain(string[]args){varbuilder=WebApplication.CreateBuilder(args);// Add services to the container.builder.Services.AddRazorPages();builder.Services.AddScoped<IValidator<Person>,PersonValidator>();builder.Services.AddFluentValidationAutoValidation();
See the full articleFluentValidation tips C#
Another example can be found here,ASP.NET Core DI constructor with parameters. In this example, a local interface is used rather than a third party interface in a NuGet package.
Also shows how register a service dependent on another service, both in the same project.
Delegates/events
There are countless reasons for needing events that can be enforced with an interface, for exampleINotifyPropertyChanged. In the following, we want to know when a process has started and another event for updates while a process runs.
These delegates are defined outside any class which may use them so that any class can use them.
/// <summary>/// Represents a container for delegate definitions used for handling events related to status updates and process initiation./// </summary>/// <remarks>/// This class defines delegates that are utilized in event-driven programming to notify subscribers about specific actions or changes./// </remarks>publicclassDelegates{publicdelegatevoidUpdateStatusEventHandler(stringstatus);publicdelegatevoidStartedEventHandler();}
The interface.
/// <summary>/// Defines a contract for an interface that includes events for status updates and process initiation./// </summary>/// <remarks>/// Implementers of this interface are expected to provide mechanisms to handle the <see cref="StatusUpdated"/>/// and <see cref="Started"/> events, enabling notification of status changes and the initiation of processes./// </remarks>publicinterfaceISampleInterface{eventUpdateStatusEventHandlerStatusUpdated;eventStartedEventHandlerStarted;}
A class, using the interface. To keep code clean, Delegate class is setup as a staticusing statement.
usingstaticInterfaceWithDelegate.Classes.Delegates;
/// <summary>/// Represents a class that implements the <see cref="ISampleInterface"/> interface,/// providing functionality to trigger events such as <see cref="Started"/> and <see cref="StatusUpdated"/>./// </summary>/// <remarks>/// This class is responsible for invoking the <see cref="Started"/> event when the start process is initiated/// and the <see cref="StatusUpdated"/> event to notify about status changes./// </remarks>publicclassExampleClass:ISampleInterface{publiceventUpdateStatusEventHandlerStatusUpdated;publiceventStartedEventHandlerStarted;publicvoidStart(){Started?.Invoke();StatusUpdated?.Invoke("Started");}}
Example usage which writes to the console and logs to a file using SeriLog.
internalpartialclassProgram{privatestaticvoidMain(string[]args){ExampleClassexample=new();// Subscribe to the Started eventexample.Started+=OnStarted;// Subscribe to the StatusUpdated eventexample.StatusUpdated+=OnStatusUpdated;// Call the Start method to trigger eventsAnsiConsole.MarkupLine("[yellow]Calling Start[/]");example.Start();Console.ReadLine();}/// <summary>/// Handles the <see cref="ExampleClass.Started"/> event./// </summary>/// <remarks>/// This method is invoked when the <see cref="ExampleClass.Started"/> event is triggered,/// indicating that the start process has been initiated./// </remarks>privatestaticvoidOnStarted(){AnsiConsole.MarkupLine("[cyan]Started event[/] [yellow]triggered![/]");Log.Information("Started");}/// <summary>/// Handles the <see cref="ExampleClass.StatusUpdated"/> event./// </summary>/// <param name="status">/// The updated status message provided by the <see cref="ExampleClass.StatusUpdated"/> event./// </param>/// <remarks>/// This method is invoked whenever the <see cref="ExampleClass.StatusUpdated"/> event is triggered,/// allowing the application to respond to status changes./// </remarks>privatestaticvoidOnStatusUpdated(stringstatus){AnsiConsole.MarkupLine($"[cyan]Status updated:[/] [yellow]{status}[/]");Log.Information("Status updated");}}
Default implementation
C# 8 and above allows concrete implementation for methods in interfaces as well. Earlier it was allowed only for abstract classes.
This change will now shield our concrete classes from side-effects of changing the interface after it has been implemented by a given class e.g. adding a new contract in the interface after the DLL has already been shipped for production use. So our class will still compile properly even after adding a new method signature in the interface being implemented.
In the sample belowWillNotBreakExistingApplications is not required so it will not break anything.
namespaceDefaultImplementationDemo;internalpartialclassProgram{staticvoidMain(string[]args){Demod=new();d.SomeMethod();Console.ReadLine();}}internalinterfaceISample{publicvoidSomeMethod();publicvoidWillNotBreakExistingApplications(){Console.WriteLine("Here in the interface");}}publicclassDemo:ISample{publicvoidSomeMethod(){Console.WriteLine("Some method");}}
To invokeWillNotBreakExistingApplications.
staticvoidMain(string[]args){Demod=new();d.SomeMethod();ISampledemoInstance=newDemo();demoInstance.WillNotBreakExistingApplications();Console.ReadLine();}
The following implements WillNotBreakExistingApplications in the class.
namespaceDefaultImplementationDemo;internalpartialclassProgram{staticvoidMain(string[]args){Demod=new();d.SomeMethod();d.WillNotBreakExistingApplications();Console.ReadLine();}}internalinterfaceISample{voidSomeMethod();publicvoidWillNotBreakExistingApplications(){Console.WriteLine("Here in the interface");}}publicclassDemo:ISample{publicvoidSomeMethod(){Console.WriteLine("Some method");}publicvoidWillNotBreakExistingApplications(){Console.WriteLine("Here in the class");}}
Performance considerations
Besides working with interfaces make sure the code has optimal performance. For instance, the following method usesINumber<T> to sum elements in an array.
publicclassConstrainSample{publicstaticTSum<T>(T[]numbers)whereT:INumber<T>{returnnumbers==null?thrownewArgumentNullException(nameof(numbers)):numbers.Aggregate(T.Zero,(current,item)=>current+item);}}
The code above introduces some overhead due to delegate invocation for each element in the array.
This is a common mistake by new developers, not considering performance. The Aggregate extension method may or may not be appropriate for each task. If there are performance issues consider the code sample below which will always be better performant.
publicclassConstrainSample{publicstaticTSum<T>(paramsT[]numbers)whereT:INumber<T>{if(numbers==null)thrownewArgumentNullException(nameof(numbers));varresult=T.Zero;foreach(variteminnumbers){result+=item;}returnresult;}}
Override ToString
It is not possible to do the following without trickery.
publicinterfaceIBase{stringToString();}
What is appropriate is to create an abstract class as follows.
publicabstractclassBase{publicabstractoverridestringToString();}publicclassChild:Base{publicintId{get;set;}publicstringName{get;set;}publicstringDescription{get;set;}publicoverridestringToString()=>$"{Id}{Name}{Description}";}
If there are one or more interfaces used, the (in this case) Base comes before IIdentity.
publicabstractclassBase{publicabstractoverridestringToString();}publicinterfaceIIdentity{publicintId{get;set;}}publicclassChild:Base,IIdentity{publicintId{get;set;}publicstringName{get;set;}publicstringDescription{get;set;}publicoverridestringToString()=>$"{Id}{Name}{Description}";}
IComparable<T>
A common assertion is testing whether a value is between two other values, as shown below and we could use pattern matching.
internalstaticvoidConventionalBetween(intvalue){if(value>=1&&value<=10){// do something}}
This logic has nothing wrong, but what about other numeric types? Let's use a language extension for several numeric types for demonstration purposes.
publicstaticclassConventionalExtensions{publicstaticboolIsBetween(thisintsender,intstart,intend)=>sender>=start&&sender<=end;publicstaticboolIsBetween(thisdecimalsender,decimalstart,decimalend)=>sender>=start&&sender<=end;publicstaticboolIsBetween(thisdoublesender,doublestart,doubleend)=>sender>=start&&sender<=end;}
This logic is not wrong, yet there is a better way: create one language extension that will work with the above and even dates.
The following language extension method works with any type that implement IComparable<T>.
Two types from Microsoft documentation.
publicstaticclassConventionalExtensions{publicstaticboolIsBetween<T>(thisTvalue,TlowerValue,TupperValue)whereT:struct,IComparable<T>=>Comparer<T>.Default.Compare(value,lowerValue)>=0&&Comparer<T>.Default.Compare(value,upperValue)<=0;}
Examples
internalstaticvoidWorkingSmarterWithInt(intvalue){if(value.IsBetween(1,10)){// Do something}}internalstaticvoidWorkingSmarterWithDecimal(decimalvalue){if(value.IsBetween(1.5m,10.5m)){// Do something}}internalstaticvoidWorkingSmarterWithDateTime(DateTimevalue){if(value.IsBetween(newDateTime(2020,1,1),newDateTime(2020,1,15))){// Do something}}internalstaticvoidWorkingSmarterWithDateOnly(DateOnlyvalue){if(value.IsBetween(newDateOnly(2020,1,1),newDateOnly(2020,1,15))){// Do something}}
The lesson is to work smarter by knowing not just types but also the glue that holds them together. Of course, not everyone is going to care for language extensions, but those who do can use them in perhaps a team or personal library, which keeps things consistent and also easy to remember.
For more on IComparable see thefollowing.
Do not stop with IComparable, check out other interfaces like IQueryable<T> and IOrderedQueryable ;T>. See the following GitHubrepository.
The following are against an known model.
publicstaticclassOrderingHelpers{/// <summary>/// Provides sorting by string using a key specified in <see cref="key"/> and if the key is not found the default is <see cref="Customers.CompanyName"/>/// </summary>/// <param name="query"><see cref="Customers"/> query</param>/// <param name="key">key to sort by</param>/// <param name="direction">direction to sort by</param>/// <returns>query with order by</returns>/// <remarks>Fragile in that if a property name changes this will break</remarks>publicstaticIQueryable<Customers>OrderByString(thisIQueryable<Customers>query,stringkey,Directiondirection=Direction.Ascending){Expression<Func<Customers,object>>exp=keyswitch{"LastName"=>customer=>customer.Contact.LastName,"FirstName"=>customer=>customer.Contact.FirstName,"CountryName"=>customer=>customer.CountryNavigation.Name,"Title"=>customer=>customer.ContactTypeNavigation.ContactTitle,_=>customer=>customer.CompanyName};returndirection==Direction.Ascending?query.OrderBy(exp):query.OrderByDescending(exp);}}
These are generic versions not tied to a specific model and may be too much for some developers to understand and if so, ask GitHub Copilot to explain the code.
publicstaticclassQueryableExtensions{publicstaticIOrderedQueryable<T>OrderByColumn<T>(thisIQueryable<T>source,stringcolumnPath)=>source.OrderByColumnUsing(columnPath,"OrderBy");publicstaticIOrderedQueryable<T>OrderByColumnDescending<T>(thisIQueryable<T>source,stringcolumnPath)=>source.OrderByColumnUsing(columnPath,"OrderByDescending");publicstaticIOrderedQueryable<T>ThenByColumn<T>(thisIOrderedQueryable<T>source,stringcolumnPath)=>source.OrderByColumnUsing(columnPath,"ThenBy");publicstaticIOrderedQueryable<T>ThenByColumnDescending<T>(thisIOrderedQueryable<T>source,stringcolumnPath)=>source.OrderByColumnUsing(columnPath,"ThenByDescending");privatestaticIOrderedQueryable<T>OrderByColumnUsing<T>(thisIQueryable<T>source,stringcolumnPath,stringmethod){varparameter=Expression.Parameter(typeof(T),"item");varmember=columnPath.Split('.').Aggregate((Expression)parameter,Expression.PropertyOrField);varkeySelector=Expression.Lambda(member,parameter);varmethodCall=Expression.Call(typeof(Queryable),method,new[]{parameter.Type,member.Type},source.Expression,Expression.Quote(keySelector));return(IOrderedQueryable<T>)source.Provider.CreateQuery(methodCall);}}
IGrouping<TKey,TElement> Interface
A common operation is putting data intogroups so that the elements in each group share a common attribute.
TheIGrouping interface makes this possible.
In the following example, the task is to group books by price range using the built-in extension methodGroupBy. This method uses a switch expression to group books in a list of books that everyone should be able to relate to.
In the following code, each operation has been broken out.
- Json method provides mocked book data. Note using /* lang=json*/ will flag any errors in the json structure and is not documented.
- **GroupBooksByPriceRange **method performs the group operation.
- BooksGroupings is the main method which displays the results.
Results
Price Range: $10 to $19 Id: 1, Title: Learn EF Core, Price: $19.00 Id: 2, Title: C# Basics, Price: $18.00Price Range: $30 and above Id: 3, Title: ASP.NET Core advance, Price: $30.00 Id: 5, Title: Basic Azure, Price: $59.00Price Range: Under $10 Id: 4, Title: VB.NET To C#, Price: $9.00
Full code
usingSystem.Diagnostics;usingSystem.Text.Json;usingVariousSamples.Models;namespaceVariousSamples.Classes;internalclassBookOperations{/// <summary>/// Groups a predefined collection of books by price ranges and outputs the grouped results./// </summary>/// <remarks>/// This method deserializes a JSON string containing book data into a list of <see cref="Book"/> objects./// It then groups the books into price ranges using the <see cref="GroupBooksByPriceRange"/> method./// The grouped results are written to the debug output for inspection./// </remarks>publicstaticvoidBooksGroupings(){List<Book>books=JsonSerializer.Deserialize<List<Book>>(Json())!;IEnumerable<IGrouping<string,Book>>results=GroupBooksByPriceRange(books);foreach(var(price,booksGroup)inresults.Select(group=>(group.Key,group))){Debug.WriteLine($"Price Range:{price}");foreach(varbookinbooksGroup){Debug.WriteLine($"\tId:{book.Id}, Title:{book.Title}, Price:{book.Price:C}");}}}/// <summary>/// Provides a JSON string representation of a predefined collection of books./// </summary>/// <remarks>/// The JSON string contains an array of book objects, each with properties such as/// <c>Id</c>, <c>Title</c>, and <c>Price</c>. This method is used to supply sample/// data for operations involving books./// </remarks>/// <returns>/// A JSON string representing a collection of books./// </returns>privatestaticstringJson(){varjson=/* lang=json*/"""[{"Id":1,"Title":"Learn EF Core","Price":19.0},{"Id":2,"Title":"C# Basics","Price":18.0},{"Id":3,"Title":"ASP.NET Core advance","Price":30.0},{"Id":4,"Title":"VB.NET To C#","Price":9.0},{"Id":5,"Title":"Basic Azure","Price":59.0}]""";returnjson;}/// <summary>/// Groups a collection of books into price ranges./// </summary>/// <param name="books">/// A list of <see cref="Book"/> objects to be grouped by price range./// </param>/// <returns>/// An <see cref="IEnumerable{T}"/> of <see cref="IGrouping{TKey, TElement}"/> where the key is a string/// representing the price range and the elements are the books within that range./// </returns>privatestaticIEnumerable<IGrouping<string,Book>>GroupBooksByPriceRange(List<Book>books)=>books.GroupBy(b=>bswitch{{Price:<10}=>"Under $10",{Price:>=10and<20}=>"$10 to $19",{Price:>=20and<30}=>"$20 to $29",{Price:>=30}=>"$30 and above",_=>"Unknown"});}
Model
publicclassBook{publicintId{get;set;}publicstringTitle{get;set;}publicdecimal?Price{get;set;}}
Note
The switch used can also be done as follows and some may not case for a switch expression.
privatestaticIEnumerable<IGrouping<string,Book>>GroupBooksByPriceRange(List<Book>books){returnbooks.GroupBy(b=>{if(b.Price<10)return"Under $10";if(b.Price>=10&&b.Price<20)return"$10 to $19";if(b.Price>=20&&b.Price<30)return"$20 to $29";if(b.Price>=30)return"$30 and above";return"Unknown";});}
IParsable<T>
TheIParsable interface defines a mechanism for parsing a string to a value, as shown in the following example. The list is meant to represent lines in a file, and IParsable creates a new Person.
Data is perfect for keeping the example simple. Out in the wild, assertions would be needed to validate that elements can be converted to the right type, and in the case of Gender and Language, they are correct.
#nullable enableusingSystem.Diagnostics.CodeAnalysis;usingVariousSamples.Interfaces;#pragma warning disable CS8618, CS9264namespaceVariousSamples.Models;publicclassPerson:IHuman,IIdentity,IParsable<Person>{publicintId{get;set;}publicstringFirstName{get;set;}publicstringLastName{get;set;}publicDateOnlyDateOfBirth{get;set;}publicGenderGender{get;set;}publicLanguageLanguage{get;set;}publicPerson(){}/// <summary>/// Initializes a new instance of the <see cref="Person"/> class with the specified details./// </summary>/// <param name="id">The unique identifier for the person.</param>/// <param name="firstName">The first name of the person.</param>/// <param name="lastName">The last name of the person.</param>/// <param name="dateOfBirth">The date of birth of the person.</param>/// <param name="gender">The gender of the person.</param>/// <param name="language">The preferred language of the person.</param>privatePerson(intid,stringfirstName,stringlastName,DateOnlydateOfBirth,Gendergender,Languagelanguage){Id=id;FirstName=firstName;LastName=lastName;DateOfBirth=dateOfBirth;Gender=gender;Language=language;}/// <summary>/// Parses a string representation of a <see cref="Person"/> and returns an instance of the <see cref="Person"/> class./// </summary>/// <param name="item">The string representation of a person in the format "Id,FirstName,LastName,DateOfBirth,Gender,Language".</param>/// <param name="provider">An optional <see cref="IFormatProvider"/> to provide culture-specific formatting information.</param>/// <returns>A new instance of the <see cref="Person"/> class populated with the parsed data.</returns>/// <exception cref="FormatException">/// Thrown when the input string does not match the expected format or contains invalid data./// </exception>publicstaticPersonParse(stringitem,IFormatProvider?provider){string[]personPortions=item.Split('|');if(personPortions.Length!=6){thrownewFormatException("Expected format: Id|FirstName|LastName|DateOfBirth|Gender|Language");}returnnewPerson(int.Parse(personPortions[0]),personPortions[1],personPortions[2],DateOnly.Parse(personPortions[3]),Enum.Parse<Gender>(personPortions[4]),Enum.Parse<Language>(personPortions[5]));}/// <summary>/// Attempts to parse the specified string representation of a <see cref="Person"/> into an instance of the <see cref="Person"/> class./// </summary>/// <param name="value">The string representation of a person in the format "Id,FirstName,LastName,DateOfBirth,Gender,Language".</param>/// <param name="provider">An optional <see cref="IFormatProvider"/> to provide culture-specific formatting information.</param>/// <param name="result">/// When this method returns, contains the parsed <see cref="Person"/> instance if the parsing succeeded,/// or <c>null</c> if the parsing failed. This parameter is passed uninitialized./// </param>/// <returns>/// <c>true</c> if the parsing succeeded and a valid <see cref="Person"/> instance was created; otherwise, <c>false</c>./// </returns>publicstaticboolTryParse([NotNullWhen(true)]string?value,IFormatProvider?provider,[MaybeNullWhen(false)]outPersonresult){result=null;if(value==null){returnfalse;}try{result=Parse(value,provider);returntrue;}catch{returnfalse;}}}
Sample
List<string>list=["1|John|Doe|1990-01-01|Male|English","2|Mary|Doe|1992-02-01|Female|English","3|Mark|Smith|2000-02-01|Female|Spanish"];List<Person>people=list.Select(x=>Person.Parse(x,CultureInfo.InvariantCulture)).ToList();
Helper methods
Earlier in this article, code was presented to show how to determine if a model implemented one of several interfaces. There may be a need to check to see which models implement a specific interface or to check if a model implements several interfaces. Code has been provided in source code for this article to do so.
Examples
privatestaticvoidGetAllClassesImplementing(){Customercustomer=new(){CustomerIdentifier=2,FirstName="Jane",LastName="Doe",AccountNumber="1234567890"};if(customerisIIdentityc){AnsiConsole.MarkupLine($"[cyan]Id[/] "+$"{c.Id,-3}[cyan]CustomerIdentifier[/] "+$"{customer.CustomerIdentifier}");}else{AnsiConsole.MarkupLine("[red]Customer is not an IIdentity[/]");}Console.WriteLine();varentities=Helpers.GetAllEntities<IIdentity>();foreach(varentityinentities){AnsiConsole.MarkupLine($"[cyan]{entity.Name}[/]");}Console.WriteLine();varentitiesMore=Helpers.ImplementsMoreThanOneInterface<Person>([typeof(IIdentity),typeof(IHuman)]);AnsiConsole.MarkupLine(entitiesMore?"[cyan]Yes[/]":"[red]No[/]");}
Summary
Various ideas have been presented about the benefits of using custom and .NET Framework native interfaces, which will assist those who have never used interfaces in getting started.
In the provided source code there are more samples than presented here to check out.
Top comments(7)

- LocationOhio
- WorkSoftware Development Team Lead
- Joined
This is a very good (informative) article.
I really like the first part about using the IIdenity interface.
However, I have two questions:
1) would you say that using this Interface would be applying the Adapter OOP pattern?
Just curious about that.
2) The 2nd paragraph after theBasic Example heading is a bit confusing. Can you help me understand what you were saying?
The paragraph states:
In the following classes each has an identifier that in two cases is constant while one is not along with if in code there is a need to get at the identifier additional code is required.
I'm parsing that to :
1 - In the following classes, each has an identifier, that in two cases is constant.
(but I don't see any constants)
2 - while one is not (not constant, right?)
3 - along with if in code there is a need to get at the identifier additonal code is required (unsure what you're saying here bec all the properties are public)
I'm going to re-read this article bec there is a lot here.
thanks for writing it up.

- LocationOregon, USA
- EducationMontgomery Community College
- PronounsShe, her
- WorkContractor
- Joined
Constant does not mean const.

Problem with interfaces is that they are often used without real need and just make a burden for developers. In most projects they are just useless. I mean they are talked about being best practice while they are actually not. They are just useful sometimes, very rarely.

- LocationOregon, USA
- EducationMontgomery Community College
- PronounsShe, her
- WorkContractor
- Joined
I agree that interfaces may be misused. Perhaps there needs to be a section addressing misused.
For further actions, you may consider blocking this person and/orreporting abuse