Adrian Thompson Phillips
Adrian Thompson Phillips

Reputation: 7141

Value Object as a Service Call Argument

In the book Implementing DDD, there's mention of creating a TenantId value object. This makes sense to me, as a GUID could be empty which isn't a valid TenantId, so by making a TenantId value object I can protect against this (I also have other value objects like Name, PhoneNumber, EmailAddress, etc):

public class TenantId
{
    public TenantId(Guid id)
    {
        this.SetId(id);
    }

    public Guid Id { get; private set; }

    private void SetId(Guid id)
    {
        if (id == Guid.Empty)
        {
            throw new InvalidOperationException("Id must not be an empty GUID.");
        }

        this.Id = id;
    }
}

What I'm interested in, is should I, or should I not, use this TenantId on a service method, something like:

TenantId tenantId = new TenantId(model.TenantId); // model.TenantId being a GUID.

this.tenantService.GetTenant(tenantId);

Or should I use the more raw form in the service method arguments:

this.tenantService.GetTenant(model.TenantId); // again model.TenantId is a GUID.

The book seems to sometimes do it one way and sometime another. What thoughts do people have on the pros and cons of these approaches?

Upvotes: 1

Views: 790

Answers (2)

Yugang Zhou
Yugang Zhou

Reputation: 7283

I prefer using Custom Value Object as Aggregate's identity. I think it gives more clue when other developers want to invoke the api. Consider this:

Tenant findBy(String identity); //others may pass an String instance but not id and the compliler can't check it out for you

And you can't add this below method:

Tenant findBy(String name);//assuming name unique but it failed for compiling

Why use the raw type if you have already had a specific type?

Edit

I usually use custom type for repository and application service, for domain service, please refer to @Giacomo Tesio‘ answer. I think the "use the entity directly" makes more sense.

Upvotes: 1

Giacomo Tesio
Giacomo Tesio

Reputation: 7210

If the service doesn't require restricted access to the entity, pass the value object since it is a shared identifier, but I would code it something like this:

public class TenantId : IEquatable<TentantId>
{
    public TenantId(Guid id)
    {
        if (id == Guid.Empty)
        {
            throw new InvalidOperationException("TenantId must not be an empty GUID.");
        }
        _id = id;
    }

    private readonly Guid _id;

    public Equals(TentantId other)
    {
        if (null == other) return false;
        return _id.Equals(other._id);
    }

    public GetHashcode()
    {
        return _id.GetHashcode();
    }

    public override Equals(object other)
    {
        return Equals(other as TenantId);
    }

    public ToString()
    {
        return _id.ToString();
    }
}

However in some cases, a domain service should be invoked only with entities that the user can access in the repository: in those cases, the public interface of the domain service require the entity (even if the implementation just use the identifier).

Upvotes: 3

Related Questions