Entidades do Entity Framework - alguns dados do serviço da Web - melhor arquitetura?

10

Atualmente, estamos usando o Entity Framework como um ORM em alguns aplicativos da Web e, até agora, ele nos serviu bem, pois todos os nossos dados são armazenados em um único banco de dados. Estamos usando o padrão de repositório e temos serviços (a camada de domínio) que os utilizam e retornam as entidades EF diretamente aos controladores do ASP.NET MVC.

No entanto, surgiu um requisito para utilizar uma API de terceiros (por meio de um serviço da Web) que nos fornecerá informações extras relacionadas ao usuário em nosso banco de dados. Em nosso banco de dados local de Usuários, armazenaremos um ID externo que podemos fornecer à API para obter informações adicionais. Há bastante informação disponível, mas por uma questão de simplicidade, uma delas se refere à empresa do usuário (nome, gerente, sala, cargo, local etc.). Essas informações serão usadas em vários locais em nossos aplicativos da web - em vez de serem usadas em um único local.

Então, minha pergunta é: qual é o melhor lugar para preencher e acessar essas informações? Como é usado em vários lugares, não é realmente sensato buscá-lo ad-hoc onde quer que o aplicativo seja usado - portanto, faz sentido retornar esses dados adicionais da camada de domínio.

Meu pensamento inicial era apenas criar uma classe de modelo de wrapper que contivesse a entidade EF (EFUser) e uma nova classe 'ApiUser' contendo as novas informações - e quando obtemos um usuário, obtemos o EFUser e, em seguida, obtemos o adicional informações da API e preencha o objeto ApiUser. No entanto, embora isso seja bom para obter usuários únicos, ele cai ao receber vários usuários. Não podemos acessar a API ao obter uma lista de usuários.

Meu segundo pensamento foi apenas adicionar um método singleton à entidade EFUser que retorna o ApiUser e preenchê-lo quando necessário. Isso resolve o problema acima, pois só o acessamos quando precisamos.

Ou o pensamento final era manter uma cópia local dos dados em nosso banco de dados e sincronizá-la com a API quando o usuário efetuar login. Isso é um trabalho mínimo, pois é apenas um processo de sincronização - e não temos a sobrecarga de acessar o banco de dados e a API sempre que queremos obter informações do usuário. No entanto, isso significa armazenar os dados em dois locais e também significa que os dados estão desatualizados para qualquer usuário que não esteja conectado há um tempo.

Alguém tem algum conselho ou sugestão sobre a melhor forma de lidar com esse tipo de cenário?

stevehayter
fonte
it's not really sensible to fetch it on an ad-hoc basis-- Por quê? Por razões de desempenho?
Robert Harvey
Não quero dizer que adquira a API ad-hoc - apenas mantenha a estrutura existente da entidade e, em seguida, chame-a ad-hoc no aplicativo da Web quando necessário - apenas quis dizer que isso não seria possível. sensato, pois precisaria ser feito em muitos lugares.
Stevehayter

Respostas:

3

Seu caso

No seu caso, todas as três opções são viáveis. Acho que a melhor opção é provavelmente sincronizar suas fontes de dados em algum lugar em que o aplicativo asp.net não esteja ciente. Ou seja, evite sempre as duas buscas em primeiro plano, sincronize a API com o banco de dados silenciosamente). Então, se essa é uma opção viável no seu caso - eu digo.

Uma solução em que você faz a busca 'uma vez' como a outra resposta sugere não parece muito viável, pois não persiste a resposta em nenhum lugar e o ASP.NET MVC fará a busca para cada solicitação repetidamente.

Eu evitaria o singleton, não acho que seja uma boa ideia por muitas das razões usuais.

Se a terceira opção não for viável - uma opção é carregá-la com preguiça. Ou seja, faça com que uma classe estenda a entidade e atinja a API conforme a necessidade . Essa é uma abstração muito perigosa, já que é um estado ainda mais mágico e não óbvio.

Eu acho que realmente se resume a várias perguntas:

  • Com que frequência os dados de chamada da API são alterados? Não frequente? Terceira opção. Frequentemente? De repente, a terceira opção não é muito viável. Não sei se sou tão contra chamadas ad-hoc quanto você.
  • Qual é o preço de uma chamada de API? Você paga por chamada? Eles são rápidos? Livre? Se eles são rápidos, fazer uma ligação a cada vez pode funcionar; se eles são lentos, você precisa ter algum tipo de previsão e fazer as chamadas. Se eles custam dinheiro - isso é um grande incentivo para o cache.
  • Qual a velocidade do tempo de resposta? Obviamente, mais rápido é melhor, mas sacrificar a velocidade pela simplicidade pode valer a pena em alguns casos, especialmente se ele não estiver enfrentando diretamente um usuário.
  • Qual a diferença entre os dados da API e os seus dados? São duas coisas conceitualmente diferentes? Nesse caso, pode ser ainda melhor expor a API para fora do que retornar o resultado da API diretamente com o resultado e deixar o outro lado fazer a segunda chamada e gerenciar o gerenciamento.

Uma ou duas palavras sobre separação de preocupações

Permitam-me argumentar contra o que Bobson está dizendo sobre a separação de preocupações aqui. No final das contas, colocar essa lógica em entidades como essa viola a separação de preocupações da mesma forma.

Ter esse repositório viola a separação de preocupações com a mesma lógica, colocando a lógica centrada na apresentação na camada de lógica de negócios. Agora, seu repositório está ciente das coisas relacionadas à apresentação, como como você exibe o usuário nos controladores asp.net mvc.

Em esta questão relacionada Pedi sobre como acessar as entidades directamente a partir de um controlador. Permitam-me citar uma das respostas aqui:

"Bem-vindo à BigPizza, a pizzaria personalizada, posso receber seu pedido?" "Bem, eu gostaria de ter uma pizza com azeitonas, mas molho de tomate por cima e queijo no fundo e asse no forno por 90 minutos até que fique preto e duro como uma pedra plana de granito." "OK, senhor, pizzas personalizadas são a nossa profissão, nós vamos conseguir."

O caixa vai para a cozinha. "Tem um psicopata no balcão, ele quer tomar uma pizza com ... é uma pedra de granito com ... espera ... precisamos ter um nome primeiro", diz o cozinheiro.

"Não!", Grita a cozinheira, "de novo! Você sabe que já tentamos isso." Ele pega uma pilha de papel com 400 páginas, "aqui temos pedras de granito de 2005, mas ... não tinha azeitonas, mas paprica em vez disso ... ou aqui está o melhor tomate ... mas o cliente queria cozido apenas meio minuto. " "Talvez devêssemos chamá-lo de TopTomatoGraniteRockSpecial?" "Mas isso não leva em conta o queijo no fundo ..." O caixa: "Isso é o que o Special deve expressar." "Mas ter a pedra da pizza formada como uma pirâmide também seria especial", responde o cozinheiro. "Hmmm ... é difícil ...", diz o caixa desesperado.

"A MINHA PIZZA JÁ ESTÁ NO FORNO?", De repente ela grita pela porta da cozinha. "Vamos parar essa discussão, apenas me diga como fazer essa pizza, não vamos ter uma pizza dessa segunda vez", decide a cozinheira. "OK, é uma pizza com azeitonas, mas molho de tomate por cima e queijo no fundo e asse no forno por 90 minutos até que fique preto e duro como uma rocha plana de granito."

(Leia o resto da resposta, é realmente legal).

É ingênuo ignorar o fato de que há um banco de dados - existe um banco de dados e, por mais que você queira abstrair isso, ele não vai a lugar nenhum. Seu aplicativo estará ciente da fonte de dados. Você não poderá trocá-lo a quente. Os ORMs são úteis, mas vazam devido à complexidade do problema que resolvem e por várias razões de desempenho (como o Select n + 1, por exemplo).

Benjamin Gruenbaum
fonte
Obrigado pela sua resposta completa @Benjamin. Inicialmente, comecei a criar um protótipo usando a solução de Bobson acima (mesmo antes de ele postar sua resposta), mas você levanta alguns pontos importantes. Para responder às suas perguntas:: - A chamada à API não é muito cara (é grátis e também rápida). - Algumas partes dos dados mudam com bastante regularidade (algumas até a cada duas horas). - A velocidade é bastante importante, mas o público do aplicativo é tal que o carregamento rápido e extremamente rápido não é um requisito absoluto.
Stevehayter
@stevehayter Nesse caso, eu provavelmente faria as chamadas para a API do lado do cliente. É mais barato e rápido, e oferece um controle mais refinado.
Benjamin Gruenbaum
1
Com base nessas respostas, estou menos inclinado a manter uma cópia local dos dados. Na verdade, estou inclinado a expor a API separadamente e manipular os dados adicionais dessa maneira. Acho que esse pode ser um bom compromisso entre a simplicidade da solução da @ Bobson, mas também adiciona um grau de separação com o qual me sinto um pouco mais confortável. Vou olhar para essa estratégia no meu protótipo e relatar com minhas descobertas (e premiar a recompensa!).
Stevehayter
@BenjaminGruenbaum - Não tenho certeza se sigo o seu argumento. Como minha sugestão conscientiza o repositório da apresentação? Claro, está ciente de que um campo respaldado por API foi acessado, mas isso não tem nada a ver com o que a exibição está fazendo com essas informações.
21413 Bobson
1
Optei por mover tudo para o lado do cliente - mas como um método de extensão no EFUser (que existe na camada de apresentação, em um assembly separado). O método simplesmente retorna os dados da API e define um singleton para que não seja atingido repetidamente. A vida útil dos objetos é tão curta que não tenho problema em usar um singleton aqui. Dessa forma, existe algum grau de separação, mas ainda tenho a conveniência de trabalhar com a entidade EFUser. Obrigado a todos os entrevistados por sua ajuda. Definitivamente uma discussão interessante :).
stevehayter
2

Com uma separação adequada de preocupações , nada acima do nível do Entity Framework / API deve perceber de onde os dados vêm. A menos que a chamada à API seja cara (em termos de tempo ou processamento), o acesso aos dados que a utilizam deve ser tão transparente quanto o acesso aos dados do banco de dados.

A melhor maneira de implementar isso, então, seria adicionar propriedades extras ao EFUserobjeto que carregasse preguiçosamente os dados da API, conforme necessário. Algo assim:

partial class EFUser
{
    private APIUser _apiUser;
    private APIUser ApiUser
    {
       get { 
          if (_apiUser == null) _apiUser = API.LoadUser(this.ExternalID);
          return _apiUser;
       }
    }
    public string CompanyName { get { return ApiUser.UserCompanyName; } }
    public string JobTitle{ get { return ApiUser.UserJobTitle; } }
}

Externamente, a primeira vez CompanyNameou JobTitleé usado haverá uma única chamada de API feita (e, portanto, um pequeno atraso), mas todas as chamadas subsequentes até que o objeto é destruído será tão rápido e fácil como acessar banco de dados.

Bobson
fonte
Obrigado @Bobson ... essa foi realmente a rota inicial que comecei a seguir (com alguns métodos de extensão adicionados para carregar em massa os detalhes das listas de usuários - por exemplo, exibindo o nome da empresa para uma lista de usuários). Parece atender bem às minhas necessidades até agora - mas Benjamin abaixo levanta alguns pontos importantes, então vou continuar avaliando esta semana.
Stevehayter
0

Uma idéia é modificar o ApiUser para nem sempre ter as informações extras. Em vez disso, você coloca um método no ApiUser para buscá-lo:

ApiUser apiUser = backend.load($id);
//Locally available data directly on the ApiUser like so:
String name = apiUser.getName();
//Expensive extra data available after extra call:
UserExtraData extraData = apiUser.fetchExtraData();
String managerName = extraData.getManagerName();

Você também pode modificar isso levemente para usar o carregamento lento de dados extras, para que você não precise extrair UserExtraData do objeto ApiUser:

//Extra data fetched on first get:
String managerName = apiUser.lazyGetExtraData().getManagerName();

Dessa forma, quando você tiver uma lista, os dados extras não serão buscados por padrão. Mas você ainda pode acessá-lo enquanto percorre a lista!

Alexander Torstling
fonte
Mas não tenho muita certeza do que você quer dizer aqui - em backend.load (), já estamos fazendo uma carga -, com certeza isso carregaria os dados da API?
stevehayter
Quero dizer que você deve esperar fazendo a carga extra até ser solicitado explicitamente - carregue preguiçosamente os dados da API.
Alexander Torstling