Compare commits

..

2 commits

Author SHA1 Message Date
sam
7e6698c3fb
update to .net 9 and add new OpenAPI packages 2024-12-10 15:28:44 +01:00
sam
80b7f192f1
clean up RemoteAuthService 2024-12-10 14:09:32 +01:00
21 changed files with 468 additions and 1026 deletions

View file

@ -26,6 +26,7 @@ using Microsoft.EntityFrameworkCore;
namespace Foxnouns.Backend.Controllers.Authentication; namespace Foxnouns.Backend.Controllers.Authentication;
[Route("/api/internal/auth")] [Route("/api/internal/auth")]
[ApiExplorerSettings(IgnoreApi = true)]
public class AuthController( public class AuthController(
Config config, Config config,
DatabaseContext db, DatabaseContext db,

View file

@ -31,6 +31,7 @@ using NodaTime;
namespace Foxnouns.Backend.Controllers.Authentication; namespace Foxnouns.Backend.Controllers.Authentication;
[Route("/api/internal/auth/discord")] [Route("/api/internal/auth/discord")]
[ApiExplorerSettings(IgnoreApi = true)]
public class DiscordAuthController( public class DiscordAuthController(
[UsedImplicitly] Config config, [UsedImplicitly] Config config,
ILogger logger, ILogger logger,

View file

@ -30,6 +30,7 @@ using NodaTime;
namespace Foxnouns.Backend.Controllers.Authentication; namespace Foxnouns.Backend.Controllers.Authentication;
[Route("/api/internal/auth/email")] [Route("/api/internal/auth/email")]
[ApiExplorerSettings(IgnoreApi = true)]
public class EmailAuthController( public class EmailAuthController(
[UsedImplicitly] Config config, [UsedImplicitly] Config config,
DatabaseContext db, DatabaseContext db,

View file

@ -28,6 +28,7 @@ using NodaTime;
namespace Foxnouns.Backend.Controllers.Authentication; namespace Foxnouns.Backend.Controllers.Authentication;
[Route("/api/internal/auth/fediverse")] [Route("/api/internal/auth/fediverse")]
[ApiExplorerSettings(IgnoreApi = true)]
public class FediverseAuthController( public class FediverseAuthController(
ILogger logger, ILogger logger,
DatabaseContext db, DatabaseContext db,

View file

@ -31,6 +31,7 @@ using NodaTime;
namespace Foxnouns.Backend.Controllers.Authentication; namespace Foxnouns.Backend.Controllers.Authentication;
[Route("/api/internal/auth/google")] [Route("/api/internal/auth/google")]
[ApiExplorerSettings(IgnoreApi = true)]
public class GoogleAuthController( public class GoogleAuthController(
[UsedImplicitly] Config config, [UsedImplicitly] Config config,
ILogger logger, ILogger logger,

View file

@ -17,6 +17,7 @@ using NodaTime;
namespace Foxnouns.Backend.Controllers.Authentication; namespace Foxnouns.Backend.Controllers.Authentication;
[Route("/api/internal/auth/tumblr")] [Route("/api/internal/auth/tumblr")]
[ApiExplorerSettings(IgnoreApi = true)]
public class TumblrAuthController( public class TumblrAuthController(
[UsedImplicitly] Config config, [UsedImplicitly] Config config,
ILogger logger, ILogger logger,

View file

@ -26,6 +26,7 @@ namespace Foxnouns.Backend.Controllers;
[Route("/api/internal/data-exports")] [Route("/api/internal/data-exports")]
[Authorize("identify")] [Authorize("identify")]
[ApiExplorerSettings(IgnoreApi = true)]
public class ExportsController( public class ExportsController(
ILogger logger, ILogger logger,
Config config, Config config,

View file

@ -80,6 +80,7 @@ public class FlagsController(
[HttpPatch("{id}")] [HttpPatch("{id}")]
[Authorize("user.update")] [Authorize("user.update")]
[ProducesResponseType<PrideFlagResponse>(statusCode: StatusCodes.Status200OK)]
public async Task<IActionResult> UpdateFlagAsync(Snowflake id, [FromBody] UpdateFlagRequest req) public async Task<IActionResult> UpdateFlagAsync(Snowflake id, [FromBody] UpdateFlagRequest req)
{ {
ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, null)); ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, null));

View file

@ -25,6 +25,7 @@ namespace Foxnouns.Backend.Controllers;
[ApiController] [ApiController]
[Route("/api/internal")] [Route("/api/internal")]
[ApiExplorerSettings(IgnoreApi = true)]
public partial class InternalController(DatabaseContext db) : ControllerBase public partial class InternalController(DatabaseContext db) : ControllerBase
{ {
[GeneratedRegex(@"(\{\w+\})")] [GeneratedRegex(@"(\{\w+\})")]

View file

@ -144,6 +144,7 @@ public class MembersController(
} }
[HttpPatch("/api/v2/users/@me/members/{memberRef}")] [HttpPatch("/api/v2/users/@me/members/{memberRef}")]
[ProducesResponseType<MemberResponse>(statusCode: StatusCodes.Status200OK)]
[Authorize("member.update")] [Authorize("member.update")]
public async Task<IActionResult> UpdateMemberAsync( public async Task<IActionResult> UpdateMemberAsync(
string memberRef, string memberRef,

View file

@ -25,6 +25,7 @@ namespace Foxnouns.Backend.Controllers;
"CA1862:Use the \'StringComparison\' method overloads to perform case-insensitive string comparisons", "CA1862:Use the \'StringComparison\' method overloads to perform case-insensitive string comparisons",
Justification = "Not usable with EFCore" Justification = "Not usable with EFCore"
)] )]
[ApiExplorerSettings(IgnoreApi = true)]
public class SidController(Config config, DatabaseContext db) : ApiControllerBase public class SidController(Config config, DatabaseContext db) : ApiControllerBase
{ {
[HttpGet("{**id}")] [HttpGet("{**id}")]

View file

@ -15,6 +15,7 @@
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Text.Json;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Newtonsoft.Json; using Newtonsoft.Json;
using NodaTime; using NodaTime;
@ -23,6 +24,7 @@ using JsonSerializer = Newtonsoft.Json.JsonSerializer;
namespace Foxnouns.Backend.Database; namespace Foxnouns.Backend.Database;
[JsonConverter(typeof(JsonConverter))] [JsonConverter(typeof(JsonConverter))]
[System.Text.Json.Serialization.JsonConverter(typeof(SystemJsonConverter))]
[TypeConverter(typeof(TypeConverter))] [TypeConverter(typeof(TypeConverter))]
public readonly struct Snowflake(ulong value) : IEquatable<Snowflake> public readonly struct Snowflake(ulong value) : IEquatable<Snowflake>
{ {
@ -96,6 +98,21 @@ public readonly struct Snowflake(ulong value) : IEquatable<Snowflake>
// ReSharper disable once ClassNeverInstantiated.Global // ReSharper disable once ClassNeverInstantiated.Global
public class ValueConverter() : ValueConverter<Snowflake, long>(x => x, x => x); public class ValueConverter() : ValueConverter<Snowflake, long>(x => x, x => x);
private class SystemJsonConverter : System.Text.Json.Serialization.JsonConverter<Snowflake>
{
public override Snowflake Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options
) => ulong.Parse(reader.GetString()!);
public override void Write(
Utf8JsonWriter writer,
Snowflake value,
JsonSerializerOptions options
) => writer.WriteStringValue(value.Value.ToString());
}
private class JsonConverter : JsonConverter<Snowflake> private class JsonConverter : JsonConverter<Snowflake>
{ {
public override void WriteJson( public override void WriteJson(

View file

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
@ -8,39 +8,39 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Coravel" Version="5.0.4"/> <PackageReference Include="Coravel" Version="6.0.0"/>
<PackageReference Include="Coravel.Mailer" Version="5.0.1"/> <PackageReference Include="Coravel.Mailer" Version="7.0.0"/>
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3"/> <PackageReference Include="EFCore.NamingConventions" Version="9.0.0"/>
<PackageReference Include="EntityFrameworkCore.Exceptions.PostgreSQL" Version="8.1.2"/> <PackageReference Include="EntityFrameworkCore.Exceptions.PostgreSQL" Version="8.1.3"/>
<PackageReference Include="Humanizer.Core" Version="2.14.1"/> <PackageReference Include="Humanizer.Core" Version="2.14.1"/>
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0"/> <PackageReference Include="JetBrains.Annotations" Version="2024.3.0"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.7"/> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.0"/>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.7"/> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7"/> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0"/> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0"/>
<PackageReference Include="Minio" Version="6.0.3"/> <PackageReference Include="Minio" Version="6.0.3"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/> <PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
<PackageReference Include="NodaTime" Version="3.1.11"/> <PackageReference Include="NodaTime" Version="3.2.0"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="8.0.4"/> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.2"/>
<PackageReference Include="Npgsql.Json.NET" Version="8.0.3"/> <PackageReference Include="Npgsql.Json.NET" Version="9.0.2"/>
<PackageReference Include="prometheus-net" Version="8.2.1"/> <PackageReference Include="prometheus-net" Version="8.2.1"/>
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/> <PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
<PackageReference Include="Roslynator.Analyzers" Version="4.12.9"> <PackageReference Include="Roslynator.Analyzers" Version="4.12.9">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Sentry.AspNetCore" Version="4.9.0"/> <PackageReference Include="Scalar.AspNetCore" Version="1.2.51"/>
<PackageReference Include="Serilog" Version="4.0.1"/> <PackageReference Include="Sentry.AspNetCore" Version="4.13.0"/>
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1"/> <PackageReference Include="Serilog" Version="4.2.0"/>
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0"/> <PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0"/>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5"/> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.6"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2"/>
<PackageReference Include="System.Text.Json" Version="9.0.0"/> <PackageReference Include="System.Text.Json" Version="9.0.0"/>
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1"/> <PackageReference Include="System.Text.RegularExpressions" Version="4.3.1"/>
</ItemGroup> </ItemGroup>

View file

@ -12,14 +12,18 @@
// //
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
using System.Text.Json;
using System.Text.Json.Serialization;
using Foxnouns.Backend; using Foxnouns.Backend;
using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Extensions;
using Foxnouns.Backend.Services; using Foxnouns.Backend.Services;
using Foxnouns.Backend.Utils; using Foxnouns.Backend.Utils;
using Foxnouns.Backend.Utils.OpenApi;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Serialization; using Newtonsoft.Json.Serialization;
using Prometheus; using Prometheus;
using Scalar.AspNetCore;
using Sentry.Extensibility; using Sentry.Extensibility;
using Serilog; using Serilog;
@ -46,6 +50,13 @@ builder
builder builder
.Services.AddControllers() .Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
options.JsonSerializerOptions.Converters.Add(
new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper)
);
})
.AddNewtonsoftJson(options => .AddNewtonsoftJson(options =>
{ {
options.SerializerSettings.ContractResolver = new PatchRequestContractResolver options.SerializerSettings.ContractResolver = new PatchRequestContractResolver
@ -60,6 +71,16 @@ builder
); );
}); });
builder.Services.AddOpenApi(
"v2",
options =>
{
options.AddSchemaTransformer<PropertyKeySchemaTransformer>();
options.AddSchemaTransformer<ExampleFixingSchemaTransformer>();
options.AddDocumentTransformer(new DocumentTransformer(config));
}
);
// Set the default converter to snake case as we use it in a couple places. // Set the default converter to snake case as we use it in a couple places.
JsonConvert.DefaultSettings = () => JsonConvert.DefaultSettings = () =>
new JsonSerializerSettings new JsonSerializerSettings
@ -70,7 +91,7 @@ JsonConvert.DefaultSettings = () =>
}, },
}; };
builder.AddServices(config).AddCustomMiddleware().AddEndpointsApiExplorer().AddSwaggerGen(); builder.AddServices(config).AddCustomMiddleware();
WebApplication app = builder.Build(); WebApplication app = builder.Build();
@ -83,11 +104,16 @@ app.UseRouting();
// so it's locked behind a config option. // so it's locked behind a config option.
if (config.Logging.SentryTracing) if (config.Logging.SentryTracing)
app.UseSentryTracing(); app.UseSentryTracing();
app.UseSwagger();
app.UseSwaggerUI();
app.UseCors(); app.UseCors();
app.UseCustomMiddleware(); app.UseCustomMiddleware();
app.MapControllers(); app.MapControllers();
app.MapOpenApi("/api-docs/openapi/{documentName}.json");
app.MapScalarApiReference(options =>
{
options.Title = "pronouns.cc API";
options.OpenApiRoutePattern = "/api-docs/openapi/{documentName}.json";
options.EndpointPathPrefix = "/api-docs/{documentName}";
});
app.Urls.Clear(); app.Urls.Clear();
app.Urls.Add(config.Address); app.Urls.Add(config.Address);

View file

@ -12,8 +12,7 @@
// //
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization;
using JetBrains.Annotations;
namespace Foxnouns.Backend.Services.Auth; namespace Foxnouns.Backend.Services.Auth;
@ -66,22 +65,12 @@ public partial class RemoteAuthService
if (user == null) if (user == null)
throw new FoxnounsError("Discord user response was null"); throw new FoxnounsError("Discord user response was null");
return new RemoteUser(user.id, user.username); return new RemoteUser(user.Id, user.Username);
} }
[SuppressMessage( // ReSharper disable once ClassNeverInstantiated.Local
"ReSharper", private record DiscordUserResponse(
"InconsistentNaming", [property: JsonPropertyName("id")] string Id,
Justification = "Easier to use snake_case here, rather than passing in JSON converter options" [property: JsonPropertyName("username")] string Username
)] );
[UsedImplicitly]
private record DiscordTokenResponse(string access_token, string token_type);
[SuppressMessage(
"ReSharper",
"InconsistentNaming",
Justification = "Easier to use snake_case here, rather than passing in JSON converter options"
)]
[UsedImplicitly]
private record DiscordUserResponse(string id, string username);
} }

View file

@ -12,7 +12,6 @@
// //
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
using System.Diagnostics.CodeAnalysis;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@ -69,7 +68,7 @@ public partial class RemoteAuthService
return new RemoteUser(user.Id, user.Email); return new RemoteUser(user.Id, user.Email);
} }
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] // ReSharper disable once ClassNeverInstantiated.Local
private record GoogleTokenResponse([property: JsonPropertyName("id_token")] string IdToken); private record GoogleTokenResponse([property: JsonPropertyName("id_token")] string IdToken);
private record GoogleUser( private record GoogleUser(

View file

@ -14,6 +14,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
// ReSharper disable ClassNeverInstantiated.Local
namespace Foxnouns.Backend.Services.Auth; namespace Foxnouns.Backend.Services.Auth;
public partial class RemoteAuthService public partial class RemoteAuthService
@ -83,11 +85,6 @@ public partial class RemoteAuthService
return new RemoteUser(blog.Uuid, blog.Name); return new RemoteUser(blog.Uuid, blog.Name);
} }
private record OauthTokenResponse(
[property: JsonPropertyName("access_token")] string AccessToken,
[property: JsonPropertyName("token_type")] string TokenType
);
// tumblr why // tumblr why
private record TumblrData( private record TumblrData(
[property: JsonPropertyName("meta")] TumblrMeta Meta, [property: JsonPropertyName("meta")] TumblrMeta Meta,

View file

@ -13,6 +13,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using System.Web; using System.Web;
using Foxnouns.Backend.Database; using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Database.Models;
@ -35,6 +36,12 @@ public partial class RemoteAuthService(
public record RemoteUser(string Id, string Username); public record RemoteUser(string Id, string Username);
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")]
private record OauthTokenResponse(
[property: JsonPropertyName("access_token")] string AccessToken,
[property: JsonPropertyName("token_type")] string TokenType
);
/// <summary> /// <summary>
/// Validates whether a user can still add a new account of the given AuthType, and throws an error if they can't. /// Validates whether a user can still add a new account of the given AuthType, and throws an error if they can't.
/// </summary> /// </summary>

View file

@ -0,0 +1,69 @@
using Foxnouns.Backend.Database;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Newtonsoft.Json.Serialization;
namespace Foxnouns.Backend.Utils.OpenApi;
public class PropertyKeySchemaTransformer : IOpenApiSchemaTransformer
{
private static readonly DefaultContractResolver SnakeCaseConverter =
new() { NamingStrategy = new SnakeCaseNamingStrategy() };
public Task TransformAsync(
OpenApiSchema schema,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken
)
{
Dictionary<string, OpenApiSchema> newProperties = new();
foreach (KeyValuePair<string, OpenApiSchema> property in schema.Properties)
{
newProperties[SnakeCaseConverter.GetResolvedPropertyName(property.Key)] =
property.Value;
}
schema.Properties = newProperties;
schema.Required = schema
.Required.Select(SnakeCaseConverter.GetResolvedPropertyName)
.ToHashSet();
return Task.CompletedTask;
}
}
public class ExampleFixingSchemaTransformer : IOpenApiSchemaTransformer
{
public Task TransformAsync(
OpenApiSchema schema,
OpenApiSchemaTransformerContext context,
CancellationToken cancellationToken
)
{
if (context.JsonTypeInfo.Type == typeof(Snowflake))
{
schema.Type = "string";
schema.Example = new OpenApiString("999999999999999999");
}
return Task.CompletedTask;
}
}
public class DocumentTransformer(Config config) : IOpenApiDocumentTransformer
{
public Task TransformAsync(
OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken cancellationToken
)
{
document.Info.Title = "pronouns.cc API";
document.Info.Version = "2.0.0";
document.Servers.Clear();
document.Servers.Add(new OpenApiServer { Url = config.BaseUrl });
return Task.CompletedTask;
}
}

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
@ -14,13 +14,13 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Humanizer.Core" Version="2.14.1"/> <PackageReference Include="Humanizer.Core" Version="2.14.1"/>
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.1.0"/> <PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.1.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7"/> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4"/> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="8.0.4"/> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.2"/>
</ItemGroup> </ItemGroup>
</Project> </Project>