De acuerdo, no está claro exactamente qué IMappercontiene, pero sugiero algunas cosas, algunas de las cuales pueden no ser factibles debido a otras restricciones. Lo he escrito casi como lo he pensado, creo que ayuda ver el tren de ideas en acción, ya que eso hará que sea más fácil para ti hacer lo mismo la próxima vez. (Suponiendo que te gusten mis soluciones, por supuesto :)
LINQ es intrínsecamente funcional en estilo. Eso significa que, idealmente, las consultas no deberían tener efectos secundarios. Por ejemplo, esperaría un método con una firma de:
public IEnumerable<User> MapFrom(IEnumerable<User> users)
para devolver una nueva secuencia de objetos de usuario con información adicional, en lugar de mutar los usuarios existentes. La única información que agregas actualmente es el avatar, por lo que agregaría un método en Userla línea de:
public User WithAvatar(Image avatar)
{
// Whatever you need to create a clone of this user
User clone = new User(this.Name, this.Age, etc);
clone.FacebookAvatar = avatar;
return clone;
}
Incluso podría querer hacer que sea Usercompletamente inmutable; existen varias estrategias en torno a eso, como el patrón del constructor. Pregúntame si quieres más detalles. De todos modos, lo principal es que hemos creado un nuevo usuario que es una copia del antiguo, pero con el avatar especificado.
Primer intento: unión interna
Ahora volviendo a su asignador ... actualmente tiene tres métodos públicos , pero mi suposición es que solo el primero debe ser público y que el resto de la API en realidad no necesita exponer a los usuarios de Facebook. Parece que tu GetFacebookUsersmétodo está básicamente bien, aunque probablemente alinearé la consulta en términos de espacios en blanco.
Entonces, dada una secuencia de usuarios locales y una colección de usuarios de Facebook, nos queda hacer el bit de asignación real. Una cláusula de "unión" directa es problemática, ya que no generará usuarios locales que no tengan un usuario de Facebook coincidente. En cambio, necesitamos alguna forma de tratar a un usuario que no pertenece a Facebook como si fuera un usuario de Facebook sin un avatar. Esencialmente este es el patrón de objeto nulo.
Podemos hacer eso creando un usuario de Facebook que tenga un uid nulo (suponiendo que el modelo de objetos lo permita):
// Adjust for however the user should actually be constructed.
private static readonly FacebookUser NullFacebookUser = new FacebookUser(null);
Sin embargo, en realidad queremos una secuencia de estos usuarios, porque eso es lo que Enumerable.Concatusa:
private static readonly IEnumerable<FacebookUser> NullFacebookUsers =
Enumerable.Repeat(new FacebookUser(null), 1);
Ahora podemos simplemente "agregar" esta entrada ficticia a nuestra real, y hacer una combinación interna normal. Tenga en cuenta que esto supone que la búsqueda de los usuarios de Facebook siempre encontrará un usuario para cualquier UID de Facebook "real". Si ese no es el caso, tendríamos que volver a visitar esto y no usar una unión interna.
Incluimos al usuario "nulo" al final, luego hacemos la unión y el proyecto usando WithAvatar:
public IEnumerable<User> MapFrom(IEnumerable<User> users)
{
var facebookUsers = GetFacebookUsers(users).Concat(NullFacebookUsers);
return from user in users
join facebookUser in facebookUsers on
user.FacebookUid equals facebookUser.uid
select user.WithAvatar(facebookUser.Avatar);
}
Entonces la clase completa sería:
public sealed class FacebookMapper : IMapper
{
private static readonly IEnumerable<FacebookUser> NullFacebookUsers =
Enumerable.Repeat(new FacebookUser(null), 1);
public IEnumerable<User> MapFrom(IEnumerable<User> users)
{
var facebookUsers = GetFacebookUsers(users).Concat(NullFacebookUsers);
return from user in users
join facebookUser in facebookUsers on
user.FacebookUid equals facebookUser.uid
select user.WithAvatar(facebookUser.pic_square);
}
private Facebook.user[] GetFacebookUsers(IEnumerable<User> users)
{
var uids = (from u in users
where u.FacebookUid != null
select u.FacebookUid.Value).ToList();
// return facebook users for uids using WCF
}
}
Algunos puntos aquí:
- Como se señaló anteriormente, la unión interna se vuelve problemática si el UID de Facebook de un usuario no se puede recuperar como un usuario válido.
- De manera similar, tenemos problemas si tenemos usuarios duplicados de Facebook: ¡cada usuario local terminaría saliendo dos veces!
- Esto reemplaza (elimina) el avatar para usuarios que no son de Facebook.
Un segundo enfoque: unirse al grupo
Veamos si podemos abordar estos puntos. Asumiré que si hemos buscado varios usuarios de Facebook para un UID de Facebook único, entonces no importa a cuál de ellos tomemos el avatar: deben ser iguales.
Lo que necesitamos es una unión grupal, de modo que para cada usuario local obtengamos una secuencia de usuarios de Facebook coincidentes. Lo usaremos DefaultIfEmptypara hacer la vida más fácil.
Podemos mantenernos WithAvatarcomo estaba antes, pero esta vez solo vamos a llamarlo si tenemos un usuario de Facebook para tomar el avatar. Un grupo unirse en expresiones de consulta C # está representado por join ... into. Esta consulta es razonablemente larga, ¡pero no es demasiado aterradora, honesta!
public IEnumerable<User> MapFrom(IEnumerable<User> users)
{
var facebookUsers = GetFacebookUsers(users);
return from user in users
join facebookUser in facebookUsers on
user.FacebookUid equals facebookUser.uid
into matchingUsers
let firstMatch = matchingUsers.DefaultIfEmpty().First()
select firstMatch == null ? user : user.WithAvatar(firstMatch.pic_square);
}
Aquí está la expresión de consulta nuevamente, pero con comentarios:
// "Source" sequence is just our local users
from user in users
// Perform a group join - the "matchingUsers" range variable will
// now be a sequence of FacebookUsers with the right UID. This could be empty.
join facebookUser in facebookUsers on
user.FacebookUid equals facebookUser.uid
into matchingUsers
// Convert an empty sequence into a single null entry, and then take the first
// element - i.e. the first matching FacebookUser or null
let firstMatch = matchingUsers.DefaultIfEmpty().First()
// If we've not got a match, return the original user.
// Otherwise return a new copy with the appropriate avatar
select firstMatch == null ? user : user.WithAvatar(firstMatch.pic_square);
La solución no LINQ
Otra opción es usar LINQ muy levemente. Por ejemplo:
public IEnumerable<User> MapFrom(IEnumerable<User> users)
{
var facebookUsers = GetFacebookUsers(users);
var uidDictionary = facebookUsers.ToDictionary(fb => fb.uid);
foreach (var user in users)
{
FacebookUser fb;
if (uidDictionary.TryGetValue(user.FacebookUid, out fb)
{
yield return user.WithAvatar(fb.pic_square);
}
else
{
yield return user;
}
}
}
Esto utiliza un bloque iterador en lugar de una expresión de consulta LINQ. ToDictionarylanzará una excepción si recibe la misma clave dos veces; una opción para evitar esto es cambiar GetFacebookUserspara asegurarse de que solo busque ID distintos:
private Facebook.user[] GetFacebookUsers(IEnumerable<User> users)
{
var uids = (from u in users
where u.FacebookUid != null
select u.FacebookUid.Value).Distinct().ToList();
// return facebook users for uids using WCF
}
Eso supone que el servicio web funciona correctamente, por supuesto, pero si no lo hace, probablemente quiera lanzar una excepción de todos modos :)
Conclusión
Elija entre los tres. La unión grupal es probablemente la más difícil de entender, pero se comporta mejor. La solución de bloque iterador es posiblemente la más simple, y debería comportarse bien con la GetFacebookUsersmodificación.
Sin Userembargo, hacer que sea inmutable sería un paso positivo.
Un buen subproducto de todas estas soluciones es que los usuarios salen en el mismo orden en que ingresaron. Eso puede no ser importante para usted, pero puede ser una buena propiedad.
Espero que esto ayude, ha sido una pregunta interesante :)
EDITAR: ¿Es la mutación el camino a seguir?
Después de haber visto en sus comentarios que el tipo de Usuario local es en realidad un tipo de entidad del marco de la entidad, puede que no sea apropiado tomar este curso de acción. Hacer que sea inmutable es casi imposible, y sospecho que la mayoría de los usos del tipo esperarán una mutación.
Si ese es el caso, puede valer la pena cambiar tu interfaz para que quede más claro. En lugar de devolver una IEnumerable<User>(lo que implica, en cierta medida, una proyección), es posible que desee cambiar tanto la firma como el nombre, dejándolo con algo como esto:
public sealed class FacebookMerger : IUserMerger
{
public void MergeInformation(IEnumerable<User> users)
{
var facebookUsers = GetFacebookUsers(users);
var uidDictionary = facebookUsers.ToDictionary(fb => fb.uid);
foreach (var user in users)
{
FacebookUser fb;
if (uidDictionary.TryGetValue(user.FacebookUid, out fb)
{
user.Avatar = fb.pic_square;
}
}
}
private Facebook.user[] GetFacebookUsers(IEnumerable<User> users)
{
var uids = (from u in users
where u.FacebookUid != null
select u.FacebookUid.Value).Distinct().ToList();
// return facebook users for uids using WCF
}
}
Una vez más, esto ya no es una solución particularmente "LINQ-y" (en la operación principal), pero eso es razonable, ya que en realidad no estás "preguntando"; estás "actualizando"