SignalR and serializing object array - serialization

I'm new to SignalR and have done a simple test hack. I wish to serialize an object array with typed objects. By default SignalR has configured the JSon.NET serializer to not provide with type information. And I found that I could register a custom serializer in the DependencyResolver by:
var serializer =
new EventHubJsonSerializer(
new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
TypeNameHandling = TypeNameHandling.Objects
});
GlobalHost.DependencyResolver.Register(typeof(IJsonSerializer), () => serializer);
However when I recieve my object array it will not resolve the types, instead it is a JSonContainer. Can I solve this in any way?
The event emitted from the Hub:
public sealed class SignalREvent
{
public string Group { get; set; }
public string EventName { get; set; }
public string TypeFullName { get; set; }
public IList<object> EventArguments { get; set; }
}
And the receptor have to unwrap the boolean via casting to dynamic:
public sealed class TestEventArgs : EventArgs
{
#region Public Properties
/// <summary>
/// Gets or sets a value indicating whether do not print.
/// </summary>
public bool DoNotPrint { get; set; }
/// <summary>
/// Gets or sets the event name.
/// </summary>
public string EventName { get; set; }
#endregion
}
this.subscription = this.client.On<SignalREvent>(
"PushEvent",
data =>
{
dynamic eventArg1 = data.EventArguments[0];
if (eventArg1.DoNotPrint.Value)
{
return;
}
});
What I've done is a postsharp aspect to apply on events in order to allow them to propagate via SignalR via my EventHub. For example:
[ExternalizeEvent]
public event ASimpleDelegate SimpleEvent;
It's a darn simple aspect, but it would really be good to have type info when in the .net world - other clients would of course not benefit of this.
Update
This is the output for my JSon.NET configuration - types are propagated in the $type but it seems that it is not used during deseralization.
{
"$id": "11",
"$type": "<>f__AnonymousType0`3[[System.String, mscorlib],[System.String, mscorlib],[System.Object[], mscorlib]], SignalR",
"Hub": "Externalize.EventHub",
"Method": "PushEvent",
"Args": [
{
"$id": "12",
"$type": "DataDuctus.SignalR.Aspects.SignalREvent, DataDuctus.SignalR.Aspects",
"Group": "all",
"EventName": "SimpleEvent",
"TypeFullName": "TestConsole.TestEvents",
"EventArguments": [
{
"$id": "13",
"$type": "TestConsole.TestEventArgs, TestConsole",
"DoNotPrint": false,
"EventName": "second event (test)"
}
]
}
]
}
Cheers,
Mario

The SignalR .NET client does not use the DependencyResolver from the server and currently does not have an IoC container of its own. Because of this, as you note in your question, your custom JsonSerializerSettings are used for serialization on the server but not for deserialization on the client.
In the next release of SignalR we plan to add a DependencyResolver to the .NET client that will allow you to provide your own Newtonsoft.Json.JsonSerializer or Newtonsoft.Json.JsonSerializerSettings to be used during deserialization. There are currently no plans to allow the use of a non-Json.NET (de)serializer in the .NET client.
If you need this functionality now, you could clone https://github.com/SignalR/SignalR.git and modify the private static T Convert<T>(JToken obj) method in SignalR.Client\Hubs\HubProxyExtensions.cs to return obj.ToObject<T>(yourJsonSerializer).

Related

How to make an IOptions section optional in .NET Core?

Consider an example service that optionally supports LDAP authentication, otherwise, it does something like local Identity authentication. When LDAP is completely configured, appsettings.json might look like this...
{
"LdapOptions": {
"Host": "ldap.example.com",
"Port": 389
}
}
With an options class.
public class LdapOptions
{
public string Host { get; set; }
public int Port { get; set; } = 389;
}
And Startup has the expected Configure call.
service.Configure<LdapOptions>(nameof(LdapOptions));
This work great when I have a complete valid "LdapOptions" section. But, it's not so great if I intentionally leave the section out of my appsettings.
An IOptions<TOptions> instance resolves even if I leave the section out of my appsettings entirely; it even resolves if I remove the Startup configure call entirely! I get an object that appears, based on property values, to be default(TOptions).
public AuthenticationService(IOptions<LdapOptions> ldapOptions)
{
this.ldapOptions = ldapOptions.Value; // never null, sometimes default(LdapOptions)!
}
I don't want to depend on checking properties if a section is intentionally left out. I can imagine scenarios where all of the properties in an object have explicit defaults and this wouldn't work. I'd like something like a Maybe<TOptions> with a HasValue property, but I'll take a null.
Is there any way to make an options section optional?
Update: Be aware that I also intend to validate data annotations...
services.AddOptions<LdapOptions>()
.Configure(conf.GetSection(nameof(LdapOptions)))
.ValidateDataAnnotations();
So, what I really want is for optional options to be valid when the section is missing (conf.Exists() == false) and then normal validations to kick in when the section is partially or completely filled out.
I can't imagine any solution working with data annotation validations that depends on the behavior of creating a default instance (for example, there is no correct default for Host, so a default instance will always be invalid).
The whole idea of IOptions<T> is to have non-null default values, so that your settings file doesn't contain hundreds/thousands sections to configure the entire ASP pipeline
So, its not possible to make it optional in the sense that you will get null, but you can always defined some "magic" property to indicate whether this was configured or not:
public class LdapOptions
{
public bool IsEnabled { get; set; } = false;
public string Host { get; set; }
public int Port { get; set; } = 389;
}
and your app settings file:
{
"LdapOptions": {
"IsEnabled: true,
"Host": "ldap.example.com",
"Port": 389
}
}
Now, if you keep 'IsEnabled' consistently 'true' in your settings, if IsEnabled is false, that means the section is missing.
An alternative solution is to use a different design approach, e.g. put the auth type in the settings file:
public class LdapOptions
{
public string AuthType { get; set; } = "Local";
public string Host { get; set; }
public int Port { get; set; } = 389;
}
And your app settings:
{
"LdapOptions": {
"AuthType : "LDAP",
"Host": "ldap.example.com",
"Port": 389
}
}
This is IMO a cleaner & more consistent approach
If you must have a logic that is based on available/missing section, you can also configure it directly:
var section = conf.GetSection(nameof(LdapOptions));
var optionsBuilder = services.AddOptions<LdapOptions>();
if section.Value != null {
optionsBuilder.Configure(section).ValidateDataAnnotations();
}
else {
optionsBuilder.Configure(options => {
// Set defaults here
options.Host = "Deafult Host";
}
}
I wanted to avoid lambdas in Startup that would need to be copy/pasted correctly for every "optional" section and I wanted to be very explicit about optionality (at the expense of some awkward naming).
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddOption<Optional<LdapOptions>>()
.ConfigureOptional(conf.GetSection(nameof(LdapOptions)))
.ValidateOptionalDataAnnotations();
}
The Optional type is pretty straightforward, but may need a better name (to avoid interfering with other implementations of the generic Option/Some/Maybe pattern). I thought about just using null, but that seemed contrary to Options insistence on returning something no matter what.
Optional.cs
public class Optional<TOptions> where TOptions : class
{
public TOptions Value { get; set; }
public bool HasValue { get => !(Value is null); }
}
The configure extension method takes into account section existence.
OptionalExtensions.cs
public static class OptionalExtensions
{
public static OptionsBuilder<Optional<TOptions>> ConfigureOptional<TOptions>(this OptionsBuilder<Optional<TOptions>> optionsBuilder, IConfigurationSection config) where TOptions : class
{
return optionsBuilder.Configure(options =>
{
if (config.Exists())
{
options.Value = config.Get<TOptions>();
}
});
}
public static OptionsBuilder<Optional<TOptions>> ValidateOptionalDataAnnotations<TOptions>(this OptionsBuilder<Optional<TOptions>> optionsBuilder) where TOptions : class
{
optionsBuilder.Services.AddSingleton<IValidateOptions<Optional<TOptions>>>(new DataAnnotationValidateOptional<TOptions>(optionsBuilder.Name));
return optionsBuilder;
}
}
The validate extension method works with a custom options validator that also takes into account how missing sections work (like the comment says, "missing optional options are always valid").
DataAnnotationValidateOptional.cs
public class DataAnnotationValidateOptional<TOptions> : IValidateOptions<Optional<TOptions>> where TOptions : class
{
private readonly DataAnnotationValidateOptions<TOptions> innerValidator;
public DataAnnotationValidateOptional(string name)
{
this.innerValidator = new DataAnnotationValidateOptions<TOptions>(name);
}
public ValidateOptionsResult Validate(string name, Optional<TOptions> options)
{
if (options.Value is null)
{
// Missing optional options are always valid.
return ValidateOptionsResult.Success;
}
return this.innerValidator.Validate(name, options.Value);
}
}
Now, anywhere you need to use an optional option, like, say, a login controller, you can take the following actions...
LdapLoginController.cs
[ApiController]
[Route("/api/login/ldap")]
public class LdapLoginController : ControllerBase
{
private readonly Optional<LdapOptions> ldapOptions;
public LdapLoginController(IOptionsSnapshot<Optional<LdapOptions>> ldapOptions)
{
// data annotations should trigger here and possibly throw an OptionsValidationException
this.ldapOptions = ldapOptions.Value;
}
[HttpPost]
public void Post(...)
{
if (!ldapOptions.Value.HasValue)
{
// a missing section is valid, but indicates that this option was not configured; I figure that relates to a 501 Not Implemented
return StatusCode((int)HttpStatusCode.NotImplemented);
}
// else we can proceed with valid options
}
}

Deserialization of reference types without parameterless constructor is not supported

I have this API
public ActionResult AddDocument([FromBody]AddDocumentRequestModel documentRequestModel)
{
AddDocumentStatus documentState = _documentService.AddDocument(documentRequestModel, DocumentType.OutgoingPosShipment);
if (documentState.IsSuccess)
return Ok();
return BadRequest();
}
And this is my request model
public class AddDocumentRequestModel
{
public AddDocumentRequestModel(int partnerId, List<ProductRequestModel> products)
{
PartnerId = partnerId;
Products = products;
}
[Range(1, int.MaxValue, ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public int PartnerId { get; private set; }
[Required, MustHaveOneElement(ErrorMessage = "At least one product is required")]
public List<ProductRequestModel> Products { get; private set; }
}
so when I'm trying to hit the API with this body
{
"partnerId": 101,
"products": [{
"productId": 100,
"unitOfMeasureId": 102,
"quantity":5
}
]
}
this is the request : System.NotSupportedException: Deserialization of reference types without parameterless constructor is not supported. Type 'Alati.Commerce.Sync.Api.Controllers.AddDocumentRequestModel'
I don't need parameterless constructor,because it doesn't read the body parameters.Is there any other way for deserialization?
You can achieve your desired result. You need to switch to NewtonsoftJson serialization (from package Microsoft.AspNetCore.Mvc.NewtonsoftJson)
Call this in Startup.cs in the ConfigureServices method:
services.AddControllers().AddNewtonsoftJson();
After this, your constructor will be called by deserialization.
Extra info: I am using ASP Net Core 3.1
Later Edit: I wanted to give more info on this, as it seems that this can also be achieved by using System.Text.Json, although custom implementation is necessary. The answer from jawa states that Deserializing to immutable classes and structs can be achieved with System.Text.Json, by creating a custom converter (inherit from JsonConverter) and registering it to the converters collection (JsonSerializerOptions.Converters) like so:
public class ImmutablePointConverter : JsonConverter<ImmutablePoint>
{
...
}
and then...
var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add(new ImmutablePointConverter());
serializeOptions.WriteIndented = true;
Just in case someone have the same issue I had, I was using abstract class, once removed the abstract key word, it all worked just fine.
Just Add [JsonConstructor] before your constructor
like this
public class Person
{
public string Name { get; set; }
public int LuckyNumber { get; private set; }
[JsonConstructor]
public Person(int luckyNumber)
{
LuckyNumber = luckyNumber;
}
public Person() { }
}
There are still some limitations using System.Text.Json - have a look here: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to#table-of-differences-between-newtonsoftjson-and-systemtextjson
Deserialization without parameterless constructor using a parameterized constructor is not supported yet (but it's on their plan). You can implement your custom JsonConverter (like in this example: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to#deserialize-to-immutable-classes-and-structs) or - like Adrian Nasul above suggested: use Newtonsoft.Json and then you can use the [JsonConstructor] attribute
In my case I had set a class as internal and when I made it public it worked. The error message was really of little help with this specific circumstance.
Old (actual class name changed to ClassName in the example
internal class Rootobject
{
[JsonConstructor]
public Rootobject(ClassName className)
{
ClassName = className?? throw new ArgumentNullException(nameof(className));
}
public ClassName ClassName { get; set; }
}
New:
public class Rootobject
{
[JsonConstructor]
public Rootobject(ClassName className)
{
ClassName = branding ?? throw new ArgumentNullException(nameof(className));
}
public ClassName ClassName { get; set; }
}
In my case error, caused was inside InnerException. There is my class had a field with a custom class type that did not have a parameterless constructor. I've added a parameterless constructor to the inner class and the problem has gone away.

WebInvoke Post Error : Method Not Allowed

I have created one WCF Data Service with simple entity as below.
namespace DataService
{
using System;
using System.Collections.Generic;
public partial class MemoryPackageData
{
public long c1 { get; set; }
public long c2 { get; set; }
public long c3 { get; set; }
public long c4 { get; set; }
}
}
namespace DataService
{
public class WCFDataService : DataService<DBEntities>
{
// This method is called only once to initialize service-wide policies.
public static void InitializeService(DataServiceConfiguration config)
{
// TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc.
// Examples:
config.SetEntitySetAccessRule("MemoryPackageDatas", EntitySetRights.All);
config.SetServiceOperationAccessRule("InsertEntityData", ServiceOperationRights.All);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
}
//[WebInvoke(UriTemplate = "InsertEntityData/?package_id={package_id}&package_size={package_size}")]
[WebInvoke(UriTemplate = "InsertEntityData?package_id={package_id}")]
public void InsertEntityData(Int64 package_id, Int64 package_Size = 10)
{
// some stuff
}
Now, when I run this service in firefox and pass one parameter which is mandatory in the URL.
I have tried many different ways to call this method here. But not sure how to deal with these parameters list.
Method is inserting data to table.
Can any one please guide me here?
Thank you,
Mittal.
WebInvoke considers Method="POST" by default.
So you can use either WebGet attribute or specify Method="GET"
In browser, the default method is "Get". That's why the service will return 405.
You can use Fiddler to compose a Request with "Post" and try.

Return Entity Framework objects over WCF

We have a problem concerning Entity Framework objects and sending them through WCF.
We have a database, and Entity Framework created classes from that database, a 'Wallet' class in this particular situation.
We try to transfer a Wallet using this code:
public Wallet getWallet()
{
Wallet w = new Wallet();
w.name = "myname";
w.walletID = 123;
return w;
}
We need to transfer that Wallet class, but it won't work, we always encounter the same exception:
"An error occurred while receiving the HTTP response to localhost:8860/ComplementaryCoins.svc. This could be due to the service endpoint binding not using the HTTP protocol. This could also be due to an HTTP request context being aborted by the server (possibly due to the service shutting down). See server logs for more details."
We searched on the internet, and there is a possibility that the error is due to the need of serialization of Entity Framework-objects.
We have absolutely no idea if this could be the case, and if this is the case, how to solve it.
Our DataContract looks like this (very simple):
[DataContract]
public partial class Wallet
{
[DataMember]
public int getwalletID { get { return walletID; } }
[DataMember]
public string getname { get { return name; } }
}
Does anyone ever encountered this problem?
EDIT: Our Entity Framework created class looks like this:
namespace ComplementaryCoins
{
using System;
using System.Collections.Generic;
public partial class Wallet
{
public Wallet()
{
this.Transaction = new HashSet<Transaction>();
this.Transaction1 = new HashSet<Transaction>();
this.User_Wallet = new HashSet<User_Wallet>();
this.Wallet_Item = new HashSet<Wallet_Item>();
}
public int walletID { get; set; }
public string name { get; set; }
public virtual ICollection<Transaction> Transaction { get; set; }
public virtual ICollection<Transaction> Transaction1 { get; set; }
public virtual ICollection<User_Wallet> User_Wallet { get; set; }
public virtual ICollection<Wallet_Item> Wallet_Item { get; set; }
}
}
Thanks for helping us.
I had the same problem some time ago and the solution for this was:
The entity framework was returning a serialized class instead of normal class.
eg. Wallet_asfawfklnaewfklawlfkawlfjlwfejlkef instead of Wallet
To solve that you can add this code:
base.Configuration.ProxyCreationEnabled = false;
in your Context file.
Since the context file is auto generated you can add it in the Context.tt
In the Context.tt file it can be added around lines 55-65:
<#=Accessibility.ForType(container)#> partial class <#=code.Escape(container)#> : DbContext
{
public <#=code.Escape(container)#>()
: base("name=<#=container.Name#>")
{
base.Configuration.ProxyCreationEnabled = false;
<#
if (!loader.IsLazyLoadingEnabled(container))
{
#>
this.Configuration.LazyLoadingEnabled = false;
<#
Try specifying a setter for the properties, something like this :
[DataContract]
public partial class Wallet
{
[DataMember]
public int getwalletID { get { return walletID; } set { } }
[DataMember]
public string getname { get { return name; } set { } }
}
If it still doesn't work, you may consider creating an intermediate POCO class for this purpose, and use mapper library like AutoMapper or ValueInjecter to transfer the data from the EF objects.
The POCO class should have same properties as your EF class :
[DataContract]
public class WalletDTO
{
[DataMember]
public int walletID { get; set; }
[DataMember]
public string name { get; set; }
}
And modify your method to return this class instead :
public WalletDTO getWallet()
{
Wallet w = new Wallet(); // or get it from db using EF
var dto = new WalletDTO();
//assuming we are using ValueInjecter, this code below will transfer all matched properties from w to dto
dto.InjectFrom(w);
return dto;
}
Are you trying to recieve a IEnumerable<Wallets>? If - yes, please modify your server class that returns the IEnumerable by adding .ToArray() method

WorkItemChangedEvent and AddedRelations field

I am trying to capture links that were added to a work item in TFS by catching WorkItemChangedEvent via TFS services. Here is the relevant XML part of the message that comes through:
<AddedRelations><AddedRelation><WorkItemId>8846</WorkItemId></AddedRelation></AddedRelations>
This is declared as a field in WorkItemChangedEvent class that should be deserialized into object upon receiving the event:
public partial class WorkItemChangedEvent
{
private string[] addedRelations;
/// <remarks/>
[XmlArrayItemAttribute("WorkItemId", IsNullable = false)]
public string[] AddedRelations
{
get { return this.addedRelations; }
set { this.addedRelations = value; }
}
}
I cannot figure out why the AddedRelations does not get deserialized properly.
I can only suspect that the object structure does not match the XML schema.
I have changed the structure of my WorkItemChangedEvent class a little bit to match the XML:
public partial class WorkItemChangedEvent
{
private AddedRelation[] addedRelations;
/// <remarks/>
[XmlArrayItemAttribute("AddedRelation", IsNullable = false)]
public AddedRelation[] AddedRelations
{
get { return this.addedRelations; }
set { this.addedRelations = value; }
}
[GeneratedCodeAttribute("xsd", "2.0.50727.42")]
[SerializableAttribute()]
[DebuggerStepThroughAttribute()]
[DesignerCategoryAttribute("code")]
[XmlTypeAttribute(Namespace = "")]
public partial class AddedRelation
{
#region Fields
private string workItemId;
#endregion
/// <remarks/>
public string WorkItemId
{
get { return this.workItemId; }
set { this.workItemId = value; }
}
}
}
I still think that there must be some logic behind the original solution since it was designed by TFS authors (MS)? Anyway I am glad it works now and that I answered my question first ;]