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);
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 theGetBySpecrepository method. The initial idea was to help users to avoid doing mistakes and passing wrong specifications toGetBySpec. 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:
GetBySpecmethods.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 givenSpecification<T, TResultspecification withISingleResultSpecification, the usage can no longer be inferred correctly and the wrong method is called.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
Tinstead ofTResulteven though they have appliedSelectand working withSpecification<T, TResult.FirstOrDefaultAsync, not usingSingleOrDefaultAsync. So, it provides a false sense of security.int id, and an additional oneIEnumerable<int> ids. Now, they must mark it withISingleResultSpecification, which is confusing.Proposal:
Perhaps we should admit our mistake and do the following:
GetBySpecmethod. This will not be a breaking change, existing code will still work. All calls withSpecification<T, TResultwill be inferred correctly now again. The methods will have the following signatureISingleResultSpecification,ISingleResultSpecification<T>andISingleResultSpecification<T, TResult>. Users can utilize them as they wish.SingleOrDefaultAsyncEF method, and that would be a more correct approach for these cases.For convenience, they can also create the following base specifications too