Comparaison sensible à la casse LINQ to Entities

115

Ce n'est pas une comparaison sensible à la casse dans LINQ to Entities:

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

Comment puis-je réaliser une comparaison sensible à la casse avec LINQ to Entities?

Ronnie Overby
la source
@Ronnie: en êtes-vous sûr? Voulez-vous dire comparaison insensible à la casse ?
Michael Petrotta
14
Absolument certain. Non, je ne veux pas dire ça.
Ronnie Overby
12
Non, sur mon ordinateur exécutant EF 4.0 avec SQL Server 2008 R2, ce qui précède n'est pas sensible à la casse. Je sais que beaucoup d'endroits disent que EF est sensible à la casse par défaut, mais ce n'est pas ce que j'ai vécu.
tster
3
Cela ne dépendra-t-il pas de la base de données sous-jacente?
codymanix
1
@codymanix: C'est une bonne question! Linq to EF traduit-il l'expression lambda pour une requête DB? Je ne connais pas la réponse.
Tergiver

Réponses:

163

En effet, vous utilisez LINQ To Entities, qui convertit finalement vos expressions Lambda en instructions SQL. Cela signifie que le respect de la casse est à la merci de votre serveur SQL qui par défaut a SQL_Latin1_General_CP1_CI_AS et qui n'est PAS sensible à la casse.

L'utilisation d' ObjectQuery.ToTraceString pour voir la requête SQL générée qui a été réellement soumise à SQL Server révèle le mystère:

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

Lorsque vous créez une requête LINQ to Entities , LINQ to Entities exploite l'analyseur LINQ pour commencer le traitement de la requête et la convertit en une arborescence d'expression LINQ. L'arborescence d'expression LINQ est ensuite transmise à l' API Object Services , qui convertit l'arborescence d'expression en une arborescence de commandes. Il est ensuite envoyé au fournisseur du magasin (par exemple SqlClient), qui convertit l'arborescence de commandes en texte de commande de la base de données native. La requête est exécutée sur le magasin de données et les résultats sont matérialisés en objets d'entité par Object Services. Aucune logique n'a été mise entre les deux pour tenir compte du respect de la casse. Ainsi, quel que soit le cas que vous mettez dans votre prédicat, il sera toujours traité de la même manière par votre serveur SQL, sauf si vous modifiez vos assemblages SQL Server pour cette colonne.

Solution côté serveur:

Par conséquent, la meilleure solution serait de modifier le classement de la colonne Nom dans la table Thingies en COLLATE Latin1_General_CS_AS, ce qui est sensible à la casse en exécutant ceci sur votre serveur SQL:

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

Pour plus d'informations sur les assemblages SQL Server , jetez un œil à la recherche de requêtes SQL sensible à la casse de l' assemblage SQL SERVER

Solution côté client:

La seule solution que vous pouvez appliquer côté client est d'utiliser LINQ to Objects pour faire encore une autre comparaison qui ne semble pas très élégante:

Thingies.Where(t => t.Name == "ThingamaBob")
        .AsEnumerable()
        .First(t => t.Name == "ThingamaBob");
Morteza Manavi
la source
Je génère le schéma de base de données avec Entity Framework, donc une solution utilisant mon code d'appel serait la meilleure. Je suppose que je ferai une vérification après que les résultats seront revenus. Merci.
Ronnie Overby
Aucun problème. Oui, c'est correct et j'ai mis à jour ma réponse avec une solution côté client, mais ce n'est pas très élégant et je recommande toujours d'utiliser la solution de stockage de données.
Morteza Manavi,
18
@eglasius Ce n'est pas tout à fait vrai: il ne récupère pas TOUTES les données, il ne récupère que les données qui correspondent à la casse sans tenir compte de la casse, et après cela, il est filtré à nouveau sur le cas client avec sensibilité. Bien sûr, si vous avez des milliers d'entrées qui ne sont pas sensibles à la casse, mais qu'une seule d'entre elles est la bonne, c'est une surcharge. Mais je ne pense pas que la réalité présentera de tels scénarios ... :)
Achim
1
@MassoodKhaari Cette solution que vous avez publiée la rendrait insensible à la casse, car vous gérez les deux côtés de la comparaison. L'OP a besoin d'une comparaison sensible à la casse.
Jonny
1
"Par conséquent, la meilleure solution serait de changer le classement de la colonne Name dans la table Thingies en COLLATE Latin1_General_CS_AS" - je ne pense pas que ce soit le meilleur. La plupart du temps, j'ai besoin d'un filtre LIKE insensible à la casse (.Contains ()) mais parfois il devrait être sensible à la casse. Je vais essayer votre «solution côté client» - c'est beaucoup plus élégant pour mon cas d'utilisation je pense (ce serait bien de comprendre ce que cela fait mais vous ne pouvez pas tout avoir :)).
L'incroyable janvier
11

Vous pouvez ajouter l'annotation [CaseSensitive] pour EF6 + Code-first

Ajouter ces classes

[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());
    }
}

Modifiez votre DbContext, ajoutez

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

Alors fais

Add-Migration CaseSensitive

Update-Database

basé sur l'article https://milinaudara.wordpress.com/2015/02/04/case-sensitive-search-using-entity-framework-with-custom-annotation/ avec une correction de bogue

RouR
la source
11

WHEREles conditions dans SQL Server sont insensibles à la casse par défaut. Faites-le sensible à la casse en remplaçant les classements par défaut de la colonne ( SQL_Latin1_General_CP1_CI_AS) par SQL_Latin1_General_CP1_CS_AS.

La manière fragile de faire ceci est avec le code. Ajoutez un nouveau fichier de migration, puis ajoutez-le dans la Upméthode:

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

Mais

Vous pouvez créer une annotation personnalisée appelée "CaseSensitive" à l'aide des nouvelles fonctionnalités EF6 et vous pouvez décorer vos propriétés comme ceci:

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

Cet article de blog explique comment faire cela.

Milina Udara
la source
Dans cet article ont un bug
RouR
3

La réponse donnée par @Morteza Manavi résout le problème. Pourtant, pour une solution côté client , une manière élégante serait la suivante (ajouter une double vérification).

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

J'ai aimé la réponse de Morteza et je préférerais normalement la corriger côté serveur. Pour le côté client, j'utilise normalement:

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

En gros, vérifiez d'abord s'il y a un utilisateur avec les critères requis, puis vérifiez si le mot de passe est le même. Un peu long, mais je pense que c'est plus facile à lire quand il peut y avoir tout un tas de critères impliqués.

Rune Borgen
la source
2
Cette réponse implique que vous stockez les mots de passe sous forme de texte brut dans votre base de données, ce qui constitue une énorme vulnérabilité de sécurité.
Jason Coyne
2
@JasonCoyne Le mot de passe avec lequel il compare pourrait déjà être haché
Peter Morris
-4

Aucun des deux n'a StringComparison.IgnoreCasefonctionné pour moi. Mais cela a fait:

context.MyEntities.Where(p => p.Email.ToUpper().Equals(muser.Email.ToUpper()));
saquib adil
la source
2
Cela n'aiderait pas avec la question qui a été posée, à savoir,How can I achieve case sensitive comparison
Reg Edit
-4

Utilisez une chaîne.

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

De plus, vous n'avez pas à vous soucier de null et ne récupérez que les informations que vous souhaitez.

Utilisez StringComparision.CurrentCultureIgnoreCase pour ne pas respecter la casse.

Thingies.First(t => string.Equals(t.Name, "ThingamaBob", StringComparison.CurrentCultureIgnoreCase);
Darshan Joshi
la source
Equals () ne peut pas être converti en SQL ... De plus, si vous essayez d'utiliser la méthode d'instance, la StringComparison est ignorée.
LMK
Avez-vous essayé cette solution? J'ai essayé cela à ma fin pour bien travailler avec EF.
Darshan Joshi
-6

Je ne suis pas sûr de EF4, mais EF5 prend en charge ceci:

Thingies
    .First(t => t.Name.Equals(
        "ThingamaBob",
        System.StringComparison.InvariantCultureIgnoreCase)
bloparod
la source
Curieux de savoir ce que SQL génère.
Ronnie Overby le
J'ai vérifié cela avec EF5, il a simplement généré un WHERE ... = ... en SQL. Encore une fois, cela dépend des paramètres de classement côté serveur SQL.
Achim
Même avec un classement sensible à la casse dans la base de données, je ne pouvais pas faire en sorte que cette énumération ou l'une des autres StringComparisonenums fasse une différence. J'ai vu suffisamment de gens suggérer que ce genre de chose devrait fonctionner pour penser que le problème se trouve quelque part dans le fichier EDMX (db-first), bien que stackoverflow.com/questions/841226
...