Domain Driven Design with Web API revisited Part 13: view models

Introduction

In the previous post we tested the load testing database context. We saw how EF could transform the database objects into domain ones through reflection. However, we had to add some changes to the domain code in order to support this feature. The domain objects lost their persistence ignorance feature to some extent but I think we were able to minimise the “damage”. One such change was the addition of a private parameterless constructor which really has no role for the domain but helps us implement EntityFramework as the backing store mechanism.

In this post we’ll look at view models.

View models

View models are objects that show the view representation of a domain. E.g. a domain may have a decimal price property with no specific formatting. You may want to show that price in your visitor’s native format based on their culture settings. That will be the view representation of the price. Normally domain objects are not concerned with view details. That task is delegated to view models. Also, as we said before, domains should be void of technology-related things, such as MVC attributes. This is where a view model comes in handy as you are free to do to it what you want. Add attributes, format strings, add extra properties that the UI is expecting etc.

In an MVC UI layer it is quite customary to have view models injected into the views instead of the plain domains otherwise the Razor code may need to perform extra formatting which is not what it’s supposed to do. View models are therefore a technique to decouple the domain from its view-specific representation(s). A domain can have multiple different views depending on e.g. who is requesting the data: an admin, a simple user, Elvis Presley etc.

Also note that the type of view-model I’m talking about now is not the same as view-models in the Model-View-ViewModel (MVVM) pattern used extensively in XAML-based technologies, such as Silverlight, WPF Windows 8+ store apps. In MVVM a view-model is a lot richer than the DTO (data transfer object) variants I’ve described above. In MVVM the view-models can have logic, event handlers, property change notifications etc., which will not fit “our” view-models.

You can put view models pretty much anywhere in your DDD application. We have at least two very good use cases for them:

  • We know that the ultimate consuming layer of our application will be Web API 2. External users will be able to add load tests by posting a collection of load tests to a web endpoint, such as /loadtest . Currently the Timetable object accepts Loadtest domain objects in the AddOrUpdateLoadtests method. That’s fine, we won’t change that. However, from the end user’s point of view it’s not realistic that they will know the IDs of each load test component, such as the Agent, the Customer etc. Instead they will prefer to use some more readable properties such as the Customer name, or the Agent location in plain text. A view model can help with that.
  • Imagine that the end user wants to view the load tests in a given time frame. We already have a method for that: GetLoadtestsForTimePeriod. Again, it returns a list of load test domain objects with IDs only. We can solve the problem with a view model that includes all the readable properties of a load test.

We’ll try to solve both issues with the same view model. We must be able to convert the domain to a view model and back. We’ll put those functions into a new repository interface so that ITimetableRepository doesn’t grow too large.

The LoadtestViewModel object

Insert the following class to the WebSuiteDemo.Loadtesting.Domain layer:

public class LoadtestViewModel
{
	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 DateTime StartDateUtc { get; set; }
	public int UserCount { get; set; }
	public int DurationSec { get; set; }
}

As you see we’ve just copied a couple of readable properties from the constituent objects, like the customer name or the user count.

Insert the following interface to the Domain layer:

public interface ITimetableViewModelRepository
{
	IList<LoadtestViewModel> ConvertToViewModels(IEnumerable<Loadtest> domains);
	IList<Loadtest> ConvertToDomain(IEnumerable<LoadtestViewModel> viewModels);
}

…and we implement it in the WebSuiteDemo.Loadtest.Repository.EF layer. Insert the following class to the Repositories folder:

public class TimetableViewModelRepository : ITimetableViewModelRepository
{
	public IList<LoadtestViewModel> ConvertToViewModels(IEnumerable<Loadtest> domains)
	{
		LoadTestingContext context = new LoadTestingContext();
		List<LoadtestViewModel> viewModels = new List<LoadtestViewModel>();
		foreach (Loadtest lt in domains)
		{
			LoadtestViewModel vm = new LoadtestViewModel();
			vm.Id = lt.Id;
			Agent agent = (from a in context.Agents where a.Id == lt.AgentId select a).FirstOrDefault();
			if (agent == null) throw new ArgumentException("There is no load test agent with the given ID.");
			vm.AgentCountry = agent.Location.Country;
			vm.AgentCity = agent.Location.City;

			Customer customer = (from c in context.Customers where c.Id == lt.CustomerId select c).FirstOrDefault();
			if (customer == null) throw new ArgumentException("There is no customer with the given ID.");
			vm.CustomerName = customer.Name;

			if (lt.EngineerId.HasValue)
			{
				Engineer engineer = (from e in context.Engineers where e.Id == lt.EngineerId.Value select e).FirstOrDefault();
				if (engineer == null) throw new ArgumentException("There is no engineer with the given ID.");
				vm.EngineerName = engineer.Name;
			}

			LoadtestType loadtestType = (from t in context.LoadtestTypes where t.Id == lt.LoadtestTypeId select t).FirstOrDefault();
			if (loadtestType == null) throw new ArgumentException("There is no load test type with the given ID.");
			vm.LoadtestTypeShortDescription = loadtestType.Description.ShortDescription;

			Project project = (from p in context.Projects where p.Id == lt.ProjectId select p).FirstOrDefault();
			if (project == null) throw new ArgumentException("There is no project with the given ID.");
			vm.ProjectName = project.Description.ShortDescription;

			Scenario scenario = (from s in context.Scenarios where s.Id == lt.ScenarioId select s).FirstOrDefault();
			if (scenario == null) throw new ArgumentException("There is no scenario with the given ID.");
			vm.ScenarioUriOne = scenario.UriOne;
			vm.ScenarioUriTwo = scenario.UriTwo;
			vm.ScenarioUriThree = scenario.UriThree;

			vm.UserCount = lt.Parameters.UserCount;
			vm.StartDateUtc = lt.Parameters.StartDateUtc;
			vm.DurationSec = lt.Parameters.DurationSec;

			viewModels.Add(vm);
		}
		return viewModels;
	}


	public IList<Loadtest> ConvertToDomain(IEnumerable<LoadtestViewModel> viewModels)
	{
		List<Loadtest> loadtests = new List<Loadtest>();
		LoadTestingContext context = new LoadTestingContext();
		foreach (LoadtestViewModel vm in viewModels)
		{
			Guid id = vm.Id;
			LoadtestParameters ltParams = new LoadtestParameters(vm.StartDateUtc, vm.UserCount, vm.DurationSec);
			Agent agent = (from a in context.Agents
							where a.Location.City.Equals(vm.AgentCity, StringComparison.InvariantCultureIgnoreCase)
								&& a.Location.Country.ToLower() == vm.AgentCountry.ToLower()
							select a).FirstOrDefault();
			if (agent == null) throw new ArgumentException("There is no agent with the given properties.");

			Customer customer = (from c in context.Customers where c.Name.Equals(vm.CustomerName, StringComparison.InvariantCultureIgnoreCase) select c).FirstOrDefault();
			if (customer == null) throw new ArgumentException("There is no customer with the given properties.");

			Guid? engineerId = null;
			if (!string.IsNullOrEmpty(vm.EngineerName))
			{
				Engineer engineer = (from e in context.Engineers where e.Name.Equals(vm.EngineerName, StringComparison.InvariantCultureIgnoreCase) select e).FirstOrDefault();
				if (engineer == null) throw new ArgumentException("There is no engineer with the given properties.");
				engineerId = engineer.Id;
			}

			LoadtestType ltType = (from t in context.LoadtestTypes where t.Description.ShortDescription.Equals(vm.LoadtestTypeShortDescription, StringComparison.InvariantCultureIgnoreCase) select t).FirstOrDefault();
			if (ltType == null) throw new ArgumentException("There is no load test type with the given properties.");

			Project project = (from p in context.Projects where p.Description.ShortDescription.ToLower() == vm.ProjectName.ToLower() select p).FirstOrDefault();
			if (project == null) throw new ArgumentException("There is no project with the given properties.");

			Scenario scenario = (from s in context.Scenarios
									where s.UriOne.Equals(vm.ScenarioUriOne, StringComparison.InvariantCultureIgnoreCase)
										&& s.UriTwo.Equals(vm.ScenarioUriTwo, StringComparison.InvariantCultureIgnoreCase)
										&& s.UriThree.Equals(vm.ScenarioUriThree, StringComparison.InvariantCultureIgnoreCase)
									select s).FirstOrDefault();

			if (scenario == null)
			{
				List<Uri> uris = new List<Uri>();
				Uri firstUri = string.IsNullOrEmpty(vm.ScenarioUriOne) ? null : new Uri(vm.ScenarioUriOne);
				Uri secondUri = string.IsNullOrEmpty(vm.ScenarioUriTwo) ? null : new Uri(vm.ScenarioUriTwo);
				Uri thirdUri = string.IsNullOrEmpty(vm.ScenarioUriThree) ? null : new Uri(vm.ScenarioUriThree);
				if (firstUri != null) uris.Add(firstUri);
				if (secondUri != null) uris.Add(secondUri);
				if (thirdUri != null) uris.Add(thirdUri);
				scenario = new Scenario(Guid.NewGuid(), uris);
				context.Scenarios.Add(scenario);
				context.SaveChanges();
			}

			Loadtest converted = new Loadtest(id, ltParams, agent.Id, customer.Id, engineerId, ltType.Id, project.Id, scenario.Id);
			loadtests.Add(converted);
		}
		return loadtests;
	}
}

The implementation is not too sophisticated. We simply look up each search term in the database one by one. In reality we’d probably give this task to a stored procedure and possibly cache the results even, but for now it’ll do just fine.

We’re done with the database layer. In the next post we’ll start looking into the application service.

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.

One Response to Domain Driven Design with Web API revisited Part 13: view models

  1. Eslam Wahid says:

    hi its a great post , i have question if i have sales context in my bussiness case scenario tell me to great user by sales person but i suppose that these adding function must belong to customer relation managment context and in my sales context must aggregated under order aggregation root witch contain customer , order details , company .

    how can i organize it ?

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 )

Twitter picture

You are commenting using your Twitter 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: