Сериализация параметров метода хаба сигнала SignalR
Мне нужны некоторые рекомендации от разработчиков SignalR, что лучший способ настройки сериализации параметров метода HUB.
Я начал переносить свой проект из дуплексного опроса WCF (Silverlight 5 - ASP.NET 4.5) в SignalR (1.1.2). Сообщение (контракт данных) является полиморфным на основе интерфейсов. (Как IMessage, MessageA: IMessage и т.д. - на самом деле иерархия интерфейсов реализована классами, но это не имеет большого значения для вопроса).
(Я знаю, что полиморфные объекты не подходят для клиентов, но клиент будет обрабатывать его как JSON, а сопоставление с объектами выполняется только на стороне сервера или клиента, если это .NET/Silverlight)
На концентраторе я определил метод следующим образом:
public void SendMessage(IMessage data) { .. }
Я создал пользовательские JsonConverters и проверил, что сообщения могут быть сериализованы/десериализованы с использованием Json.NET. Затем я заменил JsonNetSerializer в DependencyResolver правильными настройками. Аналогично на стороне клиента Silverlight. Пока все хорошо.
Но когда я отправил сообщение от клиента на сервер (сообщение было правильно сериализовано в JSON - проверено в Fiddler), сервер ответил на ошибку, что параметр не может быть десериализован.
С помощью отладчика я обнаружил ошибку в SignalR (класс JRawValue, ответственный за десериализацию параметра, создает внутренне собственный экземпляр JsonSerializer, игнорируя предоставленный). Кажется, это довольно легко исправить, заменив
var settings = new JsonSerializerSettings
{
MaxDepth = 20
};
var serializer = JsonSerializer.Create(settings);
return serializer.Deserialize(jsonReader, type);
с
var serializer = GlobalHost.DependencyResolver.Resolve<IJsonSerializer>();
return serializer.Parse(jsonReader, type);
но я также обнаружил, что интерфейс IJsonSerializer будет удален в будущей версии SignalR. Мне нужно, в основном, получить либо сырой поток JSON (или байтовый поток) из метода HUB, чтобы я мог десериализовать его самостоятельно, либо возможность настроить сериализатор, указав преобразователи и т.д.
В настоящее время я закончил с определением метода с типом параметра JObject:
public void SendMessage(JObject data)
за которым следует ручная десериализация данных с использованием
JObject.ToObject<IMessage>(JsonSerializer)
метод. Но я бы предпочел настроить сериализатор и иметь тип/интерфейс для метода хаба. Что такое "правильный путь" для создания следующего сигнала SignalR?
Мне также было полезно иметь возможность отправлять обратно сырые JSON клиентам из моего кода, т.е. чтобы объект снова не был снова сериализован сигналом. Как я мог достичь этого?
Ответы
Ответ 1
Если вы используете API-интерфейс соединения вместо API-интерфейса Hub, вы можете обработать событие OnReceive и получить запрос как необработанный JSON (строка). Посмотрите этот пример.
Возможность отправки предварительно сериализованных данных клиентам с использованием Hub API была добавлена в версии 2.x, и я не знаю, как это сделать в 1.x(см. проблема github)
Ответ 2
Я попытался изменить конфигурацию сериализации клиента и сервера с помощью EnableJsonTypeNameHandlingConverter
, опубликованного здесь, а также следующего кода клиента и сервера для двунаправленного соединения.
Как видите, есть код для настройки пользовательской сериализации как на клиенте, так и на сервере... но он не работает!
using System;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Client;
using Newtonsoft.Json;
using Owin;
class Program
{
static void Main(string[] args)
{
// Ensure serialization and deserialization works outside SignalR
INameAndId nameId = new NameAndId(5, "Five");
string json = JsonConvert.SerializeObject(nameId, Formatting.Indented, new EnableJsonTypeNameHandlingConverter());
var clone = JsonConvert.DeserializeObject(json, typeof(INameAndId), new EnableJsonTypeNameHandlingConverter());
Console.WriteLine(json);
// Start server
// http://+:80/Temporary_Listen_Addresses is allowed by default - all other routes require special permission
string url = "http://+:80/Temporary_Listen_Addresses/example";
using (Microsoft.Owin.Hosting.WebApp.Start(url))
{
Console.WriteLine("Server running on {0}", url);
// Start client side
HubConnection conn = new HubConnection("http://127.0.0.1:80/Temporary_Listen_Addresses/example");
conn.JsonSerializer.Converters.Add(new EnableJsonTypeNameHandlingConverter());
// Note: SignalR requires CreateHubProxy() to be called before Start()
var hp = conn.CreateHubProxy(nameof(SignalRHub));
var proxy = new SignalRProxy(hp, new SignalRCallback());
conn.Start().Wait();
proxy.Foo();
// AggregateException on server: Could not create an instance of type
// SignalRSelfHost.INameAndId. Type is an interface or abstract class
// and cannot be instantiated.
proxy.Bar(nameId);
Console.ReadLine();
}
}
}
class Startup
{
// Magic method expected by OWIN
public void Configuration(IAppBuilder app)
{
//app.UseCors(CorsOptions.AllowAll);
var hubCfg = new HubConfiguration();
var jsonSettings = new JsonSerializerSettings();
jsonSettings.Converters.Add(new EnableJsonTypeNameHandlingConverter());
hubCfg.EnableDetailedErrors = true;
hubCfg.Resolver.Register(typeof(JsonSerializer), () => JsonSerializer.Create(jsonSettings));
GlobalHost.DependencyResolver.Register(typeof(JsonSerializer), () => JsonSerializer.Create(jsonSettings));
app.MapSignalR(hubCfg);
}
}
// Messages that can be sent to the server
public interface ISignalRInterface
{
void Foo();
void Bar(INameAndId param);
}
// Messages that can be sent back to the client
public interface ISignalRCallback
{
void Baz();
}
// Server-side hub
public class SignalRHub : Hub<ISignalRCallback>, ISignalRInterface
{
protected ISignalRCallback GetCallback(string hubname)
{
// Note: SignalR hubs are transient - they connection lives longer than the
// Hub - so it is generally unwise to store information in member variables.
// Therefore, the ISignalRCallback object is not cached.
return GlobalHost.ConnectionManager.GetHubContext<ISignalRCallback>(hubname).Clients.Client(Context.ConnectionId);
}
public virtual void Foo() { Console.WriteLine("Foo!"); }
public virtual void Bar(INameAndId param) { Console.WriteLine("Bar!"); }
}
// Client-side proxy for server-side hub
public class SignalRProxy
{
private IHubProxy _Proxy;
public SignalRProxy(IHubProxy proxy, ISignalRCallback callback)
{
_Proxy = proxy;
_Proxy.On(nameof(ISignalRCallback.Baz), callback.Baz);
}
public void Send(string method, params object[] args)
{
_Proxy.Invoke(method, args).Wait();
}
public void Foo() => Send(nameof(Foo));
public void Bar(INameAndId param) => Send(nameof(Bar), param);
}
public class SignalRCallback : ISignalRCallback
{
public void Baz() { }
}
[Serializable]
public class NameAndId : INameAndId
{
public NameAndId(int id, string name)
{
Id = id;
Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
}
[EnableJsonTypeNameHandling]
public interface INameAndId
{
string Name { get; }
int Id { get; }
}
SignalR вызывает лямбду, переданную GlobalHost.DependencyResolver
, не менее 8 раз, но в итоге игнорирует предоставленный сериализатор.
Я не смог найти никакой документации по сериализации параметров SignalR, поэтому я использовал отладчик декомпиляции Rider, чтобы выяснить, что происходит.
Внутри SignalR есть метод HubRequestParser.Parse
, который использует правильный JsonSerializer
, но он фактически не десериализует параметры. Параметры десериализируются позже в DefaultParameterResolver.ResolveParameter()
, который косвенно вызывает CreateDefaultSerializerSettings()
в следующем стеке вызовов:
JsonUtility.CreateDefaultSerializerSettings() in Microsoft.AspNet.SignalR.Json, Microsoft.AspNet.SignalR.Core.dll
JsonUtility.CreateDefaultSerializer() in Microsoft.AspNet.SignalR.Json, Microsoft.AspNet.SignalR.Core.dll
JRawValue.ConvertTo() in Microsoft.AspNet.SignalR.Json, Microsoft.AspNet.SignalR.Core.dll
DefaultParameterResolver.ResolveParameter() in Microsoft.AspNet.SignalR.Hubs, Microsoft.AspNet.SignalR.Core.dll
Enumerable.<ZipIterator>d__61<ParameterDescriptor, IJsonValue, object>.MoveNext() in System.Linq, System.Core.dll
new Buffer<object>() in System.Linq, System.Core.dll
Enumerable.ToArray<object>() in System.Linq, System.Core.dll
DefaultParameterResolver.ResolveMethodParameters() in Microsoft.AspNet.SignalR.Hubs, Microsoft.AspNet.SignalR.Core.dll
HubDispatcher.InvokeHubPipeline() in Microsoft.AspNet.SignalR.Hubs, Microsoft.AspNet.SignalR.Core.dll
HubDispatcher.OnReceived() in Microsoft.AspNet.SignalR.Hubs, Microsoft.AspNet.SignalR.Core.dll
PersistentConnection.<>c__DisplayClass64_1.<ProcessRequestPostGroupRead>b__5() in Microsoft.AspNet.SignalR, Microsoft.AspNet.SignalR.Core.dll
TaskAsyncHelper.FromMethod() in Microsoft.AspNet.SignalR, Microsoft.AspNet.SignalR.Core.dll
PersistentConnection.<>c__DisplayClass64_0.<ProcessRequestPostGroupRead>b__4() in Microsoft.AspNet.SignalR, Microsoft.AspNet.SignalR.Core.dll
WebSocketTransport.OnMessage() in Microsoft.AspNet.SignalR.Transports, Microsoft.AspNet.SignalR.Core.dll
DefaultWebSocketHandler.OnMessage() in Microsoft.AspNet.SignalR.WebSockets, Microsoft.AspNet.SignalR.Core.dll
WebSocketHandler.<ProcessWebSocketRequestAsync>d__25.MoveNext() in Microsoft.AspNet.SignalR.WebSockets, Microsoft.AspNet.SignalR.Core.dll
AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext() in System.Runtime.CompilerServices, mscorlib.dll [5]
ExecutionContext.RunInternal() in System.Threading, mscorlib.dll [5]
ExecutionContext.Run() in System.Threading, mscorlib.dll [5]
AsyncMethodBuilderCore.MoveNextRunner.Run() in System.Runtime.CompilerServices, mscorlib.dll [5]
...
В исходном коде SignalR проблема очевидна:
// in DefaultParameterResolver
public virtual object ResolveParameter(ParameterDescriptor descriptor, IJsonValue value)
{
// [...]
return value.ConvertTo(descriptor.ParameterType);
}
// in JRawValue
public object ConvertTo(Type type)
{
// A non generic implementation of ToObject<T> on JToken
using (var jsonReader = new StringReader(_value))
{
var serializer = JsonUtility.CreateDefaultSerializer();
return serializer.Deserialize(jsonReader, type);
}
}
// in JsonUtility
public static JsonSerializer CreateDefaultSerializer()
{
return JsonSerializer.Create(CreateDefaultSerializerSettings());
}
public static JsonSerializerSettings CreateDefaultSerializerSettings()
{
return new JsonSerializerSettings() { MaxDepth = DefaultMaxDepth };
}
Таким образом, SignalR использует ваш собственный (де) сериализатор для части своей работы, а не для десериализации параметров.
Я не могу понять, что ответ 2015 года на этот другой вопрос имеет 8 голосов, что, по-видимому, подразумевает, что это решение в какой-то момент сработало для кого-то за последние 4 года, но если это так, то должно быть быть обманом, о котором мы не знаем.
Возможно, .NET Core версия SignalR решает эту проблему. Похоже, что эта версия была значительно реорганизована и больше не имеет файла DefaultParameterResolver.cs
. Кто-нибудь хочет проверить?