Blog post image

Comunicación en tiempo real con SignalR y VueJS

AuthorGenesis Rivera Rios
Date
9/21/2020
Time19 min read

Producto final

Summary of post

En este tutorial estaremos creando una aplicación de chat muy simple utilizando VueJS y SignalR.

Les advierto que el UI o el diseño no lo hice yo ya que esa parte no me gusta tanto suelo utilizar frameworks o código ya echo. En este caso escogí utilizar código hecho por otra persona él link original esta aquí. Eso si le hice unos cuantos cambios los cuales veremos en el transcurso del post.

La aplicación será un chat en el cual los usuarios se van a autogenerar y serán temporeros, no vamos a tener una base de datos persistente ya que estaremos utilizando una base de datos en memoria.

Esta aplicación tiene tres partes.

Project overview diagram

Vamos a comenzar creando nuestro API así que vamos a Visual Studio (Si no tienes visual studio utiliza esta guía para crear el API por el CLI)

En Visual Studio vamos a crear un nuevo proyecto y vamos a seleccionar ASP.NET Core Web Application y luego API.

Project template

Project template

Cuando tengamos nuestro proyecto creado vamos a remover las clases que Visual Studio nos auto-genero. Primero vamos a remover WeatherForecast.cs y luego en el directorio de Controller WeatherForecastController.cs

Ahora vamos a crear un nuevo proyecto el cual tendrá nuestros servicios y se encargara de interactuar con la base de datos.

El proyecto nuevo será un Class Library el cual nombrare Domain

Class Library

Si Visual Studio auto-genero una clase en el proyecto de Domain la puedes remover ya que no la utilizaremos. Ahora vamos a crear un directorio llamado Persistance en el nuevo proyecto en el cual vamos a crear nuestro Context.

Ahora adentro de ese nuevo directorio llamado Persistance vamos a crear una clase llamada Context.

Entity Framework

Nuestras interacciones con la base de datos serán utilizando Entity Framework. Vamos a instalar Entity Framework Core en nuestro proyecto, estaré instalando la versión 3.1.8 de Microsoft.EntityFrameworkCore la cual podemos instalar por nuestro Nuget Package Manager o por Package Manager Console.

Ahora volvemos a nuestra clase llamada Context en el proyecto de Domain. Este Contexto el cual va a heredar de DbContext var a representar una sesión con la base de datos que utilicemos con la cual podemos guardar instancias de nuestras entidades. Ahora vamos a crear nuestras entidades, las entidades van a representar tablas en nuestra base de datos.

Entities

Vamos a crear tres entidades una llamada User.cs.

public class User
{
	public Guid Id { get; set; }
	public string UserName { get; set; }
	public string PrimaryColorHex { get; set; }
	public string ProfilePicture { get; set; }
}

Contact.cs

public class Contact
{
	[Key]
	public int Id{ get; set; }

	public Guid UserId { get; set; }

	public Guid ContactId { get; set; }
}

Message.cs

public class Message
{
	[Key]
	public int Id{ get; set; }
	public Guid UserId { get; set; }
	public Guid ContactId { get; set; }
}

Ahora vamos a agregarlas a nuestro contexto así que vamos a volver a la clase de Context.cs.

public class Context : DbContext
{
	public DbSet<User> Users { get; set; }
	public DbSet<Message> Messages { get; set; }
	public DbSet<Contact> Contacts { get; set; }
}

Ahora vamos a configurar la base de datos en memoria. Así que tenemos que instalar otro paquete de Nuget llamado Microsoft.EntityFrameworkCore.InMemory

Luego de instalar este paquete vamos a volver a la clase de Context.cs y vamos a anular un método de la clase base (DbContext).

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
	optionsBuilder.UseInMemoryDatabase("InMemory");
}

Ya terminamos aquí, ahora vamos a crear un directorio llamado Services en el cual vamos a crear tres clases llamadas ContactService.cs, MessageService.cs y UserService.cs las cuales van a interactuar con la base de datos.

Vamos a comenzar con la clase de UserService en la cual vamos añadir un método llamado GetUser el cual nos va a traer un usuario de la base de datos el cual tenga un Id especifico.

public async Task<User> GetUser(Guid userId)
{
	var user = new User();
	try
	{
		using (var context = new Context())
		{
			user = await context.Users.Where(x => x.Id == userId).SingleOrDefaultAsync();
		}
	}
	catch (Exception ex)
	{
	}
	return user;
}

También vamos a añadir un método llamado GetUser igual que el anterior pero va a filtrar por nombre de usuario en vez de Id.

public User GetUser(string username)
{
	var user = new User();
	try
	{
		using (var context = new Context())
		{
			user = context.Users.Where(x => x.UserName == username).SingleOrDefault();
		}
	}
	catch (Exception ex)
	{
	}
	return user;
}

Para que estas líneas de código compilen necesitamos importar varios namespaces.

using System.Linq;
using Microsoft.EntityFrameworkCore;
using Domain.Models;
using Domain.Persistance;
using System.Threading.Tasks;

Ahora vamos a añadir otro método

public async Task<bool> CreateUser(User user)
{
	bool userWasCreated = false;
	try
	{
		using (var context = new Context())
		{
			await context.Users.AddAsync(user);
			userWasCreated = await context.SaveChangesAsync() == 1;
		}
	}
	catch (Exception ex)
	{
		userWasCreated = false;
	}
	return userWasCreated;
}

y

public async Task<List<User>> GetUserByName(string contactName, Guid userSearching)
{
	List<User> results = new List<User>();
	try
	{
		using (var context = new Context())
		{
			results = await context.Users.Where(x => x.UserName.Contains(contactName) && x.Id != userSearching).ToListAsync();
		}
	}
	catch (Exception ex)
	{

	}
	return results;
}

Luego de añadir estos métodos que interactúan con la base de datos vamos a añadir tres métodos que nos van a autogenerar el nombre de usuario, color e imagen de perfil en otras clases aparte.

Primero vamos a crear un directorio en nuestro proyecto llamado Domain el cual se va a llamar Helpers y vamos a crear tres clases una llamada AnimalList, ColorList e ImageList. Estas clases van a simplemente tener listas de valores que utilizaremos.

Ya que son listas largas solamente voy a ensenarles parte del código pero puede presionar aquí para ver la lista entera.

AnimaList.cs

public class AnimalList
{
	private static List<string> Animals { get; set; } = new List<string>()
	{
		"Aardvark",
		"Albatross",
		"Alligator",
		"Alpaca",
		"Ant",
		"Anteater",
		"Antelope",
		"Ape",
		"Armadillo",
public class ColorList
{
	private static Dictionary<string, string> Colors { get; set; } = new Dictionary<string, string>
	{
		{"Brown","#77dd77"},
		{"Baby Blue","#89cff0"},
		{"Purple","#b39eb5"},
		{"Red","#ff6961"},
		{"Pink","#ff9899"},
		{"Dim Gray","#3B3638"}
	};
public class Images
{
	private static List<string> ImageList { get; set; } = new List<string>
	{
		"analytics-graph-bar.svg",
		"baby-trolley.svg",
		"baggage.svg",
		"beach-parasol-water-1.svg",
		"biking-person.svg",
		"bin-2.svg",
		"binocular.svg",
		"bomb-grenade.svg",
		"building-modern-1.svg",
		"camera-flash.svg",

Ahora vamos a añadirle a cada clase el método que nos devolverá un valor aleatorio de cada una.

ImageList.cs

public static string ReturnRandomImage()
{
	var random = new Random();
	var imagesListCount = ImageList.Count;
	var imageListIndex = random.Next(0, imagesListCount);
	var image = ImageList[imageListIndex];
	return image;
}

ColorList.cs

public static (string, string) ReturnRandomColor()
{
	var random = new Random();
	var colorListCount = Colors.Count;
	var colorListIndex = random.Next(0, colorListCount);
	var colorName = Colors.ElementAt(colorListIndex).Key;
	var colorHex = Colors.ElementAt(colorListIndex).Value;
	return (colorName, colorHex);
}

AnimalList

public static string ReturnAnimal()
{
	var random = new Random();
	var animalListCount = Animals.Count;
	var animalListIndex = random.Next(0, animalListCount);
	var animal = Animals[animalListIndex];
	return animal;
}

Ahora vamos a comenzar a escribir nuestros métodos para el ContactService al igual que horita tenemos que importar varios namespaces.

Primero vamos a añadir el método llamado AddContact el cual va a verificar si el usuario ya tiene de contacto a quien tratamos de añadir como nuevo contacto y si esto es falso añade el nuevo contacto a la base de datos.

public async Task<(bool, string)> AddContact(Contact contact)
{
	try
	{
		//TODO FIX Messages for errors
		using (var context = new Context())
		{
			var alreadyContacts = await context.Contacts
				.Where(x => x.ContactId == contact.ContactId 
						&& x.UserId == contact.UserId)
				.AnyAsync();

			if (alreadyContacts)
				return (false, "Already contacts");

			context.Contacts.Add(new Contact
			{
				ContactId = contact.ContactId,
				UserId = contact.UserId
			});

			await context.SaveChangesAsync();
		}
	}
	catch (Exception ex)
	{

	}
	return (true, "Contact added");
}

Ahora vamos a añadir otro método el cual se llamara GetUserContacts, este método va a utilizar una clase que todavía no hemos creado llamada ContactInformationDTO la cual crearemos después de crear el método.

El método de GetUserContacts primero nos trae una lista de los contactos de nuestro usuario por id luego va a recorrer esa lista y va a traer el último mensaje si hay alguno despues va a cortar ese mensaje para que sea 100 caracteres, trae la foto y nombre del contacto y por ultimo lo añade a una lista de contactos que devolvemos.

public async Task<List<ContactInformationDTO>> GetUserContacts(Guid user_id)
{
	var contactList = new List<ContactInformationDTO>();

	try
	{
		using (var context = new Context())
		{
			var contacts = context.Contacts.Where(x => x.UserId == user_id);

			await contacts.ForEachAsync(async x =>
			{
				var lastMessageAndLastMessageDate = await context.Messages
					.Where(w => w.From == user_id && w.To == x.ContactId)
					.Select(m => new
					{
						m.Content,
						LastMessageDate = m.TimeSent
					}).LastOrDefaultAsync();

				var lastMessage = lastMessageAndLastMessageDate?.Content?
						.Substring(0, Math.Min(lastMessageAndLastMessageDate.Content.Length, 100));

				var UserNameAndProfilePicture = await context.Users
					.Where(w => w.Id == x.ContactId)
						.Select(s => new
						{
							Name = s.UserName,
							s.ProfilePicture
						}).FirstOrDefaultAsync();

				contactList.Add(new ContactInformationDTO
				{
					LastMessage = lastMessage,
					UserId = x.ContactId,
					UserName = UserNameAndProfilePicture.Name,
					ProfilePicture = UserNameAndProfilePicture?.ProfilePicture,
					LastMessageDate = lastMessageAndLastMessageDate?.LastMessageDate.DateTime.ToShortTimeString()
				});

			});

		};
	}
	catch (Exception ex)
	{

	}
	return contactList;
}

Ahora vamos a crear nuestra clase ContactInformationDTO así que vamos a crear un directorio llamado DTOs y adentro del una clase llamada ContactInformationDTO

[JsonProperty("user_name")]
public string UserName { get; set; }
[JsonProperty("last_message_excerpt")]
public string LastMessage { get; set; }
[JsonProperty("user_id")]
public Guid UserId { get; set; }
[JsonProperty("profile_picture")]
public string ProfilePicture { get; set; }
[JsonProperty("last_message_date")]
public string LastMessageDate { get; set; }

Como podemos observar esta clase tiene las propiedades decoradas con valores llamados JsonProperty, estos los vamos a utilizar para serializar (enviar nuestro objeto a Javascript) con un nombre que le asignemos.

Así que necesitamos importar otro paquete llamado Newtonsoft.Json.

Ahora vamos al MessageService y vamos a agregar dos nuevos métodos una para enviar mensajes y el otro para devolver una lista de mensajes.

public async Task<bool> SendMessage(Message message)
{
	using (var context = new Context())
	{
		await context.Messages.AddAsync(message);
		return await context.SaveChangesAsync() == 1;
	}
}
public async Task<List<ConversationDTO>> GetMessageList(Guid userId, Guid contactId)
{
	var results = new List<ConversationDTO>();
	try
	{
		using (var context = new Context())
		{
			results = await (from messages in context.Messages
								where messages.From == userId && messages.To == contactId
								|| messages.From == contactId && messages.To == userId
								join users in context.Users on messages.From equals users.Id
								select new ConversationDTO
								{
									Message = messages.Content,
									MessageId = messages.Id,
									FromId = messages.From,
									ToId = messages.To,
									SentByMe = messages.From == userId,
									ProfilePicture = users.ProfilePicture,
									TimeSent = messages.TimeSent.DateTime.ToShortTimeString(),
									DateTimeSent = messages.TimeSent
								}).OrderBy(x => x.TimeSent).ToListAsync();
		}
	}
	catch (Exception ex)
	{

	}
	return results;
}

Como podemos observar necesitamos añadir un nuevo DTO llamado ConversationDTO así que vamos a volver al directorio de DTOs y vamos a añadir la nueva clase.

public class ConversationDTO
{
		[JsonProperty("from_id")]
		public Guid FromId { get; set; }
		[JsonProperty("to_id")]
		public Guid ToId { get; set; }
		[JsonProperty("message")]
		public string Message { get; set; }
		[JsonProperty("profile_picture")]
		public string ProfilePicture { get; set; }
		[JsonProperty("time_sent")]
		public string TimeSent { get; set; }
		[JsonProperty("message_id")]
		public int MessageId { get; set; }
		[JsonProperty("sent_by_me")]
		public bool SentByMe { get; set; }

		[JsonIgnoreAttribute]
		public DateTimeOffset DateTimeSent { get; set; }
}

Después de añadir esta clase vamos a añadir tres clases más las cuales utilizaremos después. Ambas pertenecerán al directorio de DTOs

public class MessageReceivedDTO
{
	[JsonProperty("from")]
	public Guid From { get; set; }

	[JsonProperty("to")]
	public Guid To { get; set; }
}

y

public class UserDTO
{
	[JsonProperty("id")]
	public Guid Id { get; set; }
	[JsonProperty("user_name")]
	public string UserName { get; set; }
	[JsonProperty("primary_color_hex")]
	public string PrimaryColorHex { get; set; }
	[JsonProperty("profile_picture")]
	public string ProfilePicture { get; set; }
}

y

public class ContactDTO
{
	[JsonProperty("user_id")]
	public Guid UserId { get; set; }
	[JsonProperty("contact_id")]
	public Guid ContactId { get; set; }
}

Ahora nos movemos al API. En el API vamos a crear un directorio llamado ReturnObjects y vamos a crear una clase llamada GenericReturnObject el cual estaremos utilizando para devolver valores.

public class GenericReturnObject<T> : GenericReturnObjectBase where T : class
{
	[JsonProperty(NullValueHandling = NullValueHandling.Ignore, PropertyName = "values")]
	public T Values { get; set; }
}
public class GenericReturnObject : GenericReturnObjectBase
{
	public GenericReturnObject() : base() { }
}
public class GenericReturnObjectBase
{
	[JsonProperty("message")]
	public string Message { get; set; }
	[JsonProperty("success")]
	public bool Success { get; set; } = true;
}

El API siempre va a devolver un mensaje y un valor cierto o falso indicativo de éxito en nuestra llamada y devolverá una propiedad llamada values la cual solamente devolvemos si no es nula.

Ahora vamos al Startup.cs y vamos a inyectar nuestros servicios los cuales creamos horita. También vamos a añadir CORS a nuestra aplicación para poder comunicarnos con el API desde VueJS.

public Startup(IConfiguration configuration)
{
	Configuration = configuration;
}

public IConfiguration Configuration { get; }
readonly string AllowClientPolicy = "_allow_frontend";

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{

	services.AddTransient<UserService>();
	services.AddTransient<MessageService>();
	services.AddTransient<ContactService>();
	services.AddControllers().AddNewtonsoftJson();
	services.AddCors(
		x =>
		{
			x.AddPolicy(AllowClientPolicy, options =>
			{
				options.WithOrigins("https://localhost:8080")
				.AllowAnyHeader()
				.AllowAnyMethod()
				.AllowCredentials();

				options.WithOrigins("http://localhost:8080")
				.AllowAnyHeader()
				.AllowAnyMethod()
				.AllowCredentials();
			});
		});
	services.AddSignalR();
}

Si te sale error al añadir estos servicios es porque tenemos que añadir referencia a nuestro proyecto de Domain. Para añadir la referencia dale right-click al API y click en Add y luego Project Reference.

En el startup.cs class también vamos a el método de ConfigureServices después de services.AddControllers() añadimos AddNewtonsoftJson() y vamos a añadir un paquete llamado Microsoft.AspNetCore.Mvc.NewtonsoftJson.

Ahora vamos a agregar tres Controllers en el directorio de Controllers. Uno llamado UserController, MessageController y ContactController. Cada clase va a heredar de una clase llamada ControllerBase la cual nos va a dar la habilidad de devolver Estatus de HTTP y otras cosas que utilizaremos.

Cada una de estas clases tendrá dos propiedades las cuales serán la ruta del endpoint y ApiControllerBase

[Route("api/[controller]")]
[ApiController]

Vamos a comenzar con la clase UserController.cs, en esta clase vamos a inyectar un servicio, el de UserService.

[Route("api/[controller]")]
[ApiController]
public class UserController : ControllerBase
{
	private readonly UserService _userService;
	public UserController(UserService userService)
	{
		_userService = userService;
	}
}

Ahora vamos a añadir un método llamado GetUser.

[HttpGet("getuser")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> GetUser([FromQuery] Guid user_id)
{
	var results = new GenericReturnObject<UserDTO>();
	try
	{
		if (user_id == Guid.Empty)
		{
			results.Message = "Invalid userid";
			results.Success = false;
			return BadRequest(results);
		}
		var user = await _userService.GetUser(user_id);
		if (user == default)
		{
			results.Message = "User doesn't exist";
			results.Success = false;
			return BadRequest(results);
		}
		//Normally we'd use automapper but let's keep it simple
		results.Values = new UserDTO
		{
			Id = user.Id,
			ProfilePicture = user.ProfilePicture,
			PrimaryColorHex = user.PrimaryColorHex,
			UserName = user.UserName
		};
	}
	catch (Exception ex)
	{
		results.Success = false;
		results.Message = ex.Message;
	}
	return Ok(JsonConvert.SerializeObject(results));
}

y un metodo llamado GetNewUser el cual el cual se encarga de llamar a las clases que creamos anteriormente con los colores, animals e imágenes para autogenerar los valores del usuario y si el usuario no existe ya nos va a crear un Nuevo usuario.

[HttpGet("getnewuser")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> GetNewUser()
{
	var results = new GenericReturnObject<UserDTO>();

	try
	{
		var userColor = ColorList.ReturnRandomColor();
		var userAnimal = AnimalList.ReturnAnimal();
		var profilePicture = Images.ReturnRandomImage();
		var userName = $"{ userColor.Item1} { userAnimal }";
		var newUser = new User
		{
			Id = Guid.NewGuid(),
			UserName = userName,
			PrimaryColorHex = userColor.Item2,
			ProfilePicture = profilePicture
		};
		var dbUser = _userService.GetUser(newUser.UserName);
		while (dbUser != default)
		{
			var retryNewUser = new User
			{
				Id = Guid.NewGuid(),
				UserName = userName,
				PrimaryColorHex = userColor.Item2,
			};
			dbUser = _userService.GetUser(retryNewUser.UserName);
		}
		results.Success = await _userService.CreateUser(newUser);
		results.Values = new UserDTO
		{
			UserName = newUser.UserName,
			Id = newUser.Id,
			PrimaryColorHex = userColor.Item2,
			ProfilePicture = profilePicture
		};
	}
	catch (Exception ex)
	{
		results.Success = false;
		results.Message = ex.Message;
	}
	return Ok(JsonConvert.SerializeObject(results));
}

Y por ultimo tendremos un método para buscar usuarios por nombre para nuestro dropdown que tendremos en el GUI más adelante.

[HttpGet("searchContactByName")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> SearchContactByName([FromQuery]Guid user_id,[FromQuery]string contact_name)
{
	var results = new GenericReturnObject<List<UserDTO>>();
	try
	{
		if(user_id == Guid.Empty || String.IsNullOrEmpty(contact_name))
		{
			results.Success = false;
			return BadRequest(results);
		}
		var userMakingRequest = await _userService.GetUser(user_id);
		if (userMakingRequest == default)
		{
			results.Success = false;
			return BadRequest(results);
		}
		var userList = await _userService.GetUserByName(contact_name, user_id);
		//TODO FIX MAPPING HERE
		results.Values = userList.Select(x => new UserDTO
		{
			UserName = x.UserName,
			Id = x.Id
		}).ToList();
	}
	catch (Exception ex)
	{
		results.Success = false;
		results.Message = ex.Message;
	}
	return Ok(JsonConvert.SerializeObject(results));
}

Ahora vamos a comenzar a trabajar en el ContactController, primero vamos a inyectarle el servicio del usuario y el servicio de contactos.

[Route("api/[controller]")]
[ApiController]
public class ContactController : ControllerBase
{
	private readonly ContactService _contactService;
	private readonly UserService _userService;
	public ContactController(ContactService contactService, UserService userService)
	{
		_contactService = contactService;
		_userService = userService;
	}
}

Ahora vamos a añadir el primer método, llamado GetContacts.

[HttpGet("getContacts")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> GetContacts([FromQuery] Guid user_id)
{
	var results = new GenericReturnObject<List<ContactInformationDTO>>();
	try
	{
		if (user_id == Guid.Empty)
		{
			results.Message = "Invalid userid";
			results.Success = false;
			return BadRequest(results);
		}
		results.Values = await _contactService.GetUserContacts(user_id);
	}
	catch (Exception ex)
	{
		results.Success = false;
		results.Message = ex.Message;
	}
	return Ok(JsonConvert.SerializeObject(results));
}

Luego añadimos otro método para agregar contactos a nuestra lista de contactos dependiendo del usuario llamado.

[HttpPost("addContact")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> AddContact([FromBody]ContactDTO contact)
{
	var results = new GenericReturnObject();
	try
	{
		if (contact.ContactId == Guid.Empty)
		{
			results.Success = false;
			results.Message = "Empty Contact ID";
			return BadRequest(JsonConvert.SerializeObject(results));
		};
		if (contact.UserId == Guid.Empty)
		{
			results.Success = false;
			results.Message = "Empty UserId";
			return BadRequest(JsonConvert.SerializeObject(results));
		};

		var userRequestingToAddContact = await _userService.GetUserById(contact.UserId);
		if (userRequestingToAddContact == default)
		{
			results.Success = false;
			results.Message = "User doesn't exist";
			return BadRequest(JsonConvert.SerializeObject(results));
		}

		var contactToAdd = await _userService.GetUserById(contact.ContactId);
		if (contactToAdd == default)
		{
			results.Success = false;
			results.Message = "Contact doesn't exist";
			return BadRequest(JsonConvert.SerializeObject(results));
		}
		var addContactResults = await _contactService.AddContact(new Contact
		{
			ContactId = contact.ContactId,
			UserId = contact.UserId
		});
		if (!addContactResults.Item1)
		{
			results.Success = false;
			results.Message = addContactResults.Item2; //Fix later with more legible code
			return BadRequest(JsonConvert.SerializeObject(results));
		}
	}
	catch (Exception ex)
	{
		results.Success = false;
		results.Message = ex.Message;
	}
	results.Success = true;
	return Ok(JsonConvert.SerializeObject(results));
}

Ya terminamos con el ContactController y ahora nos movemos al MessageController en el cual vamos a comenzar a utilizar SignalR para notificar cuando un mensaje haya sido enviado.

SignalR

SignalR viene instalado con la versión de Asp.NET Core que estamos utilizando así que vamos a ir a la clase de Startup.cs y vamos a el método de ConfigureServices a lo ultimo del método añadimos.

services.AddSignalR();

Luego en el método de Configure añadimos en app.UseEndpoints un endpoint para SignalR y se va a llamar message-hub

app.UseEndpoints(endpoints =>
{
	endpoints.MapControllers();
	endpoints.MapHub<MessageHub>("/message-hub");
});

Como puedes ver MessageHub es una clase que no existe todavía así que vamos a crearla. En el proyecto del API vamos a crear un directorio nuevo llamado Hub y en el vamos a crear la clase llamada MessageHub.

En esta clase vamos a utilizar el namespace

using Microsoft.AspNetCore.SignalR;

y vamos a heredar de la clase Hub.

public class MessageHub : Hub
{
}

Ahora en esta clase vamos a añadir un método llamado ReceiveMessage el cual va a encargarse de avisar cuando se envié un mensaje y va a devolver un objeto de tipo MessageReceiveDTO el cual usaremos para identificar si el mensaje es para nosotros o no.

Ahora volvemos a la clase de Startup.cs e importamos el namespace de nuestra nueva clase.

Ahora volvemos a la clase de MessageController y vamos a inyectar el MessageService y el nuevo MessageHub` que creamos

[Route("api/[controller]")]
[ApiController]
public class MessageController : ControllerBase
{
	private readonly MessageService _messageService;
	private readonly IHubContext<MessageHub> _messageClient;

	public MessageController(MessageService messageService, IHubContext<MessageHub> messageHub)
	{
		_messageService = messageService;
		_messageClient = messageHub;
	}
}

Ahora vamos a crear el método que va a escribir mensajes. Este método va a recibir el mensaje de nuestro front-end y va a verificar si el contenido del mensaje no está nulo o vacio luego va a escribir el mensaje en la base de datos y si esto es exitoso entonces utilizamos el servicio de SignalR para invocar en todos los clientes el método de ReceiveMessage que devolverá nuestro objeto MessageReceivedDTO el cual tendrá el id de quien envió el mensaje y para quien se supone que sea.

[HttpPost("writemessage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> WriteMessage([FromBody] Message message)
{
	var results = new GenericReturnObject();
	try
	{
		if (String.IsNullOrEmpty(message.Content))
		{
			results.Message = "Message has no content";
			results.Success = false;
			return BadRequest();
		}
		else
		{
			message.TimeSent = DateTimeOffset.Now;
			results.Success = await _messageService.SendMessage(message);
			if (results.Success)
				await _messageClient.Clients.All.SendAsync("ReceiveMessage", new MessageReceivedDTO
				{
					From = message.From,
					To = message.To
				});
		}
	}
	catch (Exception ex)
	{
		results.Success = false;
		results.Message = ex.Message;
	}
	return Ok(results);
}

Y por ultimo vamos a añadir un método que se llamara GetUserMessages el cual nos va a devolver una lista de mensajes.

[HttpGet("getmessages")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> GetUserMessages([FromQuery] Guid user_id, [FromQuery]Guid contact_id)
{
	var results = new GenericReturnObject<List<ConversationDTO>>();
	try
	{
		if (user_id == Guid.Empty)
		{
			results.Message = "Invalid userid";
			results.Success = false;
			return BadRequest(results);
		}
		if(contact_id == Guid.Empty)
		{
			results.Message = "Invalid contactid";
			results.Success = false;
			return BadRequest(results);
		}
		results.Success = true;
		results.Values = await _messageService.GetMessageList(user_id, contact_id);
	}
	catch (Exception ex)
	{
		results.Success = false;
		results.Message = ex.Message;
	}
	return Ok(JsonConvert.SerializeObject(results));
}

VueJS

Ahora vamos al front-end en esta parte no voy a profundizar sobre todo si no que les voy a ensenar los componentes que hice y las partes importantes. Si quieren ver el código completo den click aquí.

El front-end básicamente tiene tres componentes uno para que el usuario añada un nuevo contacto, este componente se llama search modal, tenemos otro que se va a utilizar para ensenar la lista de mensajes y enviar nuevos mensajes y otro que es el sidebar en donde ensenamos la lista de contactos.

Front end components

En el componente de Sidebar tenemos nuestras propiedades reactivas una para guardar el nombre de nuestro usuario, otra para guardar el color de nuestro usuario, otra para la imagen de perfil y por ultimo la lista de contactos.

Sidebar.vue

data () {
	return {
		username: null,
		userPrimaryColor: null,
		userProfileImage: null,
		contactList: []
	}
},

Y en nuestros métodos tenemos un método para traer un usuario, este método verifica en el local storage del buscador para verificar si ya hay un nombre de usuario y id existentes si no los hay entonces va a crear un nuevo usuario.

getUser: async function () {
	let self = this
	if (localStorage.username && localStorage.userId) {
	axios.get(`${this.apiUrl}api/user/getuser?user_id=${localStorage.userId}`)
		.then(function (response) {
		if (!response.data.success) {
			self.createNewUser()
		} else {
			self.assignValuesToUserInformation(response.data.values)
			self.getContactList()
		}
		}).catch(error => {
		self.createNewUser()
		})
	} else {
	self.createNewUser()
	}
},

Como pueden ver estoy utilizando la librería de Axios para hacer las llamadas al api también tengo un archivo de mixins el cual tiene el url de nuestro API.

mixins.js

export default {
  data () {
    return {
      get apiUrl () { return 'https://localhost:44375/' }
    }
  },
  methods: {
  }
}

Así se ve el método de crear un nuevo usuario

createNewUser: function () {
	let self = this
	axios.get(`${this.apiUrl}api/user/getnewuser`)
	.then(function (response) {
		let data = response.data.values
		self.assignValuesToUserInformation(data)
	}).catch(error => {
		console.log(error)
	})
},

La parte importante de este componente es el método de created el cual esta utilizando el EventBus de VueJs para escuchar eventos, el evento que escucha es el de contact-added el cual se utiliza para refrescar la lista de contactos después de añadir un contacto nuevo.

created: function () {    
	let self = this
	EventBus.$on('contact-added', function(){
		self.getContactList();
	})
},

También tenemos un método el cual emite un evento cuando se ejecuta llamado onContactMessagesClick y va a emitir el evento llamado show-contact-messages.

onContactMessagesClick: function (contactId) {
	EventBus.$emit('show-contact-messages',contactId);
}

El componente de Messages es el que está pendiente a escuchar ese evento.

Messages.vue

created: function () {
	let self = this
	EventBus.$on('show-contact-messages', function(contactId){
		self.contactId = contactId
		self.getMessages()
})

Al igual que se encarga de traer los mensajes por usuario especifico y de enviar mensajes.

getMessages: function () {
	let self = this;
	if(this.contactId != null) {
	axios.get(`${this.apiUrl}api/message/getmessages?user_id=${localStorage.userId}&contact_id=${this.contactId}`)
		.then(function (response) {
		if (response.data.success) {
			self.messageList = response.data.values              
		} else {
		}
		})
		.then(function() {
		StyleMyMessageBubbles()
		}).catch(error => {
		console.log(error)
	})
	}
},
sendMessage: function () {
	if(this.message){
		let parameters = JSON.stringify({
		from: localStorage.userId,
		to: this.contactId,
		content: this.message
		})
		axios.post(`${this.apiUrl}api/message/writemessage`, parameters, {
		headers: {
			'Content-Type': 'application/json; charset=utf-8'
		}})
		.then(function (response) {
			if (response.data.success) {
			}
		})
		.catch(function (error) {
			console.log(error)
		})
	}
	}

Ahora para utilizar SignalR con vue js instale un paquete por npm llamado latelier/vue-signalr

Así que vamos a ejecutar el siguiente comando.

npm install @latelier/vue-signalr --save

Luego nos vamos a nuestro main.js y añadimos las siguientes líneas.

import VueSignalR from '@latelier/vue-signalr'

Vue.use(VueSignalR, 'https://localhost:44375/message-hub')

Estamos importando la librería y dejándole saber que nuestro API esta en ese URL que escribimos y que nuestro hub es el message-hub

Luego nos vamos al App.vue y vamos a añadir el método de created junto a lo siguiente:

created () {
	this.$socket.start({
		log: true // Logging is optional but very helpful during development
	})
},

y

sockets: {
	ReceiveMessage (message) {
		// From what i've read this has to be here or the events wont fire
	}
}

Ahora podemos volver a nuestro Messages.vue y añadimos las siguientes líneas de código las cuales van a escuchar por el evento de ReceiveMessage y nos van a traer el objeto que definimos en nuestro API.

this.$socket.on('ReceiveMessage', (message) => { 
	if(message.to === localStorage.userId || message.from === localStorage.userId)
	self.getMessages()
});

Como podemos ver cuando recibimos el evento verificamos localStorage por el id del usuario y lo comparamos con los dos ids que devolvimos, el id de quien envió el mensaje y el id de quien se supone que lo reciba.

Categoria: dotnetcsharpvuejs