diff --git a/samples/PartialResponse.AspNetCore.Mvc.Formatters.Json.Samples/Controllers/ExecutorController.cs b/samples/PartialResponse.AspNetCore.Mvc.Formatters.Json.Samples/Controllers/ExecutorController.cs index 1c6f5c3..f7c9122 100644 --- a/samples/PartialResponse.AspNetCore.Mvc.Formatters.Json.Samples/Controllers/ExecutorController.cs +++ b/samples/PartialResponse.AspNetCore.Mvc.Formatters.Json.Samples/Controllers/ExecutorController.cs @@ -9,7 +9,7 @@ public class ExecutorController : Controller { public IActionResult Index() { - var response = new List() + var items = new List() { new { @@ -40,6 +40,12 @@ public IActionResult Index() } }; + var response = new + { + Items = items, + TotalCount = items.Count + }; + return this.PartialJson(response); } } diff --git a/samples/PartialResponse.AspNetCore.Mvc.Formatters.Json.Samples/Controllers/FormatterController.cs b/samples/PartialResponse.AspNetCore.Mvc.Formatters.Json.Samples/Controllers/FormatterController.cs index e410ea0..4d3d2cb 100644 --- a/samples/PartialResponse.AspNetCore.Mvc.Formatters.Json.Samples/Controllers/FormatterController.cs +++ b/samples/PartialResponse.AspNetCore.Mvc.Formatters.Json.Samples/Controllers/FormatterController.cs @@ -7,9 +7,9 @@ namespace PartialResponse.AspNetCore.Mvc.Formatters.Json.Samples.Controllers { public class FormatterController : Controller { - public List Index() + public dynamic Index() { - return new List() + var items = new List() { new { @@ -39,6 +39,12 @@ public List Index() } } }; + + return new + { + Items = items, + TotalCount = items.Count + }; } } } diff --git a/samples/PartialResponse.AspNetCore.Mvc.Formatters.Json.Samples/Startup.cs b/samples/PartialResponse.AspNetCore.Mvc.Formatters.Json.Samples/Startup.cs index 280546d..790f013 100644 --- a/samples/PartialResponse.AspNetCore.Mvc.Formatters.Json.Samples/Startup.cs +++ b/samples/PartialResponse.AspNetCore.Mvc.Formatters.Json.Samples/Startup.cs @@ -20,7 +20,11 @@ public Startup(IConfiguration configuration) public void ConfigureServices(IServiceCollection services) { - services.Configure(options => options.IgnoreCase = true); + services.Configure(options => + { + options.IgnoreCase = true; + options.IgnoredFields = new[] { "totalCount" }; + }); services .AddMvc(options => options.OutputFormatters.RemoveType()) diff --git a/src/PartialResponse.AspNetCore.Mvc.Formatters.Json/Internal/PartialJsonResultExecutor.cs b/src/PartialResponse.AspNetCore.Mvc.Formatters.Json/Internal/PartialJsonResultExecutor.cs index 1522a1f..8cce36a 100644 --- a/src/PartialResponse.AspNetCore.Mvc.Formatters.Json/Internal/PartialJsonResultExecutor.cs +++ b/src/PartialResponse.AspNetCore.Mvc.Formatters.Json/Internal/PartialJsonResultExecutor.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using Newtonsoft.Json; +using PartialResponse.Core; namespace PartialResponse.AspNetCore.Mvc.Formatters.Json.Internal { @@ -138,7 +139,7 @@ public Task ExecuteAsync(ActionContext context, PartialJsonResult result) if (fieldsParserResult.IsFieldsSet && !fieldsParserResult.HasError) { - jsonSerializer.Serialize(jsonWriter, result.Value, path => fieldsParserResult.Fields.Matches(path, this.Options.IgnoreCase)); + jsonSerializer.Serialize(jsonWriter, result.Value, path => this.ShouldSerialize(path, fieldsParserResult.Fields, this.Options)); } else { @@ -149,5 +150,20 @@ public Task ExecuteAsync(ActionContext context, PartialJsonResult result) return Task.CompletedTask; } + + private bool ShouldSerialize(string path, Fields fields, MvcPartialJsonOptions options) + { + var parsedIgnoredFields = options.ParsedIgnoredFields; + + if (parsedIgnoredFields.HasValue) + { + if (parsedIgnoredFields.Value.Matches(path, options.IgnoreCase)) + { + return true; + } + } + + return fields.Matches(path, options.IgnoreCase); + } } } diff --git a/src/PartialResponse.AspNetCore.Mvc.Formatters.Json/MvcPartialJsonOptions.cs b/src/PartialResponse.AspNetCore.Mvc.Formatters.Json/MvcPartialJsonOptions.cs index 8b990f6..b0ded56 100644 --- a/src/PartialResponse.AspNetCore.Mvc.Formatters.Json/MvcPartialJsonOptions.cs +++ b/src/PartialResponse.AspNetCore.Mvc.Formatters.Json/MvcPartialJsonOptions.cs @@ -1,7 +1,10 @@ // Copyright (c) Arjen Post and contributors. See LICENSE and NOTICE in the project root for license information. +using System; +using System.Collections.Generic; using Newtonsoft.Json; using PartialResponse.AspNetCore.Mvc.Formatters; +using PartialResponse.Core; namespace PartialResponse.AspNetCore.Mvc { @@ -10,6 +13,8 @@ namespace PartialResponse.AspNetCore.Mvc /// public class MvcPartialJsonOptions { + private IEnumerable ignoredFields; + /// /// Gets or sets a value indicating whether partial response allows case-insensitive matching. /// @@ -20,9 +25,44 @@ public class MvcPartialJsonOptions /// public bool IgnoreParseErrors { get; set; } = false; + /// + /// Gets or sets a list of fields which should always be serialized. + /// + public IEnumerable IgnoredFields + { + get + { + return this.ignoredFields; + } + + set + { + this.ignoredFields = value; + + this.ParsedIgnoredFields = this.ParseIgnoredFields(value); + } + } + /// /// Gets the that are used by this application. /// public JsonSerializerSettings SerializerSettings { get; } = JsonSerializerSettingsProvider.CreateSerializerSettings(); + + internal Fields? ParsedIgnoredFields { get; private set; } + + private Fields? ParseIgnoredFields(IEnumerable value) + { + if (value != null) + { + if (!Fields.TryParse(string.Join(",", value), out var fields)) + { + throw new ArgumentException($"Unable to parse {nameof(MvcPartialJsonOptions.IgnoredFields)} property.", nameof(value)); + } + + return fields; + } + + return null; + } } } \ No newline at end of file diff --git a/src/PartialResponse.AspNetCore.Mvc.Formatters.Json/PartialJsonOutputFormatter.cs b/src/PartialResponse.AspNetCore.Mvc.Formatters.Json/PartialJsonOutputFormatter.cs index 8ea6955..ed82777 100644 --- a/src/PartialResponse.AspNetCore.Mvc.Formatters.Json/PartialJsonOutputFormatter.cs +++ b/src/PartialResponse.AspNetCore.Mvc.Formatters.Json/PartialJsonOutputFormatter.cs @@ -10,6 +10,7 @@ using Newtonsoft.Json; using PartialResponse.AspNetCore.Mvc.Formatters.Json; using PartialResponse.AspNetCore.Mvc.Formatters.Json.Internal; +using PartialResponse.Core; namespace PartialResponse.AspNetCore.Mvc.Formatters { @@ -176,7 +177,7 @@ private void WriteObject(TextWriter writer, object value, FieldsParserResult fie if (fieldsParserResult.IsFieldsSet && !fieldsParserResult.HasError) { - jsonSerializer.Serialize(jsonWriter, value, path => fieldsParserResult.Fields.Matches(path, this.options.IgnoreCase)); + jsonSerializer.Serialize(jsonWriter, value, path => this.ShouldSerialize(path, fieldsParserResult.Fields, this.options)); } else { @@ -184,5 +185,20 @@ private void WriteObject(TextWriter writer, object value, FieldsParserResult fie } } } + + private bool ShouldSerialize(string path, Fields fields, MvcPartialJsonOptions options) + { + var parsedIgnoredFields = options.ParsedIgnoredFields; + + if (parsedIgnoredFields.HasValue) + { + if (parsedIgnoredFields.Value.Matches(path, options.IgnoreCase)) + { + return true; + } + } + + return fields.Matches(path, options.IgnoreCase); + } } } diff --git a/test/PartialResponse.AspNetCore.Mvc.Formatters.Json.Tests/MvcPartialJsonOptionsTests.cs b/test/PartialResponse.AspNetCore.Mvc.Formatters.Json.Tests/MvcPartialJsonOptionsTests.cs new file mode 100644 index 0000000..2940925 --- /dev/null +++ b/test/PartialResponse.AspNetCore.Mvc.Formatters.Json.Tests/MvcPartialJsonOptionsTests.cs @@ -0,0 +1,34 @@ +// Copyright (c) Arjen Post and contributors. See LICENSE and NOTICE in the project root for license information. + +using System; +using Xunit; + +namespace PartialResponse.AspNetCore.Mvc.Formatters.Json.Tests +{ + public class MvcPartialJsonOptionsTests + { + [Fact] + public void TheIgnoredFieldsPropertyShouldThrowExceptionIfInvalid() + { + // Arrange + var options = new MvcPartialJsonOptions(); + + // Assert + Assert.Throws("value", () => options.IgnoredFields = new[] { "/foo" }); + } + + [Fact] + public void TheIgnoredFieldsPropertyShouldParseIgnoredFields() + { + // Arrange + var options = new MvcPartialJsonOptions(); + var value = new[] { "foo" }; + + // Act + options.IgnoredFields = value; + + // Assert + Assert.Same(value, options.IgnoredFields); + } + } +} diff --git a/test/PartialResponse.AspNetCore.Mvc.Formatters.Json.Tests/PartialJsonOutputFormatterTests.cs b/test/PartialResponse.AspNetCore.Mvc.Formatters.Json.Tests/PartialJsonOutputFormatterTests.cs index 1576b8f..764f0f8 100644 --- a/test/PartialResponse.AspNetCore.Mvc.Formatters.Json.Tests/PartialJsonOutputFormatterTests.cs +++ b/test/PartialResponse.AspNetCore.Mvc.Formatters.Json.Tests/PartialJsonOutputFormatterTests.cs @@ -200,6 +200,83 @@ public async Task TheWriteResponseBodyAsyncMethodShouldNotIgnoreCase() Assert.Equal("{}", this.body.ToString()); } + [Fact] + public async Task TheWriteResponseBodyAsyncMethodShouldAlwaysSerializeIgnoredFields() + { + // Arrange + Mock.Get(this.httpResponse) + .SetupGet(httpResponse => httpResponse.StatusCode) + .Returns(200); + + Fields.TryParse("foo", out var fields); + + Mock.Get(this.fieldsParser) + .Setup(fieldsParser => fieldsParser.Parse(this.httpRequest)) + .Returns(FieldsParserResult.Success(fields)); + + this.partialJsonOptions.IgnoredFields = new[] { "bar" }; + + var writeContext = this.CreateWriteContext(new { bar = "baz" }); + + // Act + await this.formatter.WriteResponseBodyAsync(writeContext, Encoding.UTF8); + + // Assert + Assert.Equal("{\"bar\":\"baz\"}", this.body.ToString()); + } + + [Fact] + public async Task TheWriteResponseBodyAsyncMethodShouldAlwaysSerializeIgnoredFieldsIgnoringCase() + { + // Arrange + Mock.Get(this.httpResponse) + .SetupGet(httpResponse => httpResponse.StatusCode) + .Returns(200); + + Fields.TryParse("foo", out var fields); + + Mock.Get(this.fieldsParser) + .Setup(fieldsParser => fieldsParser.Parse(this.httpRequest)) + .Returns(FieldsParserResult.Success(fields)); + + this.partialJsonOptions.IgnoreCase = true; + this.partialJsonOptions.IgnoredFields = new[] { "BAR" }; + + var writeContext = this.CreateWriteContext(new { bar = "baz" }); + + // Act + await this.formatter.WriteResponseBodyAsync(writeContext, Encoding.UTF8); + + // Assert + Assert.Equal("{\"bar\":\"baz\"}", this.body.ToString()); + } + + [Fact] + public async Task TheWriteResponseBodyAsyncMethodShouldAlwaysSerializeIgnoredFieldsNotIgnoringCase() + { + // Arrange + Mock.Get(this.httpResponse) + .SetupGet(httpResponse => httpResponse.StatusCode) + .Returns(200); + + Fields.TryParse("foo", out var fields); + + Mock.Get(this.fieldsParser) + .Setup(fieldsParser => fieldsParser.Parse(this.httpRequest)) + .Returns(FieldsParserResult.Success(fields)); + + this.partialJsonOptions.IgnoreCase = false; + this.partialJsonOptions.IgnoredFields = new[] { "BAR" }; + + var writeContext = this.CreateWriteContext(new { bar = "baz" }); + + // Act + await this.formatter.WriteResponseBodyAsync(writeContext, Encoding.UTF8); + + // Assert + Assert.Equal("{}", this.body.ToString()); + } + [Fact] public async Task TheWriteResponseBodyAsyncMethodShouldBypassPartialResponseIfConfigured() { diff --git a/test/PartialResponse.AspNetCore.Mvc.Formatters.Json.Tests/PartialJsonResultExecutorTests.cs b/test/PartialResponse.AspNetCore.Mvc.Formatters.Json.Tests/PartialJsonResultExecutorTests.cs index 7530377..cfdf61e 100644 --- a/test/PartialResponse.AspNetCore.Mvc.Formatters.Json.Tests/PartialJsonResultExecutorTests.cs +++ b/test/PartialResponse.AspNetCore.Mvc.Formatters.Json.Tests/PartialJsonResultExecutorTests.cs @@ -172,5 +172,70 @@ public async Task TheExecuteAsyncMethodShouldNotIgnoreCase() // Assert Assert.Equal("{}", this.body.ToString()); } + + [Fact] + public async Task TheExecuteAsyncMethodShouldAlwaysSerializeIgnoredFields() + { + // Arrange + Fields.TryParse("foo", out var fields); + + Mock.Get(this.fieldsParser) + .Setup(fieldsParser => fieldsParser.Parse(this.httpRequest)) + .Returns(FieldsParserResult.Success(fields)); + + this.partialJsonOptions.IgnoredFields = new[] { "bar" }; + + var partialJsonResult = new PartialJsonResult(new { bar = "baz" }, new JsonSerializerSettings()); + + // Act + await this.executor.ExecuteAsync(this.actionContext, partialJsonResult); + + // Assert + Assert.Equal("{\"bar\":\"baz\"}", this.body.ToString()); + } + + [Fact] + public async Task TheExecuteAsyncMethodShouldAlwaysSerializeIgnoredFieldsIgnoringCase() + { + // Arrange + Fields.TryParse("foo", out var fields); + + Mock.Get(this.fieldsParser) + .Setup(fieldsParser => fieldsParser.Parse(this.httpRequest)) + .Returns(FieldsParserResult.Success(fields)); + + this.partialJsonOptions.IgnoreCase = true; + this.partialJsonOptions.IgnoredFields = new[] { "BAR" }; + + var partialJsonResult = new PartialJsonResult(new { bar = "baz" }, new JsonSerializerSettings()); + + // Act + await this.executor.ExecuteAsync(this.actionContext, partialJsonResult); + + // Assert + Assert.Equal("{\"bar\":\"baz\"}", this.body.ToString()); + } + + [Fact] + public async Task TheExecuteAsyncMethodShouldAlwaysSerializeIgnoredFieldsNotIgnoringCase() + { + // Arrange + Fields.TryParse("foo", out var fields); + + Mock.Get(this.fieldsParser) + .Setup(fieldsParser => fieldsParser.Parse(this.httpRequest)) + .Returns(FieldsParserResult.Success(fields)); + + this.partialJsonOptions.IgnoreCase = false; + this.partialJsonOptions.IgnoredFields = new[] { "BAR" }; + + var partialJsonResult = new PartialJsonResult(new { bar = "baz" }, new JsonSerializerSettings()); + + // Act + await this.executor.ExecuteAsync(this.actionContext, partialJsonResult); + + // Assert + Assert.Equal("{}", this.body.ToString()); + } } } \ No newline at end of file