Published on 10/11/2019
In the previous part, we finalized the first iteration of our database context and partially implemented our repository adaptor for the blog posts. In this part, we’re going to connect all the dots and get something that is testable end-to-end, that is, from the UI right down to an actual database and back out again.
Just run it
Before we set out on this part, we first have to disable the mocks that we put in place to provide those fake blog post entries with. We switch back to just Debug mode (we had previously put it on UITest mode). When we now run our project and click through to the blog item “Title 1”, we see the following error in the Output window:
Hooking it up
We need to provide an implementation that fulfills the requirement of the page, and the new Blazor pages use dependency injection to resolve any service references. In part 13 we added conditional compilation code that registered the fake blog handler implementation to quickly iterate over changes in the UI. We now need to amend that to provide the real implementation.
#if UITEST services.AddSingleton<IBlogServiceAdaptor, MockBlogServiceAdaptor>(); #else services.AddTransient<IBlogServiceAdaptor, BlogServiceAdaptor>(); #endif
Concept: Service Lifetime
This time we don’t instance it as a singleton, but rather a transient. The fake handler has a constant state (with constant data), so it is trivial to be hosted as a singleton since it serves the same content for all requests. The real handler implementation, on the other hand, has dependencies and constantly changing state. A different user is logged in perhaps, or a different blog entry is requested. Your use of different lifetimes depends on your design. EntityFramework, for instance, defaults to a scoped lifetime for your context. So within the scope of handling a single web request, you’ll always be dealing with the same instance of your context. In the case where we have multiple actions to take during the course of handling a request and we might be using the same service twice, you need to carefully consider your chosen service lifetime.
The blue wire or the red wire?
When we run this code, we get a different error.
Our page can now resolve the handler service, but the handler service cannot resolve the domain service. This means we need to provide a registration for the IBlogService
abstraction. Let’s go to our unit tests for this. We need two things, a service collection, and a service provider.
readonly IServiceCollection _services = new ServiceCollection(); private IServiceProvider _serviceProvider;
In a test initializer method, we can then provide registration of the required service. And we change our property to reference the service provider to resolve it when running the test.
public IBlogService Service => _serviceProvider.GetService<IBlogService>(); [TestInitialize] public void Initialize() { _services .AddTransient<IBlogService, BlogService>(); _serviceProvider = _services.BuildServiceProvider(); }
Running our tests now gives us the same error in the test results pane, except that it could not resolve services for IBlogDatabaseAdaptor
and IBlogSyndicationService
. This is because those two items are constructor parameters for the implementation BlogService
, and when IServiceProvider
tries to resolve a service, it also needs to resolve all the services it depends on. We fix this by injecting our own mocks that we already have in this test class.
[TestInitialize] public void Initialize() { _services .AddTransient(s=>_dbAdaptorMock.Object) .AddTransient(s=>_blogSyndicationServiceMock.Object) .AddTransient<IBlogService, BlogService>(); _serviceProvider = _services.BuildServiceProvider(); }
Now it can resolve our BlogService
implementation, and the registration for this test class can provide us with an instance just like when we were newing it up ourselves.
However, our problem originated in the web project. We can add the same registration line there if we wanted to fix it, but that means again that it’s coupling to the type of a concrete implementation instead of sticking only to the abstraction. Instead we can provide it with a method to do the registration for it. It doesn’t care what this method does internally, but it can assume that once called, IBlogService
will resolve correctly. We can also then use this method in our test initialize call rather, and so get code coverage on it.
namespace Microsoft.Extensions.DependencyInjection { public static class ConfigureServices { public static IServiceCollection AddDomainServices(this IServiceCollection services) { return services.AddTransient<IBlogService, BlogService>(); } } }
This class is declared in the domain project but carries a namespace that aligns with the dependency injection extensions. This is another convention established by Microsoft in their own packages and promotes discovery through IntelliSense.
We can then use this in our test initialization.
[TestInitialize] public void Initialize() { _services .AddTransient(s => _dbAdaptorMock.Object) .AddTransient(s => _blogSyndicationServiceMock.Object) .AddDomainServices(); _serviceProvider = _services.BuildServiceProvider(); }
Our tests still pass, so we are sure the registration works. I add it to the Startup
class in the web project and run again. We still get an error.
It’s the same problem, except now it’s trying to resolve the repository because it’s a dependency for the domain service. You can see how we’re moving down the stack as we’re wiring up the different layers. I follow the same process for the blog repository. In the repository project, I add another configuration class.
namespace Microsoft.Extensions.DependencyInjection { public static class ConfigureServices { public static IServiceCollection AddRepositories(this IServiceCollection services) { return services.AddTransient<IBlogDatabaseAdaptor, BlogRepository>(); } } }
In the unit test class, I use this method together with registering our own mock for the context.
readonly IServiceCollection _services = new ServiceCollection(); private IServiceProvider _serviceProvider; readonly Mock<IhelloserveContext> _contextMock = new Mock<IhelloserveContext>(); public IBlogDatabaseAdaptor Repository => _serviceProvider.GetService<IBlogDatabaseAdaptor>(); [TestInitialize] public void Initialize() { _services .AddTransient(s => _contextMock.Object) .AddRepositories(); _serviceProvider = _services.BuildServiceProvider(); }
Our unit test needs to change to reference the Repository
property instead now (we have not refactored this into a property yet) so that it’s instance is resolved through dependency injection.
//act
Blog result = await Repository.Read(title);
Since the tests pass, we add it to the Startup
class also. When we run, we still see an error in the Output pane.
This is again the same problem, at the very bottom of the stack, looking for the data context. But the registration here is a bit different. EntityFramework has a specific pattern for service registration that they make available through their own configuration extensions. We’ll put it inside our own one.
namespace helloserve.com.Database { public static class ConfigureServices { public static IServiceCollection AddhelloserveContext(this IServiceCollection services, IConfiguration configuration) { services.AddDbContext<helloserveContext>(options => { options.UseSqlServer(configuration.GetConnectionString("helloserveContext")); }); services.AddTransient<IhelloserveContext, helloserveContext>(); return services; } } }
In this snippet, we need the configuration abstraction as well, so we pass it in. And apart from registering the actual context, we also register our own abstraction for the context to just resolve to the real context itself. We then also have to add this to the Startup
class, together with the IConfiguration
instance. In total, it now looks like this.
IConfiguration Configuration; public Startup(IConfiguration configuration) { Configuration = configuration; } // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddServerSideBlazor(); services.AddSingleton<WeatherForecastService>(); #if UITEST services.AddSingleton<IBlogServiceAdaptor, MockBlogServiceAdaptor>(); #else services.AddTransient<IBlogServiceAdaptor, BlogServiceAdaptor>(); #endif services.AddDomainServices(); services.AddRepositories(); services.AddhelloserveContext(Configuration); }
Supporting Configuration
In order to fulfill that configuration item for the connection string, we need to add it to the app settings also.
"ConnectionStrings": { "helloserveContext": "<some value>" },
When we start up again we’re still getting errors. This time about the syndication services. We are not able to register an implementation for IBlogSyndicationFactory
, but the BlogService
requires it.
A mock as a component
We were never required to build this implementation before since we could happily complete our blog domain implementation with only ever mocking this. And we’ll continue to do this until we really need a full implementation. Our domain services config now looks like this.
public static IServiceCollection AddDomainServices(this IServiceCollection services) { return services .AddTransient<IBlogService, BlogService>() .AddTransient<IBlogSyndicationService, BlogSyndicationService>() .AddSingleton<IBlogSyndicationQueue, BlogSyndicationQueue>() .AddTransient(s => new Mock<IBlogSyndicationFactory>().Object); }
You’ll see that we registered the queue as a singleton. This is important because the queue is shared between all the incoming web requests that might publish a blog. Now when we run and try to browse to a blog we see the following error in the log.
This is probably because we don’t have an actual database yet. EntityFramework can sort that out for us through their migration mechanism, but I prefer not to use it due to operational and support reasons. I prefer to write the necessary SQL directly and correctly the first time and manage the migrations without a dependency on C#. Your team might do things differently though. Regardless, let’s create our table.
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = N'Blogs') CREATE TABLE dbo.[Blogs] ( [Key] NVARCHAR(250) NOT NULL, Title NVARCHAR(250) NULL, Content NVARCHAR(MAX) NULL, IsPublished BIT NOT NULL, PublishDate DATETIME NULL, CONSTRAINT PK_Blogs PRIMARY KEY NONCLUSTERED ([Key]) ON [PRIMARY] ) ON [PRIMARY]
We can script a sample blog entry into the database quickly to match with the hardcoded title list item.
INSERT INTO [Blogs] ([Key], Title, Content, IsPublished, PublishDate) VALUES ('title-1', 'Title 1', 'Lorem ipsum `dolor sit amet`, consectetur ...', 0, null)
After some troubleshooting on strange behavior, I had to change the Blog.razor
page to include that null check that we find in the sample FetchData.razor
page. Without this check, the page doesn’t handle a slow data loading scenario (on cold start for example) very well.
if (Model == null) { <p><em>Loading...</em></p> } else { <h1>@Model.Title</h1> @((MarkupString)Model.Content) <div>Footer section</div> }
And finally, with this last change, we have an entire stack setup and running.
Conclusion
With this part, we’ve finally moved from only able to run unit tests to actually testing our work end-to-end. We saw how, after having built all the different parts separately, we were able to simply wire it up using service registrations. This is an important aspect that dependency inversion gives you: the autonomy within different layers to iterate and develop without a concern for anything outside of that layer. Even though it took sixteen blog posts to get to this point, each of these items could have been developed by a separate person, at the same time, after all of this had been designed together first.
And this is an important point. The design of the software should happen as a team. The diagrams should be finalized together, and all the abstractions should be defined together. Without a common understanding of the high-level architecture of the software and the flow of the program, the individuals building the various components will miss each other which will result in a lot of rework. All of these documentation artifacts should be collated to assist any new team members with onboarding onto the project. They too need to get a full understanding of the high level and flow before they can effectively contribute to the project.