Now all individual components are ready, it's time to bring it all together in a working application.
To get all these parts together, I add the last project: the console that will run the whole thing. It wouldn't be complete with the popular syntax of invoice auto
. The Command Line Utils package allows me to convert the invoice auto args
from the Main
method into commands that can be executed. This package has a bit of a learning curve, but has a lot of options to customise the command line arguments. I'm not going to elaborate a lot on that, I liked the package and I'll let my readers figure out how to use it best.
Lastly, there's the setup of the objects in the Main function. It's basic injection of the services with a little composition for the pdfGenerator
that takes the htmlGenerator
as input.
// in my console application
var dateProvider = new SystemDateProvider();
var customerRepository = new StaticCustomerRepository();
var timeCampWorkRepository = new TimeCampWorkRepository(configuration, customerRepository);
var htmlGenerator = new FluidHtmlDocumentGenerator();
var pdfGenerator = new DinkToPdfDocumentGenerator(htmlGenerator);
var generator = new InvoiceGenerator(dateProvider,
customerRepository,
timeCampWorkRepository,
pdfGenerator);
Structuring the code
The ICustomerRepository
interface should be placed in the use case layer (orange) and the implementation should be placed in the infrastructure layer (blue). Clean Architecture suggests grouping things that change with the same frequency and for the same reason. The infrastructure layer normally changes a lot more than the use case layer. I put the WorkRepository
and the StaticCustomerRepository
in separate projects. They change for different reasons at different times, so I should be able to build and deploy them at different times.
The document generators (FluidHtmlDocumentGenerator
and DinkToPdfDocumentGenerator
) are in a different project together. They both change when I need to update the document generation. Maybe not at the exact same time, but definitely for the same reason.
Above all else, the code should be easily testable. That is why I used all the interfaces. It's easy to switch out an actual component for a fake one. The fake implementation allows me to control certain data. For example: I have a IDateProvider
interface so I can control when Now
actually is. If I would just use DateTime.Now
, I could not simulate generating an invoice for a specific date. Now I just have two implementations: the SystemDateProvider
in my actual application and the FakeDateProvider
for in my tests.
// shared interface
public interface IDateProvider
{
DateTime Now { get; }
}
// in my console application
internal class SystemDateProvider : IDateProvider
{
public DateTime Now => DateTime.Now;
}
// in my tests
internal class FakeDateProvider : IDateProvider
{
public FakeDateProvider(string now)
{
if (DateTime.TryParse(now, out var parsedNow))
{
Now = parsedNow;
}
}
public DateTime Now { get; }
}
The console project belongs in the infrastructure layer, together with the implementations of the interfaces. I put it in another layer in the image to indicate that the console brings all the other layers together.
The last blog in this series will talk about the pros and cons of this architecture.