-1

I'm hoping someone can shed some light on this for me. Normally with all my controller actions, deserialization from JSON to C# objects works just fine, but currently I'm facing an issue where it isn't. Instead of giving the desired result, it gives me a QBWebhookEventList object, but the EventNotifications List is null.

In the trial and error of trying to fix this, I found that if I change the parameter of my controller action from QBWebhookEventList eventList to [FromBody]JsonElement eventList then explicitly call the conversion, it works as expected. I'm just trying to figure out what is going on here. I would prefer to not have to do the JsonElement version because it doesn't match any of my other controller actions.

The JSON in question:

{
    "eventNotifications": [
        {
            "realmId": "1185883450",
            "dataChangeEvent": {
                "entities": [
                    {
                        "name": "Customer",
                        "id": "1",
                        "operation": "Create",
                        "lastUpdated": "2015-10-05T14:42:19-0700"
                    },
                    {
                        "name": "Vendor",
                        "id": "1",
                        "operation": "Create",
                        "lastUpdated": "2015-10-05T14:42:19-0700"
                    }
                ]
            }
        }
    ]
}

The C# classes the JSON gets deserialized into:

 public class DataChangeEvent
    {
        [JsonProperty("entities", NullValueHandling = NullValueHandling.Ignore)]
        public List<Entity> Entities { get; set; }
    }

    public class Entity
    {
        [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
        public string Name { get; set; }

        [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)]
        public string Id { get; set; }

        [JsonProperty("operation", NullValueHandling = NullValueHandling.Ignore)]
        public string Operation { get; set; }

        [JsonProperty("lastUpdated", NullValueHandling = NullValueHandling.Ignore)]
        public DateTime LastUpdated { get; set; }
    }

    public class EventNotification
    {
        [JsonProperty("realmId", NullValueHandling = NullValueHandling.Ignore)]
        public string RealmId { get; set; }

        [JsonProperty("dataChangeEvent", NullValueHandling = NullValueHandling.Ignore)]
        public DataChangeEvent DataChangeEvent { get; set; }
    }

    public class QBWebhookEventList
    {
        [JsonProperty("eventNotifications", NullValueHandling = NullValueHandling.Ignore)]
        public List<EventNotification> EventNotifications { get; set; }
    }

My Controller:

 public class QBWebhookController : Controller
    {
        private readonly ILogger<QBWebhookController> _logger;
        public QBWebhookController(ILogger<QBWebhookController> logger)
        {
            _logger = logger;
        }
        public IActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public IActionResult Invoice(QBWebhookEventList eventList)
        {
             _logger.LogInformation(eventList.EventNotifications.Count.ToString() + "\nWe Did it!");
//            var temp = JsonConvert.DeserializeObject<QBWebhookEventList>(eventList.ToString()); This way works if I change the action parameter to [FromBody]JsonElement eventList.

            return Ok();
        }
    }

Thanks in advance for any help with this.

EDIT 1

Here is my Program.cs file. I did add the option to globally ignore null properties as a commenter suggested.

EDIT 2 Added .AddNewtonsoftJson() to program.cs.

using BusinessLogicLayer.Auxillary;
using BusinessLogicLayer.Auxillary.Interfaces;
using BusinessLogicLayer.Services;
using BusinessLogicLayer.Services.Clover.Interfaces;
using BusinessLogicLayer.Services.Interfaces;
using BusinessLogicLayer.Services.Magento;
using BusinessLogicLayer.Services.Magento.Interfaces;
using BusinessLogicLayer.Services.Quickbooks.Interfaces;
using BusinessLogicLayer.Services.SyncServices;
using BusinessLogicLayer.Services.SyncServices.Interfaces;
using DataAccessModule;
using DataAccessModule.Models;
using DataAccessModule.Models.Magento;
using DataAccessModule.Models.Quickbooks;
using DataAccessModule.Repositories;
using DataAccessModule.Repositories.Magento;
using DataAccessModule.Repositories.Quickbooks;
using Flurl.Http;
using Flurl.Http.Configuration;
using InventoryManagement.Hubs;
using InventoryManagement.Models.AutoMapperProfiles;
using InventoryManagment.Data;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
using Newtonsoft.Json;
using System.Text;
using System.Text.Json.Serialization;
using tusdotnet;
using tusdotnet.Helpers;
using tusdotnet.Interfaces;

FlurlHttp.Configure(options =>
{
    var jsonSettings = new JsonSerializerSettings
    {
        NullValueHandling = NullValueHandling.Ignore
    };
    options.JsonSerializer = new NewtonsoftJsonSerializer(jsonSettings);
});

var builder = WebApplication.CreateBuilder(args);


var connectionString = builder.Configuration.GetConnectionString("InventoryManagmentContextConnection");

builder.Services.AddDbContext<InventoryManagmentContext>(options =>
    options.UseSqlServer(connectionString));

builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("MainDbConnection"), o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
    //options.EnableSensitiveDataLogging();
   
});


builder.Services.Configure<CookiePolicyOptions>(options =>
{
    // This lambda determines whether user consent for non-essential 
    // cookies is needed for a given request.
    options.CheckConsentNeeded = context => true;
    // requires using Microsoft.AspNetCore.Http;
    options.MinimumSameSitePolicy = SameSiteMode.None;
});

builder.Services.AddCors();
builder.Services.AddMemoryCache();


builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true).AddEntityFrameworkStores<InventoryManagmentContext>();

// Add services to the container.
builder.Services.AddControllersWithViews().AddNewtonsoftJson(options =>
{
    options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
});


// Setup Requirements for Azure AD (User Access Management)
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;

}).AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));


builder.Services.AddMvc().AddMicrosoftIdentityUI();

//IdentityModelEventSource.ShowPII = true;

builder.Services.AddSignalR();

builder.Services.AddAutoMapper(types =>
{
    types.AddProfile<ProductProfile>();
    types.AddProfile<CategoryProfile>();
    types.AddProfile<SyncTaskProfile>();
    types.AddProfile<CompanyProfile>();

});

builder.Services.AddScoped<IRepository<Product>, ProductRepository>();
builder.Services.AddScoped<IProductService, ProductService>();

builder.Services.AddScoped<IRepository<Category>, CategoryRepository>();
builder.Services.AddScoped<ICategoryService, CategoryService>();

builder.Services.AddScoped<IRepository<SyncTask>, SyncTaskRepository>();
builder.Services.AddScoped<ISyncTaskService, SyncTaskService>();

builder.Services.AddScoped<IRepository<Company>, CompanyRepository>();
builder.Services.AddScoped<IRepository<QBRefreshToken>, QBRefreshTokenRepository>();

builder.Services.AddScoped<IQBRepository<QBProduct>, QBProductRepository>();
builder.Services.AddScoped<IQBRepository<QBCategory>, QBCategoryRepository>();

builder.Services.AddScoped<IMagentoRepository<MagentoProduct>, MagentoProductRepository>();
builder.Services.AddScoped<IMagentoRepository<MagentoCategory>, MagentoCategoryRepository>();

builder.Services.AddScoped<ISyncTaskCreator, SyncTaskCreator>();

builder.Services.AddScoped<ICompanyInitService, CompanyInitService>();


builder.Services.AddScoped<QBCompanyService>();
builder.Services.AddScoped<IQBProductService, QBProductService>();
builder.Services.AddScoped<IQBCategoryService, QBCategoryService>();
builder.Services.AddScoped<ICategoryDeleteParentAdjustor, CategoryDeleteParentAdjustor>();
builder.Services.AddScoped<ICompanyDeleteCascader, CompanyDeleteCascader>();
builder.Services.AddScoped<ICompanyService, CompanyService>();

builder.Services.AddScoped<ICloverProductService, CloverProductService>();
builder.Services.AddScoped<ICloverCategoryService, CloverCategoryService>();

builder.Services.AddScoped<IMagentoProductService, MagentoProductService>();
builder.Services.AddScoped<IMagentoCategoryService, MagentoCategoryService>();

builder.Services.AddScoped<IExcelFileProcessor, ExcelFileProcessor>();

builder.Services.AddScoped<ICloverSyncTaskExecutor, CloverSyncTaskExecutor>();
builder.Services.AddScoped<IQBSyncTaskExecutor, QBSyncTaskExecutor>();
builder.Services.AddScoped<IMagentoSyncTaskExecutor, MagentoSyncTaskExecutor>();

builder.Services.AddScoped<IRestockService, RestockService>();

builder.Services.AddScoped<IRepository<ProductImage>, ProductImageRepository>();
builder.Services.AddScoped<IImageService, ImageService>();

builder.Services.AddScoped<IMagentoProductImageService, MagentoProductImageService>();

builder.Services.AddScoped<IRepository<BackingItem>, BackingItemRepository>();
builder.Services.AddScoped<IBackingItemService, BackingItemService>();


builder.Services.AddScoped<ICompanyPruningService, QBCompanyPruningService>();


var app = builder.Build();


app.Use((context, next) =>
{
    context.Features.Get<IHttpMaxRequestBodySizeFeature>().MaxRequestBodySize = null;
    return next.Invoke();
});

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();

}
else
{

    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseCookiePolicy();

app.UseCors(options =>
{
    options.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().WithExposedHeaders(CorsHelper.GetExposedHeaders());
});

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();



app.MapTus("/files", async httpContext => new()
{


    // This method is called on each request so different configurations can be returned per user, domain, path etc.
    // Return null to disable tusdotnet for the current request.

    // Where to store data?
    Store = new tusdotnet.Stores.TusDiskStore(builder.Configuration.GetValue<string>("UploadedFilePath")),
    Events = new()
    {
        // What to do when file is completely uploaded?
        OnFileCompleteAsync = async eventContext =>
        {
            ITusFile file = await eventContext.GetFileAsync();
            Dictionary<string, tusdotnet.Models.Metadata> metadata = await file.GetMetadataAsync(eventContext.CancellationToken);
            Stream content = await file.GetContentAsync(eventContext.CancellationToken);

            using(var fileStream = new FileStream($"{builder.Configuration.GetValue<string>("UploadedFilePath")}\\{metadata["filename"].GetString(Encoding.UTF8)}",FileMode.Create, FileAccess.ReadWrite))
            {
                await content.CopyToAsync(fileStream);
            }

            await content.DisposeAsync();
            var terminationStore = (ITusTerminationStore)eventContext.Store;
            await terminationStore.DeleteFileAsync(file.Id, eventContext.CancellationToken);
        }
    }
});

app.MapRazorPages();
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");



app.MapHub<ProgressHub>("/progressHub");


app.Run();
  • `JsonElement` is from System.Text.Json but `[JsonProperty]` is from Json.NET. If your controller is using System.Text.Json then Json.NET attributes won't work. Either replace the Json.NET attributes with equivalent System.Text.Json attributes or revert back to Json.NET as shown in [this answer](https://stackoverflow.com/a/55666898/3744182) to [Where did IMvcBuilder AddJsonOptions go in .Net Core 3.0?](https://stackoverflow.com/q/55666826/3744182). – dbc Feb 28 '23 at 18:55
  • I get what you're saying, but if that were the issue, then why would JSON Deserialization work in every other controller when using the `[JsonProperty]`? Additionally, the only reason I'm even using the `JsonElement` in this case is because it wasn't fully deserializing properly before, in this controller only. – Mister_Kweh Feb 28 '23 at 19:02
  • We would need to see your `ConfigureServices()` to answer that. – dbc Feb 28 '23 at 19:08
  • @dbc I added it to the post. – Mister_Kweh Feb 28 '23 at 19:58
  • what is `FlurlHttp`? Is it [FlurlHttp.cs](https://github.com/tmenier/Flurl/blob/dev/src/Flurl.Http/FlurlHttp.cs) from https://flurl.dev/? – dbc Feb 28 '23 at 20:03
  • Yes it is. I use it in my data access layer to interact with REST APIs – Mister_Kweh Feb 28 '23 at 20:06
  • I don't see a call to `AddNewtonsoftJson()` like the one shown in the [docs](https://learn.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting?view=aspnetcore-7.0#add-newtonsoftjson-based-json-format-support-2). Instead you're calling `.AddJsonOptions()` which configures System.Text.Json not Json.NET, so you must be using System.Text.Json. – dbc Feb 28 '23 at 20:11
  • @dbc I changed it and added the stated method, but the issue remains exactly the same. – Mister_Kweh Feb 28 '23 at 20:39
  • Then I'm sorry but I don't know what the problem might be. – dbc Feb 28 '23 at 21:02
  • 1
    @Mister_Kweh You should select what do you want Text.Json or Newtonsoft.Json before you posted the question. If you jump from one to another your code will never be working, – Serge Feb 28 '23 at 21:40

1 Answers1

0

Controller action depends on how do you request, but if [FromBody] works for JsonElement, it will work for the custom class too

  public IActionResult Invoice([FromBody]QBWebhookEventList eventList)

and I can assume that your controller uses Text.Json if it can deserialize JsonElement, so ALL your properties of ALL your classes should be like this - replace JsonProperty with JsonPropertyName

 public class QBWebhookEventList
    {
        [JsonPropertyName("eventNotifications")]
        public List<EventNotification> EventNotifications { get; set; }
    }

also NullValueHandling = NullValueHandling.Ignore is obsolete, so you can add it in your startup like this

 services.AddControllers().AddJsonOptions(option =>
           {
               option.JsonSerializerOptions.DefaultIgnoreCondition = 
               JsonIgnoreCondition.WhenWritingNull;
           });
Serge
  • 40,935
  • 4
  • 18
  • 45