i am implementing a relatively simple chat functionality, i use signalr and entity framework. My main problem is to avoid multiple database calls so I want to improve the code to get the chats.
This is my model:
public class Chat
{
[Key]
public int ChatID { get; set; }
[Required]
public DateTime FechaCreacionChat { get; set; }
public List<Mensaje> Mensaje { get; set; }
}
public class GroupChat: Chat
{
[StringLength(25)]
[Required]
public string NombreGroupChat { get; set; }
public List<UsersChat> UsersGroupChat { get; set; }
public string ImagenGroupChat { get; set; }
}
public class PrivateChat: Chat
{
public string Usuario1Id { get; set; }
public string Usuario2Id { get; set; }
public ApplicationUser Usuario1 { get; set; }
public ApplicationUser Usuario2 { get; set; }
}
I use inheritance to identify two types of chats: group and private.
public class Mensaje
{
[Key]
public int MensajeID { get; set; }
[Required]
public string TextoMensaje { get; set; }
public DateTime FechaMensaje { get; set; }
public string NombreArchivoMensaje { get; set; }
[Required]
public int ChatId { get; set; }
[Required]
public string UsuarioId { get; set; }
public ApplicationUser Usuario { get; set; }
public Chat Chat { get; set; }
}
public class UsersChat
{
[Key]
public int UsersChatID { get; set; }
[Required]
public string UsuarioId { get; set; }
[Required]
public int ChatId { get; set; }
public Chat Chat { get; set; }
public ApplicationUser Usuario { get; set; }
}
So in this method on signalr's ChatHub I get the private and group chats a user is in:
public async Task GetChats()//no debo obtener todos los chats
{
try
{
await Task.Run(async () =>
{
string name = Context.User.Identity.Name;
using (var db = new ApplicationDbContext())
{
List<ChatView> listChats = new List<ChatView>();
var usuario = await db.Users.SingleAsync(x => x.UserName == name);
var chatsPrivate = await db.Chat.OfType<PrivateChat>()
.Include(x => x.Usuario1)
.Include(x => x.Usuario2)
.Where(x => x.Usuario1Id == usuario.Id || x.Usuario2Id == usuario.Id)
.ToListAsync();
var chatsGroup = await db.UsersChat.Include(x => x.Chat)
.Where(x => x.UsuarioId == usuario.Id)
.Select(x => x.Chat)
.OfType<GroupChat>()
.ToListAsync();
var gListChats = chatsGroup.Select(chat => new ChatView
{
ID = chat.ChatID,
FechaCreacion = chat.FechaCreacionChat,
Nombre = chat.NombreGroupChat,
Conectado = false,
Mensaje = db.Mensaje.Where(m => m.ChatId == chat.ChatID).Any()?
db.Mensaje.Where(m => m.ChatId == chat.ChatID)
.OrderByDescending(m => m.FechaMensaje)
.Select(x => new MensajeView
{
FechaMensaje = x.FechaMensaje,
MensajeID = x.MensajeID,
NombreArchivoMensaje = x.NombreArchivoMensaje,
TextoMensaje = x.TextoMensaje,
Usuario = new UserView
{
Email = x.Usuario.Email,
UserName = x.Usuario.UserName,
PhoneNumber = x.Usuario.PhoneNumber
}
})
.First()
:
null,
Imagen = "ImageChat?chatId="+chat.ChatID+ "&nameImage=" + chat.ImagenGroupChat
});
var pListChats = chatsPrivate.Select(chat => new ChatView
{
ID = chat.ChatID,
FechaCreacion = chat.FechaCreacionChat,
Nombre = chat.Usuario1Id == usuario.Id ?
chat.Usuario2.UserName
:
chat.Usuario1.UserName,
Mensaje = db.Mensaje.Where(m => m.ChatId == chat.ChatID).Any()?
db.Mensaje.Where(m => m.ChatId == chat.ChatID)
.OrderByDescending(m => m.FechaMensaje)
.Select(x => new MensajeView
{
FechaMensaje = x.FechaMensaje,
MensajeID = x.MensajeID,
NombreArchivoMensaje = x.NombreArchivoMensaje,
TextoMensaje = x.TextoMensaje,
Usuario = new UserView
{
Email = x.Usuario.Email,
UserName = x.Usuario.UserName,
PhoneNumber = x.Usuario.PhoneNumber
}
})
.First()
:
null,
Conectado = chat.Usuario1Id == usuario.Id ?
db.Conexion.Any(x => x.UsuarioId == chat.Usuario2Id && x.ConectadoConexion == true)
:
db.Conexion.Any(x => x.UsuarioId == chat.Usuario1Id && x.ConectadoConexion == true),
Imagen = chat.Usuario1Id == usuario.Id ?
"ImagenPerfil?nameImage=" + chat.Usuario2.ImagenPerfil + "&name=" + chat.Usuario2.UserName
:
"ImagenPerfil?nameImage=" + chat.Usuario1.ImagenPerfil + "&name=" + chat.Usuario1.UserName
});
listChats.AddRange(gListChats);
listChats.AddRange(pListChats);
listChats.OrderByDescending(x => x.Mensaje.FechaMensaje);
await Clients.Caller.setChats(listChats);
}
});
}
catch (Exception e)
{
await Clients.Caller.showErrorMessage($"Se ha Producido un error en ChatHub => GetChats : \n{e.Message}\n{e.InnerException}");
}
}
It is working but I suspect that I make more calls than necessary, for example sometimes I have to do a Any()
to see if it exists and if so do the query, otherwise I put null to avoid an exception when I call First()
o Single()
. Example:
Mensaje = db.Mensaje.Where(m => m.ChatId == chat.ChatID).Any()?
db.Mensaje.Where(m => m.ChatId == chat.ChatID)
.OrderByDescending(m => m.FechaMensaje)
.Select(x => new MensajeView
{
FechaMensaje = x.FechaMensaje,
MensajeID = x.MensajeID,
NombreArchivoMensaje = x.NombreArchivoMensaje,
TextoMensaje = x.TextoMensaje,
Usuario = new UserView
{
Email = x.Usuario.Email,
UserName = x.Usuario.UserName,
PhoneNumber = x.Usuario.PhoneNumber
}
})
.First()
:
null
And this big snippet will be executed for each chat since the client waits for all chats with the last message of said chat.
This is what I show the client:
Instead of checking
Any()
if a record exists, you can useFirstOrDefault()
as this returnsnull
if there is no record which would be the same as returnnull
ifAny()
returnfalse
In other words, this code:
It is somewhat (see note) equivalent to:
Your code to check if there is a message would be:
This will not throw any error in case there is no record because the method
FirstOrDefault()
will be the one that executes the sql generated from the expression and if it does not return anything, then itnull
will be returned. So then you save the query by.Any()
making your query more efficient.Now, this is not applicable in all cases since if you use a
OrderBy()
in the query, you will not get the result you expect:It will not return the same as:
Since the first will get the first record without ascending order.
Note 1: I say somewhat because this:
It is not does the same as:
Because the first one has to first filter the collection and then check if a record exists in the already filtered collection, looping through the collection 2 times. While the second one, it stops going through the collection since a record is found, so it goes through the collection only 1 time.
Note 2: I would also like to clarify that:
It is equivalent to:
But only when using the Entity Framework, since it
FirstOrDefault()
is the method that executes the sql of the expression as well as theToList()
,First()
andAny()
. If you use it on an in-memory collection, note 1 shows the difference.You could reorganize the tables by normalizing and adding the basic fields:
For the initial call from the view loads : GetAllChats(int userId) then loads each user's active chat. GetAllMessages(int GroupChatId)
and signalr Update only new messages, filtered by user group UpdateNewMessage(int GroupChatId)