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();