Comparación entre mayúsculas y minúsculas de LINQ to Entities

115

Esta no es una comparación que distingue entre mayúsculas y minúsculas en LINQ to Entities:

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

¿Cómo puedo lograr una comparación sensible a mayúsculas y minúsculas con LINQ to Entities?

Ronnie Overby
fuente
@Ronnie: ¿estás seguro de eso? ¿Te refieres a una comparación que no distingue entre mayúsculas y minúsculas ?
Michael Petrotta
14
Absolutamente seguro. No, no me refiero a eso.
Ronnie Overby
12
No, en mi computadora que ejecuta EF 4.0 con SQL Server 2008 R2, lo anterior no distingue entre mayúsculas y minúsculas. Sé que muchos lugares dicen que EF distingue entre mayúsculas y minúsculas, pero eso no es lo que he experimentado.
tster
3
¿No dependerá eso de la base de datos subyacente?
codymanix
1
@codymanix: ¡Esa es una buena pregunta! ¿Linq a EF traduce la expresión lambda para una consulta DB? No conozco la respuesta.
Tergiver

Respuestas:

163

Esto se debe a que está utilizando LINQ To Entities, que en última instancia convierte sus expresiones Lambda en declaraciones SQL. Eso significa que la distinción entre mayúsculas y minúsculas está a merced de su servidor SQL, que de forma predeterminada tiene SQL_Latin1_General_CP1_CI_AS Collation y que NO distingue entre mayúsculas y minúsculas.

El uso de ObjectQuery.ToTraceString para ver la consulta SQL generada que se ha enviado realmente a SQL Server revela el misterio:

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

Cuando crea una consulta de LINQ to Entities , LINQ to Entities aprovecha el analizador LINQ para comenzar a procesar la consulta y la convierte en un árbol de expresión LINQ. Luego, el árbol de expresión LINQ se pasa a la API de Object Services , que convierte el árbol de expresión en un árbol de comandos. Luego se envía al proveedor de la tienda (por ejemplo, SqlClient), que convierte el árbol de comandos en el texto de comandos de la base de datos nativa. La consulta se ejecuta en el almacén de datos y los resultados se materializan en objetos de entidad mediante Object Services. No se ha puesto ninguna lógica en el medio para tener en cuenta la distinción entre mayúsculas y minúsculas. Entonces, no importa qué caso ponga en su predicado, su SQL Server siempre lo tratará como el mismo, a menos que cambie su SQL Server Collates para esa columna.

Solución del lado del servidor:

Por lo tanto, la mejor solución sería cambiar la intercalación de la columna Nombre en la tabla Thingies a COLLATE Latin1_General_CS_AS que distingue entre mayúsculas y minúsculas al ejecutar esto en su SQL Server:

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

Para obtener más información sobre SQL Server Collates , eche un vistazo a SQL SERVER Collate Búsqueda de consultas SQL sensibles a mayúsculas y minúsculas

Solución del lado del cliente:

La única solución que puede aplicar en el lado del cliente es usar LINQ to Objects para hacer otra comparación que no parece ser muy elegante:

Thingies.Where(t => t.Name == "ThingamaBob")
        .AsEnumerable()
        .First(t => t.Name == "ThingamaBob");
Morteza Manavi
fuente
Estoy generando el esquema de la base de datos con Entity Framework, por lo que sería mejor una solución con mi código de llamada. Supongo que lo comprobaré después de que se obtengan los resultados. Gracias.
Ronnie Overby
No hay problema. Sí, eso es correcto y he actualizado mi respuesta con una solución del lado del cliente, sin embargo, no es muy elegante y aún recomiendo usar la solución de almacenamiento de datos.
Morteza Manavi
18
@eglasius Esto no es completamente cierto: no obtiene TODOS los datos, solo obtiene los datos que coinciden sin distinción entre mayúsculas y minúsculas, y después de eso se filtra de nuevo en el cliente de forma sensible. Por supuesto, si tiene miles de entradas que no distinguen entre mayúsculas y minúsculas, pero solo una de ellas es la correcta, entonces es una gran sobrecarga. Pero no creo que la realidad presente tales escenarios ... :)
Achim
1
@MassoodKhaari Esa solución que publicó haría que no se distinga entre mayúsculas y minúsculas porque está en la parte inferior de ambos lados de la comparación. El OP necesita una comparación sensible a mayúsculas y minúsculas.
Jonny
1
"Por lo tanto, la mejor solución sería cambiar la intercalación de la columna Nombre en la tabla Thingies a COLLATE Latin1_General_CS_AS". No creo que esta sea la mejor. La mayoría de las veces necesito un filtro LIKE que no distingue entre mayúsculas y minúsculas (.Contains ()) pero a veces debería distinguir entre mayúsculas y minúsculas. Probaré su "solución del lado del cliente", creo que es mucho más elegante para mi caso de uso (sería bueno entender lo que hace, pero no puede tenerlo todo :)).
El increíble
11

Puede agregar la anotación [CaseSensitive] para EF6 + Code-first

Agregar estas clases

[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 su DbContext, agregue

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

Entonces hazlo

Agregar-migración sensible a mayúsculas y minúsculas

Actualizar base de datos

basado en el artículo https://milinaudara.wordpress.com/2015/02/04/case-sensitive-search-using-entity-framework-with-custom-annotation/ con algunas correcciones de errores

RouR
fuente
11

WHERELas condiciones en SQL Server no distinguen entre mayúsculas y minúsculas de forma predeterminada. Hágalo sensible a mayúsculas y minúsculas cambiando las intercalaciones predeterminadas de la columna ( SQL_Latin1_General_CP1_CI_AS) a SQL_Latin1_General_CP1_CS_AS.

La forma frágil de hacer esto es con código. Agregue un nuevo archivo de migración y luego agregue esto dentro del Upmétodo:

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

Pero

Puede crear una anotación personalizada llamada "CaseSensitive" utilizando las nuevas funciones de EF6 y puede decorar sus propiedades de esta manera:

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

Esta publicación de blog explica cómo hacerlo.

Milina Udara
fuente
En ese artículo tiene un error
RouR
3

La respuesta de @Morteza Manavi resuelve el problema. Aún así, para una solución del lado del cliente , una forma elegante sería la siguiente (agregando una doble verificación).

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

Me gustó la respuesta de Morteza y normalmente preferiría arreglar en el lado del servidor. Para el lado del cliente 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

Básicamente, primero verifique si hay un usuario con los criterios requeridos, luego verifique si la contraseña es la misma. Un poco prolijo, pero creo que es más fácil de leer cuando puede haber un montón de criterios involucrados.

Runa Borgen
fuente
2
Esta respuesta implica que está almacenando contraseñas como texto sin formato en su base de datos, lo cual es una gran vulnerabilidad de seguridad.
Jason Coyne
2
@JasonCoyne La contraseña con la que está comparando ya podría tener hash
Peter Morris
-4

Ninguno de los dos StringComparison.IgnoreCasefuncionó para mí. Pero esto hizo:

context.MyEntities.Where(p => p.Email.ToUpper().Equals(muser.Email.ToUpper()));
saquib adil
fuente
2
Esto no ayudaría con la pregunta que se hizo, que esHow can I achieve case sensitive comparison
Reg Edit
-4

Utilice string.Equals

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

Además, no tiene que preocuparse por nulos y recuperar solo la información que desea.

Utilice StringComparision.CurrentCultureIgnoreCase para mayúsculas y minúsculas.

Thingies.First(t => string.Equals(t.Name, "ThingamaBob", StringComparison.CurrentCultureIgnoreCase);
Darshan Joshi
fuente
Equals () no se puede convertir a SQL ... Además, si intenta utilizar el método de instancia, se ignora StringComparison.
LMK
¿Has probado esta solución? Intenté esto al final trabajando bien con EF.
Darshan Joshi
-6

No estoy seguro de EF4, pero EF5 lo admite:

Thingies
    .First(t => t.Name.Equals(
        "ThingamaBob",
        System.StringComparison.InvariantCultureIgnoreCase)
bloparod
fuente
Curioso qué sql genera.
Ronnie Overby
Verifiqué esto con EF5, simplemente generó un DONDE ... = ... en SQL. De nuevo, esto depende de la configuración de clasificación en el lado del servidor SQL.
Achim
Incluso con una intercalación que StringComparisondistingue entre mayúsculas y minúsculas en la base de datos, no pude hacer que esta o cualquiera de las otras enumeraciones marcaran la diferencia. He visto suficientes personas sugiriendo que este tipo de cosas debería funcionar para pensar que el problema está en algún lugar del archivo EDMX (db-first), aunque stackoverflow.com/questions/841226/…
drzaus