LINQ to Entities nie rozpoznaje metody „System.String ToString ()” i nie można jej przetłumaczyć na wyrażenie magazynu

126

Migruję niektóre rzeczy z jednego serwera mysql na serwer sql, ale nie mogę dowiedzieć się, jak sprawić, by ten kod działał:

using (var context = new Context())
{
    ...

    foreach (var item in collection)
    {
        IQueryable<entity> pages = from p in context.pages
                                   where  p.Serial == item.Key.ToString()
                                   select p;
        foreach (var page in pages)
        {
            DataManager.AddPageToDocument(page, item.Value);
        }
    }

    Console.WriteLine("Done!");
    Console.Read();
}

Kiedy wchodzi w sekundę foreach (var page in pages), rzuca wyjątek mówiąc:

LINQ to Entities nie rozpoznaje metody „System.String ToString ()” i tej metody nie można przetłumaczyć na wyrażenie magazynu.

Czy ktoś wie, dlaczego tak się dzieje?

Erre Efe
źródło

Odpowiedzi:

134

Po prostu zapisz ciąg w zmiennej tymczasowej, a następnie użyj tego w swoim wyrażeniu:

var strItem = item.Key.ToString();

IQueryable<entity> pages = from p in context.pages
                           where  p.Serial == strItem
                           select p;

Problem pojawia się, ponieważ ToString()nie jest tak naprawdę wykonywany, jest przekształcany w grupę MethodGroup, a następnie analizowany i tłumaczony na SQL. Ponieważ nie ma ToString()odpowiednika, wyrażenie zawodzi.

Uwaga:

Sprawdź również odpowiedź Alexa dotyczącą SqlFunctionsklasy pomocnika, która została dodana później. W wielu przypadkach może to wyeliminować potrzebę stosowania zmiennej tymczasowej.

Josh
źródło
14
Co się stanie, jeśli moja ToString () jest stosowana po lewej stronie równości? egpSerial.ToString () = item.
dotNET
3
@dotNet To nadal nie powiedzie się, ponieważ cała rzecz zostanie zamieniona w Expression, które Entity Framework próbuje przekształcić w prawidłowy SQL. Jest kilka metod, z którymi potrafi sobie poradzić, ale ToString()nie jest jedną z nich.
Josh
7
@Josh: Rozumiem, że to się nie powiedzie. To, o co prosiłem, to rozwiązanie tego scenariusza, bo powyższego rozwiązania oczywiście nie da się tam zastosować.
dotNET
3
@Josh: Walczę z jednym takim scenariuszem. Powiedz, że moja kolumna OrderNumber to int, ale mój użytkownik chce mieć możliwość filtrowania listy OrderNumbers podczas wpisywania. Jeśli wpisał 143 w polu wyszukiwania, chce tylko te rekordy, które mają numer zamówienia LIKE „% 143%” . Czy nie muszę wykonywać ToString () w kolumnie OrderNumber, aby to osiągnąć?
dotNET
5
@dotNET to jeden z tych scenariuszy, w których ORM spada na twarz. Myślę, że w takich sytuacjach można przejść do prostego SQL za pośrednictwem ExecuteQuerylub przy użyciu Entity SQL zObjectQuery<T>
Josh
69

Jak odpowiedzieli inni, to się psuje, ponieważ .ToString nie może przetłumaczyć na odpowiedni kod SQL w drodze do bazy danych.

Jednak firma Microsoft udostępnia klasę SqlFunctions, która jest zbiorem metod, których można używać w takich sytuacjach.

W tym przypadku szukasz tutaj SqlFunctions.StringConvert :

from p in context.pages
where  p.Serial == SqlFunctions.StringConvert((double)item.Key.Id)
select p;

Dobrze, gdy rozwiązanie ze zmiennymi tymczasowymi nie jest pożądane z jakichkolwiek powodów.

Podobnie jak SqlFunctions, masz również EntityFunctions (z EF6 przestarzałą przez DbFunctions ), które zapewniają inny zestaw funkcji, które są również niezależne od źródła danych (nie są ograniczone do np. SQL).

Alex
źródło
4
Dodali klasę SqlFunctions z powrotem w .NET 4 i właśnie się o tym uczę? Doskonałe znalezisko.
James Skemp
24

Problem polega na tym, że wywołujesz ToString w zapytaniu LINQ to Entities. Oznacza to, że parser próbuje przekonwertować wywołanie ToString na jego równoważny SQL (co nie jest możliwe ... stąd wyjątek).

Wszystko, co musisz zrobić, to przenieść wywołanie ToString do osobnej linii:

var keyString = item.Key.ToString();

var pages = from p in context.entities
            where p.Serial == keyString
            select p;
Justin Niessner
źródło
9

Miałem podobny problem. Rozwiązano to, wywołując ToList () w kolekcji jednostek i wysyłając zapytanie do listy. Jeśli kolekcja jest mała, jest to opcja.

IQueryable<entity> pages = context.pages.ToList().Where(p=>p.serial == item.Key.ToString())

Mam nadzieję że to pomoże.

cyniczny lekarz
źródło
42
Należy pamiętać, że spowoduje to pobranie wszystkich jednostek Page z bazy danych i wykonanie filtrowania po stronie klienta zamiast bazy danych. Zazwyczaj nie jest to dobra rzecz.
lambinator
3
To prawda, że ​​ta metoda byłaby nieefektywna dla każdej tabeli, która zawiera więcej niż jeden rekord, czyli wszystkie istniejące tabele :-). Jednak ta odpowiedź pomogła mi dzisiaj, ponieważ wykonywałem projekcję .Select, która zawierała toString (), więc wywołanie .ToList () przed rozdaniem nie miało żadnego wpływu na wydajność, a wywołanie .ToList () pozwoliło mi użyć .ToString () formatowanie i moje oświadczenie .Select ...
Nathan Prather
6

Zmień to w ten sposób i powinno działać:

var key = item.Key.ToString();
IQueryable<entity> pages = from p in context.pages
                           where  p.Serial == key
                           select p;

Powodem, dla którego wyjątek nie jest zgłaszany w wierszu, w którym zostało zadeklarowane zapytanie LINQ, ale w wierszu foreachjest funkcja odroczonego wykonywania, tj. Zapytanie LINQ nie jest wykonywane, dopóki nie spróbujesz uzyskać dostępu do wyniku. Dzieje się to w foreachi nie wcześniej.

Daniel Hilgarth
źródło
6

Rzutuj tabelę na Enumerable, a następnie wywołasz metody LINQ z użyciem ToString()metody wewnątrz:

    var example = contex.table_name.AsEnumerable()
.Select(x => new {Date = x.date.ToString("M/d/yyyy")...)

Ale bądź ostrożny, kiedy wywołujesz AsEnumerablelub ToListmetody, ponieważ będziesz żądać wszystkich danych od wszystkich encji przed tą metodą. W moim przypadku powyżej czytam wszystkie table_namewiersze jednym żądaniem.

neustart47
źródło
5
normalnie nie jest dobrym wyborem, .AsEnumerable () umieść wszystkie dane w pamięci, możesz zobaczyć więcej na ten temat tutaj: stackoverflow.com/questions/3311244/ ...
kavain
5

Uaktualnienie do Entity Framework w wersji 6.2.0 działało dla mnie.

Byłem wcześniej w wersji 6.0.0.

Mam nadzieję że to pomoże,

93Ramadan
źródło
1

W MVC załóżmy, że szukasz rekordów na podstawie swoich wymagań lub informacji. Działa poprawnie.

[HttpPost]
[ActionName("Index")]
public ActionResult SearchRecord(FormCollection formcollection)
{       
    EmployeeContext employeeContext = new EmployeeContext();

    string searchby=formcollection["SearchBy"];
    string value=formcollection["Value"];

    if (formcollection["SearchBy"] == "Gender")
    {
        List<MvcApplication1.Models.Employee> emplist = employeeContext.Employees.Where(x => x.Gender == value).ToList();
        return View("Index", emplist);
    }
    else
    {
        List<MvcApplication1.Models.Employee> emplist = employeeContext.Employees.Where(x => x.Name == value).ToList();
        return View("Index", emplist);
    }         
}
shakti
źródło
2
Aby uzyskać lepszą praktykę lub w produkcyjnych typach kodu, zdarzenia bazy danych zawsze powinny znajdować się w warstwie usług lub warstwie danych, a nie bezpośrednio w akcji.
TGarrett
0

Jeśli naprawdę chcesz wpisać ToStringwewnątrz zapytania, możesz napisać odwiedzającego drzewo wyrażeń, który przepisuje wywołanie do ToStringz wywołaniem odpowiedniej StringConvertfunkcji :

using System.Linq;
using System.Data.Entity.SqlServer;
using System.Linq.Expressions;
using static System.Linq.Expressions.Expression;
using System;

namespace ToStringRewriting {
    class ToStringRewriter : ExpressionVisitor {
        static MethodInfo stringConvertMethodInfo = typeof(SqlFunctions).GetMethods()
                 .Single(x => x.Name == "StringConvert" && x.GetParameters()[0].ParameterType == typeof(decimal?));

        protected override Expression VisitMethodCall(MethodCallExpression node) {
            var method = node.Method;
            if (method.Name=="ToString") {
                if (node.Object.GetType() == typeof(string)) { return node.Object; }
                node = Call(stringConvertMethodInfo, Convert(node.Object, typeof(decimal?));
            }
            return base.VisitMethodCall(node);
        }
    }
    class Person {
        string Name { get; set; }
        long SocialSecurityNumber { get; set; }
    }
    class Program {
        void Main() {
            Expression<Func<Person, Boolean>> expr = x => x.ToString().Length > 1;
            var rewriter = new ToStringRewriter();
            var finalExpression = rewriter.Visit(expr);
            var dcx = new MyDataContext();
            var query = dcx.Persons.Where(finalExpression);

        }
    }
}
Zev Spitz
źródło
Powinien używać FirstOrDefault, a nie tylko First ... Jeśli jest to klucz podstawowy, użyj funkcji Find, ponieważ działa lepiej.
TGarrett
@TGarrett Jedynym zastosowaniem Firsttutaj jest GetMethods()zwracanie wyników MethodInfo[]. AFAIK, MethodInfo[]nie ma Findmetody ani nie ma takiej metody rozszerzającej. Ale naprawdę powinienem użyć, Singleponieważ ta metoda jest znajdowana przez odbicie i nie będzie błędu w czasie kompilacji, jeśli nie można rozwiązać odpowiedniej metody.
Zev Spitz
0

W tym przypadku mam ten sam błąd:

var result = Db.SystemLog
.Where(log =>
    eventTypeValues.Contains(log.EventType)
    && (
        search.Contains(log.Id.ToString())
        || log.Message.Contains(search)
        || log.PayLoad.Contains(search)
        || log.Timestamp.ToString(CultureInfo.CurrentUICulture).Contains(search)
    )
)
.OrderByDescending(log => log.Id)
.Select(r => r);

Po spędzeniu zbyt dużej ilości czasu na debugowaniu zorientowałem się, że w wyrażeniu logicznym pojawił się błąd.

Pierwsza linia search.Contains(log.Id.ToString())działa dobrze, ale ostatnia linia, która zajmuje się obiektem DateTime, spowodowała żałosną awarię:

|| log.Timestamp.ToString(CultureInfo.CurrentUICulture).Contains(search)

Usuń problematyczną linię i problem rozwiązany.

Nie do końca rozumiem, dlaczego, ale wydaje się, że ToString () jest wyrażeniem LINQ dla ciągów, ale nie dla jednostek. LINQ for Entities obsługuje zapytania do bazy danych, takie jak SQL, a SQL nie ma pojęcia ToString (). W związku z tym nie możemy wrzucić ToString () do klauzuli .Where ().

Ale jak w takim razie działa pierwsza linia? Zamiast ToString (), SQL ma CASTi CONVERT, więc jak dotąd moje najlepsze przypuszczenie jest takie, że linq dla encji używa tego w niektórych prostych przypadkach. Obiekty DateTime nie zawsze są tak proste ...

pekaaw
źródło
-8

Po prostu zamień zapytanie LINQ to Entity na zapytanie LINQ to Objects (np. Wywołanie ToArray) w dowolnym momencie, gdy musisz użyć wywołania metody w zapytaniu LINQ.

T. Webster
źródło
3
„Za każdym razem, gdy trzeba użyć wywołania metody” to kiepska rada - przy wielu rekordach może to stanowić duży problem. Przyjęta odpowiedź jest znacznie lepsza w tym scenariuszu.
PeteGO