Skip to content

Commit

Permalink
Remove Newtonsoft.Json (#468)
Browse files Browse the repository at this point in the history
In the project, we have transitioned from using Newtonsoft.Json to System.Text.Json for handling JSON serialization and deserialization. This change aligns with the latest .NET practices and takes advantage of the performance improvements offered by System.Text.Json.

However, there is a specific exception to this transition: when dealing with HttpPatch requests. For these cases, we continue to use Microsoft.AspNetCore.Mvc.NewtonsoftJson. This decision is based on Microsoft's documentation, which recommends the use of Newtonsoft.Json for patching operations due to compatibility and functionality reasons. According to the Microsoft ASP.NET Core documentation (https://learn.microsoft.com/en-us/aspnet/core/web-api/jsonpatch?view=aspnetcore-8.0), System.Text.Json currently lacks certain features needed for full support of JSON Patch operations, making Newtonsoft.Json a necessary dependency for these specific scenarios.

By following this approach, we ensure that our application remains efficient and up-to-date with the core serialization library, while also maintaining full functionality where System.Text.Json's capabilities are currently limited.
  • Loading branch information
KrzysztofPajak authored Feb 16, 2024
1 parent dbdf1ab commit aced5fc
Show file tree
Hide file tree
Showing 27 changed files with 119 additions and 105 deletions.
3 changes: 2 additions & 1 deletion src/API/Grand.Api/ApiExplorer/ApiResponseTypeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ private ICollection<ApiResponseType> GetApiResponseTypes(
Type defaultErrorType)
{
var contentTypes = new MediaTypeCollection();
var responseTypeMetadataProviders = _mvcOptions.OutputFormatters.OfType<NewtonsoftJsonOutputFormatter>();
var responseTypeMetadataProviders = _mvcOptions.OutputFormatters
.OfType<SystemTextJsonOutputFormatter>();

var responseTypes = ReadResponseMetadata(
responseMetadataAttributes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ private IReadOnlyList<ApiRequestFormat> GetSupportedFormats(MediaTypeCollection
var results = new List<ApiRequestFormat>();
foreach (var contentType in contentTypes)
{
foreach (var formatter in _mvcOptions.InputFormatters.OfType<NewtonsoftJsonInputFormatter>())
foreach (var formatter in _mvcOptions.InputFormatters.OfType<SystemTextJsonInputFormatter>())
{
if (formatter is IApiRequestFormatMetadataProvider requestFormatMetadataProvider)
{
Expand Down
7 changes: 5 additions & 2 deletions src/API/Grand.Api/Controllers/OData/BrandController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,16 @@ public async Task<IActionResult> Put([FromBody] BrandDto model)
}

[SwaggerOperation(summary: "Partially update entity in Brand", OperationId = "PartiallyUpdateBrand")]
[HttpPatch]
[HttpPatch("{key}")]
[ProducesResponseType((int)HttpStatusCode.Forbidden)]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> Patch([FromODataUri] string key, [FromBody] JsonPatchDocument<BrandDto> model)
public async Task<IActionResult> Patch([FromRoute] string key, [FromBody] JsonPatchDocument<BrandDto> model)
{
if (string.IsNullOrEmpty(key))
return BadRequest("Key is null or empty");

if (!await _permissionService.Authorize(PermissionSystemName.Brands)) return Forbid();

var brand = await _mediator.Send(new GetGenericQuery<BrandDto, Domain.Catalog.Brand>(key));
Expand Down
7 changes: 5 additions & 2 deletions src/API/Grand.Api/Controllers/OData/CategoryController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,16 @@ public async Task<IActionResult> Put([FromBody] CategoryDto model)
return Ok(model);
}
[SwaggerOperation(summary: "Update entity in Category (delta)", OperationId = "UpdateCategoryPatch")]
[HttpPatch]
[HttpPatch("{key}")]
[ProducesResponseType((int)HttpStatusCode.Forbidden)]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> Patch([FromODataUri] string key, [FromBody] JsonPatchDocument<CategoryDto> model)
public async Task<IActionResult> Patch([FromRoute] string key, [FromBody] JsonPatchDocument<CategoryDto> model)
{
if (string.IsNullOrEmpty(key))
return BadRequest("Key is null or empty");

if (!await _permissionService.Authorize(PermissionSystemName.Categories)) return Forbid();

var category = await _mediator.Send(new GetGenericQuery<CategoryDto, Domain.Catalog.Category>(key));
Expand Down
7 changes: 5 additions & 2 deletions src/API/Grand.Api/Controllers/OData/CollectionController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,16 @@ public async Task<IActionResult> Put([FromBody] CollectionDto model)
}

[SwaggerOperation(summary: "Partially update entity in Collection", OperationId = "PartiallyUpdateCollection")]
[HttpPatch]
[HttpPatch("{key}")]
[ProducesResponseType((int)HttpStatusCode.Forbidden)]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> Patch([FromODataUri] string key, [FromBody] JsonPatchDocument<CollectionDto> model)
public async Task<IActionResult> Patch([FromRoute] string key, [FromBody] JsonPatchDocument<CollectionDto> model)
{
if (string.IsNullOrEmpty(key))
return BadRequest("Key is null or empty");

if (!await _permissionService.Authorize(PermissionSystemName.Collections)) return Forbid();

var collection = await _mediator.Send(new GetGenericQuery<CollectionDto, Domain.Catalog.Collection>(key));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,16 @@ public async Task<IActionResult> Put([FromBody] CustomerGroupDto model)
}

[SwaggerOperation(summary: "Partially update entity in CustomerGroup", OperationId = "PartiallyUpdateCustomerGroup")]
[HttpPatch]
[HttpPatch("{key}")]
[ProducesResponseType((int)HttpStatusCode.Forbidden)]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> Patch([FromODataUri] string key, [FromBody] JsonPatchDocument<CustomerGroupDto> model)
public async Task<IActionResult> Patch([FromRoute] string key, [FromBody] JsonPatchDocument<CustomerGroupDto> model)
{
if (string.IsNullOrEmpty(key))
return BadRequest("Key is null or empty");

if (!await _permissionService.Authorize(PermissionSystemName.Customers)) return Forbid();

var customerGroup = await _mediator.Send(new GetGenericQuery<CustomerGroupDto, Domain.Customers.CustomerGroup>(key));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,16 @@ public async Task<IActionResult> Put([FromBody] ProductAttributeDto model)
}

[SwaggerOperation(summary: "Partially update entity in ProductAttribute", OperationId = "PartiallyUpdateProductAttribute")]
[HttpPatch]
[HttpPatch("{key}")]
[ProducesResponseType((int)HttpStatusCode.Forbidden)]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> Patch([FromODataUri] string key, [FromBody] JsonPatchDocument<ProductAttributeDto> model)
public async Task<IActionResult> Patch([FromRoute] string key, [FromBody] JsonPatchDocument<ProductAttributeDto> model)
{
if (string.IsNullOrEmpty(key))
return BadRequest("Key is null or empty");

if (!await _permissionService.Authorize(PermissionSystemName.ProductAttributes)) return Forbid();

var productAttribute = await _mediator.Send(new GetGenericQuery<ProductAttributeDto, Domain.Catalog.ProductAttribute>(key));
Expand Down
9 changes: 6 additions & 3 deletions src/API/Grand.Api/Controllers/OData/ProductController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,20 +80,23 @@ public async Task<IActionResult> Put([FromBody] ProductDto model)
}

[SwaggerOperation(summary: "Partially update entity in Product", OperationId = "PartiallyUpdateProduct")]
[HttpPatch]
[HttpPatch("{key}")]
[ProducesResponseType((int)HttpStatusCode.Forbidden)]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> Patch([FromODataUri] string key, [FromBody] JsonPatchDocument<ProductDto> model)
public async Task<IActionResult> Patch([FromRoute] string key, [FromBody] JsonPatchDocument<ProductDto> model)
{
if (string.IsNullOrEmpty(key))
return BadRequest("Key is null or empty");

if (!await _permissionService.Authorize(PermissionSystemName.Products)) return Forbid();

var product = await _mediator.Send(new GetGenericQuery<ProductDto, Domain.Catalog.Product>(key));
if (!product.Any()) return NotFound();

var pr = product.FirstOrDefault();
model.ApplyTo(pr!, ModelState);
model.ApplyTo(pr);
await _mediator.Send(new UpdateProductCommand { Model = pr });
return Ok();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,16 @@ public async Task<IActionResult> Put([FromBody] SpecificationAttributeDto model)
}

[SwaggerOperation(summary: "Partially update entity in SpecificationAttribute", OperationId = "PartiallyUpdateSpecificationAttribute")]
[HttpPatch]
[HttpPatch("{key}")]
[ProducesResponseType((int)HttpStatusCode.Forbidden)]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.NotFound)]
public async Task<IActionResult> Patch([FromODataUri] string key, [FromBody] JsonPatchDocument<SpecificationAttributeDto> model)
public async Task<IActionResult> Patch([FromRoute] string key, [FromBody] JsonPatchDocument<SpecificationAttributeDto> model)
{
if (string.IsNullOrEmpty(key))
return BadRequest("Key is null or empty");

if (!await _permissionService.Authorize(PermissionSystemName.SpecificationAttributes)) return Forbid();

var specification = await _mediator.Send(new GetGenericQuery<SpecificationAttributeDto, Domain.Catalog.SpecificationAttribute>(key));
Expand Down
2 changes: 2 additions & 0 deletions src/API/Grand.Api/Grand.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<ProjectReference Include="..\..\Core\Grand.SharedKernel\Grand.SharedKernel.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.JsonPatch" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.OData" Version="8.2.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
Expand Down
25 changes: 24 additions & 1 deletion src/API/Grand.Api/Infrastructure/ODataStartup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.OData;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;

Expand Down Expand Up @@ -47,7 +50,10 @@ public void ConfigureServices(IServiceCollection services,
builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
});
//Add OData
services.AddControllers().AddOData(opt =>
services.AddControllers(options =>
{
options.InputFormatters.Insert(0, GetJsonPatchInputFormatter());
}).AddOData(opt =>
{
opt.EnableQueryFeatures(Configurations.MaxLimit);
opt.AddRouteComponents(Configurations.ODataRoutePrefix, GetEdmModel(apiConfig));
Expand All @@ -58,6 +64,21 @@ public void ConfigureServices(IServiceCollection services,

}
}
private static NewtonsoftJsonPatchInputFormatter GetJsonPatchInputFormatter()
{
var builder = new ServiceCollection()
.AddLogging()
.AddMvc()
.AddNewtonsoftJson()
.Services.BuildServiceProvider();

return builder
.GetRequiredService<IOptions<MvcOptions>>()
.Value
.InputFormatters
.OfType<NewtonsoftJsonPatchInputFormatter>()
.First();
}
public int Priority => 505;
public bool BeforeConfigure => false;

Expand Down Expand Up @@ -143,5 +164,7 @@ private void RegisterRequestHandler(IServiceCollection services)
IQueryable<PictureDto>>), typeof(GetGenericQueryHandler<PictureDto, Domain.Media.Picture>));

}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using Grand.Data;
using Grand.Domain.Seo;
using MediatR;
using Newtonsoft.Json;
using System.Text.Json;

namespace Grand.Business.Catalog.Events.Handlers
{
Expand Down Expand Up @@ -74,7 +74,7 @@ public async Task Handle(EntityDeleted<Product> notification, CancellationToken
}

//insert to deleted products
var productDeleted = JsonConvert.DeserializeObject<ProductDeleted>(JsonConvert.SerializeObject(notification.Entity));
var productDeleted = JsonSerializer.Deserialize<ProductDeleted>(JsonSerializer.Serialize(notification.Entity));
if (productDeleted != null)
{
productDeleted.DeletedOnUtc = DateTime.UtcNow;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
using Grand.Infrastructure.Extensions;
using MediatR;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Net.Http;
using System.Text.Json;

namespace Grand.Business.Marketing.Services.PushNotifications
{
Expand Down Expand Up @@ -178,8 +178,7 @@ public virtual async Task<IPagedList<PushRegistration>> GetPushReceivers(int pag
}
};

var json = JsonConvert.SerializeObject(data);

var json = JsonSerializer.Serialize(data);
try
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, FcmUrl);
Expand All @@ -196,7 +195,7 @@ public virtual async Task<IPagedList<PushRegistration>> GetPushReceivers(int pag
return (false, responseString);
}

var responseMessage = JsonConvert.DeserializeObject<JsonResponse>(responseString);
var responseMessage = JsonSerializer.Deserialize<JsonResponse>(responseString);
if (responseMessage == null) return (false, "PushNotifications.ResponseMessage.Empty");

await InsertPushMessage(new PushMessage {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using Newtonsoft.Json;
using StackExchange.Redis;
using StackExchange.Redis;
using Microsoft.Extensions.DependencyInjection;
using Grand.Infrastructure.Caching.Message;
using Grand.Infrastructure.Configuration;
using System.Diagnostics;
using System.Text.Json;

namespace Grand.Infrastructure.Caching.Redis
{
Expand Down Expand Up @@ -32,7 +32,7 @@ public async Task PublishAsync<TMessage>(TMessage msg) where TMessage : IMessage
Key = msg.Key,
MessageType = msg.MessageType
};
var message = JsonConvert.SerializeObject(client);
var message = JsonSerializer.Serialize(client);
await _subscriber.PublishAsync(RedisChannel.Literal(_redisConfig.RedisPubSubChannel), message);
}
catch(Exception ex)
Expand All @@ -47,7 +47,7 @@ public Task SubscribeAsync()
{
try
{
var message = JsonConvert.DeserializeObject<MessageEventClient>(redisValue);
var message = JsonSerializer.Deserialize<MessageEventClient>(redisValue);
if (message != null && message.ClientId != ClientId)
OnSubscriptionChanged(message);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Grand.Domain.Stores;
using Grand.Infrastructure.Plugins;
using Grand.SharedKernel.Extensions;
using System.Text.Json;

namespace Grand.Infrastructure.Extensions
{
Expand Down Expand Up @@ -42,5 +43,13 @@ public static bool IsAuthenticateGroup(this IProvider method, Customer customer)

return method.LimitedToGroups.ContainsAny(customer.Groups.Select(x => x));
}

public static class JsonSerializerOptionsProvider
{
public static JsonSerializerOptions Options { get; } = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
}
}
}
1 change: 0 additions & 1 deletion src/Core/Grand.Infrastructure/Grand.Infrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
<PackageReference Include="MassTransit.RabbitMQ" Version="8.1.3" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.22.0" />
<PackageReference Include="Microsoft.Azure.AppConfiguration.AspNetCore" Version="7.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
using Grand.Business.Core.Interfaces.Cms;
using Grand.Infrastructure;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System.Globalization;
using System.Text.Json;
using Widgets.FacebookPixel.Models;

namespace Widgets.FacebookPixel.Components
Expand Down Expand Up @@ -46,7 +46,7 @@ public async Task<IViewComponentResult> InvokeAsync(string widgetZone, object ad
//add to cart
if (widgetZone == FacebookPixelDefaults.AddToCart)
{
var model = JsonConvert.DeserializeObject<FacebookAddToCartModelModel>(JsonConvert.SerializeObject(additionalData));
var model = JsonSerializer.Deserialize<FacebookAddToCartModelModel>(JsonSerializer.Serialize(additionalData));
if (model != null)
{
return View("Default", GetAddToCartScript(model));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Moq.Protected;
using Newtonsoft.Json;
using System.Net;
using System.Net.Http;
using System.Text.Json;

namespace Grand.Business.Marketing.Tests.Services.PushNotifications
{
Expand All @@ -34,7 +34,7 @@ public void Init()

var mockMessageHandler = new Mock<HttpMessageHandler>();

var output = JsonConvert.SerializeObject(new JsonResponse { success = 1, failure = 0, canonical_ids = 1 });
var output = JsonSerializer.Serialize(new JsonResponse { success = 1, failure = 0, canonical_ids = 1 });

mockMessageHandler.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
Expand Down
Loading

0 comments on commit aced5fc

Please sign in to comment.