Что такое .NET StringComparer эквивалентно SQL Latin1_General_CI_AS

Я реализую слой кэширования между моей базой данных и кодом С#. Идея состоит в том, чтобы кэшировать результаты определенных запросов БД на основе параметров запроса. В базе данных используется сортировка по умолчанию - либо SQL_Latin1_General_CP1_CI_AS, либо Latin1_General_CI_AS, которые, как мне кажется, основаны на некотором кратком поиске, эквивалентны для равенства, просто разные для сортировки.

Мне нужен .NET StringComparer, который может дать мне такое же поведение, по крайней мере, для тестирования равенства и генерации hashcode, поскольку используется сортировка базы данных. Цель состоит в том, чтобы иметь возможность использовать StringComparer в словаре .NET в коде С#, чтобы определить, находится ли конкретный строковый ключ в кеше или нет.

Действительно упрощенный пример:

var comparer = StringComparer.??? // What goes here?

private static Dictionary<string, MyObject> cache =
    new Dictionary<string, MyObject>(comparer);

public static MyObject GetObject(string key) {
    if (cache.ContainsKey(key)) {
        return cache[key].Clone();
    } else {
        // invoke SQL "select * from mytable where mykey = @mykey"
        // with parameter @mykey set to key
        MyObject result = // object constructed from the sql result
        cache[key] = result;
        return result.Clone();
    }
}
public static void SaveObject(string key, MyObject obj) {
    // invoke SQL "update mytable set ... where mykey = @mykey" etc
    cache[key] = obj.Clone();
}

Причина, по которой важно, чтобы StringComparer соответствовала настройке базы данных, заключается в том, что и ложные срабатывания, и ложные негативы будут иметь плохие последствия для кода.

Если StringComparer говорит, что два ключа A и B равны, когда база данных считает, что они различны, тогда в базе данных могут быть две строки с этими двумя ключами, но кеш будет препятствовать тому, чтобы второй возвращался, если его спросили для A и B подряд - потому что get для B неправильно попадет в кеш и вернет объект, который был получен для A.

Проблема более тонкая, если StringComparer говорит, что A и B различаются, когда база данных считает, что они равны, но не менее проблематичны. Вызов GetObject для обоих ключей будет прекрасным и возвращать объекты, соответствующие одной и той же строке базы данных. Но тогда вызов SaveObject с ключом A оставил бы кеш неправильным; все равно будет запись кэша для ключа B, в которой есть старые данные. Последующий GetObject (B) предоставит устаревшую информацию.

Итак, чтобы мой код работал правильно, мне нужно, чтобы StringComparer соответствовал поведению базы данных для тестирования равенства и генерации хэш-кода. До сих пор мой googling дал много информации о том, что SQL-сопоставления и сравнения .NET не совсем эквивалентны, но нет подробностей о том, каковы различия, ограничены ли они только различиями в сортировке или можно ли найти StringComparer, который эквивалентен специфической сортировке SQL, если универсальное решение не требуется.

(Боковое примечание - слой кэширования является общим назначением, поэтому я не могу сделать конкретные предположения о том, что характер ключа и что такое сопоставление будет уместно. Все таблицы в моей базе данных имеют одинаковый набор настроек по умолчанию. необходимо сопоставить сортировку, поскольку она существует)

Ответ 1

Взгляните на класс CollationInfo. Он расположен в сборке под названием Microsoft.SqlServer.Management.SqlParser.dll, хотя я не совсем уверен, где это получить. Существует статический список Collations (имена) и статический метод GetCollationInfo (по имени).

Каждый CollationInfo имеет Comparer. Это не совсем то же самое, что и StringComparer, но имеет аналогичную функциональность.

EDIT: Microsoft.SqlServer.Management.SqlParser.dll является частью пакета общих объектов управления (SMO). Эта функция может быть загружена для SQL Server 2008 R2 здесь:

http://www.microsoft.com/download/en/details.aspx?id=16978#SMO

EDIT: CollationInfo имеет свойство с именем EqualityComparer, которое является IEqualityComparer<string>.

Ответ 2

Недавно я столкнулся с той же проблемой: мне нужен IEqualityComparer<string>, который ведет себя в стиле SQL. Я пробовал CollationInfo и его EqualityComparer. Если ваша БД всегда _AS (чувствительна к акценту), тогда ваше решение будет работать, но если вы измените настройку AI или WI или что бы то ни было "нечувствительным", иначе хеширование сломается.
Зачем? Если вы декомпилируете Microsoft.SqlServer.Management.SqlParser.dll и загляните внутрь, вы обнаружите, что CollationInfo внутренне использует CultureAwareComparer.GetHashCode (внутренний класс mscorlib.dll), и, наконец, он делает следующее:

public override int GetHashCode(string obj)
{
  if (obj == null)
    throw new ArgumentNullException("obj");
  CompareOptions options = CompareOptions.None;
  if (this._ignoreCase)
    options |= CompareOptions.IgnoreCase;
  return this._compareInfo.GetHashCodeOfString(obj, options);
}

Как вы можете видеть, он может создавать один и тот же хэш-код для "aa" и "AA", но не для "äå" и "aa" (что то же самое, если вы игнорируете диакритические знаки (AI) в большинстве культур, поэтому они должны иметь один и тот же хэш-код). Я не знаю, почему .NET API ограничен этим, но вы должны понять, откуда может возникнуть проблема. Чтобы получить тот же хэш-код для строк с диакритикой, вы можете сделать следующее: создать реализацию IEqualityComparer<T>, внедряя GetHashCode, который будет называть соответствующий CompareInfo object GetHashCodeOfString через отражение, потому что этот метод является внутренним и не может использоваться напрямую. Но вызов его непосредственно с правильным CompareOptions приведет к желаемому результату: См. Этот пример:

    static void Main(string[] args)
    {
        const string outputPath = "output.txt";
        const string latin1GeneralCiAiKsWs = "Latin1_General_100_CI_AI_KS_WS";
        using (FileStream fileStream = File.Open(outputPath, FileMode.Create, FileAccess.Write))
        {
            using (var streamWriter = new StreamWriter(fileStream, Encoding.UTF8))
            {
                string[] strings = { "aa", "AA", "äå", "ÄÅ" };
                CompareInfo compareInfo = CultureInfo.GetCultureInfo(1033).CompareInfo;
                MethodInfo GetHashCodeOfString = compareInfo.GetType()
                    .GetMethod("GetHashCodeOfString",
                    BindingFlags.Instance | BindingFlags.NonPublic,
                    null,
                    new[] { typeof(string), typeof(CompareOptions), typeof(bool), typeof(long) },
                    null);

                Func<string, int> correctHackGetHashCode = s => (int)GetHashCodeOfString.Invoke(compareInfo,
                    new object[] { s, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, false, 0L });

                Func<string, int> incorrectCollationInfoGetHashCode =
                    s => CollationInfo.GetCollationInfo(latin1GeneralCiAiKsWs).EqualityComparer.GetHashCode(s);

                PrintHashCodes(latin1GeneralCiAiKsWs, incorrectCollationInfoGetHashCode, streamWriter, strings);
                PrintHashCodes("----", correctHackGetHashCode, streamWriter, strings);
            }
        }
        Process.Start(outputPath);
    }
    private static void PrintHashCodes(string collation, Func<string, int> getHashCode, TextWriter writer, params string[] strings)
    {
        writer.WriteLine(Environment.NewLine + "Used collation: {0}", collation + Environment.NewLine);
        foreach (string s in strings)
        {
            WriteStringHashcode(writer, s, getHashCode(s));
        }
    }

Вывод:

Used collation: Latin1_General_100_CI_AI_KS_WS
aa, hashcode: 2053722942
AA, hashcode: 2053722942
äå, hashcode: -266555795
ÄÅ, hashcode: -266555795

Used collation: ----
aa, hashcode: 2053722942
AA, hashcode: 2053722942
äå, hashcode: 2053722942
ÄÅ, hashcode: 2053722942

Я знаю, что это выглядит как взломать, но после проверки декомпилированного .NET-кода я не уверен, есть ли какой-либо другой вариант в случае необходимости общей функции. Поэтому будьте уверены, что вы не попадете в ловушку, используя этот не совсем правильный API.
UPDATE:
Я также создал сущность с потенциальной реализацией "SQL-подобного компаратора" с помощью CollationInfo. Также должно быть уделено достаточное внимание где искать "струнные ловушки" в вашей базе кода, поэтому, если сравнение строк, hashcode, равенство должно быть измененный на "подобный SQL-сопоставление", эти места на 100% будут разбиты, поэтому вам нужно будет выяснить и проверить все места, которые могут быть повреждены.
ОБНОВЛЕНИЕ # 2:
Существует лучший и более чистый способ сделать GetHashCode() обработкой CompareOptions. Существует класс SortKey, который корректно работает с CompareOptions, и его можно найти с помощью

CompareInfo.GetSortKey(yourString, yourCompareOptions).GetHashCode()

Вот ссылка на исходный код и реализацию .NET.

Ответ 4

Следующее намного проще:

System.Globalization.CultureInfo.GetCultureInfo(1033)
              .CompareInfo.GetStringComparer(CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth)

Это происходит по адресу https://docs.microsoft.com/en-us/dotnet/api/system.globalization.globalizationextensions?view=netframework-4.8

Он правильно вычисляет хеш-код с учетом указанных параметров. Вам все равно придется урезать конечные пробелы вручную, так как они отбрасываются ANSI sql, но не в .net

Вот обертка, которая урезает пробелы.

using System.Collections.Generic;
using System.Globalization;

namespace Wish.Core
{
    public class SqlStringComparer : IEqualityComparer<string>
    {
        public static IEqualityComparer<string> Instance { get; }

        private static IEqualityComparer<string> _internalComparer =
            CultureInfo.GetCultureInfo(1033)
                       .CompareInfo
                       .GetStringComparer(CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth);



        private SqlStringComparer()
        {
        }

        public bool Equals(string x, string y)
        {
            //ANSI sql doesn't consider trailing spaces but .Net does
            return _internalComparer.Equals(x?.TrimEnd(), y?.TrimEnd());
        }

        public int GetHashCode(string obj)
        {
            return _internalComparer.GetHashCode(obj?.TrimEnd());
        }

        static SqlStringComparer()
        {
            Instance = new SqlStringComparer();
        }
    }
}