Skip to content

Commit 338c63e

Browse files
authored
Merge branch 'master' into harden-attributes
2 parents b51d681 + 5301d04 commit 338c63e

File tree

91 files changed

+3032
-491
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+3032
-491
lines changed

appveyor.yml

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
image:
22
- Ubuntu2004
3-
# Downgrade to workaround error NETSDK1194 during 'dotnet pack': The "--output" option isn't supported when building a solution.
4-
# https://stackoverflow.com/questions/75453953/how-to-fix-github-actions-dotnet-publish-workflow-error-the-output-option-i
5-
- Previous Visual Studio 2022
3+
- Visual Studio 2022
64

75
version: '{build}'
86

@@ -34,7 +32,7 @@ for:
3432
-
3533
matrix:
3634
only:
37-
- image: Previous Visual Studio 2022
35+
- image: Visual Studio 2022
3836
services:
3937
- postgresql15
4038
install:
@@ -100,6 +98,9 @@ build_script:
10098
Write-Output ".NET version:"
10199
dotnet --version
102100
101+
Write-Output "PowerShell version:"
102+
pwsh --version
103+
103104
Write-Output "PostgreSQL version:"
104105
if ($IsWindows) {
105106
. "${env:ProgramFiles}\PostgreSQL\15\bin\psql" --version

benchmarks/Serialization/OperationsSerializationBenchmarks.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using BenchmarkDotNet.Attributes;
33
using JsonApiDotNetCore.Configuration;
44
using JsonApiDotNetCore.Middleware;
5+
using JsonApiDotNetCore.Queries;
56
using JsonApiDotNetCore.Queries.Internal;
67
using JsonApiDotNetCore.Resources;
78
using JsonApiDotNetCore.Serialization.Objects;
@@ -130,6 +131,6 @@ protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGr
130131

131132
protected override IEvaluatedIncludeCache CreateEvaluatedIncludeCache(IResourceGraph resourceGraph)
132133
{
133-
return new EvaluatedIncludeCache();
134+
return new EvaluatedIncludeCache(Array.Empty<IQueryConstraintProvider>());
134135
}
135136
}

benchmarks/Serialization/ResourceSerializationBenchmarks.cs

+7-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using JsonApiDotNetCore;
55
using JsonApiDotNetCore.Configuration;
66
using JsonApiDotNetCore.Middleware;
7+
using JsonApiDotNetCore.Queries;
78
using JsonApiDotNetCore.Queries.Expressions;
89
using JsonApiDotNetCore.Queries.Internal;
910
using JsonApiDotNetCore.Resources.Annotations;
@@ -121,12 +122,12 @@ protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGr
121122

122123
protected override IEvaluatedIncludeCache CreateEvaluatedIncludeCache(IResourceGraph resourceGraph)
123124
{
124-
ResourceType resourceAType = resourceGraph.GetResourceType<OutgoingResource>();
125+
ResourceType resourceType = resourceGraph.GetResourceType<OutgoingResource>();
125126

126-
RelationshipAttribute single2 = resourceAType.GetRelationshipByPropertyName(nameof(OutgoingResource.Single2));
127-
RelationshipAttribute single3 = resourceAType.GetRelationshipByPropertyName(nameof(OutgoingResource.Single3));
128-
RelationshipAttribute multi4 = resourceAType.GetRelationshipByPropertyName(nameof(OutgoingResource.Multi4));
129-
RelationshipAttribute multi5 = resourceAType.GetRelationshipByPropertyName(nameof(OutgoingResource.Multi5));
127+
RelationshipAttribute single2 = resourceType.GetRelationshipByPropertyName(nameof(OutgoingResource.Single2));
128+
RelationshipAttribute single3 = resourceType.GetRelationshipByPropertyName(nameof(OutgoingResource.Single3));
129+
RelationshipAttribute multi4 = resourceType.GetRelationshipByPropertyName(nameof(OutgoingResource.Multi4));
130+
RelationshipAttribute multi5 = resourceType.GetRelationshipByPropertyName(nameof(OutgoingResource.Multi5));
130131

131132
var include = new IncludeExpression(new HashSet<IncludeElementExpression>
132133
{
@@ -142,7 +143,7 @@ protected override IEvaluatedIncludeCache CreateEvaluatedIncludeCache(IResourceG
142143
}.ToImmutableHashSet())
143144
}.ToImmutableHashSet());
144145

145-
var cache = new EvaluatedIncludeCache();
146+
var cache = new EvaluatedIncludeCache(Array.Empty<IQueryConstraintProvider>());
146147
cache.Set(include);
147148
return cache;
148149
}

docs/build-dev.ps1

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#Requires -Version 7.0
1+
#Requires -Version 7.3
22

33
# This script builds the documentation website, starts a web server and opens the site in your browser. Intended for local development.
44

docs/generate-examples.ps1

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#Requires -Version 7.0
1+
#Requires -Version 7.3
22

33
# This script generates response documents for ./request-examples
44

docs/getting-started/faq.md

+6-6
Original file line numberDiff line numberDiff line change
@@ -133,24 +133,24 @@ Here are some injectable request-scoped types to be aware of:
133133
- `IJsonApiRequest`: This contains routing information, such as whether a primary, secondary, or relationship endpoint is being accessed.
134134
- `ITargetedFields`: Lists the attributes and relationships from an incoming POST/PATCH resource request. Any fields missing there should not be stored (partial updates).
135135
- `IEnumerable<IQueryConstraintProvider>`: Provides access to the parsed query string parameters.
136-
- `IEvaluatedIncludeCache`: This tells the response serializer which related resources to render, which you need to populate.
137-
- `ISparseFieldSetCache`: This tells the response serializer which fields to render in the attributes and relationship objects. You need to populate this as well.
136+
- `IEvaluatedIncludeCache`: This tells the response serializer which related resources to render.
137+
- `ISparseFieldSetCache`: This tells the response serializer which fields to render in the `attributes` and `relationships` objects.
138138

139-
You may also want to inject the singletons `IJsonApiOptions` (which contains settings such as default page size) and `IResourceGraph` (the JSON:API model of resources and relationships).
139+
You may also want to inject the singletons `IJsonApiOptions` (which contains settings such as default page size) and `IResourceGraph` (the JSON:API model of resources, attributes and relationships).
140140

141141
So, back to the topic of where to intercept. It helps to familiarize yourself with the [execution pipeline](~/internals/queries.md).
142142
Replacing at the service level is the simplest. But it means you'll need to read the parsed query string parameters and invoke
143143
all resource definition callbacks yourself. And you won't get change detection (HTTP 203 Not Modified).
144144
Take a look at [JsonApiResourceService](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs) to see what you're missing out on.
145145

146-
You'll get a lot more out of the box if replacing at the repository level instead. You don't need to apply options, analyze query strings or populate caches for the serializer.
146+
You'll get a lot more out of the box if replacing at the repository level instead. You don't need to apply options or analyze query strings.
147147
And most resource definition callbacks are handled.
148148
That's because the built-in resource service translates all JSON:API aspects of the request into a database-agnostic data structure called `QueryLayer`.
149149
Now the hard part for you becomes reading that data structure and producing data access calls from that.
150150
If your data store provides a LINQ provider, you may reuse most of [QueryableBuilder](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs),
151151
which drives the translation into [System.Linq.Expressions](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/expression-trees/).
152-
Note however, that it also produces calls to `.Include("")`, which is an Entity Framework Core-specific extension method, so you'll likely need to prevent that from happening.
153-
We use this for accessing [MongoDB](https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb/blob/674889e037334e3f376550178ce12d0842d7560c/src/JsonApiDotNetCore.MongoDb/Queries/Internal/QueryableBuilding/MongoQueryableBuilder.cs).
152+
Note however, that it also produces calls to `.Include("")`, which is an Entity Framework Core-specific extension method, so you'll likely need to prevent that from happening. There's an example [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/Examples/NoEntityFrameworkExample/Repositories/InMemoryResourceRepository.cs).
153+
We use a similar approach for accessing [MongoDB](https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb/blob/674889e037334e3f376550178ce12d0842d7560c/src/JsonApiDotNetCore.MongoDb/Queries/Internal/QueryableBuilding/MongoQueryableBuilder.cs).
154154

155155
> [!TIP]
156156
> [ExpressionTreeVisualizer](https://github.com/zspitz/ExpressionTreeVisualizer) is very helpful in trying to debug LINQ expression trees!

docs/internals/queries.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Processing a request involves the following steps:
2929
To get a sense of what this all looks like, let's look at an example query string:
3030

3131
```
32-
/api/v1/blogs?
32+
/api/blogs?
3333
include=owner,posts.comments.author&
3434
filter=has(posts)&
3535
sort=count(posts)&
+2
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
#Requires -Version 7.3
2+
13
curl -s -f http://localhost:14141/api/books
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
#Requires -Version 7.3
2+
13
curl -s -f http://localhost:14141/api/people/1
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
#Requires -Version 7.3
2+
13
curl -s -f http://localhost:14141/api/books?include=author
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
#Requires -Version 7.3
2+
13
curl -s -f http://localhost:14141/api/books?fields%5Bbooks%5D=publishYear
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
#Requires -Version 7.3
2+
13
curl -s -f "http://localhost:14141/api/people?filter=contains(name,'Shell')"
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
#Requires -Version 7.3
2+
13
curl -s -f http://localhost:14141/api/books?sort=-publishYear
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
#Requires -Version 7.3
2+
13
curl -s -f "http://localhost:14141/api/books?page%5Bsize%5D=1&page%5Bnumber%5D=2"
+6-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
#Requires -Version 7.3
2+
13
curl -s -f http://localhost:14141/api/people `
24
-H "Content-Type: application/vnd.api+json" `
35
-d '{
4-
\"data\": {
5-
\"type\": \"people\",
6-
\"attributes\": {
7-
\"name\": \"Alice\"
6+
"data": {
7+
"type": "people",
8+
"attributes": {
9+
"name": "Alice"
810
}
911
}
1012
}'

docs/request-examples/011_CREATE_Book-with-Author.ps1

+12-10
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1+
#Requires -Version 7.3
2+
13
curl -s -f http://localhost:14141/api/books `
24
-H "Content-Type: application/vnd.api+json" `
35
-d '{
4-
\"data\": {
5-
\"type\": \"books\",
6-
\"attributes\": {
7-
\"title\": \"Valperga\",
8-
\"publishYear\": 1823
6+
"data": {
7+
"type": "books",
8+
"attributes": {
9+
"title": "Valperga",
10+
"publishYear": 1823
911
},
10-
\"relationships\": {
11-
\"author\": {
12-
\"data\": {
13-
\"type\": \"people\",
14-
\"id\": \"1\"
12+
"relationships": {
13+
"author": {
14+
"data": {
15+
"type": "people",
16+
"id": "1"
1517
}
1618
}
1719
}
+7-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
#Requires -Version 7.3
2+
13
curl -s -f http://localhost:14141/api/books/1 `
24
-H "Content-Type: application/vnd.api+json" `
35
-X PATCH `
46
-d '{
5-
\"data\": {
6-
\"type\": \"books\",
7-
\"id\": \"1\",
8-
\"attributes\": {
9-
\"publishYear\": 1820
7+
"data": {
8+
"type": "books",
9+
"id": "1",
10+
"attributes": {
11+
"publishYear": 1820
1012
}
1113
}
1214
}'
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
#Requires -Version 7.3
2+
13
curl -s -f http://localhost:14141/api/books/1 `
24
-X DELETE

docs/usage/options.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ options.UseRelativeLinks = true;
5555
"relationships": {
5656
"author": {
5757
"links": {
58-
"self": "/api/v1/articles/4309/relationships/author",
59-
"related": "/api/v1/articles/4309/author"
58+
"self": "/articles/4309/relationships/author",
59+
"related": "/articles/4309/author"
6060
}
6161
}
6262
}

docs/usage/routing.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ You can add a namespace to all URLs by specifying it at startup.
1111

1212
```c#
1313
// Program.cs
14-
builder.Services.AddJsonApi<AppDbContext>(options => options.Namespace = "api/v1");
14+
builder.Services.AddJsonApi<AppDbContext>(options => options.Namespace = "api/shopping");
1515
```
1616

17-
Which results in URLs like: https://yourdomain.com/api/v1/people
17+
Which results in URLs like: https://yourdomain.com/api/shopping/articles
1818

1919
## Default routing convention
2020

@@ -66,14 +66,14 @@ It is possible to override the default routing convention for an auto-generated
6666
```c#
6767
// Auto-generated
6868
[DisableRoutingConvention]
69-
[Route("v1/custom/route/summaries-for-orders")]
69+
[Route("custom/route/summaries-for-orders")]
7070
partial class OrderSummariesController
7171
{
7272
}
7373

7474
// Hand-written
7575
[DisableRoutingConvention]
76-
[Route("v1/custom/route/lines-in-order")]
76+
[Route("custom/route/lines-in-order")]
7777
public class OrderLineController : JsonApiController<OrderLine, int>
7878
{
7979
public OrderLineController(IJsonApiOptions options, IResourceGraph resourceGraph,

src/Examples/DatabasePerTenantExample/Data/AppDbContext.cs

+5-4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ public sealed class AppDbContext : DbContext
1616

1717
public DbSet<Employee> Employees => Set<Employee>();
1818

19-
public AppDbContext(IHttpContextAccessor httpContextAccessor, IConfiguration configuration)
19+
public AppDbContext(DbContextOptions<AppDbContext> options, IHttpContextAccessor httpContextAccessor, IConfiguration configuration)
20+
: base(options)
2021
{
2122
_httpContextAccessor = httpContextAccessor;
2223
_configuration = configuration;
@@ -27,16 +28,16 @@ public void SetTenantName(string tenantName)
2728
_forcedTenantName = tenantName;
2829
}
2930

30-
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
31+
protected override void OnConfiguring(DbContextOptionsBuilder builder)
3132
{
3233
string connectionString = GetConnectionString();
33-
optionsBuilder.UseNpgsql(connectionString);
34+
builder.UseNpgsql(connectionString);
3435
}
3536

3637
private string GetConnectionString()
3738
{
3839
string? tenantName = GetTenantName();
39-
string? connectionString = _configuration[$"Data:{tenantName ?? "Default"}Connection"];
40+
string? connectionString = _configuration.GetConnectionString(tenantName ?? "Default");
4041

4142
if (connectionString == null)
4243
{

src/Examples/DatabasePerTenantExample/Program.cs

+28-11
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
1+
using System.Diagnostics;
12
using DatabasePerTenantExample.Data;
23
using DatabasePerTenantExample.Models;
34
using JsonApiDotNetCore.Configuration;
45
using Microsoft.EntityFrameworkCore;
6+
using Microsoft.EntityFrameworkCore.Diagnostics;
57

68
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
79

810
// Add services to the container.
911

1012
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
11-
builder.Services.AddDbContext<AppDbContext>(options => options.UseNpgsql());
13+
14+
builder.Services.AddDbContext<AppDbContext>(options => SetDbContextDebugOptions(options));
1215

1316
builder.Services.AddJsonApi<AppDbContext>(options =>
1417
{
1518
options.Namespace = "api";
1619
options.UseRelativeLinks = true;
20+
options.IncludeTotalResourceCount = true;
21+
22+
#if DEBUG
23+
options.IncludeExceptionStackTraceInErrors = true;
24+
options.IncludeRequestBodyInErrors = true;
1725
options.SerializerOptions.WriteIndented = true;
26+
#endif
1827
});
1928

2029
WebApplication app = builder.Build();
@@ -31,6 +40,14 @@
3140

3241
app.Run();
3342

43+
[Conditional("DEBUG")]
44+
static void SetDbContextDebugOptions(DbContextOptionsBuilder options)
45+
{
46+
options.EnableDetailedErrors();
47+
options.EnableSensitiveDataLogging();
48+
options.ConfigureWarnings(builder => builder.Ignore(CoreEventId.SensitiveDataLoggingEnabledWarning));
49+
}
50+
3451
static async Task CreateDatabaseAsync(string? tenantName, IServiceProvider serviceProvider)
3552
{
3653
await using AsyncServiceScope scope = serviceProvider.CreateAsyncScope();
@@ -41,18 +58,18 @@ static async Task CreateDatabaseAsync(string? tenantName, IServiceProvider servi
4158
dbContext.SetTenantName(tenantName);
4259
}
4360

44-
await dbContext.Database.EnsureDeletedAsync();
45-
await dbContext.Database.EnsureCreatedAsync();
46-
47-
if (tenantName != null)
61+
if (await dbContext.Database.EnsureCreatedAsync())
4862
{
49-
dbContext.Employees.Add(new Employee
63+
if (tenantName != null)
5064
{
51-
FirstName = "John",
52-
LastName = "Doe",
53-
CompanyName = tenantName
54-
});
65+
dbContext.Employees.Add(new Employee
66+
{
67+
FirstName = "John",
68+
LastName = "Doe",
69+
CompanyName = tenantName
70+
});
5571

56-
await dbContext.SaveChangesAsync();
72+
await dbContext.SaveChangesAsync();
73+
}
5774
}
5875
}

src/Examples/DatabasePerTenantExample/appsettings.json

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
{
2-
"Data": {
3-
"DefaultConnection": "Host=localhost;Port=5432;Database=DefaultTenantDb;User ID=postgres;Password=###",
4-
"AdventureWorksConnection": "Host=localhost;Port=5432;Database=AdventureWorks;User ID=postgres;Password=###",
5-
"ContosoConnection": "Host=localhost;Port=5432;Database=Contoso;User ID=postgres;Password=###"
2+
"ConnectionStrings": {
3+
"Default": "Host=localhost;Database=DefaultTenantDb;User ID=postgres;Password=###;Include Error Detail=true",
4+
"AdventureWorks": "Host=localhost;Database=AdventureWorks;User ID=postgres;Password=###;Include Error Detail=true",
5+
"Contoso": "Host=localhost;Database=Contoso;User ID=postgres;Password=###;Include Error Detail=true"
66
},
77
"Logging": {
88
"LogLevel": {
99
"Default": "Warning",
10+
// Include server startup, incoming requests and SQL commands.
1011
"Microsoft.Hosting.Lifetime": "Information",
12+
"Microsoft.AspNetCore.Hosting.Diagnostics": "Information",
1113
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
1214
}
1315
},

0 commit comments

Comments
 (0)