diff --git a/CHANGELOG.md b/CHANGELOG.md index c15d6c4e..6e155839 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ Represents the **NuGet** versions. +## v3.7.2 +- *Fixed*: The `ReferenceDataMultiCollection` and `ReferenceDataMultiItem` have been replaced with the `ReferenceDataMultiDictionary` as existing resulted in an unintended format with which to return the data. This fix also removed the need for the `ReferenceDataMultiCollectionConverterFactory` as custom serialization for this is no longer required. + ## v3.7.1 - *Fixed*: The `WebApi.PutWithResultAsync` methods that support `get` function parameter have had the result nullability corrected. - *Fixed*: The `BidirectionalMapper` has been added to further simplify the specification of a bidirectional mapping capability. diff --git a/Common.targets b/Common.targets index b40d316a..7f86637e 100644 --- a/Common.targets +++ b/Common.targets @@ -1,6 +1,6 @@  - 3.7.1 + 3.7.2 preview Avanade Avanade diff --git a/README.md b/README.md index 8f8a144e..d6e43ae1 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Package | Status | Source & documentation `CoreEx.EntityFrameworkCore` | [![NuGet version](https://badge.fury.io/nu/CoreEx.EntityFrameworkCore.svg)](https://badge.fury.io/nu/CoreEx.EntityFrameworkCore) | [Link](./src/CoreEx.EntityFrameworkCore) `CoreEx.FluentValidation` | [![NuGet version](https://badge.fury.io/nu/CoreEx.FluentValidation.svg)](https://badge.fury.io/nu/CoreEx.FluentValidation) | [Link](./src/CoreEx.FluentValidation) `CoreEx.Newtonsoft` | [![NuGet version](https://badge.fury.io/nu/CoreEx.Newtonsoft.svg)](https://badge.fury.io/nu/CoreEx.Newtonsoft) | [Link](./src/CoreEx.Newtonsoft) +`CoreEx.OData` | [![NuGet version](https://badge.fury.io/nu/CoreEx.OData.svg)](https://badge.fury.io/nu/CoreEx.OData) | [Link](./src/CoreEx.OData) `CoreEx.Solace` | [![NuGet version](https://badge.fury.io/nu/CoreEx.Solace.svg)](https://badge.fury.io/nu/CoreEx.Solace) | [Link](./src/CoreEx.Solace) `CoreEx.Validation` | [![NuGet version](https://badge.fury.io/nu/CoreEx.Validation.svg)](https://badge.fury.io/nu/CoreEx.Validation) | [Link](./src/CoreEx.Validation) -- | -- | -- @@ -59,8 +60,7 @@ These other _Avanade_ repositories leverage _CoreEx_: Repo | Description -|- [Beef](https://github.com/Avanade/beef) | Code-generation capabilities to support the industrialization of API development leveraging `CoreEx` as the primary runtime framework (_Beef_ version `v5+`). -[DbEx](https://github.com/Avanade/dbex) | DbEx provides database extensions for DbUp-inspired database migrations. -[UnitTestEx](https://github.com/Avanade/unittestex) | Provides .NET testing extensions to the most popular testing frameworks (MSTest, NUnit and Xunit). +[DbEx](https://github.com/Avanade/dbex) | Provides database extensions for DbUp-inspired database migrations. [NTangle](https://github.com/Avanade/ntangle) | Change Data Capture (CDC) code generation tool and runtime.
diff --git a/samples/My.Hr/My.Hr.Api/Controllers/ReferenceDataController.cs b/samples/My.Hr/My.Hr.Api/Controllers/ReferenceDataController.cs index 64028fcc..98a54fa2 100644 --- a/samples/My.Hr/My.Hr.Api/Controllers/ReferenceDataController.cs +++ b/samples/My.Hr/My.Hr.Api/Controllers/ReferenceDataController.cs @@ -36,7 +36,7 @@ public Task GenderGetAll([FromQuery] IEnumerable? codes = _webApi.GetAsync(Request, x => _orchestrator.GetWithFilterAsync(codes, text, x.RequestOptions.IncludeInactive)); [HttpGet()] - [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(ReferenceDataMultiDictionary), (int)HttpStatusCode.OK)] [ApiExplorerSettings(IgnoreApi = true)] public Task GetNamed() => _webApi.GetAsync(Request, p => _orchestrator.GetNamedAsync(p.RequestOptions)); } \ No newline at end of file diff --git a/src/CoreEx.AspNetCore/Abstractions/ReferenceDataOrchestratorExtensions.cs b/src/CoreEx.AspNetCore/Abstractions/ReferenceDataOrchestratorExtensions.cs index 360ed1b6..cb9081e3 100644 --- a/src/CoreEx.AspNetCore/Abstractions/ReferenceDataOrchestratorExtensions.cs +++ b/src/CoreEx.AspNetCore/Abstractions/ReferenceDataOrchestratorExtensions.cs @@ -20,9 +20,9 @@ public static class ReferenceDataOrchestratorExtensions /// The . /// The . /// The . - /// The . + /// The . /// The reference data names and codes are specified as part of the query string. Either '?names=RefA,RefB,RefX' or ?RefA,RefB=CodeA,CodeB,RefX=CodeX or any combination thereof. - public static Task GetNamedAsync(this ReferenceDataOrchestrator orchestrator, WebApiRequestOptions requestOptions, CancellationToken cancellationToken = default) + public static Task GetNamedAsync(this ReferenceDataOrchestrator orchestrator, WebApiRequestOptions requestOptions, CancellationToken cancellationToken = default) { var dict = new Dictionary>(); @@ -32,7 +32,7 @@ public static Task GetNamedAsync(this ReferenceDat { foreach (var v in SplitStringValues(q.Value.Where(x => !string.IsNullOrEmpty(x)).Distinct()!)) { - dict.TryAdd(v, new List()); + dict.TryAdd(v, []); } } else diff --git a/src/CoreEx/RefData/ReferenceDataMultiCollection.cs b/src/CoreEx/RefData/ReferenceDataMultiCollection.cs deleted file mode 100644 index 3926b3ae..00000000 --- a/src/CoreEx/RefData/ReferenceDataMultiCollection.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx - -using System.Collections.Generic; - -namespace CoreEx.RefData -{ - /// - /// Represents a collection that supports multiple Reference Data () collections. - /// - public class ReferenceDataMultiCollection : List { } -} \ No newline at end of file diff --git a/src/CoreEx/RefData/ReferenceDataMultiDictionary.cs b/src/CoreEx/RefData/ReferenceDataMultiDictionary.cs new file mode 100644 index 00000000..2999054a --- /dev/null +++ b/src/CoreEx/RefData/ReferenceDataMultiDictionary.cs @@ -0,0 +1,19 @@ +// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx + +using System; +using System.Collections.Generic; + +namespace CoreEx.RefData +{ + /// + /// Represents a dictionary where the key is the and the value is the corresponding items. + /// + /// This is generally intended for the . + public class ReferenceDataMultiDictionary : Dictionary> + { + /// + /// Initializes a new instance of the class. + /// + public ReferenceDataMultiDictionary() : base(StringComparer.OrdinalIgnoreCase) { } + } +} \ No newline at end of file diff --git a/src/CoreEx/RefData/ReferenceDataMultiItem.cs b/src/CoreEx/RefData/ReferenceDataMultiItem.cs deleted file mode 100644 index 4e14ffc4..00000000 --- a/src/CoreEx/RefData/ReferenceDataMultiItem.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx - -using System; -using System.Collections.Generic; - -namespace CoreEx.RefData -{ - /// - /// Represents a named . - /// - public class ReferenceDataMultiItem - { - /// - /// Initializes a new instance of the class for a and it corresponding . - /// - /// The . - /// The items. - public ReferenceDataMultiItem(string name, IEnumerable items) - { - Name = name ?? throw new ArgumentNullException(nameof(name)); - Items = items ?? throw new ArgumentNullException(nameof(items)); - } - - /// - /// Gets or sets the reference data name. - /// - public string Name { get; set; } - - /// - /// Gets or sets the reference data collection. - /// - public IEnumerable Items { get; set; } - } -} \ No newline at end of file diff --git a/src/CoreEx/RefData/ReferenceDataOrchestrator.cs b/src/CoreEx/RefData/ReferenceDataOrchestrator.cs index a408b624..cb4f490a 100644 --- a/src/CoreEx/RefData/ReferenceDataOrchestrator.cs +++ b/src/CoreEx/RefData/ReferenceDataOrchestrator.cs @@ -521,11 +521,11 @@ public Task PrefetchAsync(IEnumerable names, CancellationToken cancellat /// The reference data names. /// Indicates whether to include inactive ( equal false) entries. /// The . - /// The . + /// The . /// Will return an empty collection where no are specified. - public async Task GetNamedAsync(IEnumerable names, bool includeInactive = false, CancellationToken cancellationToken = default) + public async Task GetNamedAsync(IEnumerable names, bool includeInactive = false, CancellationToken cancellationToken = default) { - var mc = new ReferenceDataMultiCollection(); + var mc = new ReferenceDataMultiDictionary(); if (names != null) { @@ -533,7 +533,7 @@ public async Task GetNamedAsync(IEnumerable GetNamedAsync(IEnumerableThe reference data names and related codes. /// Indicates whether to include inactive ( equal false) entries. /// The . - /// The . + /// The . /// Will return an empty collection where no are specified. - public async Task GetNamedAsync(IEnumerable>> namesAndCodes, bool includeInactive = false, CancellationToken cancellationToken = default) + public async Task GetNamedAsync(IEnumerable>> namesAndCodes, bool includeInactive = false, CancellationToken cancellationToken = default) { - var mc = new ReferenceDataMultiCollection(); + var mc = new ReferenceDataMultiDictionary(); if (namesAndCodes != null) { @@ -558,7 +558,7 @@ public async Task GetNamedAsync(IEnumerable ContainsName(x.Key))) { - mc.Add(new ReferenceDataMultiItem(_nameToType[kvp.Key].Name, await GetWithFilterAsync(kvp.Key, codes: kvp.Value, includeInactive: includeInactive, cancellationToken: cancellationToken).ConfigureAwait(false))); + mc.Add(_nameToType[kvp.Key].Name, await GetWithFilterAsync(kvp.Key, codes: kvp.Value, includeInactive: includeInactive, cancellationToken: cancellationToken).ConfigureAwait(false)); } } diff --git a/src/CoreEx/Text/Json/ReferenceDataContentJsonSerializer.cs b/src/CoreEx/Text/Json/ReferenceDataContentJsonSerializer.cs index 0e43cbc6..41f0b3e5 100644 --- a/src/CoreEx/Text/Json/ReferenceDataContentJsonSerializer.cs +++ b/src/CoreEx/Text/Json/ReferenceDataContentJsonSerializer.cs @@ -11,7 +11,8 @@ namespace CoreEx.Text.Json /// Provides the JSON Serialize and Deserialize implementation to allow types to serialize contents. /// /// Generally, types will serialize the as the value; this allows for full contents to be serialized. - public class ReferenceDataContentJsonSerializer : JsonSerializer, IReferenceDataContentJsonSerializer + /// The . Defaults to . + public class ReferenceDataContentJsonSerializer(Stj.JsonSerializerOptions? options = null) : JsonSerializer(options ?? DefaultOptions), IReferenceDataContentJsonSerializer { /// /// Gets or sets the default without to allow types to serialize contents. @@ -22,7 +23,7 @@ public class ReferenceDataContentJsonSerializer : JsonSerializer, IReferenceData /// = false /// = . /// = . - /// = , and . + /// = , , and . /// /// public static new Stj.JsonSerializerOptions DefaultOptions { get; set; } = new Stj.JsonSerializerOptions(Stj.JsonSerializerDefaults.Web) @@ -31,13 +32,7 @@ public class ReferenceDataContentJsonSerializer : JsonSerializer, IReferenceData WriteIndented = false, DictionaryKeyPolicy = SubstituteNamingPolicy.Substitute, PropertyNamingPolicy = SubstituteNamingPolicy.Substitute, - Converters = { new JsonStringEnumConverter(), new ExceptionConverterFactory(), new CollectionResultConverterFactory(), new ReferenceDataMultiCollectionConverterFactory(), new ResultConverterFactory() } + Converters = { new JsonStringEnumConverter(), new ExceptionConverterFactory(), new CollectionResultConverterFactory(), new ResultConverterFactory() } }; - - /// - /// Initializes a new instance of the class. - /// - /// The . Defaults to . - public ReferenceDataContentJsonSerializer(Stj.JsonSerializerOptions? options = null) : base(options ?? DefaultOptions) { } } } \ No newline at end of file diff --git a/src/CoreEx/Text/Json/ReferenceDataMultiCollectionConverterFactory.cs b/src/CoreEx/Text/Json/ReferenceDataMultiCollectionConverterFactory.cs deleted file mode 100644 index c4f4fc2e..00000000 --- a/src/CoreEx/Text/Json/ReferenceDataMultiCollectionConverterFactory.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx - -using CoreEx.RefData; -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace CoreEx.Text.Json -{ - /// - /// Performs JSON value conversion for which must be manually serialized as System.Text.Json does not serialize correctly natively. - /// - public class ReferenceDataMultiCollectionConverterFactory : JsonConverterFactory - { - /// - public override bool CanConvert(Type typeToConvert) => typeToConvert == typeof(ReferenceDataMultiCollection); - - /// - public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) => new JsonValueConverter(); - - /// - /// Performs the "actual" JSON value conversion for a . - /// - private class JsonValueConverter : JsonConverter - { - /// - public override bool HandleNull => false; - - /// - public override ReferenceDataMultiCollection? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotSupportedException($"Deserialization of Type {nameof(ReferenceDataMultiCollection)} is not supported."); - - /// - public override void Write(Utf8JsonWriter writer, ReferenceDataMultiCollection coll, JsonSerializerOptions options) - { - writer.WriteStartArray(); - - foreach (var item in coll) - { - writer.WriteStartObject(); - writer.WritePropertyName("name"); - writer.WriteStringValue(item.Name); - writer.WritePropertyName("items"); - writer.WriteStartArray(); - - Type? rdiType = null; - foreach (var rdi in item.Items) - { - System.Text.Json.JsonSerializer.Serialize(writer, rdi, rdiType ??= rdi.GetType(), options); - } - - writer.WriteEndArray(); - writer.WriteEndObject(); - } - - writer.WriteEndArray(); - } - } - } -} \ No newline at end of file diff --git a/tests/CoreEx.Test/Framework/RefData/ReferenceDataTest.cs b/tests/CoreEx.Test/Framework/RefData/ReferenceDataTest.cs index 5ffad331..db957910 100644 --- a/tests/CoreEx.Test/Framework/RefData/ReferenceDataTest.cs +++ b/tests/CoreEx.Test/Framework/RefData/ReferenceDataTest.cs @@ -604,18 +604,18 @@ public void GetNamed() var mc = o.GetNamedAsync(new string[] { "state", "bananas", "suburb" }).GetAwaiter().GetResult(); Assert.NotNull(mc); Assert.AreEqual(2, mc.Count); - Assert.AreEqual("State", mc[0].Name); - Assert.AreEqual(new string[] { "AZ", "CO", "IL", "SC", "WA" }, mc[0].Items.Select(x => x.Code)); - Assert.AreEqual("Suburb", mc[1].Name); - Assert.AreEqual(new string[] { "B", "H", "R" }, mc[1].Items.Select(x => x.Code)); + var mc1 = mc["State"]; + Assert.AreEqual(new string[] { "AZ", "CO", "IL", "SC", "WA" }, mc1.Select(x => x.Code)); + var mc2 = mc["Suburb"]; + Assert.AreEqual(new string[] { "B", "H", "R" }, mc2.Select(x => x.Code)); mc = o.GetNamedAsync(new string[] { "state", "bananas", "suburb" }, includeInactive: true).GetAwaiter().GetResult(); Assert.NotNull(mc); Assert.AreEqual(2, mc.Count); - Assert.AreEqual("State", mc[0].Name); - Assert.AreEqual(new string[] { "AZ", "CO", "IL", "XX", "SC", "WA"}, mc[0].Items.Select(x => x.Code)); - Assert.AreEqual("Suburb", mc[1].Name); - Assert.AreEqual(new string[] { "B", "H", "R" }, mc[1].Items.Select(x => x.Code)); + mc1 = mc["State"]; + Assert.AreEqual(new string[] { "AZ", "CO", "IL", "XX", "SC", "WA"}, mc1.Select(x => x.Code)); + mc2 = mc["Suburb"]; + Assert.AreEqual(new string[] { "B", "H", "R" }, mc2.Select(x => x.Code)); } [Test] @@ -628,28 +628,28 @@ public void GetNamed_HttpRequest() var mc = o.GetNamedAsync(hr.GetRequestOptions()).GetAwaiter().GetResult(); Assert.NotNull(mc); Assert.AreEqual(2, mc.Count); - Assert.AreEqual("State", mc[0].Name); - Assert.AreEqual(new string[] { "AZ", "CO", "IL", "SC", "WA" }, mc[0].Items.Select(x => x.Code)); - Assert.AreEqual("Suburb", mc[1].Name); - Assert.AreEqual(new string[] { "B", "H", "R" }, mc[1].Items.Select(x => x.Code)); + var mc1 = mc["State"]; + Assert.AreEqual(new string[] { "AZ", "CO", "IL", "SC", "WA" }, mc1.Select(x => x.Code)); + var mc2 = mc["Suburb"]; + Assert.AreEqual(new string[] { "B", "H", "R" }, mc2.Select(x => x.Code)); hr = test.CreateHttpRequest(HttpMethod.Get, "https://unittest/ref?state=co,il&suburb&state=xx"); mc = o.GetNamedAsync(hr.GetRequestOptions()).GetAwaiter().GetResult(); Assert.NotNull(mc); Assert.AreEqual(2, mc.Count); - Assert.AreEqual("State", mc[0].Name); - Assert.AreEqual(new string[] { "CO", "IL" }, mc[0].Items.Select(x => x.Code)); - Assert.AreEqual("Suburb", mc[1].Name); - Assert.AreEqual(new string[] { "B", "H", "R" }, mc[1].Items.Select(x => x.Code)); + mc1 = mc["State"]; + Assert.AreEqual(new string[] { "CO", "IL" }, mc1.Select(x => x.Code)); + mc2 = mc["Suburb"]; + Assert.AreEqual(new string[] { "B", "H", "R" }, mc2.Select(x => x.Code)); hr = test.CreateHttpRequest(HttpMethod.Get, "https://unittest/ref?state=co,il&suburb=h&state=xx&include-inactive&bananas"); mc = o.GetNamedAsync(hr.GetRequestOptions()).GetAwaiter().GetResult(); Assert.NotNull(mc); Assert.AreEqual(2, mc.Count); - Assert.AreEqual("State", mc[0].Name); - Assert.AreEqual(new string[] { "CO", "IL", "XX" }, mc[0].Items.Select(x => x.Code)); - Assert.AreEqual("Suburb", mc[1].Name); - Assert.AreEqual(new string[] { "H" }, mc[1].Items.Select(x => x.Code)); + mc1 = mc["State"]; + Assert.AreEqual(new string[] { "CO", "IL", "XX" }, mc1.Select(x => x.Code)); + mc2 = mc["Suburb"]; + Assert.AreEqual(new string[] { "H" }, mc2.Select(x => x.Code)); } [Test] diff --git a/tests/CoreEx.Test/Framework/WebApis/WebApiWithResultTest.cs b/tests/CoreEx.Test/Framework/WebApis/WebApiWithResultTest.cs index b581d9bb..42d3bed5 100644 --- a/tests/CoreEx.Test/Framework/WebApis/WebApiWithResultTest.cs +++ b/tests/CoreEx.Test/Framework/WebApis/WebApiWithResultTest.cs @@ -281,7 +281,7 @@ public void PutWithResultAsync_AutoConcurrency_NoIfMatch() using var test = FunctionTester.Create(); test.Type() .Run(f => f.PutWithResultAsync(test.CreateJsonHttpRequest(HttpMethod.Put, "https://unittest", new { id = "A", name = "B", price = 2.99m }), - r => Task.FromResult(Result.Ok(new Product { Id = "A", Name = "B", Price = 1.99m })), + r => Task.FromResult>(Result.Ok(new Product { Id = "A", Name = "B", Price = 1.99m })), r => { ObjectComparer.Assert(new Product { Id = "A", Name = "B", Price = 2.99m }, r.Value); return Task.FromResult(Result.Ok(new Product { Id = "Y", Name = "Z", Price = 3.99m })); }, simulatedConcurrency: true)) .ToActionResultAssertor() @@ -295,7 +295,7 @@ public void PutWithResultAsync_AutoConcurrency_NoMatch() using var test = FunctionTester.Create(); test.Type() .Run(f => f.PutWithResultAsync(test.CreateJsonHttpRequest(HttpMethod.Put, "https://unittest", new { id = "A", name = "B", price = 2.99m }, new HttpRequestOptions { ETag = "bbb" }), - r => Task.FromResult(Result.Ok(new Product { Id = "A", Name = "B", Price = 1.99m })), + r => Task.FromResult>(Result.Ok(new Product { Id = "A", Name = "B", Price = 1.99m })), r => { ObjectComparer.Assert(new Product { Id = "A", Name = "B", Price = 2.99m }, r.Value); return Task.FromResult(Result.Ok(new Product { Id = "Y", Name = "Z", Price = 3.99m })); }, simulatedConcurrency: true)) .ToActionResultAssertor() @@ -309,7 +309,7 @@ public void PutWithResultAsync_AutoConcurrency_Match() using var test = FunctionTester.Create(); test.Type() .Run(f => f.PutWithResultAsync(test.CreateJsonHttpRequest(HttpMethod.Put, "https://unittest", new { id = "A", name = "B", price = 2.99m }, new HttpRequestOptions { ETag = "98Oe+fRzgTuVae59mLwf0Mj+iKySTlgUxEQt18huJZg=" }), - r => Task.FromResult(Result.Ok(new Product { Id = "A", Name = "B", Price = 1.99m })), + r => Task.FromResult>(Result.Ok(new Product { Id = "A", Name = "B", Price = 1.99m })), r => { ObjectComparer.Assert(new Product { Id = "A", Name = "B", Price = 2.99m }, r.Value); return Task.FromResult(Result.Ok(new Product { Id = "Y", Name = "Z", Price = 3.99m })); }, simulatedConcurrency: true)) .ToActionResultAssertor()