受欢迎的博客标签

https://chsakell.com/2016/10/10/real-time-applications-using-asp-net-core-signalr-angular/  

Real-time web applications are apps that push user experience to the limits while trying to immediately reflect data changes to a great number of connected clients. You make use of such applications on a daily basis, Facebook and Twitter are some of them. There are several ways to design and implement Real-time web applications and of course Microsoft made sure to provide you with a remarkable library named SignalR. The idea behind SignalR is let the server push changes automatically to connected clients instead of having each client polling the server on time intervals. And what does connected clients means anyway? The answer is hidden behind the concept of the HTTP persistent connections which are connections that may remain opened for a long time, in contrast with the tradional HTTP Connections that can be disconnected. The persistent connection remains opened due to certain type of packet exchanging between a client and the server. When a client calls a SignalR method on the server, the server is able to uniquely identify the connection ID of the caller.

What this post is all about
SignalR has been out for a long time but ASP.NET Core and Angular 2 aren’t. On this post we ‘ll see what takes to bind all those frameworks and libraries together and build a Real time application. This is not an Angular tutorial nor a SignalR one. Because of the fact that the final project associated to this post contains code that we have already seen on previous posts, I will only explain the parts that you actually need to know in order to build a real time application. And this is why I will strongly recomend you to download the Live-Game-Feed app and study the code along with me without typing it. Here’s what we ‘ll see in more detail..

Fire up an empty ASP.NET Core web application using yeoman
Configure and install MVC and SignalR Server dependencies
Install SignalR Client-Typescript dependencies
Create a SignalR hub
Integrate MVC Controllers (API) with SignalR
Create the Angular-SignalR service to communicate with SignalR hubs
Add Reccurent Tasks on a ASP.NET Core application
Have fun with the final App!
About the LiveGameFeed app
The app simulates a web application that users may visit and watch matches live. I am sure you are aware of plenty of such websites, most of them are related to betting. The idea is that there will be two matches running, and every time score is updated all connected clients will receive the update. On the other hand, if a user also wants to getting live feed for a specific match then he/she has to be subscibed to the match. More over, if subscribed, the user will be able to post messages related to that match while those messages will be pushed and read only by users also subscribed to the that match. Why don’t we take a look at the LiveGameFeed app (zoom out a little bit if needed so that you can see both clients)..

Are you ready? Let’s start!

Fire up an empty ASP.NET Core web application using yeoman
I assume you have already installed .NET Core on your platform and you have opened the Live-Game-Feed app on your favorite text editor. You can start a .NET Core application either using the dotnet-new cli command or using the open-source yeoman tool. I picked the latter choise cause there are some great options to fire up a ASP.NET Core application. In order to use yeoman you need to run the following commands.

intall yo and bower
1
npm install -g yo bower
install the ASP.NET generator
1
npm install -g generator-aspnet
Next, open a console and navigate where you want to fire up the project and run the following command:

Create a new .NET Core app
1
yo aspnet
The tool will give you some options to start with.

Select Empty Web Application and give a name for your app.

Open the created folder in your editor (mine is Visual Studio Code) and check the files created. Those are the minimum files required for an empty Web Application. Navigate inside the app’s root folder and restore .NET packages by running the following command.

restore packages
1
dotnet restore
As you can see, Visual Studio Code has also an integrated terminal which certainly makes your life easier.

Then make sure that all have been set properly by running the app..

run the app
1
dotnet run
Of course you will only get the famous Hello world! response but it’s more than enough at the moment.

Configure and install MVC and SignalR Server dependencies
The next step in to install ASP.NET Core MVC and SignalR packages and add them into the pipeline as well. Your project.json file should look like this:

project.json
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
{
  "dependencies": {
    "AutoMapper.Data": "1.0.0-beta1",
    "Microsoft.NETCore.App": {
      "version": "1.0.0-*",
      "type": "platform"
    },
    "Microsoft.AspNet.WebApi.Client": "5.1.1",
    "Microsoft.AspNetCore.Mvc": "1.0.1",
    "Microsoft.AspNetCore.Routing": "1.0.1",
    "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.1",
    "Microsoft.AspNetCore.SignalR.Server": "0.2.0-*",
    "Microsoft.AspNetCore.StaticFiles": "1.1.0-*",
    "Microsoft.AspNetCore.WebSockets": "0.2.0-*",
    "Microsoft.EntityFrameworkCore": "1.0.1",
    "Microsoft.EntityFrameworkCore.InMemory": "1.0.0",
    "Microsoft.EntityFrameworkCore.Relational": "1.0.1",
    "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
    "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0",
    "Microsoft.Extensions.Configuration.Json": "1.0.0",
    "Microsoft.Extensions.Configuration.CommandLine": "1.0.0",
    "Microsoft.Extensions.Logging": "1.0.0",
    "Microsoft.Extensions.Logging.Console": "1.0.0",
    "Microsoft.Extensions.Logging.Debug": "1.0.0",
    "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0",
    "RecurrentTasks": "3.0.0-beta1"
  },
 
  "tools": {
    "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final"
  },
 
  "frameworks": {
    "netcoreapp1.0": {
      "imports": [
        "dotnet5.6",
        "portable-net45+win8"
      ]
    }
  },
 
  "buildOptions": {
    "emitEntryPoint": true,
    "preserveCompilationContext": true,
    "debugType": "portable"
  },
 
  "runtimeOptions": {
    "configProperties": {
      "System.GC.Server": true
    }
  },
 
  "publishOptions": {
    "include": [
      "wwwroot",
      "Views",
      "Areas/**/Views",
      "appsettings.json",
      "web.config"
    ]
  },
 
  "scripts": {
    "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
  },
 
  "tooling": {
    "defaultNamespace": "LiveGameFeed"
  }
}
Following are the most interesting packages to notice:

1
2
3
"Microsoft.AspNetCore.Mvc": "1.0.1"
"Microsoft.AspNetCore.SignalR.Server": "0.2.0-*"
"Microsoft.AspNetCore.WebSockets": "0.2.0-*"
If you try to restore the packages you will get the following error..

1
2
3
4
5
6
7
8
9
10
11
12
13
log  : Restoring packages for c:\Users\chsakell\Desktop\LiveGameFeed\project.json...                                                                                                            
error: Unable to resolve 'Microsoft.AspNetCore.SignalR.Server (>= 0.2.0)' for '.NETCoreApp,Version=v1.0'.                                                                                                   
error: Unable to resolve 'Microsoft.AspNetCore.StaticFiles (>= 1.1.0)' for '.NETCoreApp,Version=v1.0'.                                                                                                      
error: Unable to resolve 'Microsoft.AspNetCore.WebSockets (>= 0.2.0)' for '.NETCoreApp,Version=v1.0'.                                                                                                       
log  : Restoring packages for tool 'Microsoft.AspNetCore.Server.IISIntegration.Tools' in c:\Users\chsakell\Desktop\LiveGameFeed\project.json...                                                 
log  : Writing lock file to disk. Path: c:\Users\chsakell\Desktop\LiveGameFeed\project.lock.json                                                                                                
log  : c:\Users\chsakell\Desktop\LiveGameFeed\project.json                                                                                                                                      
log  : Restore failed in 10232ms.                                                                                                                                                                           
                                                                                                                                                                                                              
Errors in c:\Users\chsakell\Desktop\LiveGameFeed\project.json                                                                                                                                   
Unable to resolve 'Microsoft.AspNetCore.SignalR.Server (>= 0.2.0)' for '.NETCoreApp,Version=v1.0'.                                                                                                      
Unable to resolve 'Microsoft.AspNetCore.StaticFiles (>= 1.1.0)' for '.NETCoreApp,Version=v1.0'.                                                                                                         
Unable to resolve 'Microsoft.AspNetCore.WebSockets (>= 0.2.0)' for '.NETCoreApp,Version=v1.0'.
This error occurred cause you miss NuGet package configuration which is needed in order to install the SignalR and WebSockets packages. Add a NuGet.config file at the root of your app and set it as follow:

NuGet.config
1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="AspNetCore" value="https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json" />
    <add key="NuGet" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
</configuration>
Now the dotnet restore command will not fail. You add MVC and SignalR into the pipeline in the same way you add any other middleware. In the Startup.cs file you will find the following commands into the ConfigureServices method..

part of ConfigureServices
1
2
3
4
5
6
7
// Add framework services.
services
    .AddMvc()
    .AddJsonOptions(options => options.SerializerSettings.ContractResolver =
        new DefaultContractResolver());
 
services.AddSignalR(options => options.Hubs.EnableDetailedErrors = true);
.. and in the Configure method..

part of Configure method
1
2
3
4
5
6
7
app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});
app.UseSignalR();
You will find that in the finished Startup.cs file I have also set dependency injection for the data repositories, Entity Framework InMemoryDatabase provider and some recurrent tasks to run using the RecurrentTasks package. We ‘ll talk about the latter little bit before firing the final app.

Install SignalR Client-Typescript dependencies
The client side will be written in TypeScript entirely and this is something new since in most of the SignalR tutorials the client side was written in pure javascript and jQuery. In case you are familiar with Angular 2 then you already know how to intall npm packages. You need to create a package.json file under the root and also make sure you add the signalr as a dependency.

package.json
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
47
48
49
50
51
52
53
54
55
56
57
{
  "version": "1.0.0",
  "description": "live game feed",
  "name": "livegamefeed",
  "readme": "chsakell's blog all right reserved",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/chsakell/aspnet-core-signalr-angular"
  },
  "dependencies": {
    "@angular/common": "2.0.0",
    "@angular/compiler": "2.0.0",
    "@angular/core": "2.0.0",
    "@angular/forms": "2.0.0",
    "@angular/http": "2.0.0",
    "@angular/platform-browser": "2.0.0",
    "@angular/platform-browser-dynamic": "2.0.0",
    "@angular/router": "3.0.0",
    "@angular/upgrade": "2.0.0",
    "angular2-in-memory-web-api": "0.0.20",
    "bower": "1.7.9",
    "core-js": "^2.4.1",
    "jquery": "^3.1.0",
    "reflect-metadata": "^0.1.3",
    "rxjs": "5.0.0-beta.12",
    "signalr": "^2.2.1",
    "systemjs": "0.19.27",
    "zone.js": "^0.6.23"
  },
  "devDependencies": {
    "concurrently": "^2.2.0",
    "gulp": ">=3.9.1",
    "gulp-concat": ">=2.5.2",
    "gulp-copy": ">=0.0.2",
    "gulp-cssmin": ">=0.1.7",
    "gulp-rename": ">=1.2.2",
    "gulp-rimraf": ">=0.2.0",
    "gulp-tsc": ">=1.2.0",
    "gulp-uglify": ">=1.2.0",
    "gulp-watch": ">=4.3.9",
    "jasmine-core": "2.4.1",
    "tslint": "^3.15.1",
    "typescript": "^2.0.0",
    "typings": "^1.3.2"
  },
  "scripts": {
    "start": "concurrently \"npm run gulp\" \"npm run watch\" \"npm run tsc:w\"",
    "postinstall": "typings install",
    "tsc": "tsc",
    "tsc:w": "tsc -w",
    "typings": "typings",
    "gulp": "gulp",
    "watch": "gulp watch",
    "ngc": "ngc"
  }
}
Next you need to add the required typings by adding a typings.json file.

typings.json
1
2
3
4
5
6
7
8
9
{
  "globalDependencies": {
    "core-js": "registry:dt/core-js",
    "node": "registry:dt/node",
    "jquery": "registry:dt/jquery",
    "signalr": "registry:dt/signalr",
    "jasmine": "registry:dt/jasmine"
  }
}
The tsconfig.json TypeScript configuration file.

tsconfig.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "suppressImplicitAnyIndexErrors": true
  },
  "compileOnSave": true,
  "angularCompilerOptions": {
    "genDir": ".",
    "debug": true
  }
}
And finally the bower.json.

bower.json
1
2
3
4
5
6
7
8
9
10
11
12
{
    "name": "livegamefeed",
    "private": true,
    "dependencies": {
        "bootstrap": "3.3.5",
        "jquery": "2.1.4",
        "jquery-validation": "1.14.0",
        "jquery-validation-unobtrusive": "3.2.4",
        "signalr": "2.2.0"
    },
    "ignore": ""
}
At this point you can run the npm install command to install all the NPM packages and typings as well.

Create a SignalR hub
A Hub is nothing but a C# class derived from the Microsoft.AspNetCore.SignalR.Hub. The idea is that clients may connect to a certain Hub and hence it’s logic that this class would implement methods such as OnConnected or OnDisconnected. Let’s view the abstract class in more detail.

Microsoft.AspNetCore.SignalR.Hub
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Hub : IHub, IDisposable
{
    protected Hub();
 
    [Dynamic(new[] { false, true })]
    public IHubCallerConnectionContext<dynamic> Clients { get; set; }
    public HubCallerContext Context { get; set; }
    public IGroupManager Groups { get; set; }
 
    public void Dispose();
    public virtual Task OnConnected();
    public virtual Task OnDisconnected(bool stopCalled);
    public virtual Task OnReconnected();
    protected virtual void Dispose(bool disposing);
}
A Hub can implement methods that the client may call and vice versa, the SignalR client may implement methods that the Hub may invoke. That’s the power of SignalR. Our app has a simple Hub named Broadcaster under the Hubs folder.

Broadcaster.cs
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
namespace LiveGameFeed.Hubs
{
    public class Broadcaster : Hub<IBroadcaster>
    {
        public override Task OnConnected()
        {
            // Set connection id for just connected client only
            return Clients.Client(Context.ConnectionId).SetConnectionId(Context.ConnectionId);
        }
 
        // Server side methods called from client
        public Task Subscribe(int matchId)
        {
            return Groups.Add(Context.ConnectionId, matchId.ToString());
        }
 
        public Task Unsubscribe(int matchId)
        {
            return Groups.Remove(Context.ConnectionId, matchId.ToString());
        }
    }
 
    public interface IBroadcaster
    {
        Task SetConnectionId(string connectionId);
        Task UpdateMatch(MatchViewModel match);
        Task AddFeed(FeedViewModel feed);
        Task AddChatMessage(ChatMessage message);
    }
}
Let’s discuss the above class in detail.

Broadcaster implements the OnConnected method by calling a client-side SignalR method named setConnectionId. The OnConnected event fires when the client calls the start method on the accossiated hub connection. It’s going to look like this:start a SignalR connection with a hub
1
2
3
4
// start the connection
$.connection.hub.start()
    .done(response => this.setConnectionState(SignalRConnectionStatus.Connected))
    .fail(error => this.connectionStateSubject.error(error));
The Clients property holds references to all connected clients.Clients
1
public IHubCallerConnectionContext<dynamic> Clients { get; set; }
Before invoking a client method, you can target specific clients. On the above example we targeted only the caller using the Client(Context.ConnectionId). There are other options though as you can see.

SignalR lets you group clients using the Group property.Clients
1
public IGroupManager Groups { get; set; }
Broadcaster Hub, has two server methods that clients may call in order to subscribe/unsubscribe to/from certain chat groups. In SignalR, all you have to do is add/remove the respective client connection id to/from the respective group. Here we set that the group name is equal to the matchId that the client wants to listen messages for. Later on, when the server needs to send a message to a certain group, all it takes to do is the following..

Target specific group
1
Clients.Group(message.MatchId.ToString()).AddChatMessage(message);
What the previous line of code does, is invoke the addChatMessage(message) client-side method only to those clients that have been subscribed to the group named message.MatchId.ToString().
Subscribe and Unsubscribe are the only methods that our hub implements and can be called from the client. The client though will implement much more methods and most of them will be invoked through the MVC Controllers. As you noticed, in order to call a client-side method you need reference to the IHubCallerConnectionContext Clients property but for this, we need to integrate MVC with SignalR.
We have also used an interface so we have typed support for calling client side methods. You can omit this behavior and simply derive the class from Hub.

Integrate MVC Controllers (API) with SignalR
This is the most important part of the post, making Hubs functionality available to MVC Controllers. The reason why this is that much important is based on the web application architectural patterns where clients usual make HTTP calls to REST APIs, with the only difference this time the API is also responsible to send notifications to a batch of other connected clients as well. For example, in the context of a chat conversation, if a user posts a new message to a MessagesController API Controller and that message needs to be delived to all participants, the API Controller should be able to immediately push and deliver the message to all of them.

The image denotes that SignalR server can communicate with SignalR clients either via a direct “channel” between the Hub and the client or through an integrated MVC Controller which does nothing but access and use Hub’s properties. To achieve our goal, we ‘ll make any MVC Controller that we want to use SignalR derived from the following abstract ApiHubController class. You will find that class inside the Controllers folder.

ApiHubController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class ApiHubController<T> : Controller
    where T : Hub
{
    private readonly IHubContext _hub;
    public IHubConnectionContextlt;dynamic> Clients { get; private set; }
    public IGroupManager Groups { get; private set; }
    protected ApiHubController(IConnectionManager signalRConnectionManager)
    {
        var _hub = signalRConnectionManager.GetHubContext<T>();
        Clients = _hub.Clients;
        Groups = _hub.Groups;
    }
}
The most important line of the previous class is the following:

1
var _hub = signalRConnectionManager.GetHubContext<T>();
Getting the instance of the Microsoft.AspNetCore.SignalR.IHubContext will give us access to both the Clients and the Groups properties. Let us view the interface in detail..

IHubContext.cs
1
2
3
4
5
6
7
8
9
namespace Microsoft.AspNetCore.SignalR
{
    public interface IHubContext
    {
        [Dynamic(new[] { false, true })]
        Hubs.IHubConnectionContext<dynamic> Clients { get; }
        IGroupManager Groups { get; }
    }
}
The where T : Hub means that you can create as many Hub classes as you want and make them available to any MVC Controller on demand. Now let’s see an example where we actually use this class. LiveGameFeed app has a MatchesController MVC Controller which basically is used for two reasons. First for retrieving available matches that our app serves and second, when score is updated on a match, pushes the change to all connected clients.

MatchesController.cs
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
[Route("api/[controller]")]
public class MatchesController : ApiHubController<Broadcaster>
{
    IMatchRepository _matchRepository;
    public MatchesController(
        IConnectionManager signalRConnectionManager,
        IMatchRepository matchRepository)
    : base(signalRConnectionManager)
    {
        _matchRepository = matchRepository;
    }
 
    // GET api/matches
    [HttpGet]
    public IEnumerable<MatchViewModel> Get()
    {
        IEnumerable<Match> _matches = _matchRepository.AllIncluding(m => m.Feeds);
        IEnumerable<MatchViewModel> _matchesVM = Mapper.Map<IEnumerable<Match>, IEnumerable<MatchViewModel>>(_matches);
 
        return _matchesVM;
    }
 
    // GET api/matches/5
    [HttpGet("{id}")]
    public MatchViewModel Get(int id)
    {
        Match _match = _matchRepository.GetSingle(id);
        MatchViewModel _matchVM = Mapper.Map<Match, MatchViewModel>(_match);
        return _matchVM;
    }
 
    // PUT api/matches/5
    [HttpPut("{id}")]
    public async void Put(int id, [FromBody]MatchScore score)
    {
        Match _match = _matchRepository.GetSingle(id);
        _match.HostScore = score.HostScore;
        _match.GuestScore = score.GuestScore;
        _matchRepository.Commit();
 
        MatchViewModel _matchVM = Mapper.Map<Match, MatchViewModel>(_match);
        await Clients.All.UpdateMatch(_matchVM);
    }
}
We get an instance of IHubContext for the Broadcaster Hub..

1
public class MatchesController : ApiHubController<Broadcaster>
When a match score is updated we want to notifify all connected clients, regardless if they are subscribed or not to the related feed. The client is going to implement an updateMatch function that can be called from the Hub.

1
await Clients.All.updateMatch(_matchVM);
In a similar way you will find a FeedsController MVC Controller where when a new Feed is added to a match, the API notifies those clients that not only are connected but also subscribed to that match feed. Since we want to target only the clients subscribed to the group named equal to the matchId, we use the Group property as follow.

part of FeedsController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// POST api/feeds
[HttpPost]
public async void Post([FromBody]FeedViewModel feed)
{
    Match _match = _matchRepository.GetSingle(feed.MatchId);
    Feed _matchFeed = new Feed()
    {
        Description = feed.Description,
        CreatedAt = feed.CreatedAt,
        MatchId = feed.MatchId
    };
 
    _match.Feeds.Add(_matchFeed);
 
    _matchRepository.Commit();
 
    FeedViewModel _feedVM = Mapper.Map<Feed, FeedViewModel>(_matchFeed);
 
    await Clients.Group(feed.MatchId.ToString()).AddFeed(_feedVM);
}
Create the Angular-SignalR service to communicate with SignalR hubs
Well here’s the tricky part. First of all you should know that the server will generate a client hubs proxy for you at the signalr/js location and this why you will find a reference to this file in the Views/Index.cshtml view. This script contains a jQuery.connection object that allows you to reference any hub you have defined on the server side. In many tutorials where the client side is implemented purely in jQuery you would probably find code similar to the following:

1
2
3
4
5
6
7
8
9
10
11
$(function () {
    var broadcaster = $.connection.broadcaster;
 
    broadcaster.client.message = function (text) {
        alert(text);
    };
 
    $.connection.hub.start().done(function () {
        broadcaster.server.broadcast('hello from client');
    });
  });
The code references a hub named Broadcaster and defines a client side method on the broadcaster.client object. Notice the lowercase .broadcaster declaration that connects to a Hub class named Broadcaster. You can customize both the custom Hub name and the path where the server will render the proxy library. We need though to switch to TypeScript so let’s define interfaces for the SignalR related objects. You will find them in the interfaces.ts file.

SignalR TypeScript interfaces
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
export interface FeedSignalR extends SignalR {
    broadcaster: FeedProxy
}
 
export interface FeedProxy {
    client: FeedClient;
    server: FeedServer;
}
 
export interface FeedClient {
    setConnectionId: (id: string) => void;
    updateMatch: (match: Match) => void;
    addFeed: (feed: Feed) => void;
    addChatMessage: (chatMessage: ChatMessage) => void;
}
 
export interface FeedServer {
    subscribe(matchId: number): void;
    unsubscribe(matchId: number): void;
}
 
export enum SignalRConnectionStatus {
    Connected = 1,
    Disconnected = 2,
    Error = 3
}
The SignalR interface is defined in the typings/globals/signalr/index.d.ts and we installed it via typings. The FeedProxy will contain references to the client and server hub connection objects respectively. Any client side method that we want to be invoked from the server be implemented on the client object and any server side method implemented on the server (e.g. Subscribe, Unsubscribe)will be called through the server object. The FeedClient is where you define any client side method you are going to implement and the FeedServer contains the server methods you are going to invoke. Again the methods are in lowercase and matches the uppercase relative method on the server. If you don’t use this convetion you will not be able to call the server methods. The feed.service.ts file is an @Injectable angular service where we implement our interfaces.

Implement client-side methods
The pattern is simple and we will examine the case of the addChatMessageSubject client side method. First you define an Observable property of type ChatMessage cause when called from the server, it will accept a parameter of type ChatMessage.

1
addChatMessage: Observable<ChatMessage>;
.. the ChatMessage looks like that and of course there is a relative server ViewModel on the server.

1
2
3
4
5
export interface ChatMessage {
    MatchId: number;
    Text: string;
    CreatedAt: Date;
}
Then you define rxjs/Subject property for that method.

1
private addChatMessageSubject = new Subject<ChatMessage>();
.. and you make sure to make the following assignment on the service’s constructor:

1
this.addChatMessage = this.addChatMessageSubject.asObservable();
The next step is to define a method (or event if you prefer) where you respond to the observable events.

1
2
3
private onAddChatMessage(chatMessage: ChatMessage) {
    this.addChatMessageSubject.next(chatMessage);
}
There is a last step where you actually bind this method on the client property of the hubs connection but first we need to configure our proxy. This is done on the start method as follow..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
start(debug: boolean): Observable<SignalRConnectionStatus> {
 
  // Configure the proxy
  let connection = <FeedSignalR>$.connection;
  // reference signalR hub named 'Broadcaster'
  let feedHub = connection.broadcaster;
  this.server = feedHub.server;
 
  // code omitted
 
  feedHub.client.addChatMessage = chatMessage => this.onAddChatMessage(chatMessage);
 
  // start the connection
  $.connection.hub.start()
      .done(response => this.setConnectionState(SignalRConnectionStatus.Connected))
      .fail(error => this.connectionStateSubject.error(error));
 
  return this.connectionState;
}
In case you had more than one hubs, for example a hub class OtherHub you would reference that hub as follow:

1
2
// reference signalR hub named 'OtherHub'
let otherHub = connection.otherHub;
And of course you would have to declare any methods to be called from that hub, on the otherHub.client object and so on.. We followed the observable pattern which means that any client-component that wants to react when a client method is invoked from the server, needs to be subscribed. The chat.component.ts listens for chat messages:

part of chat.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
constructor(private feedService: FeedService) { }
 
ngOnInit() {
    let self = this;
 
    self.feedService.addChatMessage.subscribe(
        message => {
            console.log('received..');
            console.log(message);
            if(!self.messages)
                self.messages = new Array<ChatMessage>();
            self.messages.unshift(message);
        }
    )
  }
But remember.. in the LiveGameFeed app, this method will be called only on those clients that are subscribed on the relative match. This is defined on the MessagesController MVC Controller, when a chat message is posted.

MessagesController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Route("api/[controller]")]
public class MessagesController : ApiHubController<Broadcaster>
{
    public MessagesController(
        IConnectionManager signalRConnectionManager)
    : base(signalRConnectionManager)
    {
 
    }
 
    // POST api/messages
    [HttpPost]
    public void Post([FromBody]ChatMessage message)
    {
        this.Clients.Group(message.MatchId.ToString()).AddChatMessage(message);
    }
}
The methods that can be called on the server are way much easier to implement since are just methods defined on the connection.server object.

part of feed.service.ts
1
2
3
4
5
6
7
8
// Server side methods
public subscribeToFeed(matchId: number) {
    this.server.subscribe(matchId);
}
 
public unsubscribeFromFeed(matchId: number) {
    this.server.unsubscribe(matchId);
}
Add Reccurent Tasks on a ASP.NET Core application
You may have noticed that in the project.json there is a RecurrentTasks package reference. I used that package in order to simulate live updates and make easier for you to see SignalR in action. In the Corefolder you will find a FeedEngine class that triggers updates on specific time intervals.

FeedEngine.cs
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class FeedEngine : IRunnable
{
    private ILogger logger;
    IMatchRepository _matchRepository;
    private string _apiURI = "http://localhost:5000/api/";
 
    public FeedEngine(IMatchRepository matchRepository,
                        ILogger<FeedEngine> logger)
    {
        this.logger = logger;
        this._matchRepository = matchRepository;
    }
    public void Run(TaskRunStatus taskRunStatus)
    {
        var msg = string.Format("Run at: {0}", DateTimeOffset.Now);
        logger.LogDebug(msg);
        UpdateScore();
    }
 
    private async void UpdateScore()
    {
        IEnumerable<Match> _matches = _matchRepository.GetAll();
 
        foreach (var match in _matches)
        {
            Random r = new Random();
            bool updateHost = r.Next(0, 2) == 1;
            int points = r.Next(2,4);
 
            if (updateHost)
                match.HostScore += points;
            else
                match.GuestScore += points;
 
            MatchScore score = new MatchScore()
            {
                HostScore = match.HostScore,
                GuestScore = match.GuestScore
            };
 
            // Update Score for all clients
            using (var client = new HttpClient())
            {
                await client.PutAsJsonAsync<MatchScore>(_apiURI + "matches/" + match.Id, score);
            }
 
            // Update Feed for subscribed only clients
 
            FeedViewModel _feed = new FeedViewModel()
            {
                MatchId = match.Id,
                Description = points + " points for " + (updateHost == true ? match.Host : match.Guest) + "!",
                CreatedAt = DateTime.Now
            };
            using (var client = new HttpClient())
            {
                await client.PostAsJsonAsync<FeedViewModel>(_apiURI + "feeds", _feed);
            }
        }
    }
}
There are two type of updates. A match score update which will be pushed to all connected clients though the MatchesController MVC Controller and feed updates being pushed through th FeedsController. In the Startup class you will also find how we configure this IRunnable task class to be triggered on time intervals.

1
2
3
4
5
6
7
8
9
10
public void ConfigureServices(IServiceCollection services)
{
    // Code omitted
    services.AddTask<FeedEngine>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    // Code omitted
    app.StartTask<FeedEngine>(TimeSpan.FromSeconds(15));
}
Have fun with the app!
I guess you have already downloaded or cloned the repository related to this post as I mentioned on start. In order to fire the app you need to run the following commands (open two terminals and navigate to the project) The first three will download NPM and Bower packages and compile the angular app. Also it will be watching for TypeScript changes during development..

1
2
3
npm install
bower install
npm start
and the .NET Core related that will restore the packages and run the server.

1
2
dotnet restore
dotnet run
Open as many browser tabs or windows as you wish and start playing with the app. Every 15 seconds the app will trigger updates and all clients will receive at least the score update. If subscribed, they will receive the feed and any messages related to the match as well. Mind that two tabs on the same window browser are two different clients for SignalR which means have a different Connection Id. The connection id for each client is displayed on the chat component. On new feed received event, the new row to be displayed is highlighted for a while. Here is the angular directive responsible for this functionality.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Directive, ElementRef, HostListener, Input, Renderer } from '@angular/core';
 
@Directive({
  selector: '[feedHighlight]'
})
export class HighlightDirective {
  constructor(private el: ElementRef, private renderer: Renderer) {
    let self = this;
      self.renderer.setElementClass(this.el.nativeElement, 'feed-highlight', true);
      setTimeout(function() {
        self.renderer.setElementClass(self.el.nativeElement,'feed-highlight-light', true);
      }, 1000);
   }
 
  private highlight(color: string) {
    this.renderer.setElementStyle(this.el.nativeElement, 'backgroundColor', color);
  }
}
Conclusion
SignalR library is awesome but you need to make sure that this is the right choice to make before using it. In case you have multiple clients that is important to push them updates on real time then you are good to go. That’s it, we finally finished! We have seen how to setup an ASP.NET Core project that leverages SignalR library through MVC Controllers. More over we used SignalR typings in order to create and use the SignalR client library using Angular and TypeScript.

Source Code: You can find the source code for this project here where you will also find instructions on how to run the application.