C# code first grpc : custom protobuf converter for DateTime - c#

I'm using something like this in dotnet asp net core 6:
<PackageReference Include="protobuf-net.Grpc.AspNetCore" Version="1.0.152" />
<PackageReference Include="protobuf-net.Grpc.AspNetCore.Reflection" Version="1.0.152" />
[DataContract]
public class TaskItem
{
//other properties omitted
[DataMember(Order = 5)]
public DateTime DueDate { get; set; } = null!;
}
Now, when I call the service with grpcurl
"DueDate": {
"value": "458398",
"scale": "HOURS"
}
And in the generated proto file
import "protobuf-net/bcl.proto"; // schema for protobuf-net's handling of core .NET types
message TaskItem {
//other properties omitted
.bcl.DateTime DueDate = 5;
Is there a way to specify a custom converter so that it will serialize to ISO 8601 string in order to better support cross platform (I'll have some clients in js where having a string is ok since I just need new Date(v) and d.toISOString()) ?
I know I can just declare DueDate as string, but then the "problem" is that when I use C# code-first client I also need to convert back to DateTime and to string ...
For example, I can do the following with JSON
.AddJsonOptions(x =>
{
x.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});

What you ask is very different from a JSON type converter. As the docs explain the standard way of serializing dates is the google.protobuf.Timestamp type. That's defined in the proto file. When you use code-first that file is generated by the open source protobuf-net.Grpc tool.
To use the Timestamp type you need to tell the tool to format that property using a well-known type with the ProtoMember attribute :
[ProtoMember(1, DataFormat = DataFormat.WellKnown)]
public DateTime Time { get; set; }
This is shown in the tool's Getting Started document.
This isn't the default for legacy reasons :
(for legacy reasons, protobuf-net defaults to a different library-specific layout that pre-dates the introduction of .google.protobuf.Timestamp). It is recommended to use DataFormat.WellKnown on DateTime and TimeSpan values whenever possible.

Related

Azure Functions and ASP.NET Web API: Do net treat missing values as empty strings

This seems to apply to both, azure functions and ASP.NET Core Web API: Assume that you have a simple class:
SomeClass
{
propA: string;
propB: string;
}
In the Controller you add something like this
... [FromBody]SomeClass data
The runtime will now instantiate a object named data with the json values provided in the body.
However, if you provide this json:
{
"propA": "SomeText"
}
it will still instantiate propB with an empty string, although no value is present in the body. Now the question would be: Is it possible to suppress that behavior, so not to set propB if there is no value provided?
You can decorate your string properties with the [Required] data annotation.
[Required]
public string A { get; set; }
This does however enable empty strings like:
{
"propA": ""
}
The data annotation [Required] has an extra property: AllowEmptyStrings. If set to false the above property will not validate.
Do take in mind these validation annotations cause 400 statuscodes in your responses.

Azure Search v11: Indexing nullable Collection of Complex Type

I'm updating the SDK for the Azure Cognitive Search service, from v10 to v11. I have followed all the steps in the guide in order to upgrade, however I have noticed a strange behavior about the indexing (merge or upload) operation: the UploadDocumentAsync (but also with other methods used to indexing data) operation fails when a property of type Collection (Edm.ComplexType) is null, with the following error:
A node of type 'PrimitiveValue' was read from the JSON reader when trying to read the contents of the property. However, a 'StartArray' node was expected json.
IndexDocumentsResult response = await searchClient.UploadDocumentsAsync<T>(documents).ConfigureAwait (false);
With v10 this problem did not arise. A workaround I found is to set collections as empty arrays and not with null value, but I would like to find a better solution.
EDITED:
I upgraded from Microsoft.Azure.Search v10.1.0 to Azure.Search.Documents v11.1.1
Following an example of a generic T class used to indexing data:
public class IndexEntity
{
[JsonProperty("#search.score")]
public double SearchScore { get; set; }
[JsonProperty("Key")]
public Guid Id { get; set; }
[JsonProperty("Code")]
public string Code { get; set; }
[JsonProperty("ComplexObj")]
public ComplexType[] CollectionOfComplexType{ get; set; }
}
Following the definition of ModelObjectToIndex
public class ComplexType
{
[JsonProperty("Id")]
public string Id { get; set; }
[JsonProperty("Value")]
public string Value { get; set; }
}
Basically when the CollectionOfComplexType property is null, I get the above error. If I set it as an empty array, the error does not occur, but as mentioned I don't like this solution, furthermore in the old version it was an allowed operation (the indexing was completed successfully)
Our Azure.Search.Documents behavior seems to have changed in this regard. I've opened https://github.com/Azure/azure-sdk-for-net/issues/18169 to track resolution.
You can workaround this issue without initializing your collections to an empty array by passing in a JsonSerializerSettings that was similar to what we did in our older Microsoft.Azure.Search library, since it seems from using the JsonPropertyAttribute you're using Newtonsoft.Json (aka Json.NET) anyway:
Add a package reference to Microsoft.Azure.Core.NewtonsoftJson if you haven't already. It recently GA'd so you don't need to use a preview if you were, which I presume since System.Text.Json - our default serializer - would not have honored your property renames.
Pass in a JsonSerializerSettings before creating your SearchClient like so:
var settings = new JsonSerializerSettings
{
// Customize anything else you want here; otherwise, defaults are used.
NullValueHandling = NullValueHandling.Ignore,
};
var options = new SearchClientOptions
{
Serializer = new NewtonsoftJsonObjectSerializer(settings),
};
var searchClient = new SearchClient(options);
We'll discuss how to resolve this by default, if we even can. One big change from the older library is the ability to customize the serializer used. By default we use System.Text.Json, but we support other serializers including Newtonsoft.Json. If someone were to pass in their own settings - or even desire the defaults - changing that could be catastrophic. So I'm curious: if we at least documented this behavior change (perhaps on the SearchClient class remarks and/or UploadDocuments and related methods) and how to retain previous behavior, would that have helped or otherwise been satisfactory?

Options Pattern in ASP.NET Core - how to return sub-options as a JSON string (not strongly typed)

The following documentation illustrates how to use the Options Pattern in ASP.NET Core to create a strongly-typed options class to access JSON configuration data.
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options
This C# class
public class MyOptions
{
public string Option1 { get; set; }
public int Option2 { get; set; }
}
represents a portion of this JSON configuration file (the first two root-level properties)
{
"option1": "value1_from_json",
"option2": -1,
"subOptions": {
"subOption1": "subvalue1_from_json",
"subOption2": 200
}
}
I want to add another C# property named SubOptions to the MyOptions class that returns the raw data of the subOptions JSON sub-section, without creating a strongly-typed class for that sub-section of the JSON configuration file, but I don't know what data type to use (or if it's even possible to do that).
If I use string, I get a runtime error when service.Configure<MyOptions>(Configuration); is called, saying System.InvalidOperationException: 'Cannot create instance of type 'System.String' because it is missing a public parameterless constructor.
If I use object or dynamic, I get a different runtime error when service.AddSingleton(cfg => cfg.GetService<IOptions<MyOptions>>().Value); is called to register an instance of the MyOptions class, saying System.ArgumentNullException: 'Value cannot be null. Parameter name: type'
If I use JObject, I get {} back when I access the SubOptions property of the MyOptions object that's injected into my API Controller.
I know I can convert the sub-section to a JSON string property by escaping the sub-section data, but I want to avoid treating the sub-section as a string, and instead leave it as raw JSON.
Is it possible to do what I want to do? Is there a data type that works with the Options Pattern that will allow me to access the JSON sub-section without having to create a strongly-typed class?
*For background, I'm trying to create an API Controller method that returns the content of the JSON sub-section to the API client. I want to avoid using a strongly-typed class for the sub-section, so that the JSON configuration file can be edited on the server, adding new properties and values to the sub-section that will be returned to the API client, without having to update the C# code and redeploy the API service. In other words, I want the JSON sub-section to be 'dynamic', and just pull it and send it to the client. *
You can sorta do get raw configuration object by forcing your SubOptions property to be of IConfigurationSection:
public class MyOptions
{
public string Option1 { get; set; }
public int Option2 { get; set; }
public IConfigurationSection SubOptions { get; set; } // returns the "raw" section now
public string SubOptions_take2 { get; set; }
}
so you would still bind your strongly typed object in your Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.Configure<MyOptions>(Configuration);
...
}
but this is where luck appears to run out, because even though it is a whole section - as far as options binder is concerned it's all been deserialised and parsed into hierarchy of values already. There appears to be no easy way to reassemble it back into one string. Injecting IOptionsMonitor allows you to get the values by opting for .GetChildren() but I could not find an obvious way to get the whole hierarchy without writing custom code to just recursively walk it (which I will leave out for you to play with should you feel this is worth the effort):
public IndexModel(IOptionsMonitor<MyOptions> options)
{
_options = options.CurrentValue;
var subOptions = _options.SubOptions as ConfigurationSection;
var children = subOptions.GetChildren(); // you see, the config has already been parsed into this hierarchy of items - it's too late to get the raw string value
var s = JsonConvert.SerializeObject(children);
// will produce something like this JSON:
//[{"Path":"SubOptions:subOption1","Key":"subOption1","Value":"subvalue1_from_json"},{"Path":"SubOptions:subOption2","Key":"subOption2","Value":"200"}]
}
one way around it will be to actually encode your json as string in the config file:
"subOptions_take2": "{\"subOption1\": \"subvalue1_from_json\",\"subOption2\": 200}"
then you can just grab it later:
public IndexModel(IOptionsMonitor<MyOptions> options)
{
_options = options.CurrentValue;
var subOptions_string = _options.SubOptions_take2;// this is valid json now: {"subOption1": "subvalue1_from_json","subOption2": 200}
}
I guess, you can use JObject from Newtonsoft.Json package - it's the default JSON parser & serializer in Asp.Net Core

Creating API models in .NET with PascalCase property names but serializing to CamelCase

I usually use a variety of text manipulation tools to extract a list of properties from some REST API documentation, and then use Newtonsoft.Json to add an annotation above the field in order to tell the program whilst this property may be called "DeliveryAddress" when we serialize to JSON please call it "deliveryAddress" using
[JsonProperty(PropertyName = "deliveryAddress")]
public string DeliveryAddress{ get; set; }
It seems a bit long winded so I was wondering if there was an easier way, or some feature in VS I could use to make a 'macro' of sorts to apply this annotation to a list of PascalCase properties.
Well that was easy, turns out I've been cluttering my code unnecessarily all this time.
Hopefully this will serve as a useful question for others in my position.
There is another class level annotation that can be used here.
[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))]
public class Order
{
public string DeliveryAddress {get;set;}
public string FirstName {get;set;}
[JsonProperty(NamingStrategyType = typeof(SnakeCaseNamingStrategy))]
public string NewlyAddedProperty {get;set;}
}
This will apply the CamelCasing upon serialization to all properties, and this can be overridden at an inline annotation level as shown above.
What a lovely library.
Property names serialize to camelCase by default in ASP.net core.
If for some reason this is not the case or you need to customize it further, the naming strategy can manually be specified by setting the NamingStrategy in the JSON serializer settings:
services.AddMvc().AddJsonOptions(options =>
{
var resolver = options.SerializerSettings.ContractResolver as DefaultContractResolver;
resolver.NamingStrategy = new CamelCaseNamingStrategy();
});
Then any time you return an object from an API, it will be serialized with camel case names.
If you're manually serializing the JSON to a string, you can inject IOptions<MvcJsonOptions> to access the default serializer settings which MVC uses:
var jsonString = JsonConvert.SerializeObject(obj, options.Value.SerializerSettings);
You can manually build a serializer with a case converter:
var jsonSerializersettings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
var myJsonOutput = JsonConvert.DeserializeObject<object>myJsonInput.ToString(),jsonSerializersettings);

protobuf-net serialization without attributes

I have an assembly with DataContracts and I need to generate .proto schema for it to be able to exchange the data with java system. The DataContracts code can be changed but I cannot add [ProtoContract] and [ProtoMember] attributes in it because it will result in protobuf-net assembly dependency. We use WCF in C# parts of the system so we would not want to have dependency on proto-buf assembly in most of C# projects that don't work with java system.
On the protobuf-net site in a GettingStarted section it's said that:
Don't Like Attributes?
In v2, everything that can be done with attributes can also be configured at runtime via RuntimeTypeModel.
However I've no clue how to actually configure serialization without attributes and I haven't seen any examples of that.
I'm trying to do
[DataContract]
public class MyEntity
{
[DataMember(Order = 1)]
public String PropertyA { get; set; }
[DataMember(Order = 2)]
public int PropertyB { get; set; }
}
RuntimeTypeModel.Default.Add(typeof(MyEntity), false);
string proto = Serializer.GetProto<MyEntity>();
And get the following as the value of proto
package ProtobufTest;
message MyEntity {
}
Clarification: most of this answer relates to the pre-edit question, where false was passed to RuntimeTypeModel.Add(...)
I've used your exact code (I inferred that this was in namespace ProtobufTest, but the rest was copy/paste from the question) with r2.0.0.640 (the current NuGet deployment), and I get:
package ProtobufTest;
message MyEntity {
optional string PropertyA = 1;
optional int32 PropertyB = 2 [default = 0];
}
Further, you get the exact same result even if you remove the RuntimeTypeModel.Default.Add(...) line.
It is unclear to me why you are seeing something different - can you clarify:
which protobuf-net version you are using exactly
if those [DataContract] / [DataMember] attributes are the System.Runtime.Serialization.dll ones, or your own (sorry if that seems a bizarre question)
To answer the question fully: if you couldn't have any attributes (and the ones you have are just fine), you could also do:
RuntimeTypeModel.Default.Add(typeof(MyEntity), false)
.Add(1, "PropertyA")
.Add(2, "PropertyB");
which would configure PropertyA as key 1, and PropertyB as key 2.

Categories