Sunday, July 9, 2017

Dependency Inversion and Microsoft Web Technologies

When I started learning C# and ASP.NET about 10 or 15 years ago (!!), we rendered HTML with Web Forms and published services with ASMX Web Services. A little while later we moved on to WCF services.

One of the limitations of those technologies was that the framework would only call the default, parameterless constructor of the page and service classes. To allow unit testing, I used "poor man's" dependency injection (DI), which I learned from Jean-Paul Boodhoo's videos on DnRTV. This is the approach where you have 2 constructors: a default one that calls the other one, which takes instances of all needed dependencies. The default constructor created instances of concrete classes that implemented interfaces. E.g.

public interface InterfaceA
{
string GetSomethingA();
}
public class ClassA : InterfaceA
{
private readonly InterfaceB b;
public ClassA() : this(new ClassB())
{
}
public ClassA(InterfaceB b)
{
this.b = b;
}
public string GetSomethingA()
{
return this.b.GetSomethingB();
}
}
public interface InterfaceB
{
string GetSomethingB();
}
public class ClassB : InterfaceB
{
private readonly IRestService service;
public ClassB() : this(new RestService())
{
}
public ClassB(IRestService service)
{
this.service = service;
}
public string GetSomethingB()
{
return this.service.GetDataFromRestService();
}
}
public interface IRestService
{
string GetDataFromRestService();
}
public class RestService : IRestService
{
public string GetDataFromRestService()
{
return new WebClient().DownloadString("http://example.com");
}
}
view raw PoorMansDi.cs hosted with ❤ by GitHub
I don't remember using, or even looking to mock frameworks. I think we created custom mocks by creating test classes that implement the interfaces and allowed customizing responses. I'm not sure which inversion of control (IoC) containers existed for C# then, or mock frameworks. If they existed, they were probably open source, which was frowned on at the bank at the time, even if just for testing. If they were allowed it involved some paperwork, anyway. (Banks like to have big companies they can call when they need support and who will be around a while.) Our pages and unit tests looked something like this:

public partial class _Default : Page
{
private readonly InterfaceA a;
private Label somethingLabel = new Label();
public Label SomethingLabel { get { return this.somethingLabel; } }
public _Default() : this(new ClassA())
{
}
public _Default(InterfaceA a)
{
this.a = a;
}
public void Page_Load(object sender, EventArgs e)
{
this.somethingLabel.Text = a.GetSomethingA();
}
}
// Test class
[TestFixture]
public class DefaultTests
{
[Test]
public void SetSomethingLabel()
{
const string response = "something";
var mockA = new MockA { Response = response };
var defaultPage = new _Default(mockA);
defaultPage.Page_Load(this, new EventArgs());
Assert.That(defaultPage.SomethingLabel.Text, Is.EqualTo(response));
}
}
public class MockA : InterfaceA
{
public string Response { get; set; }
public string GetSomethingA()
{
return Response;
}
}
This is fine if all you want to do is unit test. But what if you want to do integration testing, where you mock out the layer where you cross into other people's code e.g. the database or an external service? You'd have to do something like this:

[TestFixture]
public class DefaultIntegrationTests
{
[Test]
public void SetSomethingLabel()
{
const string response = "something";
var classA = new ClassA(new ClassB(new MockRestService { Response = response }));
var defaultPage = new _Default(classA);
defaultPage.Page_Load(this, new EventArgs());
Assert.That(defaultPage.SomethingLabel.Text, Is.EqualTo(response));
}
}
public class MockRestService : IRestService
{
public string Response { get; set; }
public string GetDataFromRestService()
{
return this.Response;
}
}
This wasn't something we tried to do at the time. We were a pretty inexperienced bunch. (Programming at a bank vs. programming at a software company is like practicing law at a bank vs. at a law firm.)

If I'd have known more about the dependency inversion principle then, I would have been very skeptical about putting up with this limitation. I would have immediately gone searching for ways to insert something into the web request pipeline to control page and service creation. If I need to replace an implementation at the bottom of a dependency graph, it should be as easy as replacing one interface registration in an IoC container.

Searching the web now it looks like it was possible, but not in an a way that made you feel good. They look like hacks, or the domain of .NET experts. If you wanted to stick to the SOLID principles, though, it's what you should have done.

I do find it surprising that Microsoft gave us frameworks based primarily on object oriented languages (C#, VB.NET) that didn't let you observe object-oriented (OO) practices. Every example from that time involved creating instances of concrete classes in the page or service classes themselves. I even remember one example that told developers to drag and drop a new SqlConnection object onto every page. Not very maintainable. Perhaps Microsoft didn't think the existing MS web developers of the day could embrace these concepts, so many being ASP/VB6 devs.

Whatever the reason, these limitations lead to some particularly gnarly code, completely lacking in abstractions and injection points. For example, the WCF service I'm currently tearing apart to allow mocking out the bottom-most layer. It goes something like this:

// The WCF service class
public class Service1 : IService1
{
public string GetData(int value)
{
return ServiceLogic.GetData(value);
}
}
// Class with static methods and a static instance of DB/service instances
public class ServiceLogic
{
private static Context context;
internal static Context Context
{
get
{
if (context == null)
{
initializeContext();
}
return context;
}
}
// Used for unit testing. Must be set back to null or previous value after test or before next test.
internal static void InjectContext(Context context)
{
ServiceLogic.context = context;
}
static void initializeContext()
{
context = new Context();
}
public static string GetData(int value)
{
return Context.GetData(value);
}
}
public class Context
{
private DataLayer dataLayer;
public Context()
{
this.dataLayer = new DataLayer();
}
public string GetData(int value)
{
return this.dataLayer.GetData(value);
}
}
public class DataLayer
{
public string GetData(int value)
{
return string.Format("You entered: {0}", value);
}
}
The WCF class calls static methods. The first time one of them is called, it initializes a static instance, that is, a singleton, of an object that has instances of database and service classes. If you replace one of those instances during a test, you must set it back to null for reinitialization when you're done, or have every test set it before it runs. This is what you get when you ignore the SOLID principles.

It wasn't until ASP.NET MVC 3 that Microsoft built IoC into ASP.NET MVC from the start, allowing controllers to take dependencies in constructors. Until then, people used IoC containers that implemented the complex looking code that allowed using DI in ASP.NET. I'm still surprised that it took so long for them to bake this in.

The next time I look at a technology that is based primarily on an OO language, I'll be looking for the injection points, no matter how complex they are to use. If they don't exist, the technology will need a very compelling reason for me to use it.

No comments:

Post a Comment