Entidades Entidades del marco - Algunos datos del servicio web - ¿La mejor arquitectura?

10

Actualmente estamos utilizando Entity Framework como ORM en algunas aplicaciones web, y hasta ahora, nos ha ido bien, ya que todos nuestros datos se almacenan en una sola base de datos. Estamos utilizando el patrón de repositorio, y tenemos servicios (la capa de dominio) que los utilizan, y devuelven las entidades EF directamente a los controladores ASP.NET MVC.

Sin embargo, ha surgido un requisito para utilizar una API de terceros (a través de un servicio web) que nos proporcionará información adicional relacionada con el usuario en nuestra base de datos. En nuestra base de datos de usuarios local, almacenaremos una identificación externa que podemos proporcionar a la API para obtener información adicional. Hay bastante información disponible, pero en aras de la simplicidad, una de ellas se refiere a la empresa del usuario (nombre, gerente, sala, cargo, ubicación, etc.). Esta información se usará en varios lugares a través de nuestras aplicaciones web, en lugar de usarse en un solo lugar.

Entonces mi pregunta es, ¿dónde está el mejor lugar para poblar y acceder a esta información? Como se usa en varios lugares, no es realmente sensato buscarlo ad-hoc donde sea que lo usemos en la aplicación web, por lo que tiene sentido devolver estos datos adicionales de la capa de dominio.

Mi pensamiento inicial fue crear una clase de modelo de contenedor que contendría la entidad EF (EFUser), y una nueva clase 'ApiUser' que contiene la nueva información, y cuando obtenemos un usuario, obtenemos el EFUser y luego obtenemos el adicional información de la API y rellene el objeto ApiUser. Sin embargo, si bien esto estaría bien para obtener usuarios individuales, se cae cuando se obtienen múltiples usuarios. No podemos alcanzar la API cuando obtenemos una lista de usuarios.

Mi segundo pensamiento fue simplemente agregar un método singleton a la entidad EFUser que devuelve el ApiUser, y completarlo cuando sea necesario. Esto resuelve el problema anterior ya que solo accedemos a él cuando lo necesitamos.

O el pensamiento final fue mantener una copia local de los datos en nuestra base de datos y sincronizarla con la API cuando el usuario inicia sesión. Esto es un trabajo mínimo, ya que es solo un proceso de sincronización, y no tenemos la carga de golpear. la base de datos y la API cada vez que queremos obtener información del usuario. Sin embargo, esto significa almacenar los datos en dos lugares, y también significa que los datos están desactualizados para cualquier usuario que no haya iniciado sesión por un tiempo.

¿Alguien tiene algún consejo o sugerencia sobre la mejor manera de manejar este tipo de escenario?

stevehayter
fuente
it's not really sensible to fetch it on an ad-hoc basis-- ¿Por qué? Por razones de rendimiento?
Robert Harvey
No me refiero a presionar la API de manera ad-hoc; solo me refiero a mantener la estructura de la entidad existente tal como está, y luego llamar a la API ad-hoc en la aplicación web cuando sea necesario, solo quería decir que esto no sería sensible, ya que tendría que hacerse en muchos lugares.
stevehayter

Respuestas:

3

Tu caso

En su caso, las tres opciones son viables. Creo que la mejor opción es probablemente sincronizar sus fuentes de datos en algún lugar que la aplicación asp.net ni siquiera conoce. Es decir, evite las dos recuperaciones en primer plano cada vez, sincronice la API con la base de datos en silencio). Entonces, si esa es una opción viable en su caso, le digo que lo haga.

Una solución en la que realiza la búsqueda 'una vez' como sugiere la otra respuesta no parece muy viable, ya que no persiste la respuesta en ningún lado y ASP.NET MVC solo hará la búsqueda para cada solicitud una y otra vez.

Evitaría el singleton, no creo que sea una buena idea por muchas de las razones habituales.

Si la tercera opción no es viable, una opción es cargarla perezosamente. Es decir, haga que una clase extienda la entidad y haga que llegue a la API según sea necesario . Sin embargo, esa es una abstracción muy peligrosa, ya que es aún más mágico y un estado no obvio.

Supongo que realmente se reduce a varias preguntas:

  • ¿Con qué frecuencia cambian los datos de llamadas de API? ¿No a menudo? Tercera opción ¿A menudo? De repente, la tercera opción no es demasiado viable. No estoy seguro de estar tan en contra de las llamadas ad-hoc como tú.
  • ¿Cuánto cuesta una llamada API? ¿Pagas por llamada? ¿Son rápidos? ¿Gratis? Si son rápidos, hacer una llamada cada vez podría funcionar, si son lentos, debe tener algún tipo de predicción y hacer las llamadas. Si cuestan dinero, es un gran incentivo para el almacenamiento en caché.
  • ¿Qué tan rápido debe ser el tiempo de respuesta? Obviamente, más rápido es mejor, pero sacrificar la velocidad por simplicidad puede valer la pena en algunos casos, especialmente si no se enfrenta directamente a un usuario.
  • ¿Qué tan diferentes son los datos API de sus datos? ¿Son dos cosas conceptualmente diferentes? Si es así, podría ser incluso mejor exponer la API al exterior en lugar de devolver el resultado de la API con el resultado directamente y dejar que el otro lado haga la segunda llamada y se encargue de administrarlo.

Una o dos palabras sobre la separación de preocupaciones

Permítanme argumentar en contra de lo que dice Bobson sobre la separación de preocupaciones aquí. Al final del día, poner esa lógica en entidades como esa viola la separación de las preocupaciones igual de malo.

Tener un repositorio de este tipo viola la separación de las preocupaciones de la misma manera al poner la lógica centrada en la presentación en la capa de lógica de negocios. Su repositorio ahora se da cuenta de repente de las cosas relacionadas con la presentación, como cómo muestra al usuario en sus controladores aspvc mvc.

En esta pregunta relacionada , he preguntado sobre el acceso a entidades directamente desde un controlador. Permítanme citar una de las respuestas allí:

"Bienvenido a BigPizza, la pizzería personalizada, ¿puedo tomar su pedido?" "Bueno, me gustaría tener una pizza con aceitunas, pero salsa de tomate encima y queso en la parte inferior y hornearla en el horno durante 90 minutos hasta que esté negra y dura como una roca plana de granito". "Bien, señor, las pizzas personalizadas son nuestra profesión, lo lograremos".

El cajero va a la cocina. "Hay un psicópata en el mostrador, quiere una pizza con ... es una roca de granito con ... espera ... necesitamos tener un nombre primero", le dice al cocinero.

"¡No!", Grita el cocinero, "¡no otra vez! Sabes que ya lo hemos intentado". Toma una pila de papel con 400 páginas, "aquí tenemos roca de granito de 2005, pero ... no tenía aceitunas, sino paprica en su lugar ... o aquí está el tomate superior ... pero el cliente lo quería horneado solo medio minuto ". "¿Tal vez deberíamos llamarlo TopTomatoGraniteRockSpecial?" "Pero no tiene en cuenta el queso en la parte inferior ..." El cajero: "Eso es lo que se supone que Special exprese". "Pero tener la roca Pizza formada como una pirámide también sería especial", responde el cocinero. "Hmmm ... es difícil ...", dice el cajero desesperado.

"¿YA ESTÁ MI PIZZA EN EL HORNO?", De repente grita por la puerta de la cocina. "Detengamos esta discusión, solo dígame cómo hacer esta pizza, no vamos a tener una pizza así por segunda vez", decide el cocinero. "Está bien, es una pizza con aceitunas, pero salsa de tomate en la parte superior y queso en la parte inferior y hornea en el horno durante 90 minutos hasta que esté negra y dura como una roca plana de granito".

(Lea el resto de la respuesta, es realmente bueno).

Es ingenuo ignorar el hecho de que hay una base de datos , hay una base de datos, y no importa cuán duro quiera abstraer eso, no va a ninguna parte. Su aplicación estará al tanto de la fuente de datos. No podrá 'intercambiarlo en caliente'. Los ORM son útiles, pero se filtran debido a lo complicado que es el problema que resuelven y por muchas razones de rendimiento (como Seleccionar n + 1, por ejemplo).

Benjamin Gruenbaum
fuente
Gracias por su minuciosa respuesta @Benjamin. Inicialmente comencé a crear un prototipo usando la solución de Bobson anterior (incluso antes de que publicara su respuesta), pero usted plantea algunos puntos importantes. Para responder a sus preguntas: - La llamada a la API no es muy costosa (son gratuitas y también rápidas). - Algunas partes de los datos cambiarán con bastante regularidad (algunas incluso cada dos horas). - La velocidad es bastante importante, pero la audiencia de la aplicación es tal que aligerar la carga rápida no es un requisito absoluto.
stevehayter
@stevehayter En ese caso, lo más probable es que realice las llamadas a la API desde el lado del cliente. Es más barato y más rápido, y le brinda un control más preciso.
Benjamin Gruenbaum
1
Según estas respuestas, me inclino menos por mantener una copia local de los datos. De hecho, me inclino por exponer la API por separado y manejar los datos adicionales de esa manera. Creo que esto puede ser un buen compromiso entre la simplicidad de la solución de @ Bobson, pero también agrega un grado de separación con el que me siento un poco más cómodo. Veré esta estrategia en mi prototipo e informaré con mis hallazgos (¡y otorgaré la recompensa!).
stevehayter
@BenjaminGruenbaum - No estoy seguro de seguir tu argumento. ¿Cómo hace mi sugerencia para que el repositorio tenga conocimiento de la presentación? Claro, es consciente de que se ha accedido a un campo respaldado por API, pero eso no tiene nada que ver con lo que la vista está haciendo con esa información.
Bobson
1
Opté por mover todo al lado del cliente, pero como método de extensión en el EFUser (que existe en la capa de presentación, en un ensamblaje separado). El método simplemente devuelve los datos de la API y establece un singleton para que no se golpee repetidamente. La vida útil de los objetos es tan corta que no tengo problemas para usar un singleton aquí. De esta manera, existe cierto grado de separación, pero aún así tengo la conveniencia de trabajar con la entidad EFUser. Gracias a todos los encuestados por su ayuda. Definitivamente una discusión interesante :).
stevehayter
2

Con una separación adecuada de las preocupaciones , nada por encima del nivel de Entity Framework / API debería darse cuenta de dónde provienen los datos. A menos que la llamada a la API sea costosa (en términos de tiempo o procesamiento), el acceso a los datos que lo utiliza debe ser tan transparente como el acceso a los datos desde la base de datos.

La mejor manera de implementar esto, entonces, sería agregar propiedades adicionales al EFUserobjeto que cargue de manera diferida los datos de la API según sea necesario. Algo como esto:

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, la primera vez que se use CompanyNameo JobTitlehabrá una sola llamada API (y, por lo tanto, un pequeño retraso), pero todas las llamadas posteriores hasta que se destruya el objeto serán tan rápidas y fáciles como el acceso a la base de datos.

Bobson
fuente
Gracias @Bobson ... esta fue realmente la ruta inicial que comencé a bajar (con algunos métodos de extensión agregados para cargar en masa los detalles de las listas de usuarios, por ejemplo, mostrar el nombre de la compañía para una lista de usuarios). Parece que se ajusta bien a mis necesidades hasta ahora, pero Benjamin a continuación plantea algunos puntos importantes, por lo que continuaré evaluando esta semana.
stevehayter
0

Una idea es modificar ApiUser para que no siempre tenga la información adicional. En cambio, pones un método en ApiUser para buscarlo:

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

También puede modificar esto ligeramente para usar la carga diferida de datos adicionales, de modo que no tenga que extraer UserExtraData del objeto ApiUser:

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

De esta manera, cuando tenga una lista, los datos adicionales no se recuperarán de forma predeterminada. ¡Pero aún puede acceder a él mientras recorre la lista!

Alexander Torstling
fuente
Sin embargo, no estoy muy seguro de lo que quiere decir aquí: en backend.load (), ya estamos haciendo una carga, ¿así que seguramente cargaría los datos de la API?
stevehayter
Quiero decir que debes esperar haciendo la carga adicional hasta que se solicite explícitamente: carga lenta los datos de la API.
Alexander Torstling