Special Case Pattern

“Use Case: When you need to return an alternate object of the same base type which will controll the flow of the application. Example: UserServiceClient can return a UserNotFound object instead of throwing a custom UserServiceException or having serialization exception caught in the consumer of the service.”

Definition

“A subclass that provides special behavior for particular cases.” - martinfowler.com

Example

Consider the following UserServiceClient with method GetUserAsync

1
2
3
4
5
6
7
8
public async Task<UserDto> GetUserAsync(Guid userId, CancellationToken cancellationToken)
{
using var request = new HttpRequestMessage(HttpMethod.Get, $"Users/{userId}");
var response = await _client.SendAsync(request, cancellationToken);

var content = await response.Content.ReadAsStringAsync(cancellationToken);
return JsonSerializer.Deserialize<UserDto>(content);
}

The return type UserDto is a simple data transfer object.

1
2
3
4
5
6
public class UserDto
{
public Guid Id { get; set; }

public string Name { get; set; }
}

If the service returns 404 with no content this will throw JsonException as content will be null. We can fix this with the following steps:

  1. Add a virtual method to UserDto called Found() and return true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UserDto
{
public Guid Id { get; set; }

public string Name { get; set; }

/// <summary>
/// Confirms if the User exists and was returned from the User Service.
/// </summary>
/// <returns></returns>
public virtual bool Found()
{
return true;
}
}
  1. Create the UserNotFound object that inherits from UserDto and overrides the Found method which can then return false
1
2
3
4
5
6
7
8
public class UserNotFound : UserDto
{
///<inheritdoc/>
public override bool Found()
{
return false;
}
}
  1. In the GetUserAsync method inside UserServiceClient check the status code. Here we assume 404 (not found) but you could check for any other code like 500 and respond with either UserServiceException or another object like UserError
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public async Task<UserDto> GetUserAsync(Guid userId, CancellationToken cancellationToken)
{
using var request = new HttpRequestMessage(HttpMethod.Get, $"Users/{userId}");
var response = await _client.SendAsync(request, cancellationToken);

if (!response.IsSuccessStatusCode)
{
// additionally we would add logging here
return new UserNotFound();
}

var content = await response.Content.ReadAsStringAsync(cancellationToken);
return JsonSerializer.Deserialize<UserDto>(content);
}
  1. In the consumer we would then check the response
1
2
3
4
5
6
7
8
var user = await _userServiceClient.GetUserAsync(userId, cancellationToken)

if (!user.Found())
{
// not found logic, probably return here
}

// assume found, normal logic here

This code now has some defensive checks using the Special Case Pattern.

References