Agosto de 2018

Volume 33 – Número 8

Novidade - Notificações Sociais com SignalR do ASP.NET Core

Por Dino Esposito | Agosto de 2018

Dino EspositoRedes sociais e sistemas operacionais móveis feitos pop-up, notificações de estilo de balão incrivelmente populares e importantes, mas o Microsoft Windows 2000 provavelmente foi o primeiro software extensivamente usá-los. Notificação de balão possibilitam a se comunicar eventos notáveis para o usuário sem a necessidade de uma ação imediata e atenção — ao contrário das janelas pop-up convencionais. Chave para essas notificações de estilo de balão é a infraestrutura subjacente que entrega a mensagem em tempo real, à direita para a janela aberta em que o usuário está trabalhando.

Neste artigo, você verá como usar o SignalR do ASP.NET Core para produzir notificações pop-up. O artigo apresenta um aplicativo de exemplo que controla os usuários conectados e dá a cada uma chance de criar e manter uma rede de amigos. Como em um cenário de rede social, qualquer usuário conectado pode ser adicionado ou removido de uma lista de amigos a qualquer momento. Quando isso acontece no aplicativo de exemplo, o usuário conectado recebe uma notificação sensível ao contexto.

Autenticar usuários de aplicativo

Notificações de estilo de balão não são simples notificações enviadas para a pessoa que está escutando para mensagens do SignalR em um canal de soquete da Web. Em vez disso, elas são enviadas para usuários específicos, conectados ao aplicativo. Abrindo e escuta em um canal de soquete da Web simples são uma boa maneira de abordar o problema, mas apenas SignalR do ASP.NET Core fornece um resumo mais da interface de programação e oferece suporte a protocolos de rede alternativo além do WebSocket.

A primeira etapa na criação do aplicativo é adicionar uma camada para autenticação do usuário. O usuário é apresentado um formulário de logon canônico e fornece as credenciais. Depois que o reconhecido corretamente como um usuário válido do sistema, ela recebe um cookie de autenticação fornecido com um número de declarações, como mostrado aqui:

var claims = new[]
{
  new Claim(ClaimTypes.Name, input.UserName),
  new Claim(ClaimTypes.Role, actualRole)
};
var identity = new ClaimsIdentity(claims,
  CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignInAsync(
  CookieAuthenticationDefaults.AuthenticationScheme,
    new ClaimsPrincipal(identity));

O código de exemplo mostra como criar um objeto IPrincipal ad-hoc no ASP.NET Core em torno de um nome de usuário depois que credenciais foram verificadas com êxito. Essencial observar que para a autenticação funcione corretamente no ASP.NET Core, quando usado em combinação com a biblioteca mais recente do SignalR, você deve habilitar a autenticação no método Configure da classe startup bem no início (e em qualquer caso, anteriores ao concluir o Inicialização de roteiro de SignalR). Retornarei este ponto daqui a pouco; Enquanto isso, vamos analisar a infraestrutura de amizade no aplicativo de exemplo.

Definindo amigos no aplicativo

Para fins de aplicativo de exemplo, uma relação de amigo é meramente um link entre dois usuários do sistema. Observe que o aplicativo de demonstração não usa qualquer banco de dados para manter usuários e relações. Alguns usuários e relações de amigos estão codificados e qualquer reinicialização quando o aplicativo for reiniciado ou o modo de exibição atual será recarregado. Aqui está a classe que representa uma relação de amigo:

public class FriendRelationship
{
  public FriendRelationship(string friend1, string friend2)
  {
    UserName1 = friend1;
    UserName2 = friend2;
  }
  public string UserName1 { get; set; }
  public string UserName2 { get; set; }
}

Conforme o usuário fizer logon, ele tenha servido uma página de índice que fornece a lista de amigos. Por meio da interface do usuário da página, o usuário pode adicionar novos amigos e remover as existentes (consulte Figura 1).

A Home Page de um usuário conectado
Figura 1 a Home Page de um usuário conectado

Quando o usuário digita o nome de um novo amigo, posta um formulário HTML e uma nova relação de amigo é criada na memória. Se o nome digitado não corresponder a um usuário existente, um usuário novo objeto é criado e adicionado à lista na memória, da seguinte forma:

[HttpPost]
public IActionResult Add(string friend)
{
  var currentUser = User.Identity.Name;
  UserRepository.AddFriend(currentUser, friend);
  // More code here
  ...
  return new EmptyResult();
}

Como você pode perceber, o método do controlador retorna o resultado de uma ação vazia. Na verdade, ele será assumido que o formulário HTML lança seu conteúdo por meio de JavaScript. Portanto, clique em um JavaScript manipulador está anexado ao botão de envio do formulário programaticamente, conforme mostrado aqui:

<form class="form-horizontal" id="add-form" method="post"
      action="@Url.Action("add", "friend")">
      <!-- More input controls here -->
      <!-- SUBMIT button -->
  <button id="add-form-submit-button"
       class="btn btn-danger" type="button">
       SAVE  </button></form>

O código de lançamento de JavaScript dispara a operação do lado do servidor e o retorna. A nova lista de amigos, como uma matriz JSON ou uma cadeia de caracteres HTML, pode ser retornada pelo método do controlador e integrada no modelo de objeto de documento de página atual, o mesmo código chamador JavaScript. Ele funciona bem, mas há uma falha leve em consideração que poderia ser um problema em alguns cenários.

Imagine que o usuário mantém várias conexões para a mesma página de servidor. Por exemplo, o usuário tem várias janelas do navegador abertas na mesma página e interage com uma dessas páginas. Em uma situação em que a chamada traz de volta de uma resposta direta (se JSON ou HTML), somente a página do que originou a solicitação acaba sendo atualizado. Qualquer outra janela do navegador aberto permanece estático e não serão afetadas. Para contornar o problema, você pode aproveitar um recurso do SignalR do ASP.NET Core que lhe permite alterações de difusão para todas as conexões relacionadas a mesma conta de usuário.

Transmitindo para conexões de usuário

A classe de controlador do ASP.NET MVC que recebe as chamadas para adicionar ou remover amigos incorpora uma referência para o contexto do hub SignalR. Este código mostra como isso é feito:

[Authorize]
public class FriendController : Controller
{
  private readonly IHubContext<FriendHub> _friendHubContext;
  public FriendController(IHubContext<FriendHub> friendHubContext)
  {
    _friendHubContext = friendHubContext;
  }
  // Methods here
  ...
}

Como de costume na programação do SignalR, o hub de friend é definido na classe startup, conforme mostrado na Figura 2.

Figura 2 definição de Hub

public void Configure(IApplicationBuilder app)
{
  // Enable security
  app.UseAuthentication();
  // Add MVC
  app.UseStaticFiles();
  app.UseMvcWithDefaultRoute();
  // SignalR (must go AFTER authentication)
  app.UseSignalR(routes =>
  {
    routes.MapHub<FriendHub>("/friendDemo");
  });
}

É fundamental que chamar o UseSignalR segue a chamada de UseAuthentication. Isso garante que quando uma conexão SignalR é estabelecida na rota de determinado que informações sobre o usuário conectado e declarações estão disponíveis. Figura 3 oferece uma análise mais profunda o código que manipula a postagem de formulário quando o usuário adiciona um novo amigo à lista.

Figura 3 manipulação a postagem de formulário

[HttpPost]
public IActionResult Add(string friend)
{
  var currentUser = User.Identity.Name;
  UserRepository.AddFriend(currentUser, friend);
  // Broadcast changes to all user-related windows
  _friendHubContext.Clients.User(currentUser).SendAsync("refreshUI");
  // More code here  ...
  return new EmptyResult();
}

A propriedade Clients do contexto do hub tem uma propriedade chamada de usuário, o que leva a uma ID de usuário. Quando chamado no objeto do usuário, o método SendAsync notifica a mensagem fornecida para todas as janelas de navegador conectados atualmente sob o mesmo nome de usuário. Em outras palavras, o SignalR tem a capacidade de agrupar automaticamente todas as conexões do mesmo usuário autenticado com um único pool. Sabendo disso, SendAsync invocado de usuário tem a capacidade de transmitir a mensagem para todas as janelas relacionadas ao usuário, se eles vêm de vários navegadores da área de trabalho, modos de exibição no aplicativo Web, navegadores de dispositivos móveis, os clientes de desktop ou qualquer outra coisa que. Eis o código:

_friendHubContext.Clients.User(currentUser).SendAsync("refreshUI");

Enviar a mensagem de refreshUI para todas as conexões com o mesmo nome de usuário garante que todas as janelas abertas de forma sincronizada com a função friend de adicionar. O código-fonte, você verá que a mesma coisa acontece quando o usuário conectado no momento remove um amigo da sua lista.

Configurando o Proxy de cliente do usuário

No SignalR, o objeto de usuário não tem nada a ver com o objeto de usuário associado ao contexto HTTP. Apesar do nome da propriedade, o objeto de usuário do SignalR é um proxy do cliente e um contêiner de declarações. Ainda assim, existe uma relação sutil entre o proxy de cliente específicas do usuário do SignalR e o objeto que representa o usuário autenticado. Como você pode ter notado, o proxy de cliente do usuário requer um parâmetro de cadeia de caracteres. No código da Figura 3, o parâmetro de cadeia de caracteres é o nome do usuário conectado no momento. Isso é apenas um identificador de cadeia de caracteres, no entanto e pode ser qualquer coisa que você configurá-lo para ser.

Por padrão, a ID de usuário reconhecida pelo proxy do cliente de usuário é o valor da declaração NameIdentifier. Se a lista de declarações do usuário autenticado não incluir NameIdentifier, não há nenhuma chance da que difusão funcionaria. Portanto, você tem duas opções: Uma é adicionar a declaração NameIdentifier ao criar o cookie de autenticação e o outro é escrever seu próprio provedor de ID de usuário do SignalR. Para adicionar a declaração NameIdentifier, você precisa do seguinte código no processo de logon:

var claims = new[]
{
  new Claim(ClaimTypes.Name, input.UserName),
  new Claim(ClaimTypes.NameIdentifier, input.UserName),
  new Claim(ClaimTypes.Role, actualRole),
};

O valor atribuído para a declaração NameIdentifier não importa, desde que ele seja exclusivo para cada usuário. Internamente, usa o SignalR raiz de um componente IUserIdProvider para corresponder a uma ID de usuário aos grupos de conexão para o usuário conectado no momento, da seguinte forma:

public interface IUserIdProvider
{
  string GetUserId(HubConnectionContext connection);
}

A interface IUserIdProvider tem uma implementação padrão na infraestrutura de injeção de dependência. A classe é DefaultUserIdProvider e é codificada da seguinte maneira:

public class DefaultUserIdProvider : IUserIdProvider
{
  public string GetUserId(HubConnectionContext connection)
  {
    var first = connection.User.FindFirst(ClaimTypes.NameIdentifier);
    if (first == null)
      return  null;
    return first.Value;
  }
}

Como você pode ver, a classe DefaultUserIdProvider usa o valor da declaração NameIdentifier para agrupar as IDs de conexão de usuário específica. Destina-se a declaração de nome para indicar o nome do usuário, mas não necessariamente para fornecer o identificador exclusivo por meio do qual um usuário é identificado no sistema. A declaração NameIdentifier, em vez disso, foi projetada para manter um valor exclusivo, se um GUID, uma cadeia de caracteres ou um número inteiro. Se você alternar para o usuário em vez de NameIdentifer, certifique-se de qualquer valor atribuído ao nome será exclusivo por usuário.

Todas as conexões provenientes de uma conta com um identificador de nome correspondente serão agrupadas juntos e serão notificadas automaticamente quando o proxy de cliente do usuário é usado. Alternar para a declaração de nome canônica, você precisa de um IUserIdProvider personalizado, da seguinte maneira:

public class MyUserIdProvider : IUserIdProvider
{
  public string GetUserId(HubConnectionContext connection)
  {
    return connection.User.Identity.Name;
  }
}

Obviamente, esse componente deve ser registrado com a infraestrutura de injeção de dependência durante a fase de inicialização. Aqui está o código para incluir no método ConfigureServices da classe de inicialização:

services.AddSignalR();
services.AddSingleton(typeof(IUserIdProvider), typeof(MyUserIdProvider));

Neste ponto, tudo está configurado para ter todas as janelas de usuário correspondente sincronizadas no mesmo estado e modo de exibição. Sobre notificações de estilo de balão?

O toque Final

Adicionando e removendo amigos fazer com que a necessidade de notificações de atualização que está sendo enviado para a página de índice que o usuário atual está exibindo. Se um determinado usuário tem duas janelas de navegador abertas em diferentes páginas do mesmo aplicativo (índice e outra página), ela receberá notificações de atualização apenas para a página de índice. No entanto, adicionando e removendo amigos também faz com que adicionar e remove notificações sejam enviadas aos usuários que foram adicionados ou removidos da lista de relação de amigo. Por exemplo, se o usuário Dino decidir remover usuário Mary de sua lista de amigos, usuário Mary também receberá uma notificação de remoção. O ideal é que uma notificação de remoção (ou adicionar) deve alcançar o usuário, independentemente da página que está sendo visualizado, se índice ou qualquer outro.

Para fazer isso, há duas opções:

  • Use um único hub SignalR com a configuração de conexão movido para a página de layout e, em seguida, herdada por todas as páginas com base em que o layout.
  • Usar dois hubs distintos — uma para atualizar a interface do usuário após a adição ou remoção de amigos e outra para notificar adicionados ou removidos os usuários.

Se você decidir ir com hubs distintos, o hub de notificação de adicionar/remover deve ser configurado em todas as páginas onde você deseja que as notificações aparecem — provavelmente as páginas de layout que você tem no aplicativo.

O aplicativo de exemplo usa um único hub completamente configurado na página de layout. Observe que o objeto JavaScript que faz referência a conexão atual é globalmente compartilhado dentro do cliente, que significa que o código de inicialização do SignalR é mais bem colocado na parte superior do corpo do layout e antes da seção de RenderBody do Razor.

Vamos examinar o código final do método Add no controlador. Esse método é onde o formulário na Figura 1 , por fim, postagens. O método faz as alterações necessárias no back-end para salvar as relações de amigo e, em seguida, emite duas mensagens do SignalR — uma para visualmente atualizar a lista de amigos do usuário atual que realizou a operação e um segundo para notificar o usuário tiver adicionado (ou removido). O usuário é notificado, na verdade, se ela estiver presentemente conectada ao aplicativo e de uma página configurado para receber e lidar com essas notificações específicas. Figura 4 mostra isso.

Figura 4 o último adicionar código do método

public IActionResult Add(string addedFriend)
{
  var currentUser = User.Identity.Name;
  // Save changes to the backend
  UserRepository.AddFriend(currentUser, addedFriend);
  // Refresh the calling page to reflect changes
  _friendHubContext.Clients.User(currentUser).SendAsync("refreshUI");
  // Notify the added user (if connected)
  _friendHubContext.Clients.User(addedFriend).SendAsync("added", currentUser);
  return new EmptyResult();
}

Na página de layout, o código JavaScript exibe uma estilo de balão de notificação (ou qualquer tipo de ajuste de interface do usuário que você deseja fazer). No aplicativo de exemplo, a notificação assume a forma de uma mensagem exibido em uma barra de notificação para até 10 segundos, conforme indicado no código aqui:

friendConnection.on("added", (user) => {
  $("#notifications").html("ADDED by <b>" + user + "</b>");
  window.setTimeout(function() {
            $("#notifications").html("NOTIFICATIONS");
    },
    10000);
});

Os resultados são mostrados na Figura 5.

Notificações de entre usuários
Figura 5 entre usuários notificações

Uma palavra em grupos de SignalR

Neste artigo, abordei o suporte do SignalR para selecionado notificações enviadas a um grupo relacionado de usuários. O SignalR oferece duas abordagens — o proxy de cliente de usuário e grupos. A diferença é sutil: O proxy de cliente de usuário implicitamente gera um número de grupos em que o nome é determinado pela ID de usuário e os membros são todas as conexões abertas pelo mesmo usuário do aplicativo. Um grupo é um mecanismo mais geral que acrescenta programaticamente as conexões a um grupo lógico. Conexões e o nome do grupo são definidas por meio de programação.

Notificações de estilo de balão foram implementadas em ambas as abordagens, mas para esse cenário específico, o usuário o proxy do cliente era a solução mais natural. Código-fonte pode ser encontrado em bit.ly/2HVyLp5.


Dino Esposito é autor de mais de 20 livros e de 1.000 artigos em seus 25 anos de carreira. Autor de “The Sabbatical Break”, um show de estilo cênico, Esposito se ocupa escrevendo software para um mundo mais ecológico, como estrategista digital da BaxEnergy. Siga-o no Twitter: @despos.


Discuta esse artigo no fórum do MSDN Magazine