diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln
index 5d6f3e99a2..3ff151439f 100644
--- a/framework/Volo.Abp.sln
+++ b/framework/Volo.Abp.sln
@@ -283,7 +283,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.TextTemplating", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.TextTemplating.Tests", "test\Volo.Abp.TextTemplating.Tests\Volo.Abp.TextTemplating.Tests.csproj", "{251C7FD3-D313-4BCE-8068-352EC7EEA275}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Validation.Abstractions", "src\Volo.Abp.Validation.Abstractions\Volo.Abp.Validation.Abstractions.csproj", "{FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.Validation.Abstractions", "src\Volo.Abp.Validation.Abstractions\Volo.Abp.Validation.Abstractions.csproj", "{FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Volo.Abp.AspNetCore.SignalR", "src\Volo.Abp.AspNetCore.SignalR\Volo.Abp.AspNetCore.SignalR.csproj", "{B64FCE08-E9D2-4984-BF12-FE199F257416}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -847,6 +849,10 @@ Global
{FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B64FCE08-E9D2-4984-BF12-FE199F257416}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B64FCE08-E9D2-4984-BF12-FE199F257416}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B64FCE08-E9D2-4984-BF12-FE199F257416}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B64FCE08-E9D2-4984-BF12-FE199F257416}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -991,6 +997,7 @@ Global
{9E53F91F-EACD-4191-A487-E727741F1311} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{251C7FD3-D313-4BCE-8068-352EC7EEA275} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{FA5D1D6A-2A05-4A3D-99C1-2B6C1D1F99A3} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
+ {B64FCE08-E9D2-4984-BF12-FE199F257416} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}
diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/FodyWeavers.xml b/framework/src/Volo.Abp.AspNetCore.SignalR/FodyWeavers.xml
new file mode 100644
index 0000000000..be0de3a908
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.SignalR/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/FodyWeavers.xsd b/framework/src/Volo.Abp.AspNetCore.SignalR/FodyWeavers.xsd
new file mode 100644
index 0000000000..3f3946e282
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.SignalR/FodyWeavers.xsd
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.
+
+
+
+
+ A comma-separated list of error codes that can be safely ignored in assembly verification.
+
+
+
+
+ 'false' to turn off automatic generation of the XML Schema file.
+
+
+
+
+
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Properties/launchSettings.json b/framework/src/Volo.Abp.AspNetCore.SignalR/Properties/launchSettings.json
new file mode 100644
index 0000000000..418c7f2621
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Properties/launchSettings.json
@@ -0,0 +1,27 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:53900/",
+ "sslPort": 44362
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "Volo.Abp.SignalR": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:5001;http://localhost:5000"
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo.Abp.AspNetCore.SignalR.csproj b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo.Abp.AspNetCore.SignalR.csproj
new file mode 100644
index 0000000000..f4c833cb90
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo.Abp.AspNetCore.SignalR.csproj
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ netcoreapp3.1
+ Volo.Abp.AspNetCore.SignalR
+ Volo.Abp.AspNetCore.SignalR
+ $(AssetTargetFallback);portable-net45+win8+wp8+wpa81;
+ false
+ false
+ false
+ true
+ Library
+
+
+
+
+
+
+
+
diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRModule.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRModule.cs
new file mode 100644
index 0000000000..e26de184af
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpAspNetCoreSignalRModule.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http.Connections;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Volo.Abp.Modularity;
+
+namespace Volo.Abp.AspNetCore.SignalR
+{
+ [DependsOn(
+ typeof(AbpAspNetCoreModule)
+ )]
+ public class AbpAspNetCoreSignalRModule : AbpModule
+ {
+ private static readonly MethodInfo MapHubGenericMethodInfo =
+ typeof(AbpAspNetCoreSignalRModule)
+ .GetMethod("MapHub", BindingFlags.Static | BindingFlags.NonPublic);
+
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.AddConventionalRegistrar(new AbpSignalRConventionalRegistrar());
+
+ AutoAddHubTypes(context.Services);
+ }
+
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.AddSignalR();
+
+ Configure(options =>
+ {
+ options.EndpointConfigureActions.Add(endpointContext =>
+ {
+ var signalROptions = endpointContext
+ .ScopeServiceProvider
+ .GetRequiredService>()
+ .Value;
+
+ foreach (var hubConfig in signalROptions.Hubs)
+ {
+ MapHubType(
+ hubConfig.HubType,
+ endpointContext.Endpoints,
+ hubConfig.RoutePattern,
+ opts =>
+ {
+ foreach (var configureAction in hubConfig.ConfigureActions)
+ {
+ configureAction(opts);
+ }
+ }
+ );
+ }
+ });
+ });
+ }
+
+ private void AutoAddHubTypes(IServiceCollection services)
+ {
+ var hubTypes = new List();
+
+ services.OnRegistred(context =>
+ {
+ if (typeof(Hub).IsAssignableFrom(context.ImplementationType))
+ {
+ hubTypes.Add(context.ImplementationType);
+ }
+ });
+
+ services.Configure(options =>
+ {
+ foreach (var hubType in hubTypes)
+ {
+ options.Hubs.Add(HubConfig.Create(hubType));
+ }
+ });
+ }
+
+ private void MapHubType(
+ Type hubType,
+ IEndpointRouteBuilder endpoints,
+ string pattern,
+ Action configureOptions)
+ {
+ MapHubGenericMethodInfo
+ .MakeGenericMethod(hubType)
+ .Invoke(
+ this,
+ new object[]
+ {
+ endpoints,
+ pattern,
+ configureOptions
+ }
+ );
+ }
+
+ // ReSharper disable once UnusedMember.Local (used via reflection)
+ private static void MapHub(
+ IEndpointRouteBuilder endpoints,
+ string pattern,
+ Action configureOptions)
+ where THub : Hub
+ {
+ endpoints.MapHub(
+ pattern,
+ configureOptions
+ );
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalRConventionalRegistrar.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalRConventionalRegistrar.cs
new file mode 100644
index 0000000000..9dcb46d6bc
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalRConventionalRegistrar.cs
@@ -0,0 +1,43 @@
+using System;
+using Microsoft.AspNetCore.SignalR;
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.AspNetCore.SignalR
+{
+ public class AbpSignalRConventionalRegistrar : ConventionalRegistrarBase
+ {
+ public override void AddType(IServiceCollection services, Type type)
+ {
+ if (IsConventionalRegistrationDisabled(type))
+ {
+ return;
+ }
+
+ if (!IsHub(type))
+ {
+ return;
+ }
+
+ var serviceTypes = ExposedServiceExplorer.GetExposedServices(type);
+
+ TriggerServiceExposing(services, type, serviceTypes);
+
+ foreach (var serviceType in serviceTypes)
+ {
+ services.Add(
+ ServiceDescriptor.Describe(
+ serviceType,
+ type,
+ ServiceLifetime.Transient
+ )
+ );
+ }
+ }
+
+ private static bool IsHub(Type type)
+ {
+ return typeof(Hub).IsAssignableFrom(type);
+ }
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions.cs
new file mode 100644
index 0000000000..c0a07a2ea3
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/AbpSignalROptions.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+
+namespace Volo.Abp.AspNetCore.SignalR
+{
+ public class AbpSignalROptions
+ {
+ public List Hubs { get; }
+
+ public AbpSignalROptions()
+ {
+ Hubs = new List();
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfig.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfig.cs
new file mode 100644
index 0000000000..59c9342fce
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubConfig.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using Microsoft.AspNetCore.Http.Connections;
+using Microsoft.AspNetCore.SignalR;
+
+namespace Volo.Abp.AspNetCore.SignalR
+{
+ public class HubConfig
+ {
+ [NotNull]
+ public Type HubType { get; }
+
+ [NotNull]
+ public string RoutePattern { get; set; }
+
+ [NotNull]
+ public List> ConfigureActions { get; set; }
+
+ public HubConfig(
+ [NotNull] Type hubType,
+ [NotNull] string routePattern)
+ {
+ HubType = Check.NotNull(hubType, nameof(hubType));
+ RoutePattern = Check.NotNullOrWhiteSpace(routePattern, nameof(routePattern));
+ ConfigureActions = new List>();
+ }
+
+ public static HubConfig Create()
+ where THub : Hub
+ {
+ return Create(typeof(THub));
+ }
+
+ public static HubConfig Create(Type hubType)
+ {
+ return new HubConfig(
+ hubType,
+ HubRouteAttribute.GetRoutePattern(hubType)
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubRouteAttribute.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubRouteAttribute.cs
new file mode 100644
index 0000000000..5de916a89d
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/HubRouteAttribute.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Reflection;
+using Microsoft.AspNetCore.SignalR;
+
+namespace Volo.Abp.AspNetCore.SignalR
+{
+ public class HubRouteAttribute : Attribute
+ {
+ public string RoutePattern { get; set; }
+
+ public HubRouteAttribute(string routePattern)
+ {
+ RoutePattern = routePattern;
+ }
+
+ public static string GetRoutePattern()
+ where THub : Hub
+ {
+ return GetRoutePattern(typeof(THub));
+ }
+
+ public static string GetRoutePattern(Type hubType)
+ {
+ var routeAttribute = hubType.GetSingleAttributeOrNull();
+ if (routeAttribute != null)
+ {
+ return routeAttribute.RoutePattern;
+ }
+
+ return "/signalr-hubs/" + hubType.Name.RemovePostFix("Hub").ToKebabCase();
+ }
+ }
+}
\ No newline at end of file
diff --git a/nupkg/common.ps1 b/nupkg/common.ps1
index 6dfe0041aa..661f630a0b 100644
--- a/nupkg/common.ps1
+++ b/nupkg/common.ps1
@@ -41,6 +41,7 @@ $projects = (
"framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared",
"framework/src/Volo.Abp.AspNetCore.Mvc.UI.Widgets",
"framework/src/Volo.Abp.AspNetCore.Serilog",
+ "framework/src/Volo.Abp.AspNetCore.SignalR",
"framework/src/Volo.Abp.AspNetCore.TestBase",
"framework/src/Volo.Abp.Auditing",
"framework/src/Volo.Abp.Authorization",