Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Working with interfaces
Karen Payne
Karen Payne

Posted on

     

Working with interfaces

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.

Source code

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;}}
Enter fullscreen modeExit fullscreen mode

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;}}
Enter fullscreen modeExit fullscreen mode

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;}}
Enter fullscreen modeExit fullscreen mode

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();}}
Enter fullscreen modeExit fullscreen mode

Note person, customer and product display Id from IIdentity while category shows that the class does not implement IIdentity usingtype checking with pattern matching.

Displayed results from code above

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;}}
Enter fullscreen modeExit fullscreen mode

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;}}
Enter fullscreen modeExit fullscreen mode

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");}}
Enter fullscreen modeExit fullscreen mode

results from above method

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;}}
Enter fullscreen modeExit fullscreen mode

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));}}
Enter fullscreen modeExit fullscreen mode

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;}}
Enter fullscreen modeExit fullscreen mode

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[/]");}
Enter fullscreen modeExit fullscreen mode

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
Enter fullscreen modeExit fullscreen mode

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;}}
Enter fullscreen modeExit fullscreen mode

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();
Enter fullscreen modeExit fullscreen mode

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();}
Enter fullscreen modeExit fullscreen mode

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;}
Enter fullscreen modeExit fullscreen mode

A class, using the interface. To keep code clean, Delegate class is setup as a staticusing statement.

usingstaticInterfaceWithDelegate.Classes.Delegates;
Enter fullscreen modeExit fullscreen mode
/// <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");}}
Enter fullscreen modeExit fullscreen mode

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");}}
Enter fullscreen modeExit fullscreen mode

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");}}
Enter fullscreen modeExit fullscreen mode

To invokeWillNotBreakExistingApplications.

staticvoidMain(string[]args){Demod=new();d.SomeMethod();ISampledemoInstance=newDemo();demoInstance.WillNotBreakExistingApplications();Console.ReadLine();}
Enter fullscreen modeExit fullscreen mode

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");}}
Enter fullscreen modeExit fullscreen mode

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);}}
Enter fullscreen modeExit fullscreen mode

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;}}
Enter fullscreen modeExit fullscreen mode

Override ToString

It is not possible to do the following without trickery.

publicinterfaceIBase{stringToString();}
Enter fullscreen modeExit fullscreen mode

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}";}
Enter fullscreen modeExit fullscreen mode

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}";}
Enter fullscreen modeExit fullscreen mode

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}}
Enter fullscreen modeExit fullscreen mode

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;}
Enter fullscreen modeExit fullscreen mode

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.

Shows definitions for DateOnly and Int32

publicstaticclassConventionalExtensions{publicstaticboolIsBetween<T>(thisTvalue,TlowerValue,TupperValue)whereT:struct,IComparable<T>=>Comparer<T>.Default.Compare(value,lowerValue)>=0&&Comparer<T>.Default.Compare(value,upperValue)<=0;}
Enter fullscreen modeExit fullscreen mode

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}}
Enter fullscreen modeExit fullscreen mode

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);}}
Enter fullscreen modeExit fullscreen mode

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);}}
Enter fullscreen modeExit fullscreen mode

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
Enter fullscreen modeExit fullscreen mode

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"});}
Enter fullscreen modeExit fullscreen mode

Model

publicclassBook{publicintId{get;set;}publicstringTitle{get;set;}publicdecimal?Price{get;set;}}
Enter fullscreen modeExit fullscreen mode

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";});}
Enter fullscreen modeExit fullscreen mode

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;}}}
Enter fullscreen modeExit fullscreen mode

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();
Enter fullscreen modeExit fullscreen mode

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[/]");}
Enter fullscreen modeExit fullscreen mode

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)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
sanampakuwal profile image
Sanam
Exploring!

Love this series!! ❤️

CollapseExpand
 
raddevus profile image
raddevus
Software Dev & Tech Writer. Created CYaPass.com app so you never have to make up, type or memorize a password again.
  • Location
    Ohio
  • Work
    Software 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.

CollapseExpand
 
karenpayneoregon profile image
Karen Payne
Microsoft MVP, Microsoft TechNet author, Code magazine author, developer advocate.Have a passion for driving race cars.
  • Location
    Oregon, USA
  • Education
    Montgomery Community College
  • Pronouns
    She, her
  • Work
    Contractor
  • Joined

Constant does not mean const.

CollapseExpand
 
raddevus profile image
raddevus
Software Dev & Tech Writer. Created CYaPass.com app so you never have to make up, type or memorize a password again.
  • Location
    Ohio
  • Work
    Software Development Team Lead
  • Joined

Image description

CollapseExpand
 
aleksa_b8581c222038e8477b profile image
Aleksa
  • Joined
• Edited on• Edited

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.

CollapseExpand
 
karenpayneoregon profile image
Karen Payne
Microsoft MVP, Microsoft TechNet author, Code magazine author, developer advocate.Have a passion for driving race cars.
  • Location
    Oregon, USA
  • Education
    Montgomery Community College
  • Pronouns
    She, her
  • Work
    Contractor
  • Joined

I agree that interfaces may be misused. Perhaps there needs to be a section addressing misused.

CollapseExpand
 
silviu_mihaipreda_68cbac profile image
Silviu Mihai Preda
  • Joined

The base class of the C# type system, object, exposes ToString(). There is no need to create Base with the sole purpose of virtualizing that method

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Microsoft MVP, Microsoft TechNet author, Code magazine author, developer advocate.Have a passion for driving race cars.
  • Location
    Oregon, USA
  • Education
    Montgomery Community College
  • Pronouns
    She, her
  • Work
    Contractor
  • Joined

More fromKaren Payne

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp