受欢迎的博客标签

ASP.NET Core SignalR(3):Getting started ASP.NET Core web project 8.x with SignalR step by step

Published

There will be 2 projects created:

.Server is net5.0 and using Microsoft.NET.Sdk.Web SDK

.Client is net5.0 and connect to Server

 

Prerequisites

vs 2022

windows server

Table of Content

Create a web project with mvc

install Microsoft.AspNetCore.SignalR

create hubs

Add the SignalR client library

 

step 1:Create a web project with mvc

step 2:Install required packages for SignalR

Install Microsoft.AspNetCore.SignalR

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

	<ItemGroup>		
		<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
		<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.2" />
		<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
		<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="5.0.0" />
		
	</ItemGroup>	

</Project>

Step 3:Create hubs

使用SignalR 即时更新状态。

首先建立SchedulerHub作为SignalR伺服端控管前后端沟通的渠道,能够接收client端的请求,并且也能主动通知 client状态异动。

public class SchedulerHub : Hub
{
    private QuartzHostedService _quartzHostedService;

    /// <summary>
    /// 建構子
    /// </summary>
    /// <param name="quartzHostedService">Quartz排程服務</param>
    public SchedulerHub (QuartzHostedService quartzHostedService)
    {
        _quartzHostedService = quartzHostedService;
    }

    /// <summary>
    /// 要求取得Job狀態
    /// </summary>
    public async Task RequestJobStatus()
    {
        if (Clients != null)
        {
            var jobs = await _quartzHostedService.GetJobSchedules();
            var jobSummary = jobs.Select(e => 
                    new JobScheduleSummary { 
                        JobName = e.JobName, 
                        CronExpression = e.CronExpression, 
                        JobStatusName = e.JobStatus.GetDescription(), 
                        JobStatusId = e.JobStatus, 
                        JobType = e.JobType.FullName 
                    }
                );

            await Clients.Caller.SendAsync("ReceiveJobStatus", jobSummary);
        }
    }

    /// <summary>
    /// 通知Job狀態改變
    /// </summary>
    public async Task NotifyJobStatusChange()
    {
        if (Clients != null)
        {
            await Clients.All.SendAsync("JobStatusChange");
        }
    }

    /// <summary>
    /// 手動觸發Job執行
    /// </summary>
    public async Task TriggerJob(string jobName)
    {
        await _quartzHostedService.TriggerJobAsync(jobName);
    }

    /// <summary>
    /// 手動中斷Job執行
    /// </summary>
    public async Task InterruptJob(string jobName)
    {
        await _quartzHostedService.InterruptJobAsync(jobName);
    }

        
    /// <summary>
    /// 開啟排程器
    /// </summary>
    public async Task StartScheduler()
    {
        await _quartzHostedService.StartAsync(_quartzHostedService.CancellationToken);
    }

    /// <summary>
    /// 關閉排程器
    /// </summary>
    public async Task StopScheduler()
    {
        await _quartzHostedService.StopAsync(_quartzHostedService.CancellationToken);
    }


    /// <summary>
    /// 用戶連線事件
    /// </summary>
    public override async Task OnConnectedAsync()
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, "SignalR Users");
        await NotifyJobStatusChange();
        await base.OnConnectedAsync();
    }


    /// <summary>
    /// 用戶斷線事件
    /// </summary>
    public override async Task OnDisconnectedAsync(Exception exception)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, "SignalR Users");
        await base.OnDisconnectedAsync(exception);
    }
}

Step 4:SignalR伺服端设置

于DI容器中注册JobListener、SchedulerListener及SchedulerHub后,加入SignalR服务及设定SignalR Router


//https://learn.microsoft.com/en-us/aspnet/core/signalr/hubs?view=aspnetcore-8.0
using Microsoft.AspNetCore.SignalR;

namespace Frpcharp.Signal.Server
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddControllersWithViews();

            builder.Services.AddSignalR();

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthorization();

            app.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");

            app.MapHub<MyHub>("/myhub");

            app.Run();
        }
    }


    public class MyHub : Hub
    {
        public async Task SendMessage(string user, string message)
            => await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

至此SignalR伺服端设置已经完成了。

Step 5:前端设置

ASP.NET Core SignalR .NET Client

Add the SignalR client library

 

In Solution Explorer, right-click the project, and select Add > Client-Side Library.
In the Add Client-Side Library dialog, for Provider select unpkg.
For Library, enter @aspnet/signalr

Add Client-Side Library dialog

enter @aspnet/signalr

 

VUE+ASP.NET Core SignalR JavaScript client

使用vue来完成前端Home/index.cshtml的互动.

前端就只要依照SignalR开发方式进行即可,首先在_Layout.cshtml中载入所需要的js档案。

@*加入 signalr.js + vue.js *@
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/3.1.7/signalr.min.js"></script>
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/http-vue-loader"></script>

使用vue来完成前端Home/index.cshtml的互动。主要就是使用 signalR.HubConnectionBuilder建立起与后端的连线,接着定义可被后端触发的事件,以及可以直接呼叫后端SchedulerHub方法的动作,程式逻辑约略如下。

@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">

    <div id="app">
        <h1 class="display-4">System Scheduler</h1>
        <div class="scheduler-actions">
            <button v-on:click="startScheduler" :disabled="!isSchedulerStop">Start Scheduler</button>
            <button v-on:click="stopScheduler" :disabled="isSchedulerStop">Stop Scheduler</button>
            <button v-on:click="refresh">Refresh {{counter}}</button>
        </div>

        <table class="schedule-table">
            <thead class="schedule-table__header">
                <tr>
                    <td align="center">
                        Job Name
                    </td>
                    <td align="center">
                        Job Type
                    </td>
                    <td align="center">
                        CRON
                    </td>
                    <td align="center">
                        Status
                    </td>
                    <td>
                        Actions
                    </td>
                </tr>
            </thead>
            <tr v-for="(job,index) in jobs" :key="index">
                <td align="center">
                    {{job.jobName}}
                </td>
                <td align="center">
                    {{job.jobType}}
                </td>
                <td align="center">
                    {{job.cronExpression}}
                </td>
                <td align="center" :class="isExecuting(job.jobStatusId) ? 'schedule-table--active' : ''">
                    {{job.jobStatusName}}
                </td>
                <td>
                    <button v-on:click="()=>trigger(job.jobName)" :disabled="!isStandby(job.jobStatusId)">Trigger</button>
                    <button v-on:click="()=>Interrupt(job.jobName)" :disabled="!isExecuting(job.jobStatusId)">Interrupt</button>
                </td>
            </tr>
        </table>

        <div class="scheduler-actions">
             {{time}}
        </div>
    </div>
</div>


<script>


    var app = new Vue({
        el: '#app',
        data: {
            connection: null,
            jobs: [],
            counter: 0,
            time: '',
            timeInterval: null
        },
        async mounted() {

            this.connection = new signalR.HubConnectionBuilder()
                .withUrl("/schedulerHub")
                .build();

            // 被後端呼叫接收 Job 目前狀態
            this.connection.on("ReceiveJobStatus", (jobs) => {
                this.jobs = jobs;
                this.counter += 1;
            });

            // 被後端呼叫接收 Job 狀態改變的通知
            this.connection.on("JobStatusChange", () => {
                this.connection.invoke("RequestJobStatus").catch(err => console.error(err));
            });

            this.connection.start().catch(err => console.error(err));

            this.time = this.getTime();
            this.timeInterval = window.setInterval(() => this.time = this.getTime(), 300);
        },
        destroyed() {
            window.clearInterval(this.timeInterval);
        },
        computed: {
            isSchedulerStop() {
                return this.jobs.findIndex(j => j.jobStatusId === 3) > -1
            }
        },
        methods: {
            isExecuting(status) {
                return status === 2;
            },
            isStandby(status) {
                return status === 1;
            },
            refresh() {
                // 呼叫後端提供所有 Job 的狀態
                this.connection.invoke("RequestJobStatus").catch(err => console.error(err));
            },
            trigger(jobName) {
                // 呼叫後端觸發特定Job
                this.connection.invoke("TriggerJob", jobName).catch(err => console.error(err));
            },
            Interrupt(jobName) {
                // 呼叫後端終止特定Job
                this.connection.invoke("InterruptJob", jobName).catch(err => console.error(err));
            },
            startScheduler() {
                // 呼叫後端啟動排程
                this.connection.invoke("StartScheduler").catch(err => console.error(err));
            },
            stopScheduler() {
                // 呼叫後端終止排程
                this.connection.invoke("StopScheduler").catch(err => console.error(err));
            },
            getTime() {
                var dt = new Date();
                var DD = ("0" + dt.getDate()).slice(-2);
                var MM = ("0" + (dt.getMonth() + 1)).slice(-2);
                var YYYY = dt.getFullYear();
                var hh = ("0" + dt.getHours()).slice(-2);
                var mm = ("0" + dt.getMinutes()).slice(-2);
                var ss = ("0" + dt.getSeconds()).slice(-2);
                return YYYY + "-" + MM + "-" + DD + " " + hh + ":" + mm + ":" + ss;
            }
        }
    });


</script>

useful links

Quartz.Net 排程作业Host 于ASP.NET Core 网站中,并以 SignalR +vue实现Dashboard 监控页面