[c#] LINQ 문 내부에서 부분 클래스 속성 사용



Answers

추가 자산은 모델의 데이터를 계산하는 것으로 EF가 자연스럽게 해석 할 수있는 것이 아닙니다. SQL은이 동일한 계산을 완벽하게 수행 할 수 있으며 EF 계산을 변환 할 수 있습니다 . 쿼리에 필요할 경우 속성 대신 사용하십시오.

Total = i.Lines.Sum( l => l.Price * l.Quantity)
Question

나는 내가 쉽게 생각할 수있는 최선의 방법을 찾아 내려고 노력하고있다. 나는 Invoice의 라인을 나타내는 Line이라는 데이터베이스 모델을 가지고있다.

그것은 대략 다음과 같이 보입니다 :

public partial class Line 
{
    public Int32 Id { get; set; }
    public Invoice Invoice { get; set; }
    public String Name { get; set; }
    public String Description { get; set; }
    public Decimal Price { get; set; }
    public Int32 Quantity { get; set; }
}

이 클래스는 db 모델에서 생성됩니다.
속성을 하나 더 추가하는 또 다른 클래스가 있습니다.

public partial class Line
{
    public Decimal Total
    {
        get
        {
            return this.Price * this.Quantity
        }
    }
}

자, 내 고객 컨트롤러에서 나는 다음과 같이하고 싶다 :

var invoices = ( from c in _repository.Customers
                         where c.Id == id
                         from i in c.Invoices
                         select new InvoiceIndex
                         {
                             Id = i.Id,
                             CustomerName = i.Customer.Name,
                             Attention = i.Attention,
                             Total = i.Lines.Sum( l => l.Total ),
                             Posted = i.Created,
                             Salesman = i.Salesman.Name
                         }
        )

하지만 악명 높은 사람들 덕분에

The specified type member 'Total' is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported.

이것을 리펙토링하는 가장 좋은 방법은 무엇입니까?

LinqKit, i.Lines.AsEnumerable ()을 시도하고 i.Lines를 InvoiceIndex 모델에 넣은 다음 뷰의 합계를 계산해야합니다.

그 마지막 솔루션 '작품'하지만 그 데이터를 정렬 할 수 없습니다. 내가 마지막에 할 수 있기를 원하는 것은

var invoices = ( from c in _repository.Customers
                         ...
        ).OrderBy( i => i.Total )

또한 내 데이터를 페이지하고 싶기 때문에 .AsEnumerable ()을 사용하여 전체 c.Invoices를 목록으로 변환하는 데 시간을 낭비하고 싶지 않습니다.

하사품

나는 이것이 어떤 사람들에게는 다소 큰 문제 일 것임을 알고있다. 인터넷을 수색 한 후 나는 행복한 결론을 내리지 못했다는 결론에 도달했습니다. 그러나 이것은 ASP MVC를 사용하여 페이징 및 정렬을 시도하는 사람들에게 상당히 보편적 인 방책이라고 생각합니다. 나는 속성이 SQL에 매핑 될 수 없으므로 페이징 전에 정렬 할 수 없다는 것을 이해하지만, 내가 원하는 것은 원하는 결과를 얻는 방법이다.

완벽한 솔루션을위한 요구 사항 :

  • DRY, 즉 총 계산은 1 곳에서 이루어집니다.
  • 정렬과 페이징을 모두 지원하며 그 순서대로
  • .AsEnumerable 또는 .AsArray를 사용하여 데이터의 전체 테이블을 메모리로 가져 오지는 않습니다.

내가 정말 행복하게 찾을 수있는 방법은 확장 된 부분 클래스에서 엔티티 SQL에 Linq를 지정하는 방법입니다. 그러나 나는 이것이 가능하지 않다는 말을 들었다. 솔루션에서 Total 속성을 직접 사용할 필요는 없습니다. IQueryable에서 해당 속성을 호출하는 것은 전혀 지원되지 않습니다. 나는 다른 방법을 통해 동일한 결과를 얻을 수있는 방법을 찾고 있지만, 똑같이 단순하고 직교합니다.

현상금의 우승자는 누군가가 완벽한 해결책을 게시하지 않는 한, 가장 높은 표를 얻은 해결책이 될 것입니다. :)

답을 읽을 때까지 다음을 무시하십시오.

{1} Jacek의 솔루션을 사용하여 한 단계 더 나아가 LinqKit을 사용하여 속성을 호출 할 수있게했습니다. 이 방법은 .AsQueryable (). Sum ()도 부분 클래스로 묶습니다. 제가 지금하고있는 일의 몇 가지 예가 있습니다 :

public partial class Line
{
    public static Expression<Func<Line, Decimal>> Total
    {
        get
        {
            return l => l.Price * l.Quantity;
        }
    }
}

public partial class Invoice
{
    public static Expression<Func<Invoice, Decimal>> Total
    {
        get
        {
            return i => i.Lines.Count > 0 ? i.Lines.AsQueryable().Sum( Line.Total ) : 0;
        }
    }
}

public partial class Customer
{
    public static Expression<Func<Customer, Decimal>> Balance
    {
        get
        {
            return c => c.Invoices.Count > 0 ? c.Invoices.AsQueryable().Sum( Invoice.Total ) : 0;
        }
    }
}

첫 번째 요령은 .Count 수표입니다. 그것들은 빈 세트에서 호출 할 수 없다고 생각하기 때문에 필요합니다. Null materialization에 대한 오류가 발생합니다.

이 3 개의 부분 클래스를 배치하면 다음과 같은 트릭을 수행 할 수 있습니다.

var customers = ( from c in _repository.Customers.AsExpandable()
                           select new CustomerIndex
                           {
                               Id = c.Id,
                               Name = c.Name,
                               Employee = c.Employee,
                               Balance = Customer.Balance.Invoke( c )
                           }
                    ).OrderBy( c => c.Balance ).ToPagedList( page - 1, PageSize );

var invoices = ( from i in _repository.Invoices.AsExpandable()
                         where i.CustomerId == Id 
                         select new InvoiceIndex
                        {
                            Id = i.Id,
                            Attention = i.Attention,
                            Memo = i.Memo,
                            Posted = i.Created,
                            CustomerName = i.Customer.Name,
                            Salesman = i.Salesman.Name,
                            Total = Invoice.Total.Invoke( i )
                        } )
                        .OrderBy( i => i.Total ).ToPagedList( page - 1, PageSize );

아주 멋지다.

캐치가 있는데, LinqKit은 속성의 호출을 지원하지 않습니다. PropertyExpression을 LambaExpression으로 캐스팅하려고 시도하는 것에 대한 오류가 발생합니다. 이 문제를 해결하는 방법은 두 가지가 있습니다. 첫째, 표현 자체를 그렇게 끌어 당기는 것입니다.

var tmpBalance = Customer.Balance;
var customers = ( from c in _repository.Customers.AsExpandable()
                           select new CustomerIndex
                           {
                               Id = c.Id,
                               Name = c.Name,
                               Employee = c.Employee,
                               Balance = tmpBalance.Invoke( c )
                           }
                    ).OrderBy( c => c.Balance ).ToPagedList( page - 1, PageSize );

나는 바보 같다고 생각했다. 그래서 LinqKit이 속성을 만났을 때 get {} 값을 빼도록 수정했습니다. 표현식에서 작동하는 방식은 리플렉션과 비슷하므로 컴파일러와 달리 고객을 해결할 것입니다. 우리를위한 균형. ExpressionExpander.cs에서 TransformExpr에 3 행 변경했습니다. 아마도 가장 안전한 코드가 아니며 다른 것들을 깨뜨릴 수도 있지만 현재는 제대로 작동하며 저자에게 결함에 대해 통보했습니다.

Expression TransformExpr (MemberExpression input)
{
        if( input.Member is System.Reflection.PropertyInfo )
        {
            return Visit( (Expression)( (System.Reflection.PropertyInfo)input.Member ).GetValue( null, null ) );
        }
        // Collapse captured outer variables
        if( input == null

사실 나는이 코드가 어떤 것들을 깨뜨릴 것이라는 것을 거의 보증하지만, 지금은 제대로 작동한다. :)




이 문제를 해결하는 가장 쉬운 방법DelegateDecompiler.EntityFramework ( Alexander Zaytsev 작성 )를 사용하는 것입니다.

델라 컴파일러는 델리게이트 나 메서드 본문을 람다 표현으로 디 컴파일 할 수있는 라이브러리입니다.

설명:

우리는 계산 된 속성을 가진 클래스를 가진다.

class Employee
{
    [Computed]
    public string FullName
    {
        get { return FirstName + " " + LastName; }
    }

    public string LastName { get; set; }

    public string FirstName { get; set; }
}

그리고 당신은 그들의 전체 이름으로 직원들을 질의 할 것입니다.

var employees = (from employee in db.Employees
                 where employee.FullName == "Test User"
                 select employee).Decompile().ToList();

.Decompile 메서드를 호출하면 계산 된 속성이 기본 표현으로 디 컴파일되며 쿼리는 다음 쿼리와 유사하게됩니다

var employees = (from employee in db.Employees
                 where (employee.FirstName + " " + employee.LastName)  == "Test User"
                 select employee).ToList();

클래스에 [계산 된] 특성이 없으면 .Computed () 확장 메서드를 사용할 수 있습니다.

var employees = (from employee in db.Employees
                 where employee.FullName.Computed() == "Test User"
                 select employee).ToList();

또한 다음과 같이 동일한 방법으로 다른 메소드뿐만 아니라 단일 항목 (Any, Count, First, Single 등)을 리턴하는 메소드를 호출 할 수 있습니다.

bool exists = db.Employees.Decompile().Any(employee => employee.FullName == "Test User");

다시 FullName 속성이 디 컴파일됩니다.

bool exists = db.Employees.Any(employee => (employee.FirstName + " " + employee.LastName) == "Test User");

EntityFramework를 사용한 비동기 지원

DelegateDecompiler.EntityFramework 패키지는 EF의 비동기 작업에 대한 지원을 추가하는 DecompileAsync 확장 메서드를 제공합니다.

또한

여기에 EF 에서 일부 속성 값을 함께 혼합하는 8 가지 방법을 찾을 수 있습니다.

계산 된 속성 및 엔터티 프레임 워크 . ( Dave Glick 작문)




그냥 내 자신의 생각을 여기에 추가하십시오. 나는 완벽 주의자이기 때문에 테이블의 전체 내용을 메모리로 가져 와서 계산을 수행 한 다음 페이지를 정렬 할 수는 없습니다. 그래서 어떻게 든 테이블은 총계가 무엇인지를 알아야합니다.

내가 생각했던 'CalcTotal'라는 라인 테이블에 계산 된 총 라인을 포함하는 새로운 필드를 생성하는 것이었다. 이 값은 .Total 값에 따라 행이 변경 될 때마다 설정됩니다.

여기에는 2 가지 장점이 있습니다. 첫째, 쿼리를 다음과 같이 변경할 수 있습니다.

var invoices = ( from c in _repository.Customers
                     where c.Id == id
                     from i in c.Invoices
                     select new InvoiceIndex
                     {
                         Id = i.Id,
                         CustomerName = i.Customer.Name,
                         Attention = i.Attention,
                         Total = i.Lines.Sum( l => l.CalcTotal ),
                         Posted = i.Created,
                         Salesman = i.Salesman.Name
                     }
    )

정렬 및 페이징은 db 필드가 있기 때문에 작동합니다. 그리고 내 컨트롤러에서 수표를 (line.CalcTotal == line.Total) 할 수있는 모든 멍청이를 감지합니다. 내 저장소에서 약간의 오버 헤드가 발생합니다. 즉, 저장하거나 새 행을 만들 때 추가해야합니다.

line.CalcTotal = line.Total

그러나 나는 그것이 그것의 가치가 있을지도 모른다라고 생각한다.

같은 데이터를 가진 2 개의 속성을 갖는 데 왜 어려움이 있습니까?

그럼 사실, 나는 넣을 수있어.

line.CalcTotal = line.Quantity * line.Price;

내 저장소에. 이것은 매우 직교하지는 않을 것입니다. 라인 합계에 대한 일부 변경이 필요할 경우 Line 저장소보다 부분 Line 클래스를 편집하는 것이 훨씬 더 합리적입니다.






Related