Repositórios devem retornar IQueryable?

66

Eu tenho visto muitos projetos que têm repositórios que retornam instâncias de IQueryable. Isso permite que filtros adicionais e classificação possam ser executados IQueryablepor outro código, que se traduz em diferentes SQL sendo gerados. Estou curioso de onde veio esse padrão e se é uma boa ideia.

Minha maior preocupação é que um IQueryableé uma promessa de atingir o banco de dados algum tempo depois, quando for enumerado. Isso significa que um erro seria lançado fora do repositório. Isso pode significar que uma exceção do Entity Framework é lançada em uma camada diferente do aplicativo.

Também já tive problemas com o MARS (Multiple Active Result Sets) no passado (especialmente ao usar transações) e essa abordagem parece que levaria a isso acontecer com mais frequência.

Eu sempre liguei AsEnumerableou ToArrayno final de cada uma das minhas expressões LINQ para garantir que o banco de dados seja atingido antes de sair do código do repositório.

Gostaria de saber se o retorno IQueryablepoderia ser útil como um bloco de construção para uma camada de dados. Eu vi um código bastante extravagante com um repositório chamando outro repositório para criar um ainda maior IQueryable.

Travis Parks
fonte
Qual seria a diferença se o banco de dados não estivesse disponível no momento da execução da consulta adiada versus o tempo de construção da consulta? O código consumidor teria que lidar com essa situação independentemente.
Steven Evers
Pergunta interessante, mas provavelmente será difícil de responder. Uma simples pesquisa mostra um debate bastante acalorado sobre sua pergunta: duckduckgo.com/?q=repository+return+iqueryable
Corbin de Março de
6
Tudo se resume a se você deseja permitir que seus clientes adicionem cláusulas do Linq que poderiam se beneficiar da execução adiada. Se eles adicionarem uma wherecláusula a um adiado IQueryable, você só precisará enviar esses dados pela conexão, não todo o conjunto de resultados.
Robert Harvey
Se você estiver usando o padrão de repositório - não, não deverá retornar IQueryable. Aqui está um bom porquê .
Arnis Lapsa 26/03
11
@SteveEvers Há uma enorme diferença. Se uma exceção for lançada no meu repositório, posso envolvê-la com um tipo de exceção específico da camada de dados. Se isso acontecer mais tarde, eu tenho que capturar o tipo de exceção original lá. Quanto mais eu estiver perto da fonte da exceção, mais provavelmente vou saber o que a causou. Isso é importante para criar mensagens de erro significativas. IMHO, permitir que uma exceção específica da EF saia do meu repositório é uma violação do encapsulamento.
Travis Parks

Respostas:

47

O retorno do IQueryable definitivamente proporcionará mais flexibilidade aos consumidores do repositório. Isso coloca a responsabilidade de reduzir os resultados para o cliente, o que naturalmente pode ser um benefício e uma muleta.

No lado bom, você não precisará criar toneladas de métodos de repositório (pelo menos nessa camada) - GetAllActiveItems, GetAllNonActiveItems, etc. - para obter os dados desejados. Dependendo da sua preferência, novamente, isso pode ser bom ou ruim. Você vai (/ deve) precisa definir contratos comportamentais que suas implementações aderem, mas onde isso vai é até você.

Portanto, você pode colocar a lógica de recuperação fora do repositório e deixá-la ser usada da maneira que o usuário desejar. Portanto, a exposição do IQueryable oferece a maior flexibilidade e permite consultas eficientes, em oposição à filtragem na memória, etc., e pode reduzir a necessidade de criar vários métodos específicos de busca de dados.

Por outro lado, agora você deu a seus usuários uma espingarda. Eles podem fazer coisas que você pode não ter planejado (usar excessivamente .include (), fazer consultas pesadas e filtrar na memória em suas respectivas implementações, etc.), o que basicamente ajudaria os controles de camadas e comportamentais porque você forneceu acesso total.

Portanto, dependendo da equipe, da experiência, do tamanho do aplicativo, das camadas e da arquitetura gerais ... depende: - \

hanzolo
fonte
Observe que você pode, se quiser trabalhar para isso, retornar seu IQueryable, que por sua vez chama o DB, mas adiciona controles de camadas e comportamentais.
Psr
11
@psr Você poderia explicar o que você quer dizer com isso?
Travis Parks
11
@TravisParks - O IQueryable não precisa ser implementado pelo banco de dados, você pode escrever as classes do IQueryable por conta própria - é muito difícil. Portanto, você pode escrever um wrapper IQueryable em torno de um banco de dados IQueryable e ter seu IQueryable limite de acesso total ao banco de dados subjacente. Mas isso já é difícil o suficiente para que eu não esperasse que fosse feito amplamente.
Psr
2
Observe que, mesmo quando você não retorna IQueryables, você ainda pode impedir a criação de muitos métodos (GetAllActiveItems, GetAllNonActiveItems, etc) simplesmente passando um Expression<Func<Foo,bool>>para o seu método. Esses parâmetros podem ser passados Wherediretamente para o método, permitindo ao consumidor escolher seu filtro sem precisar de acesso direto ao IQueryable <Foo>.
Flater
11
@hanzolo: Depende do que você quer dizer com refatoração. É verdade que alterar o design (por exemplo, adicionar novos campos ou alterar as relações) é mais problemático quando você tem uma base de código em camadas e pouco acoplada; mas refatorar é menos trabalhoso quando você só precisa alterar uma camada. Tudo depende se você espera reprojetar o aplicativo ou se está refatorando sua implementação da mesma arquitetura básica. Eu ainda defenderia o acoplamento solto em ambos os casos, mas você não está errado ao adicionar isso à refatoração ao alterar os conceitos principais de domínio.
Flater
29

Expor IQueryable a interfaces públicas não é uma boa prática. O motivo é fundamental: você não pode fornecer a realização IQueryable como é dito.

Interface pública é um contrato entre provedor e clientes. E na maioria dos casos, há uma expectativa de implementação completa. IQueryable é uma fraude:

  1. IQueryable é quase impossível de implementar. E atualmente não há solução adequada. Mesmo o Entity Framework possui muitas UnsupportedExceptions .
  2. Hoje, os provedores de consulta dependem muito da infraestrutura. O Sharepoint possui sua própria implementação parcial e o provedor My SQL possui uma implementação parcial distinta.

O padrão do repositório nos dá uma representação clara por camadas e, mais importante, pela independência da realização. Para que possamos mudar na fonte de dados futura.

A pergunta é " Deve haver possibilidade de substituição da fonte de dados? ".

Se amanhã parte dos dados puder ser movida para serviços SAP, banco de dados NoSQL ou simplesmente para arquivos de texto, podemos garantir a implementação adequada da interface IQueryable?

( mais pontos positivos sobre por que isso é uma má prática)

Artru
fonte
Esta resposta não se sustenta por si só sem consultar o link externo volátil. Considere resumir pelo menos os pontos principais apresentados pelo recurso externo e tente manter a opinião pessoal fora da sua resposta ("pior idéia que já ouvi"). Considere também remover o snippet de código que parece não ter relação com nada que seja IQueryable.
Lars Viklund
11
Obrigado @LarsViklund, a resposta é atualizada com exemplos e descrição do problema
Artru
19

Realisticamente, você tem três alternativas se desejar uma execução adiada:

  • Faça assim - exponha um IQueryable.
  • Implemente uma estrutura que exponha métodos específicos para filtros ou "perguntas" específicas. ( GetCustomersInAlaskaWithChildren, abaixo)
  • Implemente uma estrutura que expõe uma API de filtro / classificação / página fortemente tipada e construa a IQueryableinternamente.

Prefiro o terceiro (apesar de ter ajudado colegas a implementar o segundo também), mas obviamente há alguma configuração e manutenção envolvidas. (T4 para o resgate!)

Edit : Para esclarecer a confusão em torno do que estou falando, considere o seguinte exemplo roubado do IQueryable: Can Kill Your Dog, Steal Your Wife, Kill Your Will to Live, etc.

No cenário em que você exporia algo como isto:

public class CustomerRepo : IRepo 
{ 
     private DataContext ct; 
     public Customer GetCustomerById(int id) { ... } 
     public Customer[] GetCustomersInAlaskaWithChildren() { ... } 
} 

a API da qual estou falando permitiria que você expusesse um método que permitisse expressar GetCustomersInAlaskaWithChildren(ou qualquer outra combinação de critérios) de maneira flexível, e o repositório o executaria como um IQueryable e retornaria os resultados para você. Mas o importante é que ele seja executado dentro da camada do repositório, para que ainda aproveite a execução adiada. Depois de recuperar os resultados, você ainda poderá vincular o LINQ-to-objects ao conteúdo do seu coração.

Outra vantagem de uma abordagem como essa é que, por serem classes POCO, esse repositório pode ficar atrás de um serviço da Web ou WCF; pode ser exposto ao AJAX ou a outros chamadores que não sabem a primeira coisa sobre o LINQ.

GalacticCowboy
fonte
4
Então, você criaria uma API de consulta para substituir a API de consulta interna do .NET?
Michael Brown
4
Soa bastante inútil para mim
ozz
7
@ MikeBrown O objetivo desta pergunta - e minha resposta - é que você deseja expor o comportamento ou as vantagens de, IQueryable sem expor o IQueryablepróprio . Como você fará isso com a API incorporada?
GalacticCowboy
2
Essa é uma tautologia muito boa. Você não explicou por que deseja evitar isso. Heck WCF Data Services expõe IQueryable através do fio. Não estou tentando entender você, apenas não estou entendendo essa aversão ao IQueryable que as pessoas parecem ter.
Michael Brown
2
Se você vai usar apenas um IQueryable, por que você usaria um repositório? Repositórios destinam-se a ocultar detalhes de implementação. (Além disso, o WCF Data Services é um pouco mais novo do que a maioria das soluções em que trabalhei nos últimos 4 anos que usavam um repositório como este ... Portanto, não é provável que elas sejam atualizadas em breve, apenas para garantir utilização de um brinquedo novo brilhante).
GalacticCowboy
8

Realmente existe apenas uma resposta legítima: depende de como o repositório deve ser usado.

Em um extremo, seu repositório é um invólucro muito fino em torno de um DBContext, para que você possa injetar uma camada de testabilidade em um aplicativo orientado a banco de dados. Realmente, não há expectativa no mundo real de que isso possa ser usado de maneira desconectada sem um banco de dados compatível com LINQ por trás dele, porque seu aplicativo CRUD nunca precisará disso. Então, claro, por que não usar IQueryable? Eu provavelmente preferiria o IEnumarable, pois você obtém a maioria dos benefícios [leia-se: execução atrasada] e não parece tão sujo.

A menos que você esteja no extremo, tentarei tirar proveito do espírito do padrão de repositório e retornar coleções materializadas apropriadas que não impliquem uma conexão de banco de dados subjacente.

Wyatt Barnett
fonte
6
Em relação ao retorno IEnumerable, é importante observar que ((IEnumerable<T>)query).LastOrDefault()é muito diferente de query.LastOrDefault()(presumir que queryseja um IQueryable). Portanto, faz sentido retornar IEnumerablese você repetir todos os elementos de qualquer maneira.
Lou
7
 > Should Repositories return IQueryable?

NÃO, se você deseja fazer desenvolvimento / teste sem orientação, porque não há uma maneira fácil de criar um repositório simulado para testar sua businesslogic (= repository-consumer) isoladamente do banco de dados ou de outro provedor IQueryable.

k3b
fonte
6

Do ponto de vista da arquitetura pura, o IQueryable é uma abstração com vazamento. Como uma generalidade, fornece ao usuário muita energia. Dito isto, há lugares onde faz sentido. Se você estiver usando OData, o IQueryable torna extremamente fácil fornecer um ponto de extremidade que seja facilmente filtrável, classificável, agrupável ... etc. Na verdade, prefiro criar DTOs para os quais minhas entidades mapeiam e o IQueryable retorna o DTO. Dessa forma, pré-filtro meus dados e realmente só uso o método IQueryable para permitir a personalização dos resultados pelo cliente.

Slick86
fonte
6

Eu já enfrentei essa escolha também.

Então, vamos resumir os lados positivo e negativo:

Positivos:

  • Flexibilidade. É definitivamente flexível e conveniente permitir que o lado do cliente crie consultas personalizadas com apenas um método de repositório

Negativos:

  • Não testável . (na verdade, você estará testando a implementação IQueryable subjacente, que geralmente é o seu ORM. Mas você deve testar sua lógica)
  • Desfoca a responsabilidade . É impossível dizer o que esse método específico do seu repositório faz. Na verdade, é um método divino, que pode retornar o que você quiser em todo o banco de dados
  • Inseguro . Sem restrições sobre o que pode ser consultado por esse método de repositório, em alguns casos essas implementações podem ser inseguras do ponto de segurança.
  • Problemas de armazenamento em cache (ou, para ser genérico, qualquer problema de pré ou pós-processamento). Geralmente, não é aconselhável, por exemplo, armazenar em cache tudo em seu aplicativo. Você tentará armazenar em cache algo que cause uma carga significativa no banco de dados. Mas como fazer isso, se você tiver apenas um método de repositório, que os clientes usam para emitir praticamente qualquer consulta no banco de dados?
  • Problemas de log . O registro de algo na camada do repositório fará com que você inspecione o IQueryable primeiro para verificar se essa chamada específica é registrada ou não.

Eu recomendaria não usar essa abordagem. Em vez disso, considere criar várias especificações e implementar métodos de repositório, que podem consultar o banco de dados usando esses. O padrão de especificação é uma ótima maneira de encapsular um conjunto de condições.

Vladislav Rastrusny
fonte
5

Eu acho que expor o IQueryable no seu repositório é perfeitamente aceitável durante as fases iniciais do desenvolvimento. Existem ferramentas de interface do usuário que podem trabalhar diretamente com o IQueryable e lidar com classificação, filtragem, agrupamento etc.

Dito isto, acho que apenas expor crud no repositório e encerrar o dia é irresponsável. Ter operações úteis e auxiliares de consulta para consultas comuns permite centralizar a lógica de uma consulta enquanto expõe uma superfície de interface menor para os consumidores do repositório.

Nas primeiras iterações de um projeto, a exposição pública do IQueryable no repositório permite um crescimento mais rápido. Antes da primeira versão completa, eu recomendaria tornar a operação de consulta privada ou protegida e colocar as consultas selvagens sob o mesmo teto.

Michael Brown
fonte
4

O que eu vi feito (e o que estou me implementando) é o seguinte:

public interface IEntity<TKey>
{
    TKey Id { get; set; }
}

public interface IRepository<TEntity, in TKey> where TEntity : IEntity<TKey>
{
    void Create(TEntity entity);
    TEntity Get(TKey key);
    IEnumerable<TEntity> GetAll();
    IEnumerable<TEntity> GetAll(Expression<Func<TEntity, bool>> expression);
    void Update(TEntity entity);
    void Delete(TKey id);
}

O que isso permite que você faça é ter a flexibilidade de fazer uma IQueryable.Where(a => a.SomeFilter)consulta em seu concreteRepo.GetAll(a => a.SomeFilter)repositório, sem expor nenhum negócio LINQy.

dav_i
fonte