diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 217f7cb..23aee29 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,10 +16,14 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup .NET + - name: Setup .NET 8 uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x + - name: Setup .NET 6 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 6.0.x - name: Restore dependencies run: dotnet restore - name: Build diff --git a/F23.Hateoas.NewtonsoftJson/F23.Hateoas.NewtonsoftJson.csproj b/F23.Hateoas.NewtonsoftJson/F23.Hateoas.NewtonsoftJson.csproj new file mode 100644 index 0000000..48fa513 --- /dev/null +++ b/F23.Hateoas.NewtonsoftJson/F23.Hateoas.NewtonsoftJson.csproj @@ -0,0 +1,40 @@ + + + + F23.Hateoas.NewtonsoftJson + 2.0.0 + net8.0;netstandard2.0 + feature[23] + Newtonsoft.Json support for F23.Hateoas. + 2025, feature[23] + MIT + https://github.com/feature23/hateoas + https://github.com/feature23/hateoas.git + git + hateoas hypermedia asp.net core mvc api newtonsoft json + logo.png + README.md + false + true + true + snupkg + enable + enable + true + latest + + + + + + + + + + + + + + + + diff --git a/F23.Hateoas.NewtonsoftJson/HypermediaLinkJsonConverter.cs b/F23.Hateoas.NewtonsoftJson/HypermediaLinkJsonConverter.cs new file mode 100644 index 0000000..d4082ec --- /dev/null +++ b/F23.Hateoas.NewtonsoftJson/HypermediaLinkJsonConverter.cs @@ -0,0 +1,50 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace F23.Hateoas.NewtonsoftJson; + +public class HypermediaLinkJsonConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value is not HypermediaLink link) + { + writer.WriteNull(); + return; + } + + writer.WriteStartObject(); + + writer.WritePropertyName("rel"); + serializer.Serialize(writer, link.Rel); + + writer.WritePropertyName("href"); + serializer.Serialize(writer, link.Href); + + if (link.Method != null) + { + writer.WritePropertyName("method"); + serializer.Serialize(writer, link.Method); + } + + writer.WriteEndObject(); + } + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + var jsonObject = JObject.Load(reader); + + var rel = jsonObject["rel"] + ?? throw new JsonSerializationException("Missing required property 'rel'."); + + var href = jsonObject["href"] + ?? throw new JsonSerializationException("Missing required property 'href'."); + + var method = jsonObject["method"]; + + return new HypermediaLink(rel.Value()!, href.Value()!, method?.Value()); + } + + public override bool CanConvert(Type objectType) + => objectType == typeof(HypermediaLink); +} diff --git a/F23.Hateoas.NewtonsoftJson/HypermediaResponseJsonConverter.cs b/F23.Hateoas.NewtonsoftJson/HypermediaResponseJsonConverter.cs new file mode 100644 index 0000000..c74a2b7 --- /dev/null +++ b/F23.Hateoas.NewtonsoftJson/HypermediaResponseJsonConverter.cs @@ -0,0 +1,61 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace F23.Hateoas.NewtonsoftJson; + +public class HypermediaResponseJsonConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value is not HypermediaResponse response) + { + writer.WriteNull(); + return; + } + + writer.WriteStartObject(); + + writer.WritePropertyName("content"); + serializer.Serialize(writer, response.Content); + + if (response.Links != null) + { + writer.WritePropertyName("_links"); + writer.WriteStartArray(); + + foreach (var link in response.Links) + { + serializer.Serialize(writer, link); + } + + writer.WriteEndArray(); + } + + writer.WriteEndObject(); + } + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + var jsonObject = JObject.Load(reader); + + var content = jsonObject["content"] + ?? throw new JsonSerializationException("Missing required property 'content'."); + + var response = new HypermediaResponse(content); + + var links = jsonObject["_links"]; + + if (links != null) + { + foreach (var link in links) + { + response.Add(serializer.Deserialize(link.CreateReader())!); + } + } + + return response; + } + + public override bool CanConvert(Type objectType) + => objectType == typeof(HypermediaResponse); +} diff --git a/F23.Hateoas.NewtonsoftJson/README.md b/F23.Hateoas.NewtonsoftJson/README.md new file mode 100644 index 0000000..9e261da --- /dev/null +++ b/F23.Hateoas.NewtonsoftJson/README.md @@ -0,0 +1,40 @@ +# F23.Hateoas.NewtonsoftJson + +This library provides compatibility between [F23.Hateoas](https://github.com/feature23/hateoas) +and [Newtonsoft.Json](https://www.newtonsoft.com/json). + +## Installation + +You can install this library via NuGet: + +```bash +dotnet add package F23.Hateoas.NewtonsoftJson +``` + +## Usage + +Add the `HypermediaResponseJsonConverter` and `HypermediaLinkJsonConverter` to the `JsonSerializerSettings`: + +```csharp +using F23.Hateoas.NewtonsoftJson; + +var options = new JsonSerializerSettings +{ + Converters = + { + new HypermediaResponseJsonConverter(), + new HypermediaLinkJsonConverter(), + } +}; +``` + +This can be done during ASP.NET Core startup: + +```csharp +builder.Services.AddControllers() + .AddNewtonsoftJson(options => + { + options.SerializerSettings.Converters.Add(new HypermediaResponseJsonConverter()); + options.SerializerSettings.Converters.Add(new HypermediaLinkJsonConverter()); + }); +``` diff --git a/F23.Hateoas.NewtonsoftJson/RequiredPropertiesBackCompat.cs b/F23.Hateoas.NewtonsoftJson/RequiredPropertiesBackCompat.cs new file mode 100644 index 0000000..f4aa81e --- /dev/null +++ b/F23.Hateoas.NewtonsoftJson/RequiredPropertiesBackCompat.cs @@ -0,0 +1,41 @@ +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ +#if !NET5_0_OR_GREATER + + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit {} + +#endif // !NET5_0_OR_GREATER + +#if !NET7_0_OR_GREATER + + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + internal sealed class RequiredMemberAttribute : Attribute {} + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] + internal sealed class CompilerFeatureRequiredAttribute : Attribute + { + public CompilerFeatureRequiredAttribute(string featureName) + { + FeatureName = featureName; + } + + public string FeatureName { get; } + public bool IsOptional { get; init; } + + public const string RefStructs = nameof(RefStructs); + public const string RequiredMembers = nameof(RequiredMembers); + } + +#endif // !NET7_0_OR_GREATER +} + +namespace System.Diagnostics.CodeAnalysis +{ +#if !NET7_0_OR_GREATER + [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] + internal sealed class SetsRequiredMembersAttribute : Attribute {} +#endif +} diff --git a/F23.Hateoas.Sample.NewtonsoftJson/F23.Hateoas.Sample.NewtonsoftJson.csproj b/F23.Hateoas.Sample.NewtonsoftJson/F23.Hateoas.Sample.NewtonsoftJson.csproj index 80b813c..004d004 100644 --- a/F23.Hateoas.Sample.NewtonsoftJson/F23.Hateoas.Sample.NewtonsoftJson.csproj +++ b/F23.Hateoas.Sample.NewtonsoftJson/F23.Hateoas.Sample.NewtonsoftJson.csproj @@ -13,7 +13,8 @@ - + + diff --git a/F23.Hateoas.Sample.NewtonsoftJson/Program.cs b/F23.Hateoas.Sample.NewtonsoftJson/Program.cs index 734da51..cbc6dc7 100644 --- a/F23.Hateoas.Sample.NewtonsoftJson/Program.cs +++ b/F23.Hateoas.Sample.NewtonsoftJson/Program.cs @@ -1,4 +1,5 @@ using F23.Hateoas; +using F23.Hateoas.NewtonsoftJson; var builder = WebApplication.CreateBuilder(args); @@ -8,7 +9,11 @@ builder.Services.AddSwaggerGen(); builder.Services.AddControllers() - .AddNewtonsoftJson(); + .AddNewtonsoftJson(options => + { + options.SerializerSettings.Converters.Add(new HypermediaResponseJsonConverter()); + options.SerializerSettings.Converters.Add(new HypermediaLinkJsonConverter()); + }); var app = builder.Build(); diff --git a/F23.Hateoas.Sample.NewtonsoftJson/Properties/launchSettings.json b/F23.Hateoas.Sample.NewtonsoftJson/Properties/launchSettings.json index 39f05c5..a265483 100644 --- a/F23.Hateoas.Sample.NewtonsoftJson/Properties/launchSettings.json +++ b/F23.Hateoas.Sample.NewtonsoftJson/Properties/launchSettings.json @@ -1,13 +1,5 @@ { "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:10201", - "sslPort": 44369 - } - }, "profiles": { "http": { "commandName": "Project", @@ -28,14 +20,6 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } } } } diff --git a/F23.Hateoas.Sample.SystemTextJson/AppBaseController.cs b/F23.Hateoas.Sample.SystemTextJson/AppBaseController.cs new file mode 100644 index 0000000..358ec18 --- /dev/null +++ b/F23.Hateoas.Sample.SystemTextJson/AppBaseController.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; + +namespace F23.Hateoas.Sample.SystemTextJson; + +public abstract class AppBaseController : ControllerBase +{ + protected IActionResult HypermediaOk(object content, IList? links = null) + { + return Ok(new HypermediaResponse(content) { Links = links }); + } +} diff --git a/F23.Hateoas.Sample.SystemTextJson/ExampleController.cs b/F23.Hateoas.Sample.SystemTextJson/ExampleController.cs new file mode 100644 index 0000000..6210f9d --- /dev/null +++ b/F23.Hateoas.Sample.SystemTextJson/ExampleController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc; + +namespace F23.Hateoas.Sample.SystemTextJson; + +public class ExampleController : AppBaseController +{ + [HttpGet("api/example")] + public IActionResult GetExample() + { + var data = new + { + Message = "Hello World!", + }; + + return HypermediaOk(data, [ + new HypermediaLink("self", "/api/example"), + new HypermediaLink("google", "https://www.google.com"), + ]); + } +} diff --git a/F23.Hateoas.Sample.SystemTextJson/F23.Hateoas.Sample.SystemTextJson.csproj b/F23.Hateoas.Sample.SystemTextJson/F23.Hateoas.Sample.SystemTextJson.csproj new file mode 100644 index 0000000..1c23340 --- /dev/null +++ b/F23.Hateoas.Sample.SystemTextJson/F23.Hateoas.Sample.SystemTextJson.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/F23.Hateoas.Sample.SystemTextJson/Program.cs b/F23.Hateoas.Sample.SystemTextJson/Program.cs new file mode 100644 index 0000000..e0a8a04 --- /dev/null +++ b/F23.Hateoas.Sample.SystemTextJson/Program.cs @@ -0,0 +1,23 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.AddControllers(); // uses System.Text.Json by default + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.MapControllers(); + +app.Run(); diff --git a/F23.Hateoas.Sample.SystemTextJson/Properties/launchSettings.json b/F23.Hateoas.Sample.SystemTextJson/Properties/launchSettings.json new file mode 100644 index 0000000..f6679c7 --- /dev/null +++ b/F23.Hateoas.Sample.SystemTextJson/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5158", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7156;http://localhost:5158", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/F23.Hateoas.Tests/F23.Hateoas.Tests.csproj b/F23.Hateoas.Tests/F23.Hateoas.Tests.csproj index 6b8475f..b6795cd 100644 --- a/F23.Hateoas.Tests/F23.Hateoas.Tests.csproj +++ b/F23.Hateoas.Tests/F23.Hateoas.Tests.csproj @@ -1,10 +1,11 @@ - net8.0 + + net8.0;net6.0 enable enable - + latest false true @@ -24,6 +25,7 @@ + diff --git a/F23.Hateoas.Tests/NewtonsoftJsonSerializationTests.cs b/F23.Hateoas.Tests/NewtonsoftJsonSerializationTests.cs index 2c30f1a..f8ed1b5 100644 --- a/F23.Hateoas.Tests/NewtonsoftJsonSerializationTests.cs +++ b/F23.Hateoas.Tests/NewtonsoftJsonSerializationTests.cs @@ -1,15 +1,25 @@ +using F23.Hateoas.NewtonsoftJson; using Newtonsoft.Json; namespace F23.Hateoas.Tests; public class NewtonsoftJsonSerializationTests { + private static readonly JsonSerializerSettings _settings = new JsonSerializerSettings + { + Converters = + { + new HypermediaResponseJsonConverter(), + new HypermediaLinkJsonConverter(), + } + }; + [Fact] public void Serialize_WithNullLinks_ShouldNotIncludeLinks() { var response = new HypermediaResponse(42); - var json = JsonConvert.SerializeObject(response); + var json = JsonConvert.SerializeObject(response, _settings); Assert.Equal("""{"content":42}""", json); } @@ -22,12 +32,12 @@ public void Serialize_WithLinks_ShouldIncludeLinks() Links = new List { new HypermediaLink("self", "/api/example"), - new HypermediaLink("google", "https://www.google.com"), + new HypermediaLink("google", "https://www.google.com", "GET"), } }; - var json = JsonConvert.SerializeObject(response); + var json = JsonConvert.SerializeObject(response, _settings); - Assert.Equal("""{"content":42,"_links":[{"rel":"self","href":"/api/example"},{"rel":"google","href":"https://www.google.com"}]}""", json); + Assert.Equal("""{"content":42,"_links":[{"rel":"self","href":"/api/example"},{"rel":"google","href":"https://www.google.com","method":"GET"}]}""", json); } } diff --git a/F23.Hateoas.Tests/SystemTextJsonSerializationTests.cs b/F23.Hateoas.Tests/SystemTextJsonSerializationTests.cs new file mode 100644 index 0000000..48a5365 --- /dev/null +++ b/F23.Hateoas.Tests/SystemTextJsonSerializationTests.cs @@ -0,0 +1,33 @@ +using System.Text.Json; + +namespace F23.Hateoas.Tests; + +public class SystemTextJsonSerializationTests +{ + [Fact] + public void Serialize_WithNullLinks_ShouldNotIncludeLinks() + { + var response = new HypermediaResponse(42); + + var json = JsonSerializer.Serialize(response); + + Assert.Equal("""{"content":42}""", json); + } + + [Fact] + public void Serialize_WithLinks_ShouldIncludeLinks() + { + var response = new HypermediaResponse(42) + { + Links = new List + { + new HypermediaLink("self", "/api/example"), + new HypermediaLink("google", "https://www.google.com", "GET"), + } + }; + + var json = JsonSerializer.Serialize(response); + + Assert.Equal("""{"content":42,"_links":[{"rel":"self","href":"/api/example"},{"rel":"google","href":"https://www.google.com","method":"GET"}]}""", json); + } +} diff --git a/F23.Hateoas.sln b/F23.Hateoas.sln index bb7b3d1..c1ea49c 100644 --- a/F23.Hateoas.sln +++ b/F23.Hateoas.sln @@ -9,6 +9,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "F23.Hateoas.Sample.Newtonso EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "F23.Hateoas.Tests", "F23.Hateoas.Tests\F23.Hateoas.Tests.csproj", "{39735FE1-5D95-4BA9-AF6F-86143888CDF0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "F23.Hateoas.NewtonsoftJson", "F23.Hateoas.NewtonsoftJson\F23.Hateoas.NewtonsoftJson.csproj", "{7C417AE9-084E-4076-AB10-6AF96A663567}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "F23.Hateoas.Sample.SystemTextJson", "F23.Hateoas.Sample.SystemTextJson\F23.Hateoas.Sample.SystemTextJson.csproj", "{E40C060B-432F-4FB1-9B6C-579D3AB64228}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -30,5 +34,13 @@ Global {39735FE1-5D95-4BA9-AF6F-86143888CDF0}.Debug|Any CPU.Build.0 = Debug|Any CPU {39735FE1-5D95-4BA9-AF6F-86143888CDF0}.Release|Any CPU.ActiveCfg = Release|Any CPU {39735FE1-5D95-4BA9-AF6F-86143888CDF0}.Release|Any CPU.Build.0 = Release|Any CPU + {7C417AE9-084E-4076-AB10-6AF96A663567}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C417AE9-084E-4076-AB10-6AF96A663567}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C417AE9-084E-4076-AB10-6AF96A663567}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C417AE9-084E-4076-AB10-6AF96A663567}.Release|Any CPU.Build.0 = Release|Any CPU + {E40C060B-432F-4FB1-9B6C-579D3AB64228}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E40C060B-432F-4FB1-9B6C-579D3AB64228}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E40C060B-432F-4FB1-9B6C-579D3AB64228}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E40C060B-432F-4FB1-9B6C-579D3AB64228}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/F23.Hateoas/F23.Hateoas.csproj b/F23.Hateoas/F23.Hateoas.csproj index b67b666..0f15e1a 100644 --- a/F23.Hateoas/F23.Hateoas.csproj +++ b/F23.Hateoas/F23.Hateoas.csproj @@ -2,8 +2,8 @@ F23.Hateoas - 1.1.0 - netstandard2.0 + 2.0.0 + net8.0;netstandard2.0 feature[23] A set of basic HATEOAS (Hypermedia As The Engine Of Application State) types to help with implementing consistent hypermedia-enabled APIs. 2019, feature[23] @@ -18,15 +18,19 @@ true true snupkg + enable + enable + true + latest - - - - + + + + diff --git a/F23.Hateoas/HypermediaBase.cs b/F23.Hateoas/HypermediaBase.cs deleted file mode 100644 index 6eee5ec..0000000 --- a/F23.Hateoas/HypermediaBase.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace F23.Hateoas -{ - /// - /// A base class for including hypermedia (HATEOAS) links. - /// - public abstract class HypermediaBase - { - [JsonProperty("_links", NullValueHandling = NullValueHandling.Ignore)] - public IList Links { get; set; } - } -} diff --git a/F23.Hateoas/HypermediaLink.cs b/F23.Hateoas/HypermediaLink.cs index 6abf028..8288ca0 100644 --- a/F23.Hateoas/HypermediaLink.cs +++ b/F23.Hateoas/HypermediaLink.cs @@ -1,33 +1,29 @@ -using Newtonsoft.Json; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; -namespace F23.Hateoas +namespace F23.Hateoas; + +public class HypermediaLink { - [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] - public class HypermediaLink + public HypermediaLink() { - public HypermediaLink() - { - } - - public HypermediaLink(string rel, string href) - : this(rel, href, null) - { - } + } - public HypermediaLink(string rel, string href, string method) - { - Rel = rel; - Href = href; - Method = method; - } + [SetsRequiredMembers] + public HypermediaLink(string rel, string href, string? method = null) + { + Rel = rel; + Href = href; + Method = method; + } - [JsonProperty("rel")] - public string Rel { get; set; } + [JsonPropertyName("rel")] + public required string Rel { get; init; } - [JsonProperty("href")] - public string Href { get; set; } + [JsonPropertyName("href")] + public required string Href { get; init; } - [JsonProperty("method")] - public string Method { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("method")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Method { get; set; } +} diff --git a/F23.Hateoas/HypermediaResponse.cs b/F23.Hateoas/HypermediaResponse.cs index deb38ef..aeff844 100644 --- a/F23.Hateoas/HypermediaResponse.cs +++ b/F23.Hateoas/HypermediaResponse.cs @@ -1,27 +1,20 @@ -using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; -namespace F23.Hateoas -{ - public class HypermediaResponse - { - public HypermediaResponse(object content) - { - Content = content; - } - - [JsonProperty("content", NullValueHandling = NullValueHandling.Include)] - public object Content { get; } +namespace F23.Hateoas; - [JsonProperty("_links", NullValueHandling = NullValueHandling.Ignore)] - public IList Links { get; set; } +public class HypermediaResponse(object content) +{ + [JsonPropertyName("content")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public object Content { get; } = content; - public void Add(HypermediaLink link) - { - if (Links == null) - Links = new List(); + [JsonPropertyName("_links")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IList? Links { get; set; } - Links.Add(link); - } + public void Add(HypermediaLink link) + { + Links ??= new List(); + Links.Add(link); } } diff --git a/F23.Hateoas/RequiredPropertiesBackCompat.cs b/F23.Hateoas/RequiredPropertiesBackCompat.cs new file mode 100644 index 0000000..f4aa81e --- /dev/null +++ b/F23.Hateoas/RequiredPropertiesBackCompat.cs @@ -0,0 +1,41 @@ +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ +#if !NET5_0_OR_GREATER + + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit {} + +#endif // !NET5_0_OR_GREATER + +#if !NET7_0_OR_GREATER + + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + internal sealed class RequiredMemberAttribute : Attribute {} + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] + internal sealed class CompilerFeatureRequiredAttribute : Attribute + { + public CompilerFeatureRequiredAttribute(string featureName) + { + FeatureName = featureName; + } + + public string FeatureName { get; } + public bool IsOptional { get; init; } + + public const string RefStructs = nameof(RefStructs); + public const string RequiredMembers = nameof(RequiredMembers); + } + +#endif // !NET7_0_OR_GREATER +} + +namespace System.Diagnostics.CodeAnalysis +{ +#if !NET7_0_OR_GREATER + [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] + internal sealed class SetsRequiredMembersAttribute : Attribute {} +#endif +} diff --git a/README.md b/README.md index dab211e..c6df26c 100644 --- a/README.md +++ b/README.md @@ -64,3 +64,24 @@ public IActionResult Get() ## Contributing This package is very minimalistic and is not likely to need many updates. If you have a feature request or bug report, please file a GitHub issue. Once accepted by our team, you may submit a pull request. PRs without an accepted issue have a higher likelihood of being rejected. + +## Migrating from v1 to v2 + +Version 2.0.0 of this library introduced a breaking change by removing the Newtonsoft.Json dependency. +If you need Newtonsoft.Json support, add a reference to the `F23.Hateoas.NewtonsoftJson` package +and register the `HypermediaResponseJsonConverter` and `HypermediaLinkJsonConverter` with your `JsonSerializerSettings`. + +For example, in ASP.NET Core startup: + +```csharp +using F23.Hateoas.NewtonsoftJson; + +... + +builder.Services.AddControllers() + .AddNewtonsoftJson(options => + { + options.SerializerSettings.Converters.Add(new HypermediaResponseJsonConverter()); + options.SerializerSettings.Converters.Add(new HypermediaLinkJsonConverter()); + }); +```