Skip to content

Instantly share code, notes, and snippets.

@benj2240
Last active May 18, 2021 22:54
Show Gist options
  • Select an option

  • Save benj2240/9d2c44dc61b0ddf6d5a8b2e51c4ae47a to your computer and use it in GitHub Desktop.

Select an option

Save benj2240/9d2c44dc61b0ddf6d5a8b2e51c4ae47a to your computer and use it in GitHub Desktop.

I don't need Inversion of Control

Example: Web API

In my experience, discussions on this topic can easily get out of hand with abstract language and technical jargon. So I will anchor this discussion with an example webservice.

Let's say I have an API with CRUD endpoints, that saves objects down to a database. If this API is simple enough, I might not bother with fancy designs or tests at all... so let's say that there's some business logic on the update, where if an object is updated in a certain way, we'll reject the update and email an administrator.

Then, the core / only interesting part of this service is this function:

public bool UpdateThingy(Thingy newVersion) {
	var currentVersion = _database.GetThingy(newVersion.Id);

	if (TransitionIsInvalid(currentVersion, newVersion)) {
		_emailer.SendEmail($"Invalid update attempted: {newVersion.Id}");
		return false;
	}

	_database.UpdateThingy(newVersion);
	return true;
}

private bool TransitionIsInvalid(Thingy oldVersion, Thingy newVersion)
{ ... }

Now, I want to write a test for this method that doesn't involve spinning up local IIS or talking to a real database. So I use Dependency Injection:

public class ThingyManager {
	private readonly IDatabase _database;
	private readonly IEmailer _emailer;

	public ThingyManager(IDatabase database, IEmailer emailer) {
		_database = database;
		_emailer = emailer;
	}

	public bool UpdateThingy(Thingy newVersion)
	{ ... }
}

My actual webservice will look something like

public class ThingyService : Api {
	private readonly ThingyManager _manager;

	public ThingyService() {
		_manager = new ThingyManager(new Database(), new Emailer());
	}

	[ApiEndpoint]
	UpdateThingy(Thingy newVersion) {
		if (_manager.UpdateThingy(newVersion)) {
			return Status(200);
		}
		else {
			return Status(400);
		}
	}
}

And my tests can look like this, if I like Moq and FluentAssertions:

[TestMethod]
public void ThingyManager_AcceptValidChange() {
	// ARRANGE
	var mockDB = new Mock<IDatabase>();
	var oldVersion = ...; // Some normal intermediate state
	var newVersion = ...;

	mockDB.Setup(db => db.GetThingy(newVersion.Id)).Returns(oldVersion);

	var manager = new ThingyManager(mockDB.Object, null);

	// ACT
	var success = manager.UpdateThingy(newVersion);

	// ASSERT: success from manager, DB method was called
	success.Should().Be(true);
	mockDB.Verify(db => db.UpdateThingy(newVersion));
}

[TestMethod]
public void ThingyManager_RejectInvalidChange() {
	// ARRANGE
	var mockDB = new Mock<IDatabase>();
	var oldVersion = ...; // A final state, that shouldn't be updated
	var newVersion = ...;
	var mockMailer = new Mock<IEmailer>();

	mockDB.Setup(db => db.GetThingy(newVersion.Id)).Returns(oldVersion);

	var manager = new ThingyManager(mockDB.Object, mockMailer.Object);

	// ACT
	var success = manager.UpdateThingy(newVersion);

	// ASSERT: failure from manager, email sent, no update made
	success.Should().Be(false);
	mockMailer.Verify(mailer => mailer.SendEmail(It.IsAny<string>()));
	mockDB.Verify(db => db.UpdateThingy(It.IsAny<Thingy>()), Times.Never);
}

That's all pretty routine, right? And in my opinion, it is good enough. There are plenty of tweaks we could make without changing the substance... We could add a parameterless constructor overload to the manager class, which instantiates a real DB connection and a real emailer. We could add an IConfig dependency for the email from/to/subject, and inject that too. We may need to slap IDisposable on some of these classes, and forward Dispose() calls to the IDatabase, to ensure the connection is closed at some point. All of those are details, and I am handwaving them away for now.

Add Inversion of Control

We could also take the next step and add an Inversion of Control container. Like Autofac, SimpleInjector, Unity, DryIoc, Ninject...

The first step to using IoC is to register dependencies with it. Somehow, the container needs to know that when I ask for an IEmailer, I want an Emailer. This can be done with config files, in code, or via reflection magic on the assembly. Having set that up, I can change the webservice to something like:

public class ThingyService : Api {
	private readonly ThingyManager _manager;

	public ThingyService() {
		_manager = IOC.Get<ThingyManager>();
	}
}

What have I gained?

  • I don't have to use the word new anymore.
  • If I add a new dependency to the manager, depending on how I configured the container, it may be able to discover and inject that dependency automatically.
  • Depending on how I configured the container, it may be calling Dispose() on my behalf at the appropriate times.

What have I lost?

  • I can no longer see the call to my manager's constructor. I have to infer how and when it is called, and with what arguments, by analyzing my IoC setup.
  • I have to maintain my IoC configuration. If I've gone the reflection route, then I should pretty much never have to touch this... but only pretty much never. Eventually something will go wrong; hopefully I haven't forgotten how it works by that point.

What haven't I changed?

  • I haven't made it any easier to write my tests. I still have to define how I want my dependencies to behave (return this data, return that data, throw this error, etc), and I still want to inspect them after the action, so I'm still explicitly passing all the relevant dependencies into the manager's constructor.

Takeaways

If you have a very large application, with very many dependencies, with telescoping constructors, intricate lifetime requirements, multiple assemblies with many implementations for the various dependency interfaces, you may find it worthwhile to use an IoC container.

For the types of small-to-midsize, line-of-business applications I write, I have often found myself wishing I had used some form of Dependency Injection. I have never found myself wishing I had used Inversion of Control.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment