Developer Tips: C# Extension Methods on Interfaces
Our guest blogger, Andrew, covers the topic of creating extension methods on interfaces making dependency injection easier.
I write my code with dependency injection in mind. Given that extension methods are static and I've only ever seen them implemented on concrete classes I've avoided them for a while. While watching "C# Extension Methods by Elton Stoneman" PluralSight course I learned that you may apply extension methods to interfaces. This was reinforced with the article "Extension Methods Guidelines in C# .NET by Michael Shpilt." This has brought extension methods back into my toolkit.
Elton gave a great example of a use case involving design patterns. His example involved three different repositories that shared an interface that implemented a method called "GetItems". He continued that he wanted to add a new method "DistinctItems" to the repository interface, but did not want to create an implementation for each repository class. Instead he created an extension method on the repository interface. Cool!
I highly recommend watching Elton's PluralSight course for more information and advanced examples. All of his examples are followed up with unit tests and design pattern best practices in mind.
I expect a "DistinctItems" method to be performant and efficient, so at some point I'd expect the implementation to be unique in each repository. Instead I liked the idea of adding the "DistinctItems" method as an extension of IEnumerable<T> where T is an Item in the following example. Below is an implementation of an extension method on an interface. The code base is located with another example at:
Project: Tips.ExtensionMethods.ExtendInterfaces
Item
This is the model the repositories will return.
namespace Tips.ExtensionMethods.ExtendInterfaces.Models { internal class Item { public int Id { get; set; } public string Name { get; set; } public bool IsActive { get; set; } } }
IRepository
The interface does not contain the method "DistinctItems." We'll extend the interface next.
using System.Collections.Generic; using Tips.ExtensionMethods.ExtendInterfaces.Models;
namespace Tips.ExtensionMethods.ExtendInterfaces.Repositories { internal interface IRepository { IEnumerable<Item> GetItems(); } }
IEnumerableItemExtension
I liked this implementation of LINQ to get distinct items.
using System.Collections.Generic; using System.Linq; using Tips.ExtensionMethods.ExtendInterfaces.Models;
namespace Tips.ExtensionMethods.ExtendInterfaces.ExtensionMethods { internal static class IEnumerableItemExtension { // Inspiration // https://stackoverflow.com/questions/489258/linqs-distinct-on-a-particular-property
public static IEnumerable<Item> DistinctItems(this IEnumerable<Item> items) { return items.GroupBy(item => (item.Id, item.Name, item.IsActive)).Select(item => item.First()); } } }
Project: Tips.ExtensionMethods.ExtendInterfaces.Test
ITestItemFactory
Even though we're in a test project, I still follow dependency injection principles and created an interface for the test item factory used in the mock repository factory.
using System.Collections.Generic; using Tips.ExtensionMethods.ExtendInterfaces.Models; namespace Tips.ExtensionMethods.ExtendInterfaces.Tests.Repositories { internal interface ITestItemFactory { IEnumerable<Item> CreateExpectedUniqueItems(); IEnumerable<Item> CreateDatabaseItems(); IEnumerable<Item> CreateFileItems(); IEnumerable<Item> CreateStreamItems(); } }
TestItemFactory
I created an item factory to group the test data together for easier maintenance.
using System.Collections.Generic; using System.Linq; using Tips.ExtensionMethods.ExtendInterfaces.Models;
namespace Tips.ExtensionMethods.ExtendInterfaces.Tests.Repositories { internal class TestItemFactory : ITestItemFactory { public IEnumerable<Item> CreateExpectedUniqueItems() => new List<Item> { new Item {Id = 1, Name = "A", IsActive = true}, new Item {Id = 2, Name = "B", IsActive = false}, new Item {Id = 3, Name = "C", IsActive = true}, new Item {Id = 4, Name = "D", IsActive = false}, // different new Item {Id = 4, Name = "D", IsActive = true}, // different new Item {Id = 5, Name = "E", IsActive = true} };
public IEnumerable<Item> CreateDatabaseItems() => CreateExpectedUniqueItems().Where( item => new List<int>{ 1, 2, 3 }.Contains(item.Id));
public IEnumerable<Item> CreateFileItems() => CreateExpectedUniqueItems().Where( item => new List<int> { 2, 3, 4 }.Contains(item.Id) && !(item.Id == 4 && item.IsActive)); public IEnumerable<Item> CreateStreamItems() => CreateExpectedUniqueItems().Where( item => new List<int> { 3, 4, 5 }.Contains(item.Id) && !(item.Id == 4 && !item.IsActive)); } }
MockRepositoryFactory
This factory created the mock repositories with the test items given the test item factory.
using System.Collections.Generic; using Moq; using Tips.ExtensionMethods.ExtendInterfaces.Models; using Tips.ExtensionMethods.ExtendInterfaces.Repositories;
namespace Tips.ExtensionMethods.ExtendInterfaces.Tests.Repositories { internal class Item { public int Id { get; set; } public string Name { get; set; } public bool IsActive { get; set; } }
internal interface IRepository { IEnumerable<Item> GetItems(); }
internal class MockRepositoryFactory { private readonly TestItemFactory _testItemFactory;
public MockRepositoryFactory(TestItemFactory testItemFactory) { _testItemFactory = testItemFactory; }
// Assume this repository gets items via a database. public IRepository CreateMockDatabaseRepository() => CreateMockDatabaseRepository(_testItemFactory.CreateDatabaseItems());
// Assume this repository gets items via a file. public IRepository CreateMockFileRepository() => CreateMockDatabaseRepository(_testItemFactory.CreateFileItems());
// Assume this repository gets items via a stream. public IRepository CreateMockStreamRepository() => CreateMockDatabaseRepository(_testItemFactory.CreateStreamItems());
private static IRepository CreateMockDatabaseRepository(IEnumerable<Item> list) { var mockRepository = new Mock<IRepository>(); mockRepository.Setup(x => x.GetItems()).Returns(list); return mockRepository.Object; } } }
RepositoryTests
Test method "GetItemsDistinctTest" tests the LINQ method implementation on its own. "GetItemsDistinctExtensionTest" tests the extension method.
using System.Collections.Generic; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using Tips.ExtensionMethods.ExtendInterfaces.ExtensionMethods; using Tips.ExtensionMethods.ExtendInterfaces.Models;
namespace Tips.ExtensionMethods.ExtendInterfaces.Tests.Repositories { [TestClass] public class RepositoryTests { private readonly List<Item> _allItems = new List<Item>(); private readonly IEnumerable<Item> _expectedItems;
public RepositoryTests() { var testItemFactory = new TestItemFactory(); _expectedItems = testItemFactory.CreateExpectedUniqueItems(); var factory = new MockRepositoryFactory(testItemFactory); var repository1 = factory.CreateMockDatabaseRepository(); var repository2 = factory.CreateMockFileRepository(); var repository3 = factory.CreateMockStreamRepository(); _allItems.AddRange(repository1.GetItems()); _allItems.AddRange(repository2.GetItems()); _allItems.AddRange(repository3.GetItems()); }
[TestMethod] public void GetItemsDistinctTest() { var actualItems = _allItems.GroupBy(item => (item.Id, item.Name, item.IsActive)).Select(item => item.First()); AssertDistinctItems(_expectedItems.ToList(), actualItems.ToList()); }
[TestMethod] public void GetItemsDistinctExtensionTest() { var actualItems = _allItems.DistinctItems(); AssertDistinctItems(_expectedItems.ToList(), actualItems.ToList()); }
private static void AssertDistinctItems(IReadOnlyList<Item> expectedItems, IReadOnlyList<Item> actualItems) { Assert.IsNotNull(expectedItems); Assert.IsNotNull(actualItems);
Assert.AreEqual(expectedItems.Count, actualItems.Count);
for (var i = 0; i < expectedItems.Count; i++) { Assert.AreEqual(expectedItems[i].Id, actualItems[i].Id); Assert.AreEqual(expectedItems[i].Name, actualItems[i].Name); Assert.AreEqual(expectedItems[i].IsActive, actualItems[i].IsActive); } } } }
Conclusion
The code base can be found at:
While you can't mock static extension methods on concrete classes, you can mock them on interfaces and that adds another tool in your toolbox when working with dependency injection.
How do you use extension methods? Did you create extension methods on interfaces? How do you test your extension methods? Post your comments below and let's discuss.