Change Swagger Property Names - c#

Q. 1
I'm trying to push an entity into CRM. The json looks like this:
{
"sub_AssignedToCompanyId#odata.bind": "/accounts(f52f9dd7-35e5-e711-813b-480fcff40721)"
}
Now, my C# class has a property on it called AssignedToCompany, looks like this:
[JsonProperty(PropertyName = "sub_AssignedToCompanyId#odata.bind")]
public int AssignedToCompany{ get; set; }
What I want is a way to serialize it to sub_AssignedToCompanyId#odata.bind (which i've done using JsonProperty("sub_AssignedToCompanyId#odata.bind"). But then the swagger definition doesn't look good. I want the swagger definition of this property to say "assignedToCompanyId". Is there anyway to do this? I'm using .Net Framework Web API.
Q.2
And, is there a way that I can translate this input:
f52f9dd7-35e5-e711-813b-480fcff40721
to this output:
/accounts(f52f9dd7-35e5-e711-813b-480fcff40721)
automatically during serialization?

You can use IOperationFilter, ISchemaFilter or IDocumentFilter. Whichever best suits your needs.
An example of an ISchemaFilter could be:
public class ODataTypeSchemaFilter : ISchemaFilter
{
private const string propToRename = "sub_AssignedToCompanyId#odata.bind";
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type, type)
{
if (type == typeof(MyObject))
{
// adding new schema property, and removing the old one
schema.properties.Add(nameof(MyObject.AssignedToCompany), schema.properties[propToRename]);
schema.properties.Remove(propToRename);
}
}
}
public class MyObject
{
[JsonProperty(PropertyName = "sub_AssignedToCompanyId#odata.bind")]
public int AssignedToCompany { get; set; }
}
This is a powerful feature, changing the schema could potentially cause other problems if not done properly. See Swagger documentation for more info. I've used all three in my experience, just find which suits your needs best.

You can try also this one. The previous answers did not work for me but this did.
However I recommend to use the desired names in you model's property names because then maybe you will have to address also the problem of deserializing the json to the objects when a request happens.
public class CustomNameSchema : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (schema?.Properties == null)
{
return;
}
if (schema.Properties.Any())
{
foreach (var propType in context.Type.GetProperties().Where(x => x.CustomAttributes != null && x.CustomAttributes.Any()))
{
var schemaProp = schema.Properties.FirstOrDefault(x => x.Key.Equals(propType.Name, StringComparison.InvariantCultureIgnoreCase));
string newName = propType.GetCustomAttribute<DataMemberAttribute>()?.Name;
if (string.IsNullOrEmpty(newName))
continue;
if (schemaProp.Value.Enum != null && schemaProp.Value.Enum.Any())
{
for (int i = 0; i < schemaProp.Value.Enum.Count; i++)
{
OpenApiString curr = schemaProp.Value.Enum[i] as OpenApiString;
var memberInfo = propType.PropertyType.GetMember(curr.Value).FirstOrDefault();
string newValue = memberInfo.GetCustomAttribute<EnumMemberAttribute>()?.Value;
if (string.IsNullOrWhiteSpace(newValue))
continue;
OpenApiString newStr = new OpenApiString(newValue);
schemaProp.Value.Enum.Remove(curr);
schemaProp.Value.Enum.Insert(0, newStr);
}
}
var newSchemaProp = new KeyValuePair<string, OpenApiSchema>(newName, schemaProp.Value);
schema.Properties.Remove(schemaProp);
schema.Properties.Add(newSchemaProp);
}
}
var objAttribute = context.Type.GetCustomAttribute<DataContractAttribute>();
if (objAttribute != default && objAttribute?.Name?.Length > 0)
{
schema.Title = objAttribute.Name;
}
}
}

For me, just add [System.Text.Json.Serialization.JsonPropertyName("property")] resolved the problem
using System.Text.Json.Serialization;
[Serializable]
public class Foo
{
[JsonPropertyName("bar")]
public long Bar{ get; set; }
}

As per Answer of #bytedev :
https://stackoverflow.com/a/60276145/12417530
the following solution worked for me :
It would appear that full support for NewtonSoft JSON.net from version 5.0.0 of Swashbuckle/Swagger is provided through a separate package.
To get this working:
Install the nuget package Swashbuckle.AspNetCore.Newtonsoft version 5.0.0+
In Startup.ConfigureServices() make a call for the support:
services.AddSwaggerGenNewtonsoftSupport();
Further info can be found here.

Related

How to fix error Cannot deserialize the current JSON object? [duplicate]

Below is a (slightly) stripped down response I get from a REST API upon successful creation of a new "job code" entry. I need to deserialize the response into some classes, but I'm stumped.
For reference, I'm using JSON.NET in .NET 3.5 (running in a SSIS script in SQL Server 2008 R2) to attempt my deserialization. Here's the JSON - which I obviously have no control over as it's coming from someone else's API:
{
"results":{
"jobcodes":{
"1":{
"_status_code":200,
"_status_message":"Created",
"id":444444444,
"assigned_to_all":false,
"billable":true,
"active":true,
"type":"regular",
"name":"1234 Main Street - Jackson"
},
"2":{
"_status_code":200,
"_status_message":"Created",
"id":1234567890,
"assigned_to_all":false,
"billable":true,
"active":true,
"type":"regular",
"name":"4321 Some Other Street - Jackson"
}
}
}
}
In my C# code, I do have a "JobCode" class defined which only partially maps the JSON values to properties - I'm not interested in all of the data that's returned to me:
[JsonObject]
class JobCode
{
[JsonProperty("_status_code")]
public string StatusCode { get; set; }
[JsonProperty("_status_message")]
public string StatusMessage { get; set; }
[JsonProperty("id")]
public string Id {get; set;}
[JsonProperty("name")]
public string Name { get; set; }
//-------------------------------------------------------------------------------
// Empty constructor for JSON serialization support
//-------------------------------------------------------------------------------
public JobCode() { }
}
I'm attempting to deserialize the data via this call:
newResource = JsonConvert.DeserializeObject<JobCode>(jsonResponse);
Where jsonResponse is the code outputted above.
When I execute the code, "newResource" always comes back as null - which is not unexpected because I know that there are actually multiple jobcodes in the data and this code is trying to deserialize it into a single JobCode object. I tried creating a new class called "JobCodes" that looks like this:
class JobCodes
{
[JsonProperty("jobcodes")]
public List<JobCode>_JobCodes { get; set; }
}
And then I tried calling this:
newResource = JsonConvert.DeserializeObject<JobCodes>(jsonResponse);
But the issue persists - my return object is null.
What's throwing me off, I think, is the presence of the "1" and "2" identifiers. I don't know how to account for their presence in my object design and/or usage of the JSON.NET class / property attributes like [JsonObject],[JsonProperty], etc.
When I run the JSON data through JSON2CSharp, it constructs some weird-looking classes, so that hasn't proven too effective. I've validated the JSON with several different validators and it all checks out - I just don't know what I'm missing here.
Ultimately, I'd like to return a List from the JSON data, but I'm stumped on what I need to do to make that happen.
Your problem is twofold:
You don't have a class defined at the root level. The class structure needs to match the entire JSON, you can't just deserialize from the middle.
Whenever you have an object whose keys can change, you need to use a Dictionary<string, T>. A regular class won't work for that; neither will a List<T>.
Make your classes like this:
class RootObject
{
[JsonProperty("results")]
public Results Results { get; set; }
}
class Results
{
[JsonProperty("jobcodes")]
public Dictionary<string, JobCode> JobCodes { get; set; }
}
class JobCode
{
[JsonProperty("_status_code")]
public string StatusCode { get; set; }
[JsonProperty("_status_message")]
public string StatusMessage { get; set; }
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
}
Then, deserialize like this:
RootObject obj = JsonConvert.DeserializeObject<RootObject>(json);
Working demo here
Excellent Answers!
For those out there that may need some more help with the JSON Class Configuration, try: http://json2csharp.com/#
An excellent way of Auto Generating the Classes!
Or even easier, in VS, Goto:
Edit -> Paste Special -> Paste as JSON Classes
Because you can't change the scheme of JSON, and you can't set constant No. of properties, I'd suggest you to use JObject
var jobject = JObject.Parse(json);
var results = jobject["results"];
var jobcodes = results["jobcodes"];
var output = jobcodes.Children<JProperty>()
.Select(prop => prop.Value.ToObject<JobCode>())
.ToList();
Warning: code assumes, that JSON is always in proper schema. You should also handle invalid schema (for example where property is not of JobCode scheme).
You can also deserialize your json to an object of your target class, and then read its properties as per normal:
var obj = DeSerializeFromStrToObj<ClassToSerialize>(jsonStr);
Console.WriteLine($"Property: {obj.Property}");
where DeSerializeFromStrToObj is a custom class that makes use of reflection to instantiate an object of a targeted class:
public static T DeSerializeFromStrToObj<T>(string json)
{
try
{
var o = (T)Activator.CreateInstance(typeof(T));
try
{
var jsonDict = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
var props = o.GetType().GetProperties();
if (props == null || props.Length == 0)
{
Debug.WriteLine($"Error: properties from target class '{typeof(T)}' could not be read using reflection");
return default;
}
if (jsonDict.Count != props.Length)
{
Debug.WriteLine($"Error: number of json lines ({jsonDict.Count}) should be the same as number of properties ({props.Length})of our class '{typeof(T)}'");
return default;
}
foreach (var prop in props)
{
if (prop == null)
{
Debug.WriteLine($"Error: there was a prop='null' in our target class '{typeof(T)}'");
return default;
}
if (!jsonDict.ContainsKey(prop.Name))
{
Debug.WriteLine($"Error: jsonStr does not refer to target class '{typeof(T)}'");
return default;
}
var value = jsonDict[prop.Name];
Type t = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
object safeValue = value ?? Convert.ChangeType(value, t);
prop.SetValue(o, safeValue, null); // initialize property
}
return o;
}
catch (Exception e2)
{
Debug.WriteLine(e2.Message);
return o;
}
}
catch (Exception e)
{
Debug.WriteLine(e.Message);
return default;
}
}
A complete working example class can be found in my enhanced answer to a similar question, here

How do I define custom/dynamic columns/fields/properties in OData (v8 in .NET 6)?

There is a sample here for creating 100% dynamic OData models in Microsoft.AspNetCore.OData 8.x. However, in our case we have an existing model that we are happy with, but we want to add custom fields to it.
In other words, we want an OData model with entities that have some fixed columns/properties and some dynamically-generated columns/properties that come from the database, like this:
public class ODataEntity
{
[Key]
public int Id { get; set; }
public string Name { get; set; } = "";
// From the perspective of clients like Power BI, this should produce
// a series of additional columns (the columns are the same on all
// instances, but the schema can change at any time)
public Dictionary<string, object> CustomFields { get; set; }
}
To my tremendous surprise, the key-value pairs in CustomFields become properties in the JSON output (i.e. there is no CustomFields column; its contents are inserted into the parent object). However, the custom fields are not recognized by Power BI:
I assume that this is because there is no metadata for the custom fields in https://.../odata/$metadata. So my question is:
How can I modify the following code so that the custom columns are included in the IEdmModel?
static IEdmModel GetEdmModel(params CustomFieldDef[] customFields)
{
var builder = new ODataConventionModelBuilder() {
Namespace = "Namespace",
ContainerName = "Container", // no idea what this is for
};
builder.EntitySet<ODataEntity>("objects");
return builder.GetEdmModel();
}
public class CustomFieldDef
{
public string FieldName;
public Type Type;
}
How can I modify the following startup code so that the IEdmModel is regenerated every time https://.../odata/$metadata is accessed?
IMvcBuilder mvc = builder.Services.AddControllers();
mvc.AddOData(opt => opt.AddRouteComponents("odata", GetEdmModel())
.Select().Filter().OrderBy().Count().Expand().SkipToken());
Regarding the first question, there are two basic approaches that one could take:
1. Dynamic Everything
Use the 100% dynamic approach that is used in the ODataDynamicModel sample. This approach is difficult, especially if you already have a working model, because (i) the way the sample code works is difficult to understand, and (ii) you have to completely rewrite your code, or use reflection to help generate both the schema ($metadata) and the output data.
2. Modify the EdmModel
This is the approach I'm taking in this answer.
Step 1: change the model
The IEdmModel returned by ODataConventionModelBuilder.GetEdmModel is mutable (and actually has type EdmModel), so you can add custom fields to it.
static IEdmModel GetEdmModel(params CustomFieldDef[] customFields)
{
var builder = new ODataConventionModelBuilder() {
Namespace = "Namespace",
ContainerName = "Container", // no idea what this is for
};
builder.EntitySet<ODataEntity>("objects");
var model = (EdmModel) builder.GetEdmModel();
IODataTypeMapper mapper = model.GetTypeMapper();
foreach (var edmType in model.SchemaElements.OfType<EdmEntityType>()) {
if (edmType.Name == nameof(ODataEntity)) {
foreach (var field in customFields) {
var typeRef = mapper.GetEdmTypeReference(model, field.Type);
edmType.AddStructuralProperty(field.FieldName, typeRef);
}
}
}
return model;
}
Step 2: Obtain the custom fields
Normally you call mvc.AddOData in ConfigureServices in Startup.cs, passing it a lambda that will create the IEdmModel. But wait, are your custom field definitions stored in your database? If so, how can you access the database from inside ConfigureServices? Don't worry: the lambda passed to AddOData is called after Configure, so it is possible to access the database or any other required service with some slightly hacky code. This code also installs a class called ODataResourceSerializerForCustomFields which is the subject of step 3:
public void ConfigureServices(IServiceCollection services)
{
...
IMvcBuilder mvc = services.AddControllers(...);
// OData Configuration
mvc.AddOData(opt => {
var scopeProvider = _serviceProvider!.CreateScope().ServiceProvider;
var cfdm = scopeProvider.GetRequiredService<CustomFieldDefManager>();
var edmModel = GetEdmModel(cfdm.GetAll());
opt.AddRouteComponents("odata", edmModel, services => {
services.AddScoped<ODataResourceSerializer,
ODataResourceSerializerForCustomFields>();
}).Select().Filter().OrderBy().Count().Expand().SkipToken();
});
...
}
IServiceProvider? _serviceProvider;
public void Configure(IApplicationBuilder app, ...)
{
_serviceProvider = app.ApplicationServices;
...
}
Of course, this code is not dynamic: it generates the EdmModel only once on startup. I will figure out later how to make this dynamic.
Step 3: Stop it from crashing
Microsoft.AspNetCore.OData.dll isn't designed to support custom fields in the EdmModel. It's a young product, you understand, only version 8.0. As soon as you add a custom field, you'll get an InvalidOperationException like "The EDM instance of type '[ODataEntity Nullable=True]' is missing the property 'ExampleCustomField'., because the library assumes that all properties in the EdmModel are real CLR properties.
I found a way around this problem by overriding a few methods of ODataResourceSerializer. But first, define an IHasCustomFields interface and make sure that any OData entity with custom fields implements this interface:
public interface IHasCustomFields
{
public Dictionary<string, object?> CustomFields { get; set; }
}
Now let's add ODataResourceSerializerForCustomFields, which uses special behavior when this interface is present.
/// <summary>
/// This class modifies the behavior of ODataResourceSerializer to
/// stop it from crashing when the EdmModel contains custom fields.
/// Note: these modifications are designed for simple custom fields
/// (e.g. string, bool, DateTime).
/// </summary>
public class ODataResourceSerializerForCustomFields : ODataResourceSerializer
{
public ODataResourceSerializerForCustomFields(IODataSerializerProvider serializerProvider)
: base(serializerProvider) { }
IHasCustomFields? _hasCustomFields;
HashSet<string>? _realProps;
public override Task WriteObjectInlineAsync(
object graph, IEdmTypeReference expectedType,
ODataWriter writer, ODataSerializerContext writeContext)
{
_hasCustomFields = null;
if (graph is IHasCustomFields hasCustomFields) {
_hasCustomFields = hasCustomFields;
var BF = BindingFlags.Public | BindingFlags.Instance;
_realProps = graph.GetType().GetProperties(BF).Select(p => p.Name).ToHashSet();
}
return base.WriteObjectInlineAsync(graph, expectedType, writer, writeContext);
}
public override ODataResource CreateResource(
SelectExpandNode selectExpandNode, ResourceContext resourceContext)
{
return base.CreateResource(selectExpandNode, resourceContext);
}
public override ODataProperty CreateStructuralProperty(
IEdmStructuralProperty structuralProperty, ResourceContext resourceContext)
{
// Bypass tne base class if the current property doesn't physically exist
if (_hasCustomFields != null && !_realProps!.Contains(structuralProperty.Name)) {
_hasCustomFields.CustomFields.TryGetValue(structuralProperty.Name, out object? value);
return new ODataProperty {
Name = structuralProperty.Name,
Value = ToODataValue(value)
};
}
return base.CreateStructuralProperty(structuralProperty, resourceContext);
}
public static ODataValue ToODataValue(object? value)
{
if (value == null)
return new ODataNullValue();
if (value is DateTime date)
value = (DateTimeOffset)date;
return new ODataPrimitiveValue(value);
}
// The original implementation of this method can't be prevented from
// crashing, so replace it with a modified version based on the original
// source code. This version is simplified to avoid calling `internal`
// methods that are inaccessible, but as a result I'm not sure that it
// behaves quite the same way. If properties that aren't in the EdmModel
// aren't needed in the output, the method body is optional and deletable.
public override void AppendDynamicProperties(ODataResource resource,
SelectExpandNode selectExpandNode, ResourceContext resourceContext)
{
if (_hasCustomFields == null) {
base.AppendDynamicProperties(resource, selectExpandNode, resourceContext);
return;
}
if (!resourceContext.StructuredType.IsOpen || // non-open type
(!selectExpandNode.SelectAllDynamicProperties && selectExpandNode.SelectedDynamicProperties == null)) {
return;
}
IEdmStructuredType structuredType = resourceContext.StructuredType;
IEdmStructuredObject structuredObject = resourceContext.EdmObject;
object value;
if (structuredObject is IDelta delta) {
value = ((EdmStructuredObject)structuredObject).TryGetDynamicProperties();
} else {
PropertyInfo dynamicPropertyInfo = resourceContext.EdmModel.GetDynamicPropertyDictionary(structuredType);
if (dynamicPropertyInfo == null || structuredObject == null ||
!structuredObject.TryGetPropertyValue(dynamicPropertyInfo.Name, out value) || value == null) {
return;
}
}
IDictionary<string, object> dynamicPropertyDictionary = (IDictionary<string, object>)value;
// Build a HashSet to store the declared property names.
// It is used to make sure the dynamic property name is different from all declared property names.
HashSet<string> declaredPropertyNameSet = new HashSet<string>(resource.Properties.Select(p => p.Name));
List<ODataProperty> dynamicProperties = new List<ODataProperty>();
// To test SelectedDynamicProperties == null is enough to filter the dynamic properties.
// Because if SelectAllDynamicProperties == true, SelectedDynamicProperties should be null always.
// So `selectExpandNode.SelectedDynamicProperties == null` covers `SelectAllDynamicProperties == true` scenario.
// If `selectExpandNode.SelectedDynamicProperties != null`, then we should test whether the property is selected or not using "Contains(...)".
IEnumerable<KeyValuePair<string, object>> dynamicPropertiesToSelect =
dynamicPropertyDictionary.Where(x => selectExpandNode.SelectedDynamicProperties == null || selectExpandNode.SelectedDynamicProperties.Contains(x.Key));
foreach (KeyValuePair<string, object> dynamicProperty in dynamicPropertiesToSelect) {
if (string.IsNullOrEmpty(dynamicProperty.Key))
continue;
if (declaredPropertyNameSet.Contains(dynamicProperty.Key))
continue;
dynamicProperties.Add(new ODataProperty {
Name = dynamicProperty.Key,
Value = ToODataValue(dynamicProperty.Value)
});
}
if (dynamicProperties.Count != 0)
resource.Properties = resource.Properties.Concat(dynamicProperties);
}
}

Add custom attribute to a class generated by Entity Framework

I am trying to use a custom attribute on a Entity class generated automatically by the Entity Framework.
The problem is how to add an property attribute on an existing field?
Here the point where I am right now:
// the custom attribute class
public class MyCustomAttribute : Attribute
{
public String Key { get; set; }
}
// Entity Framework class generated automatically
public partial class EntityClass
{
public String Existent { get; set; }
//...
}
// set a metadata class for my entity
[MetadataType(typeof(EntityClassMetaData))]
public partial class EntityClass
{
// if I add a new property to the entity, it works. This attribute will be read
[MyCustomAttribute(Key = "KeyOne" )]
public int newProp { get; set; }
}
public class EntityClassMetaData
{
// adding the custom attribute to the existing property
[MyCustomAttribute(Key = "keyMeta") ]
public String Existent { get; set; }
}
Running this test:
[TestMethod]
public void test1()
{
foreach (var prop in typeof(EntityClass).GetProperties())
{
var att = prop.GetCustomAttribute<MyCustomAttribute>();
if (att != null)
{
Console.WriteLine($"Found {att.Key}");
}
}
}
will produce:
Found KeyOne
Or the Metadata class store the attribute in a different way or only works for data annotations.
I am stuck here, how can I set and read custom attributes of the generated class without having to edit the generated file?
I came across this same problem today. I figured EF magic would do the trick and map the attribute across to each model property. Turns out it does, but only for EF data annotations and I couldn't find an answered solution to pull out custom attributes so made this function. Hope it helps dude.
private object[] GetMetadataCustomAttributes(Type T, string propName)
{
if (Attribute.IsDefined(T, typeof(MetadataTypeAttribute)))
{
var metadataClassType =
(T.GetCustomAttributes(typeof(MetadataTypeAttribute), true).FirstOrDefault() as
MetadataTypeAttribute).MetadataClassType;
var metaDataClassProperty = metadataClassType.GetProperty(propName);
if (metaDataClassProperty != null)
{
return metaDataClassProperty.GetCustomAttributes(true);
}
}
return null;
}
I believe if you want to set an attribute in the metadata class, you have to use this syntax:
public class EntityClassMetaData
{
// adding the custom attribute to the existing property
[MyCustomAttribute(Key = "keyMeta") ]
public String Existent;
}
You must not have { get; set; } on your pre-existing property - just the property with the correct name and datatype.

How to ignore a property based on a runtime condition?

I have a simple pair of classes which for I've set up a mapping at initialization time.
public class Order {
public int ID { get; set; }
public string Foo { get; set; }
}
public class OrderDTO {
public int ID { get; set; }
public string Foo { get; set; }
}
...
Mapper.CreateMap<Order, OrderDTO>();
Now at a certain point I need to map an Order to an OrderDTO. BUT depending on some circumstances, I might need to ignore Foo during mapping. Let's also assume that I cannot "store" the condition in the source or destination object.
I know how I can configure the ignored properties at initialization time, but I have no idea how I could achieve such a dynamic runtime behavior.
Any help would be appreciated.
UPDATE
My use case for this behaviour is something like this. I have an ASP.NET MVC web grid view which displays a list of OrderDTOs. The users can edit the cell values individually. The grid view sends the edited data back to the server like a collection of OrderDTOs, BUT only the edited field values are set, the others are left as default. It also sends data about which fields are edited for each primary key. Now from this special scenario I need to map these "half-empty" objects to Orders, but of course, skip those properties which were not edited for each object.
The other way would be to do the manual mapping, or use Reflection somehow, but I was just thinking about if I could use AutoMapper in some way.
I've digged into the AutoMapper source code and samples, and found that there is a way to pass runtime parameters at mapping time.
A quick example setup and usage looks like this.
public class Order {
public int ID { get; set; }
public string Foo { get; set; }
}
public class OrderDTO {
public int ID { get; set; }
public string Foo { get; set; }
}
...
Mapper.CreateMap<Order, OrderDTO>()
.ForMember(e => e.Foo, o => o.Condition((ResolutionContext c) => !c.Options.Items.ContainsKey("IWantToSkipFoo")));
...
var target = new Order();
target.ID = 2;
target.Foo = "This should not change";
var source = new OrderDTO();
source.ID = 10;
source.Foo = "This won't be mapped";
Mapper.Map(source, target, opts => { opts.Items["IWantToSkipFoo"] = true; });
Assert.AreEqual(target.ID, 10);
Assert.AreEqual(target.Foo, "This should not change");
In fact this looks quite "technical", but I still think there are quite many use cases when this is really helpful. If this logic is generalized according to application needs, and wrapped into some extension methods for example, then it could be much cleaner.
Expanding on BlackjacketMack's comment for others:
In your MappingProfile, add a ForAllMaps(...) call to your constructor.
using AutoMapper;
using System.Collections.Generic;
using System.Linq;
public class MappingProfile : Profile
{
public MappingProfile()
{
ForAllMaps((typeMap, mappingExpression) =>
{
mappingExpression.ForAllMembers(memberOptions =>
{
memberOptions.Condition((o1, o2, o3, o4, resolutionContext) =>
{
var name = memberOptions.DestinationMember.Name;
if (resolutionContext.Items.TryGetValue(MemberExclusionKey, out object exclusions))
{
if (((IEnumerable<string>)exclusions).Contains(name))
{
return false;
}
}
return true;
});
});
});
}
public static string MemberExclusionKey { get; } = "exclude";
}
Then, for ease of use, add the following class to create an extension method for yourself.
public static class IMappingOperationOptionsExtensions
{
public static void ExcludeMembers(this AutoMapper.IMappingOperationOptions options, params string[] members)
{
options.Items[MappingProfile.MemberExclusionKey] = members;
}
}
Finally, tie it all together: var target = mapper.Map<Order>(source, opts => opts.ExcludeMembers("Foo"));

asp.net mvc web api partial update with OData Patch

I am using HttpPatch to partially update an object. To get that working I am using Delta and Patch method from OData (mentioned here: What's the currently recommended way of performing partial updates with Web API?). Everything seems to be working fine but noticed that mapper is case sensitive; when the following object is passed the properties are getting updated values:
{
"Title" : "New title goes here",
"ShortDescription" : "New text goes here"
}
But when I pass the same object with lower or camel-case properties, Patch doesn't work - new value is not going through, so it looks like there is a problem with deserialisation and properties mapping, ie: "shortDescription" to "ShortDescription".
Is there a config section that will ignore case sensitivity using Patch?
FYI:
On output I have camel-case properties (following REST best practices) using the following formatter:
//formatting
JsonSerializerSettings jss = new JsonSerializerSettings();
jss.ContractResolver = new CamelCasePropertyNamesContractResolver();
config.Formatters.JsonFormatter.SerializerSettings = jss;
//sample output
{
"title" : "First",
"shortDescription" : "First post!"
}
My model classes however are follwing C#/.NET formatting conventions:
public class Entry {
public string Title { get; set;}
public string ShortDescription { get; set;}
//rest of the code omitted
}
Short answer, No there is no config option to undo the case sensitiveness (as far as i know)
Long answer: I had the same problem as you today, and this is how i worked around it.
I found it incredibly annoying that it had to be case sensitive, thus i decided to do away with the whole oData part, since it is a huge library that we are abusing....
An example of this implementation can be found at my github github
I decided to implement my own patch method, since that is the muscle that we are actually lacking. I created the following abstract class:
public abstract class MyModel
{
public void Patch(Object u)
{
var props = from p in this.GetType().GetProperties()
let attr = p.GetCustomAttribute(typeof(NotPatchableAttribute))
where attr == null
select p;
foreach (var prop in props)
{
var val = prop.GetValue(this, null);
if (val != null)
prop.SetValue(u, val);
}
}
}
Then i make all my model classes inherit from *MyModel*. note the line where i use *let*, i will excplain that later. So now you can remove the Delta from you controller action, and just make it Entry again, as with the put method. e.g.
public IHttpActionResult PatchUser(int id, Entry newEntry)
You can still use the patch method the way you used to:
var entry = dbContext.Entries.SingleOrDefault(p => p.ID == id);
newEntry.Patch(entry);
dbContext.SaveChanges();
Now, let's get back to the line
let attr = p.GetCustomAttribute(typeof(NotPatchableAttribute))
I found it a security risk that just any property would be able to be updated with a patch request. For example, you might now want the an ID to be changeble by the patch. I created a custom attribute to decorate my properties with. the NotPatchable attribute:
public class NotPatchableAttribute : Attribute {}
You can use it just like any other attribute:
public class User : MyModel
{
[NotPatchable]
public int ID { get; set; }
[NotPatchable]
public bool Deleted { get; set; }
public string FirstName { get; set; }
}
This in this call the Deleted and ID properties cannot be changed though the patch method.
I hope this solve it for you as well. Do not hesitate to leave a comment if you have any questions.
I added a screenshot of me inspecting the props in a new mvc 5 project. As you can see the Result view is populated with the Title and ShortDescription.
It can be done quite easily with a custom contract resolver that inherits CamelCasePropertyNamesContractResolver and implementing CreateContract method that look at concrete type for delta and gets the actual property name instead of using the one that comes from json. Abstract is below:
public class DeltaContractResolver : CamelCasePropertyNamesContractResolver
{
protected override JsonContract CreateContract(Type objectType)
{
// This class special cases the JsonContract for just the Delta<T> class. All other types should function
// as usual.
if (objectType.IsGenericType &&
objectType.GetGenericTypeDefinition() == typeof(Delta<>) &&
objectType.GetGenericArguments().Length == 1)
{
var contract = CreateDynamicContract(objectType);
contract.Properties.Clear();
var underlyingContract = CreateObjectContract(objectType.GetGenericArguments()[0]);
var underlyingProperties =
underlyingContract.CreatedType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var property in underlyingContract.Properties)
{
property.DeclaringType = objectType;
property.ValueProvider = new DynamicObjectValueProvider()
{
PropertyName = this.ResolveName(underlyingProperties, property.PropertyName),
};
contract.Properties.Add(property);
}
return contract;
}
return base.CreateContract(objectType);
}
private string ResolveName(PropertyInfo[] properties, string propertyName)
{
var prop = properties.SingleOrDefault(p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase));
if (prop != null)
{
return prop.Name;
}
return propertyName;
}
}

Categories

Resources