C#

Lesson 04

Dependency injection boundaries

Receive collaborators through constructors so services declare their dependencies and tests can replace infrastructure.

Good Code

src/ReviewPublisher.cs
public sealed class ReviewPublisher
{
    private readonly ReviewRepository reviews;
    private readonly MessageBus bus;

    public ReviewPublisher(ReviewRepository reviews, MessageBus bus)
    {
        this.reviews = reviews;
        this.bus = bus;
    }

    public async Task PublishAsync(Guid id, CancellationToken cancellationToken)
    {
        // Constructor dependencies make infrastructure visible.
        Review review = await reviews.GetAsync(id, cancellationToken);
        await bus.PublishAsync(new ReviewPublished(id, review.Title), cancellationToken);
    }
}

Bad Code

ReviewPublisher.cs
public sealed class ReviewPublisher
{
    public async Task PublishAsync(Guid id)
    {
        // New infrastructure inside the method hides dependencies.
        var reviews = new ReviewRepository("prod");
        var bus = new MessageBus("prod");
        Review review = await reviews.GetAsync(id);
        await bus.PublishAsync(new ReviewPublished(id, review.Title));
    }
}

Review Notes

What to review

Good Code

The good version receives repository and bus dependencies through the constructor. The service boundary says what infrastructure the workflow needs.

Bad Code

The bad version creates production infrastructure inside the method. Tests cannot replace those collaborators without touching the implementation.

Takeaways

  • A C# service should reveal the collaborators it needs instead of constructing database, clock, or HTTP clients inside methods.