简体   繁体   中英

How to design a web API to remove value object from domain model

I have a Person model like below:

public class Person : AggregateRootBase<Guid>
{
    public Person(string firstName, string lastName) => (FirstName, LastName) = (firstName, lastName);

    public string FirstName{ get; private set; }

    public string LastName { get; private set; }

    public ICollection <Address> Addresses { get; private set; }

    public void AddAddress(Address address)
    {
         if(address == null)
             throw new ArgumentNullException(nameof(address));

         Addresses.Add(address);
    }
}

A person has a list of addresses and address is a value object:

public class Address : IValueObject<Address>
{
    public Address(string value, string postalCode, AddressType addressType, Guid regionId) 
        => (Value , PostalCode, AddressType, RegionId) = (value, postalCode, addressType, regionId);

    public string Value { get; private set; }

    public string PostalCode { get; private set; }

    public AddressType AddressType { get; private set; }

    public Guid RegionId { get; set; }

    public bool IsSameValueAs(Address other)
    {
        return new EqualsBuilder()
            .Append(Value, other.Value)
            .Append(PostalCode, other.PostalCode)
            .Append(AddressType, other.AddressType)
            .Append(RegionId, other.RegionId)
            .IsEquals();
    }

    public override int GetHashCode()
    {
        return new HashCodeBuilder()
            .Append(Value)
            .Append(PostalCode)
            .Append(AddressType)
            .Append(RegionId)
            .ToHashCode();
    }

    public override bool Equals(object obj)
    {
        return obj is Address other && IsSameValueAs(other);
    }
}

Here is my API to delete an address from a person:

[HttpDelete("{id:guid}/addresses")]
public async Task<IActionResult> DeleteAddress(Guid id, [FromBody] RemoveAddressCommand command)
{
    command.PersonageId = id;
    await _applicationService.RemoveAddressAsync(command);

    return Ok();
}

I have to send the whole address data because it doesn't have an identifier but normally delete API does not receive data from the request body and HttpClient doesn't have a Delete method that accepts the body. I have to call delete API with HttpClient in this way:

public async Task<ResultDto> RemoveElectronicAddressAsync(Address address)
{
    var request = new HttpRequestMessage
    {
        Method = HttpMethod.Delete,
        RequestUri = new Uri($"{ApiUrl}/{command.PersonageId}/addresses", UriKind.Relative),
        Content = new StringContent(JsonSerializer.Serialize(address), Encoding.UTF8, "application/json")
    };
    var result = await _httpClient.SendAsync(request);
    ...
}

This delete method sounds weird to me. My questions are

  • In DDD approach what's the routine way to design an API for deleting a value object?
  • Do I need to add an identifier to the value object?
  • Is my approach look fine?
public async Task<IActionResult> DeleteAddress(Guid id, [FromBody] RemoveAddressCommand command)

That looks wrong.

A payload within a DELETE request message has no defined semantics -- RFC 7231

Don't confuse HTTP Methods, which belong to the transfer of documents over a network domain, with the commands in your domain.


It is okay to use POST. That's the most natural fit

POST /d570df38-c09e-4558-978a-fe68778a734b/addresses
Content-Type: text/plain

Please remove 1060 West Addison from the address list for Elwood Blues.

The target URI can be anything you want; the best designs take advantage of HTTP cache semantics , so that when the address is removed successfully previously cached responses using the same target uri will be invalidated .

But it's not required that you do that - you can reasonably trade caching away if there is some other benefit you value more.


You could use PUT, rather than POST, if you want a resource model that aligns well with remote authoring. In this sort of design, you would request the removal of an address by providing a new representation of the /.../addresses resource with the desired address removed.

(Similarly, you could use PATCH instead of PUT).

Of course, in that case it would be up to your implementation to provide the translation from "an edit to an address list document" to "a command in your domain model".


The problem with POST /{identifier}/addresses or PUT /{identifier}/addresses is the URL conflict with create and update

You are starting from a faulty premise. That's not your fault - it's a common faulty premise, and there is a lot of supporting evidence describing this constraint as a "best practice".

But that premise isn't rooted in the web standards (RFC 7230, etc), and HTTP compliant implementations don't care whether you abide by these conventional constraints or not.

It's perfectly normal to have POST /foo introduce different effects in your domain model depending on the information encoded into the payload. We can have many different HTML forms that share the same URL action.

And the same is, of course, true for PUT. As far as general purpose web components are concerned, the body is just a new representation of the target resource; what your server does to translate that into information your domain model understands is an implementation detail, deliberately hidden behind a web facade.


unless verb added to URL PUT /{identifier}/addresses/delete which is not correct too

There's nothing wrong with verbs in the URL

Notice that this URL works exactly the way you would suspect, even though the word "delete" is present; when you click on the link, the browser uses GET to fetch from the server a representation of Merriam-Webster's definition of the English word delete -- just like every other link on the web.

See also Tilkov 2014 .

At first glance, you should analyze your problem domain more deeply. Your domain tells you, what you should do and how you can model better.

See this example. Suppose you want to model a sedan car that has 4 wheels. You can model car wheels as a Collection<wheel> and seems you doing right.

public class Car : Entity<Guid>
{
  public Guid Id { get; private set; }
  public string Color { private set; }
  public Collection<Wheel> Wheels { get; private set; }
  //details removed for brevity
}

public class Wheel : ValueObject
{
  public string Brand { get; private set; }
  public string InstallDate { get; private set; }
  public int Diameter {get; private set;}

}

Seems good. But a few days later you realize that you should model wheel sweeping too, The car owner wants to change the Front-Left wheel with a new wheel, So although in the first model. wheels don't have any identification, we already decide to add wheel identifier to our model.

After refactoring the model we have:

public class Wheel : ValueObject
{
  public string Brand { get; private set; }
  public string InstallDate { get; private set; }

  public int Diameter { get; private set; }

  public WheelPosition WeelPosition { get; set; }
}

public enum WheelPosition
{
  FrontLeft,
  FrontRight,
  BackLeft,
  BackRight
}

As you can see we add WheelPosition enum, that's role is kind of local-Id. with this local-Id (type of that have not meant outside of aggregate) we have a richer domain model.

let's back to your example. In my opinion, you should think about how you can add a local-Id to the Address value object. Your requirement to remove an address from address collection implies that all collection items are not same exactly.

In some domains, we see Person have Home-Address, Office-Address, Work-Address, ...

I think your Address have natural ID if postal code is unique and require so you can use it to remove address or maybe you can find some combination like RegionId and AddressType. if you can't find natural ID I agree with @VoiceOfUnreason. you can use post or put, and for resolve conflict maybe you can add /delete at the end of url. in another hand you can make some custom router that determine update or delete by command types or add some header to requests messages.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM