Niedawno opublikowałem wpis na temat instalacji biblioteki SignalR w projekcie ASP.NET Core z wykorzystaniem Angulara. Dzisiaj chciałem kontynuować ten temat na przykładzie z mojego projektu. Zademonstruję działającą implementację po stronie serwera.
Link do poprzedniego wpisu: SignalR w ASP.NET Core – instalacja i uruchomienie
Konfiguracji ciąg dalszy
Chociaż konfiguracja jaką podałem w ostatnim wpisie wystarczała do działania, w moim rozwiązaniu musiałem dodać jeszcze kilka ustawień. Na pierwszy ogień poszedł CamelCasePropertyNamesContractResolver
, dobrze znany wszystkim, którzy tworzyli kiedyś API w Asp.Net. Dzięki niemu, obiekty zwracane są z API w notacji CamelCase. Jest to domyślne ustawienie w .NET Core, ale jako że my mamy podpiętą bibliotekę ze starszej wersji frameworka, musimy ustawić to ręcznie. Przykładowa implementacja poniżej.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Infrastructure; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System; using System.Reflection; namespace WordHunt.WebAPI.Config { public class SignalRContractResolver : IContractResolver { private readonly Assembly assembly; private readonly IContractResolver camelCaseContractResolver; private readonly IContractResolver defaultContractSerializer; public SignalRContractResolver() { defaultContractSerializer = new DefaultContractResolver(); camelCaseContractResolver = new CamelCasePropertyNamesContractResolver(); assembly = typeof(Connection).Assembly; } public JsonContract ResolveContract(Type type) { if (type.Assembly.Equals(assembly)) { return defaultContractSerializer.ResolveContract(type); } return camelCaseContractResolver.ResolveContract(type); } public static void RegisterSerializer() { var settings = new JsonSerializerSettings { ContractResolver = new SignalRContractResolver() }; var serializer = JsonSerializer.Create(settings); GlobalHost.DependencyResolver .Register(typeof(JsonSerializer), () => serializer); } } } |
Następnie rejestrujemy nasz serializator w pliku Startup.cs
.
1 2 3 4 5 6 |
// Konfiguracja public void ConfigureServices(IServiceCollection services) { SignalRContractResolver.RegisterSerializer(); ... } |
Rozsyłanie eventów z poziomu backendu aplikacji
Wysłanie zdarzenia z serwera może odbyć w dwóch przypadkach:
- Bezpośrednio z naszej klasy, dziedziczącej po
Hub
, w momencie gdy klient wywołuje zdalną metodę na serwerze - Po wywołaniu zwykłej metody HTTP w API, pośrednio poprzez
Hub
O ile w pierwszym przypadku jest to bardzo proste, to ten drugi wymaga dodatkowego nakładu pracy. Załóżmy że nasz Hub ma tylko jedną metodę.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Hubs; using WordHunt.Data.Events; namespace WordHunt.WebAPI.Hubs { [HubName("broadcaster")] public class Broadcaster : Hub<IEventClient>, IEventServer { public string Subscribe(int gameId) { Groups.Add(Context.ConnectionId, gameId.ToString()); return Context.ConnectionId; } } } |
Klient, w momencie gdy wywoła metodę Subscribe
, zapisze się do grupy odpowiadającej identyfikatorowi gry. Zwrócone mu zostanie id jego połączenia. Wewnątrz klasy Broadcaster
możemy wysłać zdarzenie o nowej subskrypcji do wszystkich klientów. Taki przykład podałem w poprzednim poście, dlatego tutaj to pomijam (nie jest to potrzebne w moim przypadku). Efekt który chcemy tutaj osiągnąć to wysłanie do każdego klienta informacje o tym, że gracz wykonał ruch. Jako przykład posłuży tutaj zakończenie tury przez gracza.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
[Authorize] [Route("api/game")] public class GameModificationController : Controller { private readonly ITokenUserContextProvider userProvider; private readonly IGameMoveManager gameMoveManager; public GameModificationController(ITokenUserContextProvider userProvider, IGameMoveManager gameMoveManager) { this.userProvider = userProvider; this.gameMoveManager = gameMoveManager; } [Authorize] [HttpGet("{gameId}/skipround")] public async Task<IActionResult> SkipRound(int gameId) { var currentUser = userProvider.GetContextUserInfo(); var result = await gameMoveManager.SkipRound(gameId, currentUser.Id); return Ok(result); } } |
Podczas wywołania tej akcji kontrolera, wywołujemy metodę SkipRound
. Wewnątrz niej, po tym jak nastąpi zmiana kolejki, chcielibyśmy poinformować wszystkich graczy o tym fakcie. Nasz Hub to nie jest zwykła klasa, którą można po prostu wstrzyknąć do naszych obiektów. W tym celu musimy odwołać się do klasy ConnectionManager
dostarczonej przez SignalR.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using Microsoft.AspNet.SignalR; using WordHunt.Data.Events; using WordHunt.Games.Broadcaster; namespace WordHunt.WebAPI.Hubs { public class EventBroadcaster : IEventBroadcaster { public void TeamChanged(TeamChanged args) { HubContext.Clients.Group(args.GameId.ToString()).TeamChanged(args); } IHubContext<IEventClient> HubContext { get; } = GlobalHost.ConnectionManager.GetHubContext<Broadcaster, IEventClient>(); } } |
Dodałem klasę EventBroadcaster
, która będzie odpowiedzialna za przekazywanie informacji do klientów bezpośrednio z serwera. Posiada ona obiekt HubContext
, który umożliwia takie działania. Dostępny jest po wywołaniu metody GlobalHost.ConnectionManager.GetHubContext
. Ponieważ jej wywołanie jest dosyć czasochłonne, klasę EventBroadcaster
rejestrujemy w systemie jako singleton.
1 2 3 4 5 6 |
public static IServiceCollection RegisterWebApiDependencies(this IServiceCollection services) { ... services.AddSingleton<Games.Broadcaster.IEventBroadcaster, Hubs.EventBroadcaster>(); ... } |
Dzięki temu, w dowolne miejsce aplikacji możemy dodać obiekt implementujący IEventBroadcaster
i wysyłać do klientów interesujące nas zdarzenia. Przykład poniżej.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
//Moves public class GameMoveManager : IGameMoveManager { // Other dependencies ... private readonly IEventBroadcaster eventBroadcaster; public GameMoveManager( ... IEventBroadcaster eventBroadcaster) { this.eventBroadcaster = eventBroadcaster; } public async Task<TeamChanged> SkipRound(int gameId, int userId) { //Game logic... eventBroadcaster.TeamChanged(teamChanged); return teamChanged; } } |
Podsumowanie
Dzisiaj zademonstrowałem przykładową implementację rozsyłania zdarzeń w aplikacji po stronie serwera aplikacji. W kolejnym wpisie zajmiemy się implementacją po stronie klienta w Angularze. Dzięki za uwagę i zapraszam ponownie!
Pingback: dotnetomaniak.pl()