Base class: AbstractBuildBlock, Derived: TextBlock, ImageBlock, EmptyBlock, ... .
Blocks here: Site -> Pages[someIndex] -> Rows[someIndex] -> BuildBlocks
Fields BuildBlocks is of type AbstractBuildBlock, so when I'm saving Site to DB in BuildBlocks each record has descriminator AbstractBuildBlock. I try to do the next in BuildBlockRepository:
switch(obj.ContentType)
{
case "text":
obj = obj as TextBlock;
context.BuildBlocks.Add(obj);
break;
}
After obj = obj as TextBlock obj is null. The reason is that obj is of type AbstractBuildBlock. I found at msdn that this code should work:
BaseClass a = new DerivedClass()
DerivedClass b = a as DerivedClass
So I need reproduce this code at model binding. This is ajax request:
$('.saveSite').click(function () {
$.ajax({
url: '/Site/Update',
data: { site: getSite() },
method: 'POST',
success: function (data) {
console.log('Success save');
},
error: function (data) {
debugBox(data);
}
});
});
and mvc action for this request
public string Update(Site site)
{
siteRepository.Add(site);
return "Success";
}
So I send Site in json form, BuildBlocks that are in this site in form of json too, but of course their(blocks) type is not AbstractBuildBlock, they are all of TextBlock, ImageBlock, etc. and have fields with values.
The problem: Site has field BuildBlocks which type is AbstractBuildBlock and model binder do something like this:
buildBlock = new AbstractBuildBlock(); //loose derived classes fields and posibility to convert it in DerivedClass
buildBlocks.push(buildBlock)
but I need someting like this
switch(buildBlock.contenType) {
case "text" : buildBlock = new TextBlock();buidlBlocks.push(buildBlock);
}
JSON NET Custom deserializer not work at all
ASP MVC 5 How to read object from request with the help of JSON NET
look at answers in two links above, the correct ajax call is described
And the server code below
mvc action
public string Update(Site site)
{
TextBlock block = site.Pages[0].Rows[0].BuildBlocks[0] as TextBlock;
siteRepository.Add(site);
return "Success";
}
AbstractJsonCreationConverter I've created it in Infrastructure folder
public abstract class AbstractJsonCreationConverter<T> : JsonConverter
{
protected abstract T Create(Type objectType, JObject jsonObject);
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
var jsonObject = JObject.Load(reader);
var target = Create(objectType, jsonObject);
serializer.Populate(jsonObject.CreateReader(), target);
return target;
}
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
And in the same folder concrete class
public class JsonBuildBlockConverter : AbstractJsonCreationConverter<AbstractBuildBlock>
{
protected override AbstractBuildBlock Create(Type objectType, JObject jsonObject)
{
var type = jsonObject["contentType"].ToString();
switch(type)
{
case "text":
return new TextBlock();
default:
return null;
}
}
}
and one more class in Infrastructure
internal class SiteModelBinder : System.Web.Mvc.IModelBinder
{
public object BindModel(ControllerContext controllerContext, System.Web.Mvc.ModelBindingContext bindingContext)
{
// use Json.NET to deserialize the incoming Position
controllerContext.HttpContext.Request.InputStream.Position = 0; // see: https://stackoverflow.com/a/3468653/331281
Stream stream = controllerContext.RequestContext.HttpContext.Request.InputStream;
var readStream = new StreamReader(stream, Encoding.UTF8);
string json = readStream.ReadToEnd();
return JsonConvert.DeserializeObject<Site>(json, new JsonBuildBlockConverter());
}
}
The last class is ModelBinder that will be called to parse variables of type Site, to make it work you need to register it in Global.asax.cs in ApplicationStart()
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
ModelBinders.Binders.Add(typeof(Site), new SiteModelBinder()); //RegisterModelBinder for Site
}
This is all.
Related
I created a custom converter extending from JsonConverter, which would be used for an ASP.NET MVC and a Web API.
public class MyCustomConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
....
}
}
And I created this CustomObject that uses that converter:
[JsonConverter(typeof(MyCustomJsonConverter))]
public class CustomObject
{
...
}
This converter work correctly for second application (WebApi), that means method of ReadJson are running after calling it in TestOfUsingJson. And in this case I didn't have to set up anything.
For the first application (ASP.NET MVC) I have a trouble, object are converted from json, but this object are not created from my custom converter. Method of ReadJson are not running.
Method which who use custom converter are looks same on the every application
public HttpResponseMessage TestOfUsingJson([FromBody] CustomObject objs)
{
...
}
Some settings for Json Serializer in the ASP.NET MVC's Global.asax.cs:
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
Converters = new List<JsonConverter> { new MyCustomJsonConverter() }
};
What am I doing wrong?
I have prepared some solution that may be useful for someone
Create binding model
public class BindJson : IModelBinder
{
public BindJson()
{
}
public virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (controllerContext == null)
throw new SysException("Missing controller context");
if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
return null;
controllerContext.HttpContext.Request.InputStream.Position = 0;
using (var reader = new StreamReader(controllerContext.HttpContext.Request.InputStream))
{
var data = reader.ReadToEnd();
if (!String.IsNullOrEmpty(bodyText))
{
return JsonConvert.DeserializeObject(data, bindingContext.ModelType);
}
}
return null;
}
}
Assign binder in Global.asax.cs
..
ModelBinders.Binders.Add(typeof(BindJson), new BindJson());
..
Assign binder into class
[ModelBinder(typeof(BindJson))]
public class CustomObject
{
...
}
Call binder in method
public HttpResponseMessage TestOfUsingJson(CustomObject objs)
{
...
}
Update: due the answer, i found old title irrelevant and change it from the 'How to Implement Custom JsonConverter that uses JsonConvert' to what currently it is.
I'm in very tight and time limit situation, and i have to do as what the title says...
i tried many ways till morning, model doesn't bind, the method similar to FormCollection in MVC returns nulls, and so on and so on, since i add a list of interfaces to one of my model, so i need to handle it,... morning client report bugs, and their system is under pressure, so i can't help it but work till it fix.
here's my current code:
What happen? once the writer method returns server returns 500 internal server error...
naturally they do this using serializer object, but i don't know how to do this customization using serializer, so i really in need to call this method which accept a serializer setting.
public class CollectionInterfaceConverterForModels<TInterface> : JsonConverter
{
private readonly TypeNameSerializationBinder Binder;
public CollectionInterfaceConverterForModels()
{
Binder = new TypeNameSerializationBinder("Cartable.Models.{0}, Cartable");
}
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(IList<TInterface>));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
//IList<TInterface> items = serializer.Deserialize<List<TInterface>>(reader).Cast<TInterface>().ToList();
//return items;
var json = existingValue.ToString();
if (json == null)
json = "";
return JsonConvert.DeserializeObject<List<TInterface>>(json,
new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto,
Binder = Binder
});
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
//serializer.Serialize(writer, value, typeof(IList<T>));
string serializeObject = JsonConvert.SerializeObject(value, Formatting.Indented, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto,
Binder = Binder
});
writer.WriteRaw(serializeObject);
writer.Flush();
}
}
Model sample:
public class IncomingFaxesVM
{
public long Code { get; set; }
.
.
.
[JsonConverter(typeof(CollectionInterfaceConverterForModels<IMetadata>))]
public List<IMetadata> Metadata { get; set; }
}
Yet, i'm unable to do what i asked, but i solved the issue using another way,
So if you find the answer, i mark yours, other wise i change the title to interface binding so other user can find it easily, and here's my answer to solve the issue.
public class CollectionInterfaceConverterForModels<TInterface> : JsonConverter
{
private readonly TypeNameSerializationBinder Binder;
public CollectionInterfaceConverterForModels()
{
Binder = new TypeNameSerializationBinder("Cartable.Models.{0}, Cartable");
}
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(IList<TInterface>));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var currentBinder = serializer.Binder;
var currentTypeNameHandling = serializer.TypeNameHandling;
serializer.TypeNameHandling = TypeNameHandling.Auto;
serializer.Binder = Binder;
IList<TInterface> items = serializer.Deserialize<List<TInterface>>(reader).ToList();
serializer.TypeNameHandling = currentTypeNameHandling;
serializer.Binder = currentBinder;
return items;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
//The reason store these, is to leave system unchanged, in case it customized on general section by another programmer
var currentBinder = serializer.Binder;
var currentTypeNameHandling = serializer.TypeNameHandling;
serializer.TypeNameHandling=TypeNameHandling.Auto;
serializer.Binder = Binder;
serializer.Serialize(writer, value, typeof(List<TInterface>));
serializer.TypeNameHandling = currentTypeNameHandling;
serializer.Binder = currentBinder;
}
}
When I try to deserialize the json I receive from an external source my program won't map the childobjects when I use value.first().
private Class1 class1List;
public List<Class1> Class1List
{
get
{
return new List<Class1> { class1List };
}
set
{
class1List = value.First();
}
}
If I want it to work I have to alter the class1List to a List and have to remove .First(). The problem is that the Listwill always only contain one element and for internal storage and usage it would be best if I can get only the first element and store that as an object. The json I receive cannot be altered. (if Serialize my objects the json looks the same).
{
"Class1List":[
{"Class2List":[
{"Name":"test"}
]}
]}
Edit: Removing the get makes it work
Edit 2 : altering the get to this also makes it work
get
{
if (class1List != null)
{
return new List<Class1> { class1List };
}
return null;
}
If you are that particular not add private member I have tried an another solution
var selctedList = JObject.Parse("YOUR JSON").SelectToken("Class1List").ToString();
var class1List = JsonConvert.DeserializeObject<Class1[]>(selctedList, new Class1Converter());
public class Class1
{
public string Name { get; set; }
}
public class Class1Converter : JsonCreationConverter<Class1>
{
protected override Class1 Create(Type objectType, JObject jObject)
{
return new Class1();
}
}
public abstract class JsonCreationConverter<T> : JsonConverter
{
protected abstract T Create(Type objectType, JObject jObject);
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader,
Type objectType,
object existingValue,
JsonSerializer serializer)
{
try
{
// Load JObject from stream
JObject jObject = JObject.Load(reader);
var lp = jObject[jObject.Properties().Select(p => p.Name).FirstOrDefault()];
JObject kl = JsonConvert.DeserializeObject<JObject[]>(lp.ToString()).FirstOrDefault();
// Create target object based on JObject
T target = Create(objectType, kl);
// Populate the object properties
serializer.Populate(kl.CreateReader(), target);
return target;
}
catch (Exception er)
{
throw er;
}
}
public override void WriteJson(JsonWriter writer,
object value,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Disclaimer: I read it somewhere but I can't find the source now and my explanation might be wrong, but the solution work.
Json.Net require you to return the object that Json.Net created during parsing.
class BaseClass
{
internal Class1 class1List;
private List<Class1> _class1List;
public List<Class1> Class1List
{
get
{
return _class1List;
// return new List<Class1> { class1List };
}
set
{
class1List = value.First();
_class1List = value;
}
}
}
I try to add a variable of "custom data type" in my HttpGet Route.
I have this code:
[HttpGet("{idObject}")]
public ResponseSchema Get(ObjectId idObject)
{
if (idObject == null) {
throw new BodyParseException();
}
var user = _usersLogic.GetById(idObject);
if (user == null) {
_response.Success = false;
_response.ErrorCode = "UserDoesNotExist";
}
else {
_response.Objects.Add(user);
}
return _response;
}
ObjectId is a Datatype defined in using MongoDB.Bson.
For the Json Serialization and Deserialization we already have the code to automatically convert on both sides. But can this be similarly done in the Url itself.
We are right now using this Mvc version:
"Microsoft.AspNet.Mvc": "6.0.0-beta8"
So the URL looks like this:
GET Users/55b795827572761a08d735ac
The code to parse it from "string" to "ObjectId" is:
ObjectId.TryParse(idString, out idObject);
The question is where to put that TryParse code. Because I need to tell ASP.NET how it should parse the idObject from String to ObjectId. Since the URL basically is a string.
For Post or Put JSON Payload I already found a solution. I know that this is something different. But Probably it is helpful to understand the scenario, or find a solution to this scenario:
public class EntityBaseDocument
{
[JsonConverter(typeof(ObjectIdConverter))]
public ObjectId Id { get; set; }
}
// Since we have this value converter. We can use ObjectId everywhere
public class ObjectIdConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value.ToString());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
return new ObjectId(token.ToObject<string>());
}
public override bool CanConvert(Type objectType)
{
return typeof(ObjectId).IsAssignableFrom(objectType);
}
}
This will bind it from Uri objects:
public ResponseSchema Get([FromUri]ObjectId idObject)
So: ?param1=something¶m2=sometingelse
This will bind it from the body (e.g. a JSon object)
public ResponseSchema Get([FromBody]ObjectId idObject)
Or you can roll your own:
public ResponseSchema Get([ModelBinder(typeof(MyObjectBinder))]ObjectId idObject)
The example on asp.net of a model binder is:
public class GeoPointModelBinder : IModelBinder
{
// List of known locations.
private static ConcurrentDictionary<string, GeoPoint> _locations
= new ConcurrentDictionary<string, GeoPoint>(StringComparer.OrdinalIgnoreCase);
static GeoPointModelBinder()
{
_locations["redmond"] = new GeoPoint() { Latitude = 47.67856, Longitude = -122.131 };
_locations["paris"] = new GeoPoint() { Latitude = 48.856930, Longitude = 2.3412 };
_locations["tokyo"] = new GeoPoint() { Latitude = 35.683208, Longitude = 139.80894 };
}
public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(GeoPoint))
{
return false;
}
ValueProviderResult val = bindingContext.ValueProvider.GetValue(
bindingContext.ModelName);
if (val == null)
{
return false;
}
string key = val.RawValue as string;
if (key == null)
{
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, "Wrong value type");
return false;
}
GeoPoint result;
if (_locations.TryGetValue(key, out result) || GeoPoint.TryParse(key, out result))
{
bindingContext.Model = result;
return true;
}
bindingContext.ModelState.AddModelError(
bindingContext.ModelName, "Cannot convert value to Location");
return false;
}
}
I believe NikoliaDante's answer works if you have a route such as /api/users?id={{idHere}}. However, if you are looking to have more RESTful routes, the solution below will do the trick for you. I just tested this out in a Web API 2 application and it works well. This will handle the use case where you may have a route such as /api/users/{{userId}}/something/{{somethingId}}.
//Http Parameter Binding Magic
public class ObjectIdParameterBinding : HttpParameterBinding
{
public ObjectIdParameterBinding(HttpParameterDescriptor p) : base(p){ }
public override Task ExecuteBindingAsync(System.Web.Http.Metadata.ModelMetadataProvider metadataProvider, HttpActionContext actionContext, System.Threading.CancellationToken cancellationToken)
{
var value = actionContext.ControllerContext.RouteData.Values[Descriptor.ParameterName].ToString();
SetValue(actionContext, ObjectId.Parse(value));
var tsc = new TaskCompletionSource<object>();
tsc.SetResult(null);
return tsc.Task;
}
}
//Binding Attribute
public class ObjectIdRouteBinderAttribute : ParameterBindingAttribute
{
public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
{
return new ObjectIdParameterBinding(parameter);
}
}
//Controller Example
[Route("api/users/{id}")]
public async Task<User> Get([ObjectIdRouteBinder] ObjectId id)
{
//Yay!
}
ASP.NET Web API provides several approaches for do that. Take a look for
Parameter Binding in Web API documentation.
Summary:
FromUriAttribute - for simple DTO classes
TypeConverter - to help Web API treat your class as simple type
HttpParameterBinding - allow to create behaviour attribute
ValueProvider - for more complex case
IActionValueBinder - to write own parameter-binding process at all
Trying to return a correct error message instead of the WebAPI default one {"Message":"The request is invalid.","ModelState" when Json deserialzation fails.
I implemented my custom ActionFilterAttribute:
internal class ValidateModelAttribute : ActionFilterAttribute {
public override void OnActionExecuting(HttpActionContext actionContext) {
if (!actionContext.ModelState.IsValid) {
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
}
}
}
}
I decorated my Controller method with this attribute:
[ValidateModelAttribute]
public async Task<HttpResponseMessage> Put(string username, string serviceId, [FromBody] Dictionary<string, string> jsonData)
{
// code
}
If I set a breakpoint in the OnActionExecuting it only breaks when the jsonData is parsed succesfully as a json. If the json is invalid it never gets into the filter, and the same error message is returned. So looks like this is done somewhere before but all the posts I found say that this should be the place to handle this.
Any idea what's wrong?
The attribute will never get called since deserialization is failing before the method is ever called, meaning that the attributes decorating the method are not called. You need a custom converter (the technique for handling cultures was borrowed from this answer).
public class Testee {}
public class Tester
{
[JsonConverter(typeof(CustomMesssageConverter<Testee>), "Custom Error Message")]
public Testee Testee { get; set; }
}
public class CustomMesssageConverter<T> : JsonConverter where T : new()
{
private string _customErrorMessage;
public CustomMesssageConverter(string customErrorMessage)
{
_customErrorMessage = customErrorMessage;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
if (reader.TokenType == JsonToken.Null)
return null;
// Load JObject from stream
JObject jObject = JObject.Load(reader);
// Create target object based on type
var target = new T();
//Create a new reader for this jObject, and set all properties to match the original reader.
JsonReader jObjectReader = jObject.CreateReader();
jObjectReader.Culture = reader.Culture;
jObjectReader.DateParseHandling = reader.DateParseHandling;
jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
jObjectReader.FloatParseHandling = reader.FloatParseHandling;
// Populate the object properties
serializer.Populate(jObjectReader, target);
return target;
}
catch(Exception ex)
{
// log ex here
throw new Exception(_customErrorMessage);
}
}
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
}