简体   繁体   English

如何设计 web API 以从域 Z20F35E630DAF44DBFA4CZC3F68F539D 中删除值 object

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

I have a Person model like below:我有一个Person model 如下所示:

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:一个人有一个地址列表,地址是一个值 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:这是我的 API 删除一个人的地址:

[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.我必须发送整个地址数据,因为它没有标识符但通常删除 API 不接收来自请求正文的数据,并且HttpClient没有接受正文的Delete方法。 I have to call delete API with HttpClient in this way:我必须以这种方式使用HttpClient调用 delete API:

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?在 DDD 方法中,设计用于删除值 object 的 API 的常规方法是什么?
  • Do I need to add an identifier to the value object?我是否需要为值 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 DELETE 请求消息中的有效负载没有定义的语义——RFC 7231

Don't confuse HTTP Methods, which belong to the transfer of documents over a network domain, with the commands in your domain.不要将 HTTP 属于通过网络域传输文档的方法与您域中的命令混淆。


It is okay to use POST. 可以使用 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;目标 URI 可以是任何你想要的; 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 .最好的设计利用HTTP 缓存语义,这样当成功删除地址时,先前使用相同目标 uri 缓存的响应将失效

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.如果您想要一个与远程创作非常一致的资源 model,您可以使用 PUT,而不是 POST。 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.在这种设计中,您可以通过提供 /.../addresses 资源的新表示来请求删除地址,并删除所需的地址。

(Similarly, you could use PATCH instead of PUT). (同样,您可以使用 PATCH 代替 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 POST /{identifier}/addresses 或 PUT /{identifier}/addresses 的问题是 URL 与 create 和 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.但是这个前提并不植根于 web 标准(RFC 7230 等),并且符合 HTTP 的实现并不关心您是否遵守这些常规约束。

It's perfectly normal to have POST /foo introduce different effects in your domain model depending on the information encoded into the payload.根据编码到有效负载中的信息,让POST /foo在您的域 model 中引入不同的效果是完全正常的。 We can have many different HTML forms that share the same URL action.我们可以有许多不同的 HTML forms 共享相同的 URL 动作。

And the same is, of course, true for PUT.当然,PUT 也是如此。 As far as general purpose web components are concerned, the body is just a new representation of the target resource;就通用 web 组件而言,主体只是目标资源的新表示; what your server does to translate that into information your domain model understands is an implementation detail, deliberately hidden behind a web facade.您的服务器将其转换为您的域 model 理解的信息是一个实现细节,故意隐藏在 web 门面后面。


unless verb added to URL PUT /{identifier}/addresses/delete which is not correct too除非动词添加到 URL PUT /{identifier}/addresses/delete 这也不正确

There's nothing wrong with verbs in the URL URL 中的动词没有任何问题

Notice that this URL works exactly the way you would suspect, even though the word "delete" is present;请注意,即使存在“删除”一词,这个 URL 的工作方式与您所怀疑的完全一样; 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.当您单击链接时,浏览器使用GET从服务器获取 Merriam-Webster 对英文单词 delete 的定义的表示——就像 web 上的所有其他链接一样。

See also Tilkov 2014 .另见蒂尔科夫 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.您的域告诉您,您应该做什么以及如何更好地 model。

See this example.请参阅此示例。 Suppose you want to model a sedan car that has 4 wheels.假设您想要 model 一辆有 4 个轮子的轿车。 You can model car wheels as a Collection<wheel> and seems you doing right.您可以将 model 车轮作为Collection<wheel>并且似乎您做得对。

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.但是几天后你意识到你应该model轮毂扫过,车主想用一个新的轮子更换前左轮,所以虽然在第一个model。 wheels don't have any identification, we already decide to add wheel identifier to our model.车轮没有任何标识,我们已经决定将车轮标识符添加到我们的模型中。

After refactoring the model we have:重构 model 后,我们有:

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.如您所见,我们添加WheelPosition枚举,它的角色是一种本地 ID。 with this local-Id (type of that have not meant outside of aggregate) we have a richer domain model.有了这个 local-Id(那种类型并不意味着聚合之外),我们就有了一个更丰富的域 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.在我看来,您应该考虑如何将本地 ID 添加到地址值 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, ...在某些域中,我们看到 Person 有家庭地址、办公地址、工作地址……

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.如果邮政编码是唯一的并且需要,我认为您的地址具有自然 ID,因此您可以使用它来删除地址,或者您可以找到诸如 RegionId 和 AddressType 之类的组合。 if you can't find natural ID I agree with @VoiceOfUnreason.如果您找不到自然 ID,我同意 @VoiceOfUnreason。 you can use post or put, and for resolve conflict maybe you can add /delete at the end of url.您可以使用 post 或 put,为了解决冲突,您可以在 url 末尾添加 /delete。 in another hand you can make some custom router that determine update or delete by command types or add some header to requests messages.另一方面,您可以制作一些自定义路由器,通过命令类型确定更新或删除,或者在请求消息中添加一些 header。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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