1

Given a query input object used to provide filters for querying a database (CQRS) I'm never 100% sure how best to construct that object using IEnumerable<>, e.g., say we have a database that contains vehicles and I write a query that takes a GetVehiclesQueryInput that has some enumerable of vehicle ids as its input such that the query just returns those vehicles with matching id's (if the enumerable type is null, all vehicles are returned), e.g., in its simplest form we could have

public class GetVehiclesQueryInput {

  public GetVehiclesQueryInput(IEnumerable<int> vehicleIds = null) {
    VehicleIds = vehicleIds;
  }

  IEnumerable<int> VehicleIds { get; }

}

However, given this is a query it makes sense to me that the query is a 'snapshot' taken at the point in time the query is created, but given the IEnumerable injected there is the potential that the injected IEnumerable could be reference to a derived implementation (e.g. List) and coupled with the possibility of deferred execution may no longer be a 'snaphost' at the point in time the query is created. As such is it considered better practice to do something like:

public class GetVehiclesQueryInput {

  public GetVehiclesQueryInput(IEnumerable<int> vehicleIds = null) {
    VehicleIds = vehicleIds is null ? null : new ReadOnlyCollection<int>(vehicleIds .ToList());
  }

  IReadOnlyCollection<int> VehicleIds { get; }

}

or

public class GetVehiclesQueryInput {

  public GetVehiclesQueryInput(IEnumerable<int> vehicleIds = null) {
    VehicleIds = vehicleIds is null ? null : vehicleIds.ToImmutableList());
  }

  IImmutableList<int> VehicleIds { get; }

}
James B
  • 8,975
  • 13
  • 45
  • 83

1 Answers1

0

I am not sure to understand how the mutability of the query object relates to a snapshot at a given point of time. Once you create a query object you use that to retrieve the data, and what may change is the data returned for the same input list depending on several parameters, such as deferred execution, as you mentioned, but also database isolation level, if there are 1 (read-write) or 2 databases (read and write) in your CQRS architecture, etc. If you need to query a different set of plates, it seems logical to create a new query object.

As far as the mutability of the 2 options you present:

1. new ReadOnlyCollection<int>(vehicleIds.ToList())
2. vehicleIds.ToImmutableList()

It seems to me that they are equivalent. In neither of them you can modify the underlying collection = the collection passed as a parameter (IEnumerable<int> vehicleIds). I would use the second option because it makes the intent more clear. Note that ImmutableList does have and Add() method, but it returns a copy of the list with added values, leaving the original intact.

What would not work is if you had done this instead:

public GetVehiclesQueryInput(IList<int> vehicleIds = null) {
    VehicleIds = vehicleIds is null 
        ? null
        : new ReadOnlyCollection<int>(vehicleIds);
}

IReadOnlyCollection<int> VehicleIds { get; }

This allows the caller to modify the underlying collection (the IList<int> vehicleIds passed as parameter instead of IEnumerable<int>). Now this list is used directly without being copied (no ToList()) and the VehicleIds property may return different values if the list is modified after the query object is created.

References

ImmutableList vs. ReadOnlyCollection
Why use ImmutableList over ReadOnlyCollection?

ImmutableList.Add(T)
https://learn.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutablelist-1.add?view=net-7.0

evilmandarine
  • 4,241
  • 4
  • 17
  • 40