Skip to content

ISingleResultSpecification usage issue. #242

@fiseni

Description

@fiseni

Let me start by giving a brief overview of ISingleResultSpecification. This interface does not affect and is not utilized by the specification infrastructure at all. This is just a marker interface used to constrain the specification usage in the repositories, more specifically the GetBySpec repository method. The initial idea was to help users to avoid doing mistakes and passing wrong specifications to GetBySpec. At first glance, it seemed a great idea, but it turned out to be really flawed. Unfortunately, it did more harm than good. Let me summarize the issues:

Issues:

  • This is the signature of GetBySpec methods.
Task<T?> GetBySpecAsync<Spec>(Spec specification, CancellationToken cancellationToken = default) where Spec : ISingleResultSpecification, ISpecification<T>;
Task<TResult?> GetBySpecAsync<TResult>(ISpecification<T, TResult> specification, CancellationToken cancellationToken = default);

As we can see, we have no constraint for the GetBySpecAsync<TResult> method. We can't apply the same constraint since we have a method generic parameter too. That said, whenever users mark a given Specification<T, TResult specification with ISingleResultSpecification, the usage can no longer be inferred correctly and the wrong method is called.

public class CustomerNameSpec : Specification<Customer, string>, ISingleResultSpecification
{
    public CustomerNameSpec(int id)
    {
        Query.Select(x => x.Name)
             .Where(x => x.Id == id);
    }
}

var spec = new CustomerNameSpec(1);

// This call will pick the wrong method GetBySpecAsync<Spec>, instead of GetBySpecAsync<TResult>
var result = await customerRepository.GetBySpecAsync(spec)

// To mitigate this, users have to explicitly define the `TResult` all the time, and that's tedious.
var result = await customerRepository.GetBySpecAsync<string>(spec)

We added this interface to help minimize the bugs, but actually, we introduced way more potential bugs on another front. The users get confused why they're getting T instead of TResult even though they have applied Select and working with Specification<T, TResult.

  • It gives a wrong impression that applying this interface somehow affects how the results are queried. The method still uses FirstOrDefaultAsync, not using SingleOrDefaultAsync. So, it provides a false sense of security.
  • We restricted how users design their specifications. It's not uncommon to have specifications with several constructors. For instance, one accepting int id, and an additional one IEnumerable<int> ids. Now, they must mark it with ISingleResultSpecification, which is confusing.

Proposal:

Perhaps we should admit our mistake and do the following:

  • Remove the constraint for the GetBySpec method. This will not be a breaking change, existing code will still work. All calls with Specification<T, TResult will be inferred correctly now again. The methods will have the following signature
Task<T?> GetBySpecAsync(ISpecification<T> specification, CancellationToken cancellationToken = default);
Task<TResult?> GetBySpecAsync<TResult>(ISpecification<T, TResult> specification, CancellationToken cancellationToken = default);
  • We won't delete the interface. We'll have ISingleResultSpecification, ISingleResultSpecification<T> and ISingleResultSpecification<T, TResult>. Users can utilize them as they wish.
  • If users want to add constrained repository methods, they can be explicit and define the following methods. These methods perhaps can use SingleOrDefaultAsync EF method, and that would be a more correct approach for these cases.
Task<T?> SingleOrDefaultAsync(ISingleResultSpecification<T> specification, CancellationToken cancellationToken = default);
Task<TResult?> SingleOrDefaultAsync<TResult>(ISingleResultSpecification<T, TResult> specification, CancellationToken cancellationToken = default);

For convenience, they can also create the following base specifications too

public class SingleResultSpecification<T> : Specification<T>, ISingleResultSpecification<T>
{
}
public class SingleResultSpecification<T, TResult> : Specification<T, TResult>, ISingleResultSpecification<T, TResult>
{
}

// Then use these base specs as following
public class CustomerNameSpec : SingleResultSpecification<Customer, string>
{
    public CustomerNameSpec(int id)
    {
        Query.Select(x => x.Name)
             .Where(x => x.Id == id);
    }
}

var spec = new CustomerNameSpec(1);
var result = await customerRepository.SingleOrDefaultAsync(spec);

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions