79586340

Date: 2025-04-22 11:36:27
Score: 0.5
Natty:
Report link

I've created a helper class IdTempTable based on the solution proposed by @takrl .
The additional issue I was facing was that our Dapper code resides in a separate layer, so I couldn't use several execute statements.

Usage:

        public IList<int> LoadAnimalTypeIdsFromAnimalIds(IList<int> animalIds)
        {
            var idTempTable = new IdTempTable<int>(animalIds);

            string query = string.Concat(idTempTable.Create,
                    idTempTable.Insert,
                    @"SELECT a.animalID        
                    FROM dbo.animalTypes [at]",
                    idTempTable.JoinOn, @"at.animalId
                    INNER JOIN animals [a] on a.animalTypeId = at.animalTypeId
                    INNER JOIN edibleAnimals e on e.animalID = a.animalID");

            using (var db = new SqlConnection(this.connectionString))
            {
                return db.Query<int>(query).ToList();
            }
        }

IdTempTable.cs:

    /// <summary>
    /// Helper class to filter a SQL query on a set of ID's,
    /// using a temporary table instead of a WHERE clause.
    /// </summary>
    internal class IdTempTable<T> 
            where T: struct
    {
        // The limit SQL allows for the number of values in an INSERT statement.
        private readonly int _chunkSize = 1000;

        // Unique name for this instance, for thread safety.
        private string _tableName;

        /// <summary>
        /// Helper class to filter a SQL query on a set of ID's,
        /// using a temporary table instead of a WHERE clause.
        /// </summary>
        /// <param name="ids">
        /// All elements in the collection must be of an integer number type.
        /// </param>
        internal IdTempTable(IEnumerable<T> ids)
        {
            Validate(ids);
            var distinctIds = ids.Distinct();

            Initialize(distinctIds);
        }

        /// <summary>
        /// The SQL statement to create the temp table.
        /// </summary>
        internal string Create { get; private set; }

        /// <summary>
        /// The SQL statement to fill the temp table.
        /// </summary>
        internal string Insert { get; private set; }

        /// <summary>
        /// The SQL clause to join the temp table with the main table.
        /// Complete the clause by adding the foreign key from the main table.
        /// </summary>
        internal string JoinOn => $" INNER JOIN {_tableName} ON {_tableName}.Id = ";

        private void Initialize(IEnumerable<T> ids)
        {
            _tableName = BuildName();
            Create = BuildCreateStatement(ids);
            Insert = BuildInsertStatement(ids);
        }

        private string BuildName()
        {
            var guid = Guid.NewGuid();

            return "#ids_" + guid.ToString("N");
        }

        private string BuildCreateStatement(IEnumerable<T> ids)
        {
            string dataType = GetDataType(ids);
            
            return $"CREATE TABLE {_tableName} (Id {dataType} NOT NULL PRIMARY KEY); ";
        }

        private string BuildInsertStatement(IEnumerable<T> ids)
        {
            var statement = new StringBuilder();

            while (ids.Any())
            {
                string group = string.Join(") ,(", ids.Take(_chunkSize));
                statement.Append($"INSERT INTO {_tableName} VALUES ({group}); ");

                ids = ids.Skip(_chunkSize);
            }

            return statement.ToString();
        }

        private string GetDataType(IEnumerable<T> ids)
        {
            string type = !ids.Any() || ids.First() is long || ids.First() is ulong 
                    ? "BIGINT" 
                    : "INT";
            
            return type;
        }

        private void Validate(IEnumerable<T> ids)
        {
            if (ids == null)
            {
                throw new ArgumentNullException(nameof(ids));
            }

            if (!ids.Any())
            {
                return;
            }

            if (ids.Any(id => !IsInteger(id)))
            {
                throw new ArgumentException("One or more values in the collection are not an integer");
            }
        }

        private bool IsInteger(T value)
        {
            return value is sbyte ||
                    value is byte ||
                    value is short ||
                    value is ushort ||
                    value is int ||
                    value is uint ||
                    value is long ||
                    value is ulong;
        }
    }

Some advantages I can see:

Reasons:
  • Long answer (-1):
  • Has code block (-0.5):
  • User mentioned (1): @takrl
  • Low reputation (1):
Posted by: Roderik Cocquyt