Domain Driven Design with Web API extensions part 2: notifications with the decorator pattern

Introduction

In the previous post we started building an extension to our DDD skeleton project. We saw a simple way of adding a component to the TimetableService so that we could send an email upon inserting or updating a load test. We also discussed the pros and cons of the current implementation.

In this post we’ll see an alternative solution using the Decorator design pattern.

Preparations

Before we discuss the pattern and its implementation we’ll undo the code changes we made to the TimetableService object. Let’s reset it to what it was before:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WebSuiteDDD.Infrastructure.Common.Emailing;
using WebSuiteDemo.Loadtesting.ApplicationServices.Abstractions;
using WebSuiteDemo.Loadtesting.ApplicationServices.Messaging;
using WebSuiteDemo.Loadtesting.Domain;

namespace WebSuiteDemo.Loadtesting.ApplicationServices.Implementations
{
	public class TimetableService : ITimetableService
	{
		private readonly ITimetableRepository _timetableRepository;
		private readonly ITimetableViewModelRepository _timetableViewModelRepository;

		public TimetableService(ITimetableRepository timetableRepository, 
			ITimetableViewModelRepository timetableViewModelRepository)
		{
			if (timetableRepository == null) throw new ArgumentNullException("TimetableRepository");
			if (timetableViewModelRepository == null) throw new ArgumentNullException("TimetableViewModelRepository");
			_timetableRepository = timetableRepository;
			_timetableViewModelRepository = timetableViewModelRepository;
		}

		public async Task<AddOrUpdateLoadtestsResponse> AddOrUpdateLoadtestsAsync(AddOrUpdateLoadtestsRequest addOrUpdateLoadtestsRequest)
		{
			return await Task<AddOrUpdateLoadtestsResponse>.Run(() => AddOrUpdateLoadtests(addOrUpdateLoadtestsRequest));
		}

		public async Task<DeleteLoadtestResponse> DeleteLoadtestAsync(DeleteLoadtestRequest deleteLoadtestRequest)
		{
			return await Task<DeleteLoadtestResponse>.Run(() => DeleteLoadtest(deleteLoadtestRequest));
		}

		public async Task<GetLoadtestsForTimePeriodResponse> GetLoadtestsForTimePeriodAsync(GetLoadtestsForTimePeriodRequest getLoadtestsForTimePeriodRequest)
		{
			return await Task<GetLoadtestsForTimePeriodResponse>.Run(() => GetLoadtestsForTimePeriod(getLoadtestsForTimePeriodRequest));
		}

		private AddOrUpdateLoadtestsResponse AddOrUpdateLoadtests(AddOrUpdateLoadtestsRequest addOrUpdateLoadtestsRequest)
		{
			AddOrUpdateLoadtestsResponse resp = new AddOrUpdateLoadtestsResponse();
			try
			{
				//assign ID if not present, assume to be insertion
				foreach (LoadtestViewModel ltvm in addOrUpdateLoadtestsRequest.Loadtests)
				{
					if (ltvm.Id == null || ltvm.Id == default(Guid))
					{
						ltvm.Id = Guid.NewGuid();
					}
				}
				List<LoadtestViewModel> sortedByDate = addOrUpdateLoadtestsRequest.Loadtests.OrderBy(lt => lt.StartDateUtc).ToList();
				LoadtestViewModel last = sortedByDate.Last();
				IList<Loadtest> loadtests = _timetableRepository.GetLoadtestsForTimePeriod(sortedByDate.First().StartDateUtc, last.StartDateUtc.AddSeconds(last.DurationSec));
				Timetable timetable = new Timetable(loadtests);				
				IList<Loadtest> loadtestsAddedOrUpdated = _timetableViewModelRepository.ConvertToDomain(addOrUpdateLoadtestsRequest.Loadtests);
				AddOrUpdateLoadtestsValidationResult validationResult = timetable.AddOrUpdateLoadtests(loadtestsAddedOrUpdated);
				_timetableRepository.AddOrUpdateLoadtests(validationResult);
				resp.AddOrUpdateLoadtestsValidationResult = validationResult;
			}
			catch (Exception ex)
			{
				resp.Exception = ex;
			}
			return resp;
		}

		private DeleteLoadtestResponse DeleteLoadtest(DeleteLoadtestRequest deleteLoadtestRequest)
		{
			DeleteLoadtestResponse resp = new DeleteLoadtestResponse();
			try
			{
				_timetableRepository.DeleteById(deleteLoadtestRequest.Id);
			}
			catch (Exception ex)
			{
				resp.Exception = ex;
			}
			return resp;
		}

		private GetLoadtestsForTimePeriodResponse GetLoadtestsForTimePeriod(GetLoadtestsForTimePeriodRequest getLoadtestsForTimePeriodRequest)
		{
			GetLoadtestsForTimePeriodResponse resp = new GetLoadtestsForTimePeriodResponse();
			try
			{
				IList<Loadtest> loadtests = _timetableRepository.GetLoadtestsForTimePeriod(getLoadtestsForTimePeriodRequest.SearchStartDateUtc, getLoadtestsForTimePeriodRequest.SearchEndDateUtc);
				IEnumerable<LoadtestViewModel> ltVms = _timetableViewModelRepository.ConvertToViewModels(loadtests);
				resp.Loadtests = ltVms;
			}
			catch (Exception ex)
			{
				resp.Exception = ex;
			}
			return resp;
		}
	}
}

We’ll inject the email sending logic in a different way.

The decorator

We discussed the decorator in a dedicated post referenced above. In short this pattern aims to extend the functionality of objects at runtime. It achieves this goal by wrapping the object in a decorator class leaving the original object intact. The result is an enhanced implementation of the object which does not break any client code that’s using the object.

The pattern has various forms of implementation. We’ll go for the approach where the decorator implements the abstraction and also has a dependency on the same abstraction type. In our case we’ll have a new implementation of ITimetableService which also has a dependency of type ITimetableService. The “inner” ITimetableService will perform the actual job of the timetable service, i.e. the tasks we have in TimetableService.cs. The “outer” ITimetableService will also require an IEmailService. It delegates all the “real” job related tasks to the inner service and calls upon the email service upon load test job insertions or updates. Insert the following class TimetableServiceWithEmail into the Implementations folder of the ApplicationServices layer:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WebSuiteDDD.Infrastructure.Common.Emailing;
using WebSuiteDemo.Loadtesting.ApplicationServices.Abstractions;
using WebSuiteDemo.Loadtesting.ApplicationServices.Messaging;
using WebSuiteDemo.Loadtesting.Domain;

namespace WebSuiteDemo.Loadtesting.ApplicationServices.Implementations
{
	public class TimetableServiceWithEmail : ITimetableService
	{
		private readonly ITimetableService _innerTimetableService;
		private readonly IEmailService _emailService;

		public TimetableServiceWithEmail(ITimetableService innerTimetableService, IEmailService emailService)
		{
			if (innerTimetableService == null) throw new ArgumentNullException("Inner timetable service");
			if (emailService == null) throw new ArgumentNullException("Email service");
			_innerTimetableService = innerTimetableService;
			_emailService = emailService;
		}
	
		public async Task<AddOrUpdateLoadtestsResponse> AddOrUpdateLoadtestsAsync(AddOrUpdateLoadtestsRequest addOrUpdateLoadtestsRequest)
		{
			AddOrUpdateLoadtestsResponse resp = await _innerTimetableService.AddOrUpdateLoadtestsAsync(addOrUpdateLoadtestsRequest);
			if (resp.Exception == null)
			{
				AddOrUpdateLoadtestsValidationResult addOrUpdateValidationResult = resp.AddOrUpdateLoadtestsValidationResult;
				if ((addOrUpdateValidationResult.ToBeInserted.Any() || addOrUpdateValidationResult.ToBeUpdated.Any())
					&& !addOrUpdateValidationResult.Failed.Any())
				{
					EmailArguments args = new EmailArguments("Load tests added or updated", "Load tests added or updated", "The Boss", "The developer", "123.456.678");
					_emailService.SendEmail(args);
				}
			}
			return resp;
		}

		public async Task<DeleteLoadtestResponse> DeleteLoadtestAsync(DeleteLoadtestRequest deleteLoadtestRequest)
		{
			return await _innerTimetableService.DeleteLoadtestAsync(deleteLoadtestRequest);
		}

		public async Task<GetLoadtestsForTimePeriodResponse> GetLoadtestsForTimePeriodAsync(GetLoadtestsForTimePeriodRequest getLoadtestsForTimePeriodRequest)
		{
			return await _innerTimetableService.GetLoadtestsForTimePeriodAsync(getLoadtestsForTimePeriodRequest);
		}
	}
}

As you can see the DeleteLoadtestAsync and GetLoadtestsForTimePeriodAsync methods are simply delegated to _innerTimetableService. The real work of AddOrUpdateLoadtestsAsync is also forwarded to _innerTimetableService. When it returns we check if there’s anything new to tell the world about. If that’s the case then we call upon _emailService to send the email.

Decorator in StructureMap

The next technical step is to wire up the enhanced decorator with StructureMap so that it is injected into LoadtestsController. If you run the project now TimetableServiceWithEmail won’t be picked up of course as it doesn’t follow the default interface implementation naming convention. However, TimetableServiceWithEmail also requires an ITimetableService.

Fortunately that’s not a brand new software requirement and there’s a clean solution for that in StructureMap. Open DefaultRegistry.cs in the DependencyResolution folder of the WebApi project. Currently we have the following line under the Scan method call:

For<IEmailService>().Use<FakeEmailService>();

Remove that row and add the following instead:

For<ITimetableService>().Use<TimetableService>().DecorateWith(i => new TimetableServiceWithEmail(i, new FakeEmailService()));

I know it looks weird, but it works.

You can test the solution by adding one or more new loadtests like we did before – you can refer to this post how to do that. You’ll see that the decorator is called as expected.

There you have it, this is an application of the decorator pattern to enhance the functionality of the original TimetableService implementation of the ITimetableService interface.

Can we be satisfied yet? It depends on what you’d like to achieve. It is possible that inserting or updating a load test is such an important event that we don’t simply want to send an email but perform a whole range of other things. In fact there might be other systems out there that want to be notified of such an event, e.g. a Windows service that’s going to execute the load test, or some other scheduling system that will send out automated emails 1 hour before the test starts to all parties involved.

Coordinating such systems is difficult to achieve with decorators only. There’s a number of solutions to enable this type of communication. We’ll look at an example of messaging starting in the next post. We’ll also learn about another concept of Domain Driven Design: domain events.

View the list of posts on Architecture and Patterns here.

Advertisement

About Andras Nemes
I'm a .NET/Java developer living and working in Stockholm, Sweden.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Elliot Balynn's Blog

A directory of wonderful thoughts

Software Engineering

Web development

Disparate Opinions

Various tidbits

chsakell's Blog

WEB APPLICATION DEVELOPMENT TUTORIALS WITH OPEN-SOURCE PROJECTS

Once Upon a Camayoc

Bite-size insight on Cyber Security for the not too technical.

%d bloggers like this: