Domain Driven Design with Web API revisited Part 17: the POST and DELETE controller actions
October 1, 2015 Leave a comment
Introduction
In the previous post we started building the top layer of our load testing DDD demo solution. We added an empty Web API 2 project and also created a GET controller action to retrieve the open load tests 2 weeks ahead. We also managed to add an IoC controller called StructureMap which injects the required concrete dependencies into the controllers and service methods.
In this post we’ll continue building on our demo project and add the POST and DELETE controller actions as well. We’ll also see another application of the view-model concept we saw earlier.
POST
Open the WebSuiteDDD.Demo application we’ve been working on. The POST controller method will have two purposes. It will be responsible for adding new and updating existing load tests. This is in line with the AddOrUpdateLoadtests method of Timetable.cs. This type of operation is also called UPSERT, a mixture of UPDATE and INSERT. Giving two roles to POST is probably not as common as dividing the roles up between POST and PUT. In other solutions POST is most often used for insertions and PUT for updates. Here we’ll go for a single POST method to handle both.
Recall that TimetableService.cs has a method called AddOrUpdateLoadtestsAsync which accepts an AddOrUpdateLoadtestsRequest object which in turn holds an IEnumerable of LoadtestViewModels. We could use LoadtestViewModel in the controller action as well like in this example:
public async Task<IHttpActionResult> Post(IEnumerable<LoadtestViewModel> insertUpdateLoadtestViewModels)
…however we’ll go for a slightly different solution and introduce another role of view models in the solution. Recall that LoadtestViewModel as its name suggests is a view model to hold a UI-specific version of the load test domain. There’s one property that callers may find cumbersome: StartDateUtc. First they have to specify the date in some valid format regardless if they are sending the request from the US, France or Japan. Second they have to stick to UTC dates. Instead we’ll make it easier for them to specify the start date and a time zone.
There might already be a folder called Models in WebSuiteDDD.WebApi. If not, then add one. Insert the following objects into that folder.
InsertUpdateLoadtestViewModel.cs:
public class InsertUpdateLoadtestViewModel { public Guid Id { get; set; } public string AgentCity { get; set; } public string AgentCountry { get; set; } public string CustomerName { get; set; } public string EngineerName { get; set; } public string LoadtestTypeShortDescription { get; set; } public string ProjectName { get; set; } public string ScenarioUriOne { get; set; } public string ScenarioUriTwo { get; set; } public string ScenarioUriThree { get; set; } public StartDate StartDate { get; set; } public int UserCount { get; set; } public int DurationSec { get; set; } public LoadtestViewModel ConvertToViewModel() { LoadtestViewModel ltVm = new LoadtestViewModel(); if (Id == null || Id == default(Guid)) Id = Guid.NewGuid(); if (string.IsNullOrEmpty(AgentCity)) throw new ArgumentNullException("Agent city must be provided."); if (string.IsNullOrEmpty(AgentCountry)) throw new ArgumentNullException("Agent country must be provided."); if (string.IsNullOrEmpty(CustomerName)) throw new ArgumentNullException("Customer name must be provided."); if (string.IsNullOrEmpty(LoadtestTypeShortDescription)) throw new ArgumentNullException("Load test type must be provided."); if (string.IsNullOrEmpty(ProjectName)) throw new ArgumentNullException("Project name must be provided."); if (string.IsNullOrEmpty(ScenarioUriOne)) throw new ArgumentNullException("At least one URL must be provided for the scenario."); ltVm.AgentCity = AgentCity; ltVm.AgentCountry = AgentCountry; ltVm.CustomerName = CustomerName; ltVm.DurationSec = DurationSec; ltVm.EngineerName = EngineerName; ltVm.Id = Id; ltVm.LoadtestTypeShortDescription = LoadtestTypeShortDescription; ltVm.ProjectName = ProjectName; ltVm.ScenarioUriOne = ScenarioUriOne; ltVm.ScenarioUriTwo = ScenarioUriTwo; ltVm.ScenarioUriThree = ScenarioUriThree; DateTime customerDate = new DateTime(StartDate.Year, StartDate.Month, StartDate.Day, StartDate.Hour, StartDate.Minute, 0); TimeZoneInfo customerTimeZone = TimeZoneInfo.FindSystemTimeZoneById(StartDate.Timezone); ltVm.StartDateUtc = TimeZoneInfo.ConvertTimeToUtc(customerDate, customerTimeZone); ltVm.UserCount = UserCount; return ltVm; } }
public class StartDate { public int Year { get; set; } public int Month { get; set; } public int Day { get; set; } public int Hour { get; set; } public int Minute { get; set; } public string Timezone { get; set; } }
InsertUpdateLoadtestViewModel looks very much like LoadtestViewModel but the start date has been broken out into a separate object where callers can provide the date and time in a more straightforward way than through some UTC date format. You’ll notice that InsertUpdateLoadtestViewModel can be converted into LoadtestViewModel. We also perform some initial validation already here.
You should always try to avoid code and logic duplication in your software. However, if there’s one area where this is difficult to achieve then it’s validation. In a normal website where you have to fill in a form there will be at least some basic client-side validation, e.g. that the obligatory fields cannot be empty. This is done in order to prevent all HTTP requests to reach our backend and reject the outright faulty ones. The same validation rules will then be present in the domain objects. We simulate such a scenario with the above preliminary validation within InsertUpdateLoadtestViewModel.
We’re now ready to build our Post action method. Insert the following function to LoadtestsController.cs:
public async Task<IHttpActionResult> Post(IEnumerable<InsertUpdateLoadtestViewModel> insertUpdateLoadtestViewModels) { List<LoadtestViewModel> loadtestViewModels = new List<LoadtestViewModel>(); foreach (InsertUpdateLoadtestViewModel vm in insertUpdateLoadtestViewModels) { loadtestViewModels.Add(vm.ConvertToViewModel()); } AddOrUpdateLoadtestsRequest request = new AddOrUpdateLoadtestsRequest(loadtestViewModels); AddOrUpdateLoadtestsResponse response = await _timetableService.AddOrUpdateLoadtestsAsync(request); if (response.Exception == null) { return Ok<string>(response.AddOrUpdateLoadtestsValidationResult.OperationResultSummary); } return InternalServerError(response.Exception); }
We accept an IEnumerable of InsertUpdateLoadtestViewModel objects which are then converted into LoadtestViewModels using the above conversion method. We then build an AddOrUpdateLoadtestsRequest object and call the AddOrUpdateLoadtestsAsync method of ITimetableService. If everything went fine we return the summary of the actions carried out, i.e. the contents of the OperationResultSummary property. Otherwise we return a 500 internal server error with the exception details.
We won’t test this action method now, we’ll do that in the next post.
DELETE
The Delete action method is extremely simple. All it needs is an ID attached to the URL. Here’s the Delete controller method which you can add to LoadtestsController.cs:
public async Task<IHttpActionResult> Delete(Guid id) { DeleteLoadtestRequest request = new DeleteLoadtestRequest(id); DeleteLoadtestResponse response = await _timetableService.DeleteLoadtestAsync(request); if (response.Exception == null) { return Ok<string>("Deleted"); } return InternalServerError(response.Exception); }
You’ll immediately see what we’re doing here. We build a DeleteLoadtestRequest object out of the incoming id parameter. We then call the DeleteLoadtestAsync nmethod of the timetable service. We return a confirmation message to the caller in case there were no exceptions. Otherwise we send back a 500 as usual.
We’ll test both methods in the next post. The next post will also be the last in this revised DDD series.
View the list of posts on Architecture and Patterns here.