LINQ to Entities comparação com distinção entre maiúsculas e minúsculas

115

Esta não é uma comparação que diferencia maiúsculas de minúsculas no LINQ to Entities:

Thingies.First(t => t.Name == "ThingamaBob");

Como posso fazer uma comparação que diferencia maiúsculas de minúsculas com o LINQ to Entities?

Ronnie Overby
fonte
@Ronnie: você tem certeza disso? Você quer dizer comparação sem distinção entre maiúsculas e minúsculas ?
Michael Petrotta,
14
Certeza absoluta. Não, eu não quis dizer isso.
Ronnie Overby,
12
Não, no meu computador executando EF 4.0 com SQL Server 2008 R2, o acima não faz distinção entre maiúsculas e minúsculas. Eu sei que muitos lugares dizem que EF é sensível a maiúsculas e minúsculas, mas não foi isso que eu experimentei.
tster 01 de
3
Isso não vai depender do banco de dados subjacente?
codymanix
1
@codymanix: Essa é uma boa pergunta! Linq para EF traduz a expressão lambda para uma consulta de banco de dados? Não sei a resposta.
Tergiver

Respostas:

163

Isso porque você está usando LINQ To Entities, que, em última análise, converte suas expressões Lambda em instruções SQL. Isso significa que a diferenciação de maiúsculas e minúsculas está à mercê de seu SQL Server, que por padrão tem SQL_Latin1_General_CP1_CI_AS Collation e NÃO diferencia maiúsculas de minúsculas.

Usar ObjectQuery.ToTraceString para ver a consulta SQL gerada que foi realmente enviada ao SQL Server revela o mistério:

string sqlQuery = ((ObjectQuery)context.Thingies
        .Where(t => t.Name == "ThingamaBob")).ToTraceString();

Quando você cria uma consulta LINQ to Entities , LINQ to Entities aproveita o analisador LINQ para começar a processar a consulta e convertê-la em uma árvore de expressão LINQ. A árvore de expressão LINQ é então passada para a API de serviços de objeto , que converte a árvore de expressão em uma árvore de comando. Em seguida, ele é enviado ao provedor de armazenamento (por exemplo, SqlClient), que converte a árvore de comando no texto de comando do banco de dados nativo. A consulta é executada no armazenamento de dados e os resultados são materializados em objetos de entidade por serviços de objeto. Nenhuma lógica foi colocada no meio para levar em consideração a diferenciação de maiúsculas e minúsculas. Portanto, não importa que caso você coloque em seu predicado, ele sempre será tratado como o mesmo pelo seu SQL Server, a menos que você altere seus agrupamentos do SQL Server para essa coluna.

Solução do lado do servidor:

Portanto, a melhor solução seria alterar o agrupamento da coluna Nome na tabela Thingies para COLLATE Latin1_General_CS_AS, que diferencia maiúsculas de minúsculas executando-o em seu SQL Server:

ALTER TABLE Thingies
ALTER COLUMN Name VARCHAR(25)
COLLATE Latin1_General_CS_AS

Para obter mais informações sobre o SQL Server Collates , dê uma olhada em SQL SERVER Collate Pesquisa de consulta SQL sensível a maiúsculas e minúsculas

Solução do lado do cliente:

A única solução que você pode aplicar no lado do cliente é usar LINQ to Objects para fazer outra comparação que não parece ser muito elegante:

Thingies.Where(t => t.Name == "ThingamaBob")
        .AsEnumerable()
        .First(t => t.Name == "ThingamaBob");
Morteza Manavi
fonte
Estou gerando o esquema do banco de dados com o Entity Framework, portanto, uma solução usando meu código de chamada seria melhor. Acho que vou fazer uma verificação depois que os resultados chegarem. Obrigado.
Ronnie Overby,
Sem problemas. Sim, está correto e atualizei minha resposta com uma solução do lado do cliente, porém não é muito elegante e ainda recomendo usar a solução de armazenamento de dados.
Morteza Manavi,
18
@eglasius Isso não é totalmente verdade: ele não busca TODOS os dados, ele busca apenas os dados que correspondem a maiúsculas e minúsculas de forma insensível e, depois disso, é filtrado novamente no caso do cliente de forma sensível. Claro, se acontecer de você ter milhares de entradas que não fazem distinção entre maiúsculas e minúsculas, mas apenas uma delas é a correta com distinção entre maiúsculas e minúsculas, então é uma grande sobrecarga. Mas não acho que a realidade apresentará tais cenários ... :)
Achim
1
@MassoodKhaari Essa solução que você postou tornaria o it insensível a maiúsculas e minúsculas porque você está colocando letras maiúsculas em ambos os lados da comparação. O OP precisa de uma comparação com distinção entre maiúsculas e minúsculas.
Jonny
1
"Portanto, a melhor solução seria alterar o agrupamento da coluna Nome na tabela Thingies para COLLATE Latin1_General_CS_AS" - não acho que seja a melhor. Na maioria das vezes eu preciso do filtro LIKE que não diferencia maiúsculas de minúsculas (.Contains ()), mas às vezes deve ser sensível a maiúsculas e minúsculas. Vou tentar sua "solução do lado do cliente" - é muito mais elegante para o meu caso de uso, eu acho (seria bom entender o que ela faz, mas você não pode ter tudo :)).
O incrível janeiro de
11

Você pode adicionar a anotação [CaseSensitive] para EF6 + Code-first

Adicionar esta classe

[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class CaseSensitiveAttribute : Attribute
{
    public CaseSensitiveAttribute()
    {
        IsEnabled = true;
    }
    public bool IsEnabled { get; set; }
}

public class CustomSqlServerMigrationSqlGenerator : SqlServerMigrationSqlGenerator
{
    protected override void Generate(AlterColumnOperation alterColumnOperation)
    {
        base.Generate(alterColumnOperation);
        AnnotationValues values;
        if (alterColumnOperation.Column.Annotations.TryGetValue("CaseSensitive", out values))
        {
            if (values.NewValue != null && values.NewValue.ToString() == "True")
            {
                using (var writer = Writer())
                {
                    //if (System.Diagnostics.Debugger.IsAttached == false) System.Diagnostics.Debugger.Launch();

                    // https://github.com/mono/entityframework/blob/master/src/EntityFramework.SqlServer/SqlServerMigrationSqlGenerator.cs
                    var columnSQL = BuildColumnType(alterColumnOperation.Column); //[nvarchar](100)
                    writer.WriteLine(
                        "ALTER TABLE {0} ALTER COLUMN {1} {2} COLLATE SQL_Latin1_General_CP1_CS_AS {3}",
                        alterColumnOperation.Table,
                        alterColumnOperation.Column.Name,
                        columnSQL,
                        alterColumnOperation.Column.IsNullable.HasValue == false || alterColumnOperation.Column.IsNullable.Value == true ? " NULL" : "NOT NULL" //todo not tested for DefaultValue
                        );
                    Statement(writer);
                }
            }
        }
    }
}

public class CustomApplicationDbConfiguration : DbConfiguration
{
    public CustomApplicationDbConfiguration()
    {
        SetMigrationSqlGenerator(
            SqlProviderServices.ProviderInvariantName,
            () => new CustomSqlServerMigrationSqlGenerator());
    }
}

Modifique seu DbContext, adicione

protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Conventions.Add(new AttributeToColumnAnnotationConvention<CaseSensitiveAttribute, bool>(
                "CaseSensitive",
                (property, attributes) => attributes.Single().IsEnabled));
        base.OnModelCreating(modelBuilder);
    }

Então faça

Add-Migration CaseSensitive

Atualizar o banco de dados

com base no artigo https://milinaudara.wordpress.com/2015/02/04/case-sensitive-search-using-entity-framework-with-custom-annotation/ com alguma correção de bug

RouR
fonte
11

WHEREas condições no SQL Server não diferenciam maiúsculas de minúsculas por padrão. Faça distinção entre maiúsculas e minúsculas alterando os agrupamentos padrão da coluna ( SQL_Latin1_General_CP1_CI_AS) para SQL_Latin1_General_CP1_CS_AS.

A maneira frágil de fazer isso é com código. Adicione um novo arquivo de migração e, em seguida, adicione-o dentro do Upmétodo:

public override void Up()
{
   Sql("ALTER TABLE Thingies ALTER COLUMN Name VARCHAR(MAX) COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL");
}

Mas

Você pode criar uma anotação personalizada chamada "CaseSensitive" usando os novos recursos do EF6 e pode decorar suas propriedades assim:

[CaseSensitive]
public string Name { get; set; }

Esta postagem do blog explica como fazer isso.

Milina Udara
fonte
Nesse artigo tem um bug
RouR
3

A resposta dada por @Morteza Manavi resolve o problema. Ainda assim, para uma solução do lado do cliente , uma maneira elegante seria a seguinte (adicionando uma dupla verificação).

var firstCheck = Thingies.Where(t => t.Name == "ThingamaBob")
    .FirstOrDefault();
var doubleCheck = (firstCheck?.Name == model.Name) ? Thingies : null;
Swarup Rajbhandari
fonte
-4

Gostei da resposta de Morteza e normalmente prefiro consertar no servidor. Para o lado do cliente, eu normalmente uso:

Dim bLogin As Boolean = False

    Dim oUser As User = (From c In db.Users Where c.Username = UserName AndAlso c.Password = Password Select c).SingleOrDefault()
    If oUser IsNot Nothing Then
        If oUser.Password = Password Then
            bLogin = True
        End If
    End If

Basicamente, primeiro verifique se existe um usuário com os critérios exigidos, depois verifique se a senha é a mesma. Um pouco prolixo, mas acho que é mais fácil de ler quando pode haver um monte de critérios envolvidos.

Rune Borgen
fonte
2
Esta resposta implica que você está armazenando senhas como texto simples em seu banco de dados, o que é uma grande vulnerabilidade de segurança.
Jason Coyne
2
@JasonCoyne A senha com a qual ele está comparando já poderia ter sido hash
Peter Morris
-4

Nenhum dos StringComparison.IgnoreCasefuncionou para mim. Mas isso fez:

context.MyEntities.Where(p => p.Email.ToUpper().Equals(muser.Email.ToUpper()));
saquib adil
fonte
2
Isso não ajudaria com a pergunta que foi feita, que é,How can I achieve case sensitive comparison
Reg Edit
-4

Use string.Equals

Thingies.First(t => string.Equals(t.Name, "ThingamaBob", StringComparison.CurrentCulture);

Além disso, você não precisa se preocupar com null e obter de volta apenas as informações que deseja.

Use StringComparision.CurrentCultureIgnoreCase para não diferenciar maiúsculas de minúsculas.

Thingies.First(t => string.Equals(t.Name, "ThingamaBob", StringComparison.CurrentCultureIgnoreCase);
Darshan Joshi
fonte
Equals () não pode ser convertido em SQL ... Além disso, se você tentar usar o método de instância, o StringComparison será ignorado.
LMK
Você já tentou esta solução? Eu tentei isso trabalhando bem com a EF.
Darshan Joshi de
-6

Não tenho certeza sobre o EF4, mas o EF5 oferece suporte para:

Thingies
    .First(t => t.Name.Equals(
        "ThingamaBob",
        System.StringComparison.InvariantCultureIgnoreCase)
bloparod
fonte
Curioso o que sql isso gera.
Ronnie Overby
Eu verifiquei isso com EF5, ele simplesmente gerou um WHERE ... = ... no SQL. Portanto, novamente, isso depende das configurações de agrupamento no lado do servidor SQL.
Achim
Mesmo com um agrupamento StringComparisonque diferencia maiúsculas de minúsculas no banco de dados, não consegui fazer com que esta ou qualquer uma das outras enums fizesse diferença. Já vi pessoas suficientes sugerindo que esse tipo de coisa deve funcionar para pensar que o problema está em algum lugar no arquivo EDMX (db-first), embora stackoverflow.com/questions/841226/…
drzaus