How to load polymorphic objects in appsettings.json - c#

Is there any way how to read polymorphic objects from appsettings.json in a strongly-typed way? Below is a very simplified example of what I need.
I have multiple app components, named Features here. These components are created in runtime by a factory. My design intent is that each component is configured by its separate strongly-typed options. In this example FileSizeCheckerOptions and PersonCheckerOption are instances of these. Each feature can be included multiple times with different option.
But with the existing ASP.NET Core configuration system, I am not able to read polymorphic strongly typed options. If the settings were read by a JSON deserializer, I could use something like this. But this is not the case of appsettings.json, where options are just key-value pairs.
appsettings.json
{
"DynamicConfig":
{
"Features": [
{
"Type": "FileSizeChecker",
"Options": { "MaxFileSize": 1000 }
},
{
"Type": "PersonChecker",
"Options": {
"MinAge": 10,
"MaxAge": 99
}
},
{
"Type": "PersonChecker",
"Options": {
"MinAge": 15,
"MaxAge": 20
}
}
]
}
}
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.Configure<FeaturesOptions>(Configuration.GetSection("DynamicConfig"));
ServiceProvider serviceProvider = services.BuildServiceProvider();
// try to load settings in strongly typed way
var options = serviceProvider.GetRequiredService<IOptions<FeaturesOptions>>().Value;
}
Other definitions
public enum FeatureType
{
FileSizeChecker,
PersonChecker
}
public class FeaturesOptions
{
public FeatureConfig[] Features { get; set; }
}
public class FeatureConfig
{
public FeatureType Type { get; set; }
// cannot read polymorphic object
// public object Options { get; set; }
}
public class FileSizeCheckerOptions
{
public int MaxFileSize { get; set; }
}
public class PersonCheckerOption
{
public int MinAge { get; set; }
public int MaxAge { get; set; }
}

The key to answer this question is to know how the keys are generated. In your case, the key / value pairs will be:
DynamicConfig:Features:0:Type
DynamicConfig:Features:0:Options:MaxFileSize
DynamicConfig:Features:1:Type
DynamicConfig:Features:1:Options:MinAge
DynamicConfig:Features:1:Options:MaxAge
DynamicConfig:Features:2:Type
DynamicConfig:Features:2:Options:MinAge
DynamicConfig:Features:2:Options:MaxAge
Notice how each element of the array is represented by DynamicConfig:Features:{i}.
The second thing to know is that you can map any section of a configuration to an object instance, with the ConfigurationBinder.Bind method:
var conf = new PersonCheckerOption();
Configuration.GetSection($"DynamicConfig:Features:1:Options").Bind(conf);
When we put all this together, we can map your configuration to your data structure:
services.Configure<FeaturesOptions>(opts =>
{
var features = new List<FeatureConfig>();
for (var i = 0; ; i++)
{
// read the section of the nth item of the array
var root = $"DynamicConfig:Features:{i}";
// null value = the item doesn't exist in the array => exit loop
var typeName = Configuration.GetValue<string>($"{root}:Type");
if (typeName == null)
break;
// instantiate the appropriate FeatureConfig
FeatureConfig conf = typeName switch
{
"FileSizeChecker" => new FileSizeCheckerOptions(),
"PersonChecker" => new PersonCheckerOption(),
_ => throw new InvalidOperationException($"Unknown feature type {typeName}"),
};
// bind the config to the instance
Configuration.GetSection($"{root}:Options").Bind(conf);
features.Add(conf);
}
opts.Features = features.ToArray();
});
Note: all options must derive from FeatureConfig for this to work (e.g. public class FileSizeCheckerOptions : FeatureConfig). You could even use reflection to automatically detect all the options inheriting from FeatureConfig, to avoid the switch over the type name.
Note 2: you can also map your configuration to a Dictionary, or a dynamic object if you prefer; see my answer to Bind netcore IConfigurationSection to a dynamic object.

Based on Metoule answer, I've created reusable extension method, that accepts delegate that accepts section and returns instance to bind to.
Please note that not all edge cases are handled (e.g. Features must be list, not array).
public class FeaturesOptions
{
public List<FeatureConfigOptions> Features { get; set; }
}
public abstract class FeatureConfigOptions
{
public string Type { get; set; }
}
public class FileSizeCheckerOptions : FeatureConfigOptions
{
public int MaxFileSize { get; set; }
}
public class PersonCheckerOptions : FeatureConfigOptions
{
public int MinAge { get; set; }
public int MaxAge { get; set; }
}
FeaturesOptions options = new FeaturesOptions();
IConfiguration configuration = new ConfigurationBuilder()
.AddJsonFile("path-to-the-appsettings.json")
.Build();
configuration.Bind(options, (propertyType, section) =>
{
string type = section.GetValue<string>("Type");
switch (type)
{
case "FileSizeChecker": return new FileSizeCheckerOptions();
case "PersonChecker": return new PersonCheckerOptions();
default: throw new InvalidOperationException($"Unknown feature type {type}"); // or you can return null to skip the binding.
};
});
appsettings.json
{
"Features":
[
{
"Type": "FileSizeChecker",
"MaxFileSize": 1000
},
{
"Type": "PersonChecker",
"MinAge": 10,
"MaxAge": 99
},
{
"Type": "PersonChecker",
"MinAge": 15,
"MaxAge": 20
}
]
}
IConfigurationExtensions.cs
using System.Collections;
namespace Microsoft.Extensions.Configuration
{
/// <summary>
/// </summary>
/// <param name="requestedType">Abstract type or interface that is about to be bound.</param>
/// <param name="configurationSection">Configuration section to be bound from.</param>
/// <returns>Instance of object to be used for binding, or <c>null</c> if section should not be bound.</returns>
public delegate object? ObjectFactory(Type requestedType, IConfigurationSection configurationSection);
public static class IConfigurationExtensions
{
public static void Bind(this IConfiguration configuration, object instance, ObjectFactory objectFactory)
{
if (configuration is null)
throw new ArgumentNullException(nameof(configuration));
if (instance is null)
throw new ArgumentNullException(nameof(instance));
if (objectFactory is null)
throw new ArgumentNullException(nameof(objectFactory));
// first, bind all bindable instance properties.
configuration.Bind(instance);
// then scan for all interfaces or abstract types
foreach (var property in instance.GetType().GetProperties())
{
var propertyType = property.PropertyType;
if (propertyType.IsPrimitive || propertyType.IsValueType || propertyType.IsEnum || propertyType == typeof(string))
continue;
var propertySection = configuration.GetSection(property.Name);
if (!propertySection.Exists())
continue;
object? propertyValue;
if (propertyType.IsAbstract || propertyType.IsInterface)
{
propertyValue = CreateAndBindValueForAbstractPropertyTypeOrInterface(propertyType, objectFactory, propertySection);
property.SetValue(instance, propertyValue);
}
else
{
propertyValue = property.GetValue(instance);
}
if (propertyValue is null)
continue;
var isGenericList = propertyType.IsAssignableTo(typeof(IList)) && propertyType.IsGenericType;
if (isGenericList)
{
var listItemType = propertyType.GenericTypeArguments[0];
if (listItemType.IsPrimitive || listItemType.IsValueType || listItemType.IsEnum || listItemType == typeof(string))
continue;
if (listItemType.IsAbstract || listItemType.IsInterface)
{
var newListPropertyValue = (IList)Activator.CreateInstance(propertyType)!;
for (int i = 0; ; i++)
{
var listItemSection = propertySection.GetSection(i.ToString());
if (!listItemSection.Exists())
break;
var listItem = CreateAndBindValueForAbstractPropertyTypeOrInterface(listItemType, objectFactory, listItemSection);
if (listItem is not null)
newListPropertyValue.Add(listItem);
}
property.SetValue(instance, newListPropertyValue);
}
else
{
var listPropertyValue = (IList)property.GetValue(instance, null)!;
for (int i = 0; i < listPropertyValue.Count; i++)
{
var listItem = listPropertyValue[i];
if (listItem is not null)
{
var listItemSection = propertySection.GetSection(i.ToString());
listItemSection.Bind(listItem, objectFactory);
}
}
}
}
else
{
propertySection.Bind(propertyValue, objectFactory);
}
}
}
private static object? CreateAndBindValueForAbstractPropertyTypeOrInterface(Type abstractPropertyType, ObjectFactory objectFactory, IConfigurationSection section)
{
if (abstractPropertyType is null)
throw new ArgumentNullException(nameof(abstractPropertyType));
if (objectFactory is null)
throw new ArgumentNullException(nameof(objectFactory));
if (section is null)
throw new ArgumentNullException(nameof(section));
var propertyValue = objectFactory(abstractPropertyType, section);
if (propertyValue is not null)
section.Bind(propertyValue, objectFactory);
return propertyValue;
}
}
}

Related

How to reference property of another object if the current object's property is null

I have a public Dictionary<string, PostRenewalActionJobs> Jobs to store some actions I would like to trigger for specific accounts, the key of this dictionary being the account name.
public class PostRenewalActionJobs
{
public List<AlterDatabaseLinkJob> AlterDataBaseLink { get; set; }
public DatabaseConnectionCheckJob DatabaseConnectionCheck { get; set; }
public UnlockDatabaseAccountJob UnlockDatabaseAccount { get; set; }
public LinuxConnectionCheckJob LinuxConnectionCheck { get; set; }
public WindowsConnectionCheckJob WindowsConnectionCheck { get; set; }
public ReplacePasswordInFileJob ReplacePasswordInFile { get; set; }
}
The properties of PostRenewalActionJobs type (AlterDataBaseLink, DatabaseConnectionCheck, etc) can be defined for a specific account or for all accounts by using * as key in the dictionary:
By using below method I am able to retrieve the jobs for an account (if exists) or the general jobs:
public PostRenewalActionJobs GetJobsForAccount(string accountName)
{
return Jobs.ContainsKey(accountName) ? Jobs[accountName] : Jobs["*"];
}
I would like to have a dynamic way of getting a job from the all accounts object ("*") if the one from the specific account is null.
Something like below but whit out repeating the same code for all job types and also a solution that should work when new job types are introduced.
var dbConCheckJob = GetJobsForAccount("someAccount").AlterDataBaseLink;
if(dbConCheckJob == null || !dbConCheckJob.Any())
{
dbConCheckJob = GetJobsForAccount("*").AlterDataBaseLink
}
I was thinking to use some reflection, but I am not sure how to do it.
You don't need to use reflection. You can already determine whether to get the specific jobs for an account or the generic ones, you could then use a Func to get the job you want:
public TJob GetPostJobForAccount<TJob>(string accountName,
Func<PostRenewalActionJobs, TJob> jobSelector) where TJob : JobBase
{
var genericJobs = Jobs["*"];
var accountJobs = Jobs.ContainsKey(accountName) ? Jobs[accountName] : genericJobs;
// Account might be defined but without any job of the given type
// hence selecting from the defaults if need be
return jobSelector(accountJobs) ?? jobSelector(genericJobs);
}
var bobJob = GetPostJobForAccount("bob", x => x.WindowsConnectionCheck);
var aliceJob = GetPostJobForAccount("alice", x => x.UnlockDatabaseAccount);
I found a way to do it, not sure if there is a better way:
public TJob GetPostJobForAccount<TJob>(string accountName)
{
Type type = typeof(PostRenewalActionJobs);
var accountJobs = Jobs[accountName];
var generalJobs = Jobs["*"];
foreach (var item in type.GetProperties())
{
var itemType = item.PropertyType;
var currentType = typeof(TJob);
if (itemType != currentType)
{
continue;
}
var output = (TJob)accountJobs?.GetType()?.GetProperty(item.Name)?.GetValue(accountJobs, null);
if (output is null)
{
output = (TJob)accountJobs?.GetType()?.GetProperty(item.Name)?.GetValue(generalJobs, null);
}
return output;
}
return default;
}

How to use (pack) Google.Protobuf.WellknownTypes.Any in protobuf-net code first gRPC

I am creating an gRPC service and we decided to choose the code first approach with protobuf-net.
Now I am running into a scenario where we have a couple of classes that need to be wrapped.
We do not want to define KnownTypes in the MyMessage class (just a sample name to illustrate the problem).
So I am trying to use the Any type which currently gives me some struggle with packing.
The sample code has the MyMessage which defines some header values and has to possiblity to deliver any type as payload.
[ProtoContract]
public class MyMessage
{
[ProtoMember(1)] public int HeaderValue1 { get; set; }
[ProtoMember(2)] public string HeaderValue2 { get; set; }
[ProtoMember(3)] public Google.Protobuf.WellknownTypes.Any Payload { get; set; }
}
[ProtoContract]
public class Payload1
{
[ProtoMember(1)] public bool Data1 { get; set; }
[ProtoMember(2)] public string Data2 { get; set; }
}
[ProtoContract]
public class Payload2
{
[ProtoMember(1)] public string Data1 { get; set; }
[ProtoMember(2)] public string Data2 { get; set; }
}
Somewhere in the code I construct my message with a payload ...
Payload2 payload = new Payload2 {
Data1 = "abc",
Data2 = "def"
};
MyMessage msg = new MyMessage
{
HeaderValue1 = 123,
HeaderValue2 = "iAmHeaderValue2",
Payload = Google.Protobuf.WellknownTypes.Any.Pack(payload)
};
Which doesn't work because Payload1 and Payload2 need to implement Google.Protobuf.IMessage.
Since I can't figure out how and do not find a lot information how to do it at all I am wondering if I am going a wrong path.
How is it intedend to use Any in protobuf-net?
Is there a simple (yet compatible) way to pack a C# code first class into Google.Protobuf.WellknownTypes.Any?
Do I really need to implement Google.Protobuf.IMessage?
Firstly, since you say "where we have a couple of classes that need to be wrapped" (emphasis mine), I wonder if what you actually want here is oneof rather than Any. protobuf-net has support for the oneof concept, although it isn't obvious from a code-first perspective. But imagine we had (in a contract-first sense):
syntax = "proto3";
message SomeType {
oneof Content {
Foo foo = 1;
Bar bar = 2;
Blap blap = 3;
}
}
message Foo {}
message Bar {}
message Blap {}
This would be implemented (via the protobuf-net schema tools) as:
private global::ProtoBuf.DiscriminatedUnionObject __pbn__Content;
[global::ProtoBuf.ProtoMember(1, Name = #"foo")]
public Foo Foo
{
get => __pbn__Content.Is(1) ? ((Foo)__pbn__Content.Object) : default;
set => __pbn__Content = new global::ProtoBuf.DiscriminatedUnionObject(1, value);
}
public bool ShouldSerializeFoo() => __pbn__Content.Is(1);
public void ResetFoo() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__Content, 1);
[global::ProtoBuf.ProtoMember(2, Name = #"bar")]
public Bar Bar
{
get => __pbn__Content.Is(2) ? ((Bar)__pbn__Content.Object) : default;
set => __pbn__Content = new global::ProtoBuf.DiscriminatedUnionObject(2, value);
}
public bool ShouldSerializeBar() => __pbn__Content.Is(2);
public void ResetBar() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__Content, 2);
[global::ProtoBuf.ProtoMember(3, Name = #"blap")]
public Blap Blap
{
get => __pbn__Content.Is(3) ? ((Blap)__pbn__Content.Object) : default;
set => __pbn__Content = new global::ProtoBuf.DiscriminatedUnionObject(3, value);
}
public bool ShouldSerializeBlap() => __pbn__Content.Is(3);
public void ResetBlap() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__Content, 3);
optionally with an enum to help:
public ContentOneofCase ContentCase => (ContentOneofCase)__pbn__Content.Discriminator;
public enum ContentOneofCase
{
None = 0,
Foo = 1,
Bar = 2,
Blap = 3,
}
This approach may be easier and preferable to Any.
On Any:
Short version: protobuf-net has not, to date, had any particular request to implement Any. It probably isn't a huge amount of work - simply: it hasn't yet happened. It looks like you're referencing both protobuf-net and the Google libs here, and using the Google implementation of Any. That's fine, but protobuf-net isn't going to use it at all - it doesn't know about the Google APIs in this context, so: implementing IMessage won't actually help you.
I'd be more than happy to look at Any with you, from the protobuf-net side. Ultimately, time/availability is always the limiting factor, so I prioritise features that are seeing demand. I think you may actually be the first person asking me about Any in protobuf-net.
My Any implementation.
[ProtoContract(Name = "type.googleapis.com/google.protobuf.Any")]
public class Any
{
/// <summary>Pack <paramref name="value"/></summary>
public static Any Pack(object? value)
{
// Handle null
if (value == null) return new Any { TypeUrl = null!, Value = Array.Empty<byte>() };
// Get type
System.Type type = value.GetType();
// Write here
MemoryStream ms = new MemoryStream();
// Serialize
RuntimeTypeModel.Default.Serialize(ms, value);
// Create any
Any any = new Any
{
TypeUrl = $"{type.Assembly.GetName().Name}/{type.FullName}",
Value = ms.ToArray()
};
// Return
return any;
}
/// <summary>Unpack any record</summary>
public object? Unpack()
{
// Handle null
if (TypeUrl == null || Value == null || Value.Length == 0) return null;
// Find '/'
int slashIx = TypeUrl.IndexOf('/');
// Convert to C# type name
string typename = slashIx >= 0 ? $"{TypeUrl.Substring(slashIx + 1)}, {TypeUrl.Substring(0, slashIx)}" : TypeUrl;
// Get type (Note security issue here!)
System.Type type = System.Type.GetType(typename, true)!;
// Deserialize
object value = RuntimeTypeModel.Default.Deserialize(type, Value.AsMemory());
// Return
return value;
}
/// <summary>Test type</summary>
public bool Is(System.Type type) => $"{type.Assembly.GetName().Name}/{type.FullName}" == TypeUrl;
/// <summary>Type url (using C# type names)</summary>
[ProtoMember(1)]
public string TypeUrl = null!;
/// <summary>Data serialization</summary>
[ProtoMember(2)]
public byte[] Value = null!;
/// <summary></summary>
public static implicit operator Container(Any value) => new Container(value.Unpack()! );
/// <summary></summary>
public static implicit operator Any(Container value) => Any.Pack(value.Value);
/// <summary></summary>
public struct Container
{
/// <summary></summary>
public object? Value;
/// <summary></summary>
public Container()
{
this.Value = null;
}
/// <summary></summary>
public Container(object? value)
{
this.Value = value;
}
}
}
'System.Object' can be used as a field or property in a surrounding Container record:
[DataContract]
public class Container
{
/// <summary></summary>
[DataMember(Order = 1, Name = nameof(Value))]
public Any.Container Any { get => new Any.Container(Value); set => Value = value.Value; }
/// <summary>Object</summary>
public object? Value;
}
Serialization
RuntimeTypeModel.Default.Add(typeof(Any.Container), false).SetSurrogate(typeof(Any));
var ms = new MemoryStream();
RuntimeTypeModel.Default.Serialize(ms, new Container { Value = "Hello world" });
Container dummy = RuntimeTypeModel.Default.Deserialize(typeof(Container), ms.ToArray().AsMemory()) as Container;

How to define default values for parameters for the Swagger UI?

I have Swagger/Swashbuckle integrated into a .NET Core 2.2 API project. Everything works great and what I am asking is purely for convenience. Consider the following API method:
public Model SomeEstimate(SomeRequest request) {
return Manager.GetSomeEstimate(request);
}
...
public class SomeRequest {
public string StreetAddress { get; set; }
public string Zip { get; set; }
}
When I hit /swagger/index.html and want to try out this API, I always have to enter the StreetAddress and Zip values.
Is there a way to provide default values for StreetAddress and Zip? This answer suggested placing [DefaultValue("value here")] attribute each property of the SomeRequest class. It may have worked for the regular .NET, but not with .NET Core.
Is it possible to provide default values for parameters for the Swagger UI?
To define default values for parameters for Swagger UI in .NET Core, the following article defines a custom schema filter for your DefaultValue attribute in your Model class. The code shown below has been taken from this article and is purely to inform anyone else who has this question or has been facing a similar problem:
Decorate the desired properties in a model:
public class Test {
[DefaultValue("Hello")]
public string Text { get; set; }
}
Main filter:
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Project.Swashbuckle {
public class SchemaFilter : ISchemaFilter {
public void Apply(Schema schema, SchemaFilterContext context) {
if (schema.Properties == null) {
return;
}
foreach (PropertyInfo propertyInfo in context.SystemType.GetProperties()) {
// Look for class attributes that have been decorated with "[DefaultAttribute(...)]".
DefaultValueAttribute defaultAttribute = propertyInfo
.GetCustomAttribute<DefaultValueAttribute>();
if (defaultAttribute != null) {
foreach (KeyValuePair<string, Schema> property in schema.Properties) {
// Only assign default value to the proper element.
if (ToCamelCase(propertyInfo.Name) == property.Key) {
property.Value.Example = defaultAttribute.Value;
break;
}
}
}
}
}
private string ToCamelCase(string name) {
return char.ToLowerInvariant(name[0]) + name.Substring(1);
}
}
}
And finally registering it to your Swagger Options(In Startup.cs):
services.AddSwaggerGen(c => {
// ...
c.SchemaFilter<SchemaFilter>();
});
The initial credit goes to Rahul Sharma, though if someone is interested in .NET Core 3.0+, Swashbuckle v5.0.0-rc4 makes the SchemaFilter definition much simpler. Maybe there's a way to add example value with a new attribute or something like that, but I haven't found such a way.
public class SchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (schema.Properties == null)
{
return;
}
foreach (var property in schema.Properties)
{
if (property.Value.Default != null && property.Value.Example == null)
{
property.Value.Example = property.Value.Default;
}
}
}
}
Swashbuckle.AspNetCore 5.6.3 needs just DefaultValueAttribute.
For value types you can use just System.ComponentModel.DefaultValueAttribute like so:
[System.ComponentModel.DefaultValue((int)DateTypeEnum.CreationDate)]
public int DateType{ get; set; }
For primitive type property like datetime type, you cannot add a default value in the System.ComponentModelDefaultValueAttribute since attribute does not support a reference type initialize.
For this case I created a custom default value attribute that get a string value and convert it to a primitive type. then I defined a schema filter that set the value into the default memberInfo schema:
DefaultValueAttribute
public class DefaultValueAttribute : Attribute
{
public IOpenApiPrimitive Value { get; set; }
public DefaultValueAttribute(PrimitiveType type, string value = "")
{
SetValue(type, value);
}
public DefaultValueAttribute(PrimitiveType type = PrimitiveType.DateTime, int addYears = 0, int addDays = 0, int addMonths = 0)
{
SetValue(type, "", addYears, addDays, addMonths);
}
private void SetValue(PrimitiveType Type, string value = "", int addYears = 0, int addDays = 0, int addMonths = 0)
{
switch (Type)
{
case PrimitiveType.Integer:
Value = new OpenApiInteger(Convert.ToInt32(value));
break;
case PrimitiveType.Long:
Value = new OpenApiLong(Convert.ToInt64(value));
break;
case PrimitiveType.Float:
Value = new OpenApiFloat(Convert.ToUInt64(value));
break;
case PrimitiveType.Double:
Value = new OpenApiDouble(Convert.ToDouble(value));
break;
case PrimitiveType.String:
Value = new OpenApiString(value);
break;
case PrimitiveType.Byte:
Value = new OpenApiByte(Convert.ToByte(value));
break;
case PrimitiveType.Binary:
Value = new OpenApiBinary(value.ToCharArray().Select(c => Convert.ToByte(c)).ToArray());
break;
case PrimitiveType.Boolean:
Value = new OpenApiBoolean(Convert.ToBoolean(value));
break;
case PrimitiveType.Date:
break;
case PrimitiveType.DateTime:
Value = new OpenApiDate(DateTime.Now.AddYears(addYears).AddDays(addDays).AddMonths(addMonths));
break;
case PrimitiveType.Password:
Value = new OpenApiPassword(value);
break;
default:
break;
}
}
}
SchemaFilter
public class DefaultValuesSwaggerExtensions : Swashbuckle.AspNetCore.SwaggerGen.ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
var attributes = context?.MemberInfo?.GetCustomAttributes(true).OfType<DefaultValueAttribute>();
if (attributes?.Any() == true)
{
schema.Default = attributes.First().Value;
}
}
}
Add the filter to Swagger
services.AddSwaggerGen(c =>
{
c.SchemaFilter<DefaultValuesSwaggerExtensions>();
});
use the attribute on the property
[DTOs.Common.DefaultValue(Microsoft.OpenApi.Any.PrimitiveType.DateTime, -1)]
public DateTime FromDate { get; set; } = DateTime.Now.AddYears(-1);
Swashbuckle.AspNetCore 5.6.3 needs just System.ComponentModel.DefaultValueAttribute like so:
[System.ComponentModel.DefaultValue((int)DateTypeEnum.CreationDate)]
public int DateType{ get; set; }
For reference type properties like datetime type, you cannot use the System.ComponentModelDefaultValueAttribute since it does not support a reference type initialize.
For this case I created a custom default value attribute that get a string value and convert it to a primitive type. then I defined a schema filter that set the value into the default memberInfo schema:
DefaultValueAttribute
public class DefaultValueAttribute : Attribute
{
public IOpenApiPrimitive Value { get; set; }
public DefaultValueAttribute(PrimitiveType type, string value = "")
{
SetValue(type, value);
}
public DefaultValueAttribute(PrimitiveType type = PrimitiveType.DateTime, int addYears = 0, int addDays = 0, int addMonths = 0)
{
SetValue(type, "", addYears, addDays, addMonths);
}
private void SetValue(PrimitiveType Type, string value = "", int addYears = 0, int addDays = 0, int addMonths = 0)
{
switch (Type)
{
case PrimitiveType.Integer:
Value = new OpenApiInteger(Convert.ToInt32(value));
break;
case PrimitiveType.Long:
Value = new OpenApiLong(Convert.ToInt64(value));
break;
case PrimitiveType.Float:
Value = new OpenApiFloat(Convert.ToUInt64(value));
break;
case PrimitiveType.Double:
Value = new OpenApiDouble(Convert.ToDouble(value));
break;
case PrimitiveType.String:
Value = new OpenApiString(value);
break;
case PrimitiveType.Byte:
Value = new OpenApiByte(Convert.ToByte(value));
break;
case PrimitiveType.Binary:
Value = new OpenApiBinary(value.ToCharArray().Select(c => Convert.ToByte(c)).ToArray());
break;
case PrimitiveType.Boolean:
Value = new OpenApiBoolean(Convert.ToBoolean(value));
break;
case PrimitiveType.Date:
break;
case PrimitiveType.DateTime:
Value = new OpenApiDate(DateTime.Now.AddYears(addYears).AddDays(addDays).AddMonths(addMonths));
break;
case PrimitiveType.Password:
Value = new OpenApiPassword(value);
break;
default:
break;
}
}
}
SchemaFilter
public class DefaultValuesSwaggerExtensions : Swashbuckle.AspNetCore.SwaggerGen.ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
var attributes = context?.MemberInfo?.GetCustomAttributes(true).OfType<DefaultValueAttribute>();
if (attributes?.Any() == true)
{
schema.Default = attributes.First().Value;
}
}
}
Add the filter to Swagger
services.AddSwaggerGen(c =>
{
c.SchemaFilter<DefaultValuesSwaggerExtensions>();
});
add the attribute to the property
[DTOs.Common.DefaultValue(Microsoft.OpenApi.Any.PrimitiveType.DateTime, -1)]
public DateTime FromDate { get; set; } = DateTime.Now.AddYears(-1);

c# parse json value, check if null

So I have this Web API that calls a service which in turn, returns a json string.
The string looks a bit like this:
{
"title": "Test",
"slug": "test",
"collection":{ },
"catalog_only":{ },
"configurator":null
}
I have cut this down considerably to make it easier to see my issue.
When I make my API call, I then use a Factory method to factorize the response which looks something like this:
/// <summary>
/// Used to create a product response from a JToken
/// </summary>
/// <param name="model">The JToken representing the product</param>
/// <returns>A ProductResponseViewModel</returns>
public ProductResponseViewModel Create(JToken model)
{
// Create our response
var response = new ProductResponseViewModel()
{
Title = model["title"].ToString(),
Slug = model["slug"].ToString()
};
// Get our configurator property
var configurator = model["configurator"];
// If the configurator is null
if (configurator == null)
throw new ArgumentNullException("Configurator");
// For each item in our configurator data
foreach (var item in (JObject)configurator["data"])
{
// Get our current option
var option = item.Value["option"].ToString().ToLower();
// Assign our configuration values
if (!response.IsConfigurable) response.IsConfigurable = (option == "configurable");
if (!response.IsDesignable) response.IsDesignable = (option == "designable");
if (!response.HasGraphics) response.HasGraphics = (option == "graphics");
if (!response.HasOptions) response.HasOptions = (option == "options");
if (!response.HasFonts) response.HasFonts = (option == "fonts");
}
// Return our Product response
return response;
}
}
Now, as you can see I am getting my configurator property and then checking it to see if it's null.
The json string shows the configurator as null, but when I put a breakpoint on the check, it actually shows it's value as {}.
My question is how can I get it to show the value (null) as opposed to this bracket response?
You're asking for the JToken associated with the configurator key. There is such a token - it's a null token.
You can check this with:
if (configurator.Type == JTokenType.Null)
So if you want to throw if either there's an explicit null token or configurator hasn't been specified at all, you could use:
if (configurator == null || configurator.Type == JTokenType.Null)
For those who use System.Text.Json in newer .Net versions (e.g. .Net 5):
var jsonDocument = JsonDocument.Parse("{ \"configurator\": null }");
var jsonElement = jsonDocument.RootElement.GetProperty("configurator");
if (jsonElement.ValueKind == JsonValueKind.Null)
{
Console.WriteLine("Property is null.");
}
public class Collection
{
}
public class CatalogOnly
{
}
public class RootObject
{
public string title { get; set; }
public string slug { get; set; }
public Collection collection { get; set; }
public CatalogOnly catalog_only { get; set; }
public object configurator { get; set; }
}
var k = JsonConvert.SerializeObject(new RootObject
{
catalog_only =new CatalogOnly(),
collection = new Collection(),
configurator =null,
slug="Test",
title="Test"
});
var t = JsonConvert.DeserializeObject<RootObject>(k).configurator;
Here configurator is null.

Force binary deserialization to fail when type modified

I'm looking for a non-intrusive way to enforce deserialization to fail under the following circumstances:
The type is not defined in a strongly named assembly.
BinaryFormatter is used.
Since serialized, the type has been modified (e.g. a property has been added).
Below is an illustration/repro of the problem in form of a failing NUnit test. I'm looking for a generic way to make this pass without modifying the Data class, preferably by just setting up the BinaryFormatter during serialization and/or deserialization. I also don't want to involve serialization surrogates, as this is likely to require specific knowledge for each affected type.
Can't find anything in the MSDN docs that helps me though.
[Serializable]
public class Data
{
public string S { get; set; }
}
public class DataSerializationTests
{
/// <summary>
/// This string contains a Base64 encoded serialized instance of the
/// original version of the Data class with no members:
/// [Serializable]
/// public class Data
/// { }
/// </summary>
private const string Base64EncodedEmptyDataVersion =
"AAEAAAD/////AQAAAAAAAAAMAgAAAEtTc2MuU3Rvcm0uRGF0YS5UZXN0cywgV"+
"mVyc2lvbj0xLjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2"+
"VuPW51bGwFAQAAABlTc2MuU3Rvcm0uRGF0YS5UZXN0cy5EYXRhAAAAAAIAAAAL";
[Test]
public void Deserialize_FromOriginalEmptyVersionFails()
{
var binaryFormatter = new BinaryFormatter();
var memoryStream = new MemoryStream(Convert.FromBase64String(Base64EncodedEmptyDataVersion));
memoryStream.Seek(0L, SeekOrigin.Begin);
Assert.That(
() => binaryFormatter.Deserialize(memoryStream),
Throws.Exception
);
}
}
I'd recommend a "Java" way here - declare int field in every single serializable class like private int _Serializable = 0; and check that your current version & serialized version match; manually increase when you change properties. If you insist on automated way you'll have to store a lot of metadata and check if current metadata & persisted metadata matches (extra burden on performance/size of serialized data).
Here is the automatic descriptor. Basically you'll have to store TypeDescriptor instance as a part of your binary data & on retrieve check if persisted TypeDescriptor is valid for serialization (IsValidForSerialization) against current TypeDescriptor.
var persistedDescriptor = ...;
var currentDescriptor = Describe(typeof(Foo));
bool isValid = persistedDescriptor.IsValidForSerialization(currentDescriptor);
[Serializable]
[DataContract]
public class TypeDescriptor
{
[DataMember]
public string TypeName { get; set; }
[DataMember]
public IList<FieldDescriptor> Fields { get; set; }
public TypeDescriptor()
{
Fields = new List<FieldDescriptor>();
}
public bool IsValidForSerialization(TypeDescriptor currentType)
{
if (!string.Equals(TypeName, currentType.TypeName, StringComparison.Ordinal))
{
return false;
}
foreach(var field in Fields)
{
var mirrorField = currentType.Fields.FirstOrDefault(f => string.Equals(f.FieldName, field.FieldName, StringComparison.Ordinal));
if (mirrorField == null)
{
return false;
}
if (!field.Type.IsValidForSerialization(mirrorField.Type))
{
return false;
}
}
return true;
}
}
[Serializable]
[DataContract]
public class FieldDescriptor
{
[DataMember]
public TypeDescriptor Type { get; set; }
[DataMember]
public string FieldName { get; set; }
}
private static TypeDescriptor Describe(Type type, IDictionary<Type, TypeDescriptor> knownTypes)
{
if (knownTypes.ContainsKey(type))
{
return knownTypes[type];
}
var descriptor = new TypeDescriptor { TypeName = type.FullName, Fields = new List<FieldDescriptor>() };
knownTypes.Add(type, descriptor);
if (!type.IsPrimitive && type != typeof(string))
{
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).OrderBy(f => f.Name))
{
var attributes = field.GetCustomAttributes(typeof(NonSerializedAttribute), false);
if (attributes != null && attributes.Length > 0)
{
continue;
}
descriptor.Fields.Add(new FieldDescriptor { FieldName = field.Name, Type = Describe(field.FieldType, knownTypes) });
}
}
return descriptor;
}
public static TypeDescriptor Describe(Type type)
{
return Describe(type, new Dictionary<Type, TypeDescriptor>());
}
I also though about some mechanism of shortening size of persisted metadata - like calculating MD5 from xml-serialized or json-serialized TypeDescriptor; but in that case new property/field will mark your object as incompatible for serialization.

Categories

Resources