private string GetUniqueStringForModel<T>(
string originalString,
IQueryable<T> databaseQueryable,
Expression<Func<T, string>> stringFieldFunction
) where T : class {
var lowerBound = originalString.ToLowerInvariant();
var upperBound = originalString.ToUpperInvariant() + "z";
var candidates = new HashSet<string>(
databaseQueryable
.Select(stringFieldFunction)
.Where(field => field >= lowerBound && field < upperBound)
.ToSet()
);
return Enumerable
.Range(0, int.MaxValue)
.Select(i => originalString + (i == 0 ? "" : $"_{i}"))
.First(candidate => !candidates.Contains(candidate));
}
There are a few things going on here.
First, since you are using Entity Framework and received the error you did, I believe that your IEnumerable
is actually an IQueryable
. If you want to add predicates to the query that are performed on the database server, you need to maintain the IQueryable
as that data type and not prematurely force it to become an IEnumerable
. This is of crucial importance to understand and I encourage you to go study the difference between these and what effects it has. The exact same code is compiled completely differently when cast to an IQueryable instead of an IEnumerable—differently in a way necessary for being able to project your C# code to SQL for doing the majority of the work in the database.
Second, your Func
can work, but it must contain only methods and operators that work with Entity Framework and must be an Expression<Func<>>
. If you want to use a method that is not supported, that can only be done by, after whatever IQueryables have filtered it so far, materializing the result set in memory using .ToList()
and then iterate over it locally (using a regular Func
).
My goal in the above code was to fetch the set of values that could possibly match the originalString
. Notice that by assigning the values to variables, this leaves the queryable's Where
clause susceptible to being read by EF, because it will be compiled as an Expression
instead of to byte code. (That is why you need IQueryable
instead of IEnumerable
). The trick I used to select the appropriate range may not be a good one (I haven’t touched C# for 16 months). Study the operators and your data and pick a better one. Find the greatest value that can be added (not "z") and add that.
Third, the very limited set of results is being converted to a Set
so that we can get O(1) lookup time. I promise you that performing a ToList()
on an IQueryable
(masked as an IEnumerable
) on each new number suffix attempt is a superbly wonderful way to destroy performance. Please don't do that. Your code as written will read every single row from the underlying IQueryable
on every iteration of the while loop—hopefully there are some conditions on it and this isn’t the entire table!
Fourth, you have to pay attention to the string comparison in the database and for the HashSet
. Do you want case sensitivity or case insensitivity? Is there another kind of collation that might be needed to perform foreign language support? You need to provide the HashSet
a StringComparer
that will perform properly.
Fifth, why even provide a stringFieldFunction
when the caller can just do .Select(stringFieldFunction)
itself? This doesn't seem useful to me. Don't add unnecessary parameters. Doing it in the caller will avoid having to muck about with Func
vs. Expression<Func>
.
StringComparison.InvariantCultureIgnoreCase
comparison really bothers EF. Can't you handle this on your DB column? A case insensitive collationSQL_Latin1_General_CP1_CI_AS
should work (assuming that this column is always compared this way). \$\endgroup\$enumerableObjects
appear to be a hiddenIQueryable
I believe it should be possible to delegate more work to the sql server. \$\endgroup\$