Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to post a dynamic JSON property to a C# ASP.Net Core Web API using MongoDB?

How to post a dynamic JSON property to a C# ASP.Net Core Web API using MongoDB?

It seems like one of the advantages of MongoDB is being able to store anything you need to in an Object type property but it doesn't seem clear how you can get the dynamic property info from the client ajax post through a C# Web API to MongoDB.

We want to allow an administrator to create an Event with Title and Start Date/Time but we also want to allow the user to add custom form fields using Reactive Forms for whatever they want such as t-shirt size or meal preference... Whatever the user may come up with in the future. Then when someone registers for the event, they post EventID and the custom fields to the Web API.

We can have an Event MongoDB collection with _id, event_id, reg_time, and form_fields where form_fields is an Object type where the dynamic data is stored.

So we want to POST variations of this JSON with custom FormsFields:

Variation 1:

{
    "EventId": "595106234fccfc5fc88c40c2",
    "RegTime":"2017-07-21T22:00:00Z",
    "FormFields": {
        "FirstName": "John",
        "LastName": "Public",
        "TShirtSize": "XL"
    }
}

Variation 2:

{
    "EventId": "d34f46234fccfc5fc88c40c2",
    "RegTime":"2017-07-21T22:00:00Z",
    "FormFields": {
        "Email": "[email protected]",
        "MealPref": "Vegan"
    }
}

I would like to have an EventController with Post action that takes a custom C# EventReg object that maps to the JSON above.

EventController:

[HttpPost]
public void Post([FromBody]EventReg value)
{
    eventService.AddEventRegistration(value);
}

EventReg Class:

public class EventReg
{
    public EventReg()
    {
        FormFields = new BsonDocument();
    }
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    public string EventRegId { get; set; }

    [BsonElement("EventId")]
    [BsonRepresentation(BsonType.ObjectId)]
    public string EventId { get; set; }

    [BsonElement("reg_time")]
    public DateTime RegTime
    {
        set; get;
    }

    [BsonElement("form_fields")]
    public MongoDB.Bson.BsonDocument FormFields { get; set; }
}

EventService

public string AddEventRegistration(EventReg eventReg)
{
    this.database.GetCollection<EventReg>("event_regs").InsertOne(eventReg);
    return eventReg.EventRegId;
} 

Right now, if I post to the controller, my EventReg is null because it must not know how to map my JSON FormFields properties to a BsonDocument.

  • What type can I use for FormFields?
  • Can I have the FormFields property be a BsonDocument and is there an easy way to map the Web API parameter to that?
  • Is there an example of how some custom serializer might work in this case?

We could maybe use a dynamic type and loop through the posted properties but that seems ugly. I have also seen the JToken solution from a post here but that looks ugly also.

If MongoDB is meant to be used dynamically like this, shouldn't there be a clean solution to pass dynamic data to MongoDB? Any ideas out there?

like image 713
Andy Avatar asked Jul 05 '17 21:07

Andy


2 Answers

The JToken example works to get data in but upon retrieval it causes browsers and Postman to throw an error and show a warning indicating that content was read as a Document but it was in application/json format. I saw the FormFields property being returned as {{"TShirtSize":"XL"}} so maybe double braces was a problem during serialization.

I ended up using the .NET ExpandoObject in the System.Dynamic namespace. ExpandoObject is compatible with the MongoDB BsonDocument so the serialization is done automatically like you would expect. So no need for weird code to manually handle the properties like the JToken example in the question.

I still believe that a more strongly typed C# representation should be used if at all possible but if you must allow any JSON content to be sent to MongoDB through a Web API with a custom C# class as input, then the ExpandoObject should do the trick.

See how the FormFields property of EventReg class below is now ExpandoObject and there is no code to manually handle the property of the object being saved to MongoDB.

Here is the original problematic and overly complex JToken code to manually populate an object with standard type properties and a dynamic FormFields property:

[HttpPost]
public void Post([FromBody]JToken token)
{
    if (token != null)
    {
        EventReg eventReg = new EventReg();
        if (token.Type == Newtonsoft.Json.Linq.JTokenType.Object)
        {
            eventReg.RegTime = DateTime.Now;
            foreach (var pair in token as Newtonsoft.Json.Linq.JObject)
            {
                if (pair.Key == "EventID")
                {
                    eventReg.EventId = pair.Value.ToString();
                }
                else if (pair.Key == "UserEmail")
                {
                    eventReg.UserEmail = pair.Value.ToString();
                }
                else
                {
                    eventReg.FormFields.Add(new BsonElement(pair.Key.ToString(), pair.Value.ToString()));
                 }
            }
        }
        //Add Registration:
        eventService.AddEventRegistration(eventReg);
    }
}

Using ExpandoObject removes the need for all of this code. See the final code below. The Web API controller is now 1 line instead of 30 lines of code. This code now can insert and return the JSON from the Question above without issue.

EventController:

[HttpPost]
public void Post([FromBody]EventReg value)
{
    eventService.AddEventRegistration(value);
}

EventReg Class:

public class EventReg
{
    public EventReg()
    {
       FormFields = new ExpandoObject();
    }
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    public string EventRegId { get; set; }

    [BsonElement("event_id")]
    [BsonRepresentation(BsonType.ObjectId)]
    public string EventId { get; set; }

    [BsonElement("user_email")]
    public string UserEmail { get; set; }

    [BsonElement("reg_time")]
    public DateTime RegTime{ get; set; }

    [BsonElement("form_fields")]
    public ExpandoObject FormFields { get; set; }
}

EventService:

public string AddEventRegistration(EventReg eventReg)
{
    this.database.GetCollection<EventReg>("event_regs").InsertOne(eventReg);
    return eventReg.EventRegId;
}
like image 106
Andy Avatar answered Oct 16 '22 22:10

Andy


In ASP.NET Core 3.0+ Newtonsoft.Json is not the default JSON serializer anymore. Therefore I would use JsonElement:

[HttpPost("general")]
public IActionResult Post([FromBody] JsonElement elem)
{        
    var title = elem.GetProperty("title").GetString();
    ...
like image 22
beatoss Avatar answered Oct 16 '22 21:10

beatoss