Skip to content

Update examples #1269

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
image:
- Ubuntu2004
# Downgrade to workaround error NETSDK1194 during 'dotnet pack': The "--output" option isn't supported when building a solution.
# https://stackoverflow.com/questions/75453953/how-to-fix-github-actions-dotnet-publish-workflow-error-the-output-option-i
- Previous Visual Studio 2022
- Visual Studio 2022

version: '{build}'

Expand Down Expand Up @@ -34,7 +32,7 @@ for:
-
matrix:
only:
- image: Previous Visual Studio 2022
- image: Visual Studio 2022
services:
- postgresql15
install:
Expand Down Expand Up @@ -100,6 +98,9 @@ build_script:
Write-Output ".NET version:"
dotnet --version

Write-Output "PowerShell version:"
pwsh --version

Write-Output "PostgreSQL version:"
if ($IsWindows) {
. "${env:ProgramFiles}\PostgreSQL\15\bin\psql" --version
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using BenchmarkDotNet.Attributes;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.Queries.Internal;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Serialization.Objects;
Expand Down Expand Up @@ -130,6 +131,6 @@ protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGr

protected override IEvaluatedIncludeCache CreateEvaluatedIncludeCache(IResourceGraph resourceGraph)
{
return new EvaluatedIncludeCache();
return new EvaluatedIncludeCache(Array.Empty<IQueryConstraintProvider>());
}
}
13 changes: 7 additions & 6 deletions benchmarks/Serialization/ResourceSerializationBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using JsonApiDotNetCore;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.Queries.Expressions;
using JsonApiDotNetCore.Queries.Internal;
using JsonApiDotNetCore.Resources.Annotations;
Expand Down Expand Up @@ -121,12 +122,12 @@ protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGr

protected override IEvaluatedIncludeCache CreateEvaluatedIncludeCache(IResourceGraph resourceGraph)
{
ResourceType resourceAType = resourceGraph.GetResourceType<OutgoingResource>();
ResourceType resourceType = resourceGraph.GetResourceType<OutgoingResource>();

RelationshipAttribute single2 = resourceAType.GetRelationshipByPropertyName(nameof(OutgoingResource.Single2));
RelationshipAttribute single3 = resourceAType.GetRelationshipByPropertyName(nameof(OutgoingResource.Single3));
RelationshipAttribute multi4 = resourceAType.GetRelationshipByPropertyName(nameof(OutgoingResource.Multi4));
RelationshipAttribute multi5 = resourceAType.GetRelationshipByPropertyName(nameof(OutgoingResource.Multi5));
RelationshipAttribute single2 = resourceType.GetRelationshipByPropertyName(nameof(OutgoingResource.Single2));
RelationshipAttribute single3 = resourceType.GetRelationshipByPropertyName(nameof(OutgoingResource.Single3));
RelationshipAttribute multi4 = resourceType.GetRelationshipByPropertyName(nameof(OutgoingResource.Multi4));
RelationshipAttribute multi5 = resourceType.GetRelationshipByPropertyName(nameof(OutgoingResource.Multi5));

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

var cache = new EvaluatedIncludeCache();
var cache = new EvaluatedIncludeCache(Array.Empty<IQueryConstraintProvider>());
cache.Set(include);
return cache;
}
Expand Down
2 changes: 1 addition & 1 deletion docs/build-dev.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#Requires -Version 7.0
#Requires -Version 7.3

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

Expand Down
2 changes: 1 addition & 1 deletion docs/generate-examples.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#Requires -Version 7.0
#Requires -Version 7.3

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

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

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).
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).

So, back to the topic of where to intercept. It helps to familiarize yourself with the [execution pipeline](~/internals/queries.md).
Replacing at the service level is the simplest. But it means you'll need to read the parsed query string parameters and invoke
all resource definition callbacks yourself. And you won't get change detection (HTTP 203 Not Modified).
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.

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.
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.
And most resource definition callbacks are handled.
That's because the built-in resource service translates all JSON:API aspects of the request into a database-agnostic data structure called `QueryLayer`.
Now the hard part for you becomes reading that data structure and producing data access calls from that.
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),
which drives the translation into [System.Linq.Expressions](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/expression-trees/).
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.
We use this for accessing [MongoDB](https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb/blob/674889e037334e3f376550178ce12d0842d7560c/src/JsonApiDotNetCore.MongoDb/Queries/Internal/QueryableBuilding/MongoQueryableBuilder.cs).
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).
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).

> [!TIP]
> [ExpressionTreeVisualizer](https://github.com/zspitz/ExpressionTreeVisualizer) is very helpful in trying to debug LINQ expression trees!
Expand Down
2 changes: 1 addition & 1 deletion docs/internals/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Processing a request involves the following steps:
To get a sense of what this all looks like, let's look at an example query string:

```
/api/v1/blogs?
/api/blogs?
include=owner,posts.comments.author&
filter=has(posts)&
sort=count(posts)&
Expand Down
2 changes: 2 additions & 0 deletions docs/request-examples/001_GET_Books.ps1
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#Requires -Version 7.3

curl -s -f http://localhost:14141/api/books
2 changes: 2 additions & 0 deletions docs/request-examples/002_GET_Person-by-ID.ps1
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#Requires -Version 7.3

curl -s -f http://localhost:14141/api/people/1
2 changes: 2 additions & 0 deletions docs/request-examples/003_GET_Books-including-Author.ps1
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#Requires -Version 7.3

curl -s -f http://localhost:14141/api/books?include=author
2 changes: 2 additions & 0 deletions docs/request-examples/004_GET_Books-PublishYear.ps1
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#Requires -Version 7.3

curl -s -f http://localhost:14141/api/books?fields%5Bbooks%5D=publishYear
2 changes: 2 additions & 0 deletions docs/request-examples/005_GET_People-Filter_Partial.ps1
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#Requires -Version 7.3

curl -s -f "http://localhost:14141/api/people?filter=contains(name,'Shell')"
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#Requires -Version 7.3

curl -s -f http://localhost:14141/api/books?sort=-publishYear
2 changes: 2 additions & 0 deletions docs/request-examples/007_GET_Books-paginated.ps1
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#Requires -Version 7.3

curl -s -f "http://localhost:14141/api/books?page%5Bsize%5D=1&page%5Bnumber%5D=2"
10 changes: 6 additions & 4 deletions docs/request-examples/010_CREATE_Person.ps1
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#Requires -Version 7.3

curl -s -f http://localhost:14141/api/people `
-H "Content-Type: application/vnd.api+json" `
-d '{
\"data\": {
\"type\": \"people\",
\"attributes\": {
\"name\": \"Alice\"
"data": {
"type": "people",
"attributes": {
"name": "Alice"
}
}
}'
22 changes: 12 additions & 10 deletions docs/request-examples/011_CREATE_Book-with-Author.ps1
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
#Requires -Version 7.3

curl -s -f http://localhost:14141/api/books `
-H "Content-Type: application/vnd.api+json" `
-d '{
\"data\": {
\"type\": \"books\",
\"attributes\": {
\"title\": \"Valperga\",
\"publishYear\": 1823
"data": {
"type": "books",
"attributes": {
"title": "Valperga",
"publishYear": 1823
},
\"relationships\": {
\"author\": {
\"data\": {
\"type\": \"people\",
\"id\": \"1\"
"relationships": {
"author": {
"data": {
"type": "people",
"id": "1"
}
}
}
Expand Down
12 changes: 7 additions & 5 deletions docs/request-examples/012_PATCH_Book.ps1
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
#Requires -Version 7.3

curl -s -f http://localhost:14141/api/books/1 `
-H "Content-Type: application/vnd.api+json" `
-X PATCH `
-d '{
\"data\": {
\"type\": \"books\",
\"id\": \"1\",
\"attributes\": {
\"publishYear\": 1820
"data": {
"type": "books",
"id": "1",
"attributes": {
"publishYear": 1820
}
}
}'
2 changes: 2 additions & 0 deletions docs/request-examples/013_DELETE_Book.ps1
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
#Requires -Version 7.3

curl -s -f http://localhost:14141/api/books/1 `
-X DELETE
4 changes: 2 additions & 2 deletions docs/usage/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ options.UseRelativeLinks = true;
"relationships": {
"author": {
"links": {
"self": "/api/v1/articles/4309/relationships/author",
"related": "/api/v1/articles/4309/author"
"self": "/articles/4309/relationships/author",
"related": "/articles/4309/author"
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions docs/usage/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ You can add a namespace to all URLs by specifying it at startup.

```c#
// Program.cs
builder.Services.AddJsonApi<AppDbContext>(options => options.Namespace = "api/v1");
builder.Services.AddJsonApi<AppDbContext>(options => options.Namespace = "api/shopping");
```

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

## Default routing convention

Expand Down Expand Up @@ -66,14 +66,14 @@ It is possible to override the default routing convention for an auto-generated
```c#
// Auto-generated
[DisableRoutingConvention]
[Route("v1/custom/route/summaries-for-orders")]
[Route("custom/route/summaries-for-orders")]
partial class OrderSummariesController
{
}

// Hand-written
[DisableRoutingConvention]
[Route("v1/custom/route/lines-in-order")]
[Route("custom/route/lines-in-order")]
public class OrderLineController : JsonApiController<OrderLine, int>
{
public OrderLineController(IJsonApiOptions options, IResourceGraph resourceGraph,
Expand Down
9 changes: 5 additions & 4 deletions src/Examples/DatabasePerTenantExample/Data/AppDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ public sealed class AppDbContext : DbContext

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

public AppDbContext(IHttpContextAccessor httpContextAccessor, IConfiguration configuration)
public AppDbContext(DbContextOptions<AppDbContext> options, IHttpContextAccessor httpContextAccessor, IConfiguration configuration)
: base(options)
{
_httpContextAccessor = httpContextAccessor;
_configuration = configuration;
Expand All @@ -27,16 +28,16 @@ public void SetTenantName(string tenantName)
_forcedTenantName = tenantName;
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
protected override void OnConfiguring(DbContextOptionsBuilder builder)
{
string connectionString = GetConnectionString();
optionsBuilder.UseNpgsql(connectionString);
builder.UseNpgsql(connectionString);
}

private string GetConnectionString()
{
string? tenantName = GetTenantName();
string? connectionString = _configuration[$"Data:{tenantName ?? "Default"}Connection"];
string? connectionString = _configuration.GetConnectionString(tenantName ?? "Default");

if (connectionString == null)
{
Expand Down
39 changes: 28 additions & 11 deletions src/Examples/DatabasePerTenantExample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
using System.Diagnostics;
using DatabasePerTenantExample.Data;
using DatabasePerTenantExample.Models;
using JsonApiDotNetCore.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.AddDbContext<AppDbContext>(options => options.UseNpgsql());

builder.Services.AddDbContext<AppDbContext>(options => SetDbContextDebugOptions(options));

builder.Services.AddJsonApi<AppDbContext>(options =>
{
options.Namespace = "api";
options.UseRelativeLinks = true;
options.IncludeTotalResourceCount = true;

#if DEBUG
options.IncludeExceptionStackTraceInErrors = true;
options.IncludeRequestBodyInErrors = true;
options.SerializerOptions.WriteIndented = true;
#endif
});

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

app.Run();

[Conditional("DEBUG")]
static void SetDbContextDebugOptions(DbContextOptionsBuilder options)
{
options.EnableDetailedErrors();
options.EnableSensitiveDataLogging();
options.ConfigureWarnings(builder => builder.Ignore(CoreEventId.SensitiveDataLoggingEnabledWarning));
}

static async Task CreateDatabaseAsync(string? tenantName, IServiceProvider serviceProvider)
{
await using AsyncServiceScope scope = serviceProvider.CreateAsyncScope();
Expand All @@ -41,18 +58,18 @@ static async Task CreateDatabaseAsync(string? tenantName, IServiceProvider servi
dbContext.SetTenantName(tenantName);
}

await dbContext.Database.EnsureDeletedAsync();
await dbContext.Database.EnsureCreatedAsync();

if (tenantName != null)
if (await dbContext.Database.EnsureCreatedAsync())
{
dbContext.Employees.Add(new Employee
if (tenantName != null)
{
FirstName = "John",
LastName = "Doe",
CompanyName = tenantName
});
dbContext.Employees.Add(new Employee
{
FirstName = "John",
LastName = "Doe",
CompanyName = tenantName
});

await dbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync();
}
}
}
10 changes: 6 additions & 4 deletions src/Examples/DatabasePerTenantExample/appsettings.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
{
"Data": {
"DefaultConnection": "Host=localhost;Port=5432;Database=DefaultTenantDb;User ID=postgres;Password=###",
"AdventureWorksConnection": "Host=localhost;Port=5432;Database=AdventureWorks;User ID=postgres;Password=###",
"ContosoConnection": "Host=localhost;Port=5432;Database=Contoso;User ID=postgres;Password=###"
"ConnectionStrings": {
"Default": "Host=localhost;Database=DefaultTenantDb;User ID=postgres;Password=###;Include Error Detail=true",
"AdventureWorks": "Host=localhost;Database=AdventureWorks;User ID=postgres;Password=###;Include Error Detail=true",
"Contoso": "Host=localhost;Database=Contoso;User ID=postgres;Password=###;Include Error Detail=true"
},
"Logging": {
"LogLevel": {
"Default": "Warning",
// Include server startup, incoming requests and SQL commands.
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.AspNetCore.Hosting.Diagnostics": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
},
Expand Down
Loading