The problem in focus here is one of semantical naming, not of technical implementation.
First of all, because I get the feeling that some commenters may have made a wrong inference, GetOrCreate
is slightly misrepresentative name for an algorithm that should be called GetIdOrCreateAndThenGetId
which is obviously not a great name, though pedantically more correct.
But the source of your problem is that you're trying to attribute both labels. It's a query (Get
) and a command (Create
).
This is why I'm calling it a semantical argument. If I rename your method CreateIfNotExists
, and we simply understand that a create method returns the generated ID of the entity it just created, then we have exactly the same implementation and none of the labeling issues.
You don't have to rename it to CreateIfNotExists
, but it proves the point that this algorithm at its heart is a command.
Secondly, never forget the purpose of CQRS. The goal is to make it possible to separate your write and read stores so they can be scaled independently. This scaling means that you run into eventual consistency issues, which means that the read store tends to not be instantly synced with the write store whenever a change is made, there is some time inbetween the two.
The "Query" in CQRS inherently refers to a query that is executed on the read store. It doesn't include any query that would be executed on the write store. And while it's not the prime focus of CQRS, there will generally remain to be reasons to query the write store, and your scenario is one of them.
While the Get
of GetOrCreate
is technically a query because it fetches data, that doesn't automatically mean that it should query the read store. In this specific case, the purpose of the fetching of information is to direct a write operation (i.e. choosing whether to create or not), and it should therefore query the write store to ensure that it has the most recent information.
The reason for querying the write store is because of the eventual consistency. This makes it possible that the read store is out of date with the write store, which means that the read store might not know of a freshly created entity yet.
A query on the read store would wrongly tell you that no such item exists, so you create a new one, and now you have two freshly created entities. A query on the write store, however, would have access to all the latest information, and thus would be more correct in its assessment that a particular entity does or doesn't exist.
Thirdly:
When your developer follows that Command, won’t they be surprised to find out that it rarely registers a user?
Whoever called the endpoint had one goal in mind: the requested resource should exist after the endpoint finishes its task.
This expectation is upheld at all times. Either it already did exist, or you ensure its creation so that from now it does exist.
Whether the resource existed before the endpoint was called is irrelevant. Clearly, the requester didn't know it either, or they wouldn't have bothered to call the endpoint if they already knew the resource existed - there'd be no point in retrieving an ID for a resources you already know.
You're conflating two very different things: the service you provide, and how you perform the internal actions. GetOrCreate
describes the latter, not the former.
The requester only cares about the service you provide, not how you do it. They don't know or care how you decide to go about honoring their request, they just care that their request was honored.
Fourthly, I can't judge your architecture for you, but I do want to point out the difference between CQRS and the mediator pattern. These two are often conflated, because CQRS almost inherently makes itself a perfect fit for using a mediator to execute your queries and commands.
But that doesn't mean that any use of the mediator pattern must also adhere to CQRS. It's perfectly fine to not separate your read and write stores, while still maintaining the use of a mediator.
Hint: this is why Mediatr calls its handled actions "requests" and doesn't bother trying to label anything as a query or a command.