Application service

In fact, how our application service will look like and behave, is very much similar to a bunch of command handlers. A "classic" application service exposes some methods with multiple parameters, like this:

public interface IPaymentApplicationService
{
Guid Authorize(
string creditCardNumber,
int expiryYear,
int expiryMonth,
int cvcCode,
intcamount);
void Capture(Guid authorizationId);
}

Using this kind of declaration is perfectly fine, except it doesn't play that well with the composition. It is not easy to add such an application service to a pipeline, where we have logging, retry policies and so on. To make a pipeline, we need all our handlers to have compatible parameters, but these methods of the IPaymentApplicationService just don't allow us to go that way. Every other call in the pipeline must have the same set of parameters, and as soon as we want to add one more parameter to any method, we are doomed to make numerous of changes in multiple classes that form our pipeline. This is not something we'dl like to do.

Alternatively, we can have one application service class that implements multiple IHandle<T> interfaces. This would work, but each command will then require a separate bootstrapping code, although we are adding the same elements to our pipeline:

Ads.V1.Create>>(c =>
new RetryingCommandHandler<Contracts.ClassifiedAds.V1.Create>(
new CreateClassifiedAdHandler(c.GetService<RavenDbEntityStore>())));
Ads.V1.Rename>>(c =>
new RetryingCommandHandler<Contracts.ClassifiedAds.V1.Rename>(
new RenameClassifiedAdHandler(c.GetService<RavenDbEntityStore>())));
// more handlers need to be added with the same composition

Alternatively, we can generalize our application service to handle any type of command, and use the C# 7 advanced pattern matching feature again (like we did with event handling). In this case, the application service signature would look like this:

public interface IApplicationService
{
Task Handle(object command);
}

All our previous filters for the pipeline like the retry filter or logging filter can implement this simple interface. Since those classes don't need to get the hold of the command content, everything will work just fine. Our classified ad service would then look like this:

using System;
using System.Threading.Tasks;
using Marketplace.Framework;

namespace Marketplace.Api
{
public class ClassifiedAdsApplicationService : IApplicationService
{
public async Task Handle(object command)
{
switch (command)
{
case Contracts.ClassifiedAds.V1.Create cmd:
// we need to create a new Classified Ad here
break;

default:
throw new InvalidOperationException(
$"Command type {command.GetType().FullName} is unknown");
}
}
}
}

By implementing our application service like this, we will have a single dependency to handle for all our API calls, and we keep the door open to compose a more complex command processing pipeline, just as we were able to do with individual command handlers.

Of course, the apparent tradeoff here is that we have one class that handles several commands and some might see it as an SRP violation. At the same time, the level of cohesion for this class is high, and we will see more of it later in this chapter when we will adequately handle several commands and make calls to our application service from the edge.

Let's now add more commands and extend our application service and the HTTP edge accordingly.

First, we need to get back to our entity and check what actions we can command it to perform. These actions are:

  • Set title
  • Update text
  • Update price
  • Request to publish

We can add four commands to execute these actions, since we could expect, based on our EventStorming sessions, that this is what our users would like to do.

The expanded commands list would look like this:

using System;

namespace Marketplace.Contracts
{
public static class ClassifiedAds
{
public static class V1
{
public class Create
{
public Guid Id { get; set; }
public Guid OwnerId { get; set; }
}

public class SetTitle
{
public Guid Id { get; set; }
public string Title { get; set; }
}

public class UpdateText
{
public Guid Id { get; set; }
public string Text { get; set; }
}

public class UpdatePrice
{
public Guid Id { get; set; }
public decimal Price { get; set; }
public string Currency { get; set; }
}

public class RequestToPublish
{
public Guid Id { get; set; }
}
}
}
}

Each command needs to have the id of the entity it is going to operate on. Other properties are command-specific.

Second, we can extend our edge to accept these commands as HTTP requests. The code for the new API version is listed below:

using System.Threading.Tasks;
using Marketplace.Contracts;
using Microsoft.AspNetCore.Mvc;

namespace Marketplace.Api
{
[Route("/ad")]
public class ClassifiedAdsCommandsApi : Controller
{
private readonly ClassifiedAdsApplicationService _applicationService;

public ClassifiedAdsCommandsApi(
ClassifiedAdsApplicationService applicationService)
=> _applicationService = applicationService;

[HttpPost]
public async Task<IActionResult> Post(ClassifiedAds.V1.Create request)
{
await _applicationService.Handle(request);
return Ok();
}

[Route("name")]
[HttpPut]
public async Task<IActionResult> Put(ClassifiedAds.V1.SetTitle request)
{
await _applicationService.Handle(request);
return Ok();
}

[Route("text")]
[HttpPut]
public async Task<IActionResult> Put(ClassifiedAds.V1.UpdateText request)
{
await _applicationService.Handle(request);
return Ok();
}

[Route("price")]
[HttpPut]
public async Task<IActionResult> Put(ClassifiedAds.V1.UpdatePrice request)
{
await _applicationService.Handle(request);
return Ok();
}

[Route("publish")]
[HttpPut]
public async Task<IActionResult> Put(ClassifiedAds.V1.RequestToPublish request)
{
await _applicationService.Handle(request);
return Ok();
}
}
}

You might already see some candidates for creating a useful abstraction or routine. Probably you can also predict some issues with this code when it will run in production. The edge code above also violates an important principle that a client needs to ensure it only sends valid commands to a command handler. In our code, there is nothing that does such checks. Don' worry; we will get back to the API code and solve some of those issues. For now, let's concentrate on the essential bits.

As you can see, our application service is expected to handle five commands. We need to take care that it does that. The new code for our application service would be:

using System;
using System.Threading.Tasks;
using Marketplace.Contracts;
using Marketplace.Domain;
using Marketplace.Framework;

namespace Marketplace.Api
{
public class ClassifiedAdsApplicationService : IApplicationService
{
private readonly IEntityStore _store;
private ICurrencyLookup _currencyLookup;

public ClassifiedAdsApplicationService(
IEntityStore store, ICurrencyLookup currencyLookup)
{
_store = store;
_currencyLookup = currencyLookup;
}

public async Task Handle(object command)
{
ClassifiedAd classifiedAd;
switch (command)
{
case ClassifiedAds.V1.Create cmd:
if (await _store.Exists<ClassifiedAd>(cmd.Id.ToString()))
throw new InvalidOperationException($"Entity with id {cmd.Id} already exists");

classifiedAd = new ClassifiedAd(
new ClassifiedAdId(cmd.Id),
new UserId(cmd.OwnerId));

await _store.Save(classifiedAd);
break;

case ClassifiedAds.V1.SetTitle cmd:
classifiedAd = await _store.Load<ClassifiedAd>(cmd.Id.ToString());
if (classifiedAd == null)
throw new InvalidOperationException($"Entity with id {cmd.Id} cannot be found");

classifiedAd.SetTitle(ClassifiedAdTitle.FromString(cmd.Title));
await _store.Save(classifiedAd);
break;

case ClassifiedAds.V1.UpdateText cmd:
classifiedAd = await _store.Load<ClassifiedAd>(cmd.Id.ToString());
if (classifiedAd == null)
throw new InvalidOperationException($"Entity with id {cmd.Id} cannot be found");

classifiedAd.UpdateText(ClassifiedAdText.FromString(cmd.Text));
await _store.Save(classifiedAd);
break;

case ClassifiedAds.V1.UpdatePrice cmd:
classifiedAd = await _store.Load<ClassifiedAd>(cmd.Id.ToString());
if (classifiedAd == null)
throw new InvalidOperationException($"Entity with id {cmd.Id} cannot be found");

classifiedAd.UpdatePrice(Price.FromDecimal(cmd.Price, cmd.Currency, _currencyLookup));
await _store.Save(classifiedAd);
break;

case ClassifiedAds.V1.RequestToPublish cmd:
classifiedAd = await _store.Load<ClassifiedAd>(cmd.Id.ToString());
if (classifiedAd == null)
throw new InvalidOperationException($"Entity with id {cmd.Id} cannot be found");

classifiedAd.RequestToPublish();
await _store.Save(classifiedAd);
break;

default:
throw new InvalidOperationException(
$"Command type {command.GetType().FullName} is unknown");
}
}
}
}

Here we again use the IEntityStore abstraction. This interface is very simple:

using System.Threading.Tasks;

namespace Marketplace.Framework
{
public interface IEntityStore
{
/// <summary>
/// Loads an entity by id
/// </summary>
Task<T> Load<T>(string entityId) where T : Entity;

/// <summary>
/// Persists an entity
/// </summary>
Task Save<T>(T entity) where T : Entity;

/// <summary>
/// Check if entity with a given id already exists
/// <typeparam name="T">Entity type</typeparam>
Task<bool> Exists<T>(string entityId);
}
}

We will implement this interface for different persistence types later [in this book].

As you can see, handling the Create command looks different from handling all other commands. This is natural since when we create a new entity, we need to ensure it does not exist yet. When we handle operations on the existing entity, it works the other way around. In this case, we need to ensure that the entity exists, otherwise, we cannot perform the operation and must throw an exception.

Another important thing worth mentioning is that the application service is responsible for the translation of primitive types like string or decimal, to value objects. The edge always uses serializable types that have no dependencies on the domain model. The application service, however, operates with domain concerns, it needs to instruct our domain model what to do, and since our domain model prefers to receive data as value objects, the application service is then responsible for the conversion.

The code for handling commands for an existing entity looks very similar. In fact, only the line where we call the entity method is different. Therefore, we can significantly simplify the Handle method by using a straightforward generalization:

using System;
using System.Threading.Tasks;
using Marketplace.Contracts;
using Marketplace.Domain;
using Marketplace.Framework;

namespace Marketplace.Api
{
public class ClassifiedAdsApplicationService : IApplicationService
{
private readonly IEntityStore _store;
private ICurrencyLookup _currencyLookup;

public ClassifiedAdsApplicationService(
IEntityStore store, ICurrencyLookup currencyLookup)
{
_store = store;
_currencyLookup = currencyLookup;
}

public async Task Handle(object command)
{
switch (command)
{
case ClassifiedAds.V1.Create cmd:
if (await _store.Exists<ClassifiedAd>(cmd.Id.ToString()))
throw new InvalidOperationException($"Entity with id {cmd.Id} already exists");

var classifiedAd = new ClassifiedAd(
new ClassifiedAdId(cmd.Id),
new UserId(cmd.OwnerId));

await _store.Save(classifiedAd);
break;

case ClassifiedAds.V1.SetTitle cmd:
await HandleUpdate(cmd.Id,
c => c.SetTitle(ClassifiedAdTitle.FromString(cmd.Title));
break;

case ClassifiedAds.V1.UpdateText cmd:
await HandleUpdate(cmd.Id,
c => c.UpdateText(ClassifiedAdText.FromString(cmd.Text));
break;

case ClassifiedAds.V1.UpdatePrice cmd:
await HandleUpdate(cmd.Id,
c => c.UpdatePrice(Price.FromDecimal(cmd.Price, cmd.Currency, _currencyLookup));
break;

case ClassifiedAds.V1.RequestToPublish cmd:
await HandleUpdate(cmd.Id,
c => c.RequestToPublish();
break;

default:
throw new InvalidOperationException(
$"Command type {command.GetType().FullName} is unknown");
}
}

private async Task HandleUpdate(Guid classifiedAdId, Action<ClassifiedAd> operation)
{
var classifiedAd = await _store.Load<ClassifiedAd>(classifiedAdId.ToString());
if (classifiedAd == null)
throw new InvalidOperationException($"Entity with id {classifiedAdId} cannot be found");

operation(classifiedAd);

await _store.Save(classifiedAd);
}
}
}

From the application service code, it becomes clear that it plays the vital role of an intermediate between the application edge and the domain model. An edge could be anything that communicates with the outside world. We used the HTTP API as an example, but it could also be a messaging interface or something entirely different. The important requirement for the edge would be the ability to accept commands, check them (we will get back to this later) and engage the application service to handle those commands.

When we handle a command, no matter if we use multiple command handlers or single application service, the sequence of operations is usually very similar. A command handler needs to fetch a persisted entity from the entity store, call the domain model to do the work and then persist changes. In our example, we only called one method of the entity, but this is not always so. We will look deeper into this when we discuss the consistency and transactional boundaries in the next chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.149.238