Skip to content

Commit

Permalink
Merge branch 'master' into merge-master-dapper-into-openapi
Browse files Browse the repository at this point in the history
  • Loading branch information
bkoelman committed Nov 12, 2023
2 parents 65a356f + 24b9546 commit a3975b7
Show file tree
Hide file tree
Showing 133 changed files with 14,008 additions and 71 deletions.
4 changes: 2 additions & 2 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
"version": "2023.2.2",
"version": "2023.2.3",
"commands": [
"jb"
]
Expand All @@ -21,7 +21,7 @@
]
},
"docfx": {
"version": "2.71.1",
"version": "2.72.1",
"commands": [
"docfx"
]
Expand Down
5 changes: 3 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,21 @@
<CSharpGuidelinesAnalyzerVersion>3.8.*</CSharpGuidelinesAnalyzerVersion>
<CodeAnalysisVersion>4.7.*</CodeAnalysisVersion>
<CoverletVersion>6.0.*</CoverletVersion>
<DapperVersion>2.1.*</DapperVersion>
<DateOnlyTimeOnlyVersion>2.1.*</DateOnlyTimeOnlyVersion>
<EntityFrameworkCoreVersion>7.0.*</EntityFrameworkCoreVersion>
<FluentAssertionsVersion>6.12.*</FluentAssertionsVersion>
<GitHubActionsTestLoggerVersion>2.3.*</GitHubActionsTestLoggerVersion>
<InheritDocVersion>1.3.*</InheritDocVersion>
<JetBrainsAnnotationsVersion>2023.2.*</JetBrainsAnnotationsVersion>
<JetBrainsAnnotationsVersion>2023.3.*</JetBrainsAnnotationsVersion>
<MicrosoftApiClientVersion>7.0.*</MicrosoftApiClientVersion>
<NSwagApiClientVersion>13.20.*</NSwagApiClientVersion>
<NewtonsoftJsonVersion>13.0.*</NewtonsoftJsonVersion>
<NpgsqlVersion>7.0.*</NpgsqlVersion>
<SourceLinkVersion>1.1.*</SourceLinkVersion>
<SwashbuckleVersion>6.5.*</SwashbuckleVersion>
<SystemTextJsonVersion>7.0.*</SystemTextJsonVersion>
<TestSdkVersion>17.7.*</TestSdkVersion>
<TestSdkVersion>17.8.*</TestSdkVersion>
<XunitVersion>2.5.*</XunitVersion>
</PropertyGroup>

Expand Down
30 changes: 30 additions & 0 deletions JsonApiDotNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatabasePerTenantExample",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnnotationTests", "test\AnnotationTests\AnnotationTests.csproj", "{24B0C12F-38CD-4245-8785-87BEFAD55B00}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DapperExample", "src\Examples\DapperExample\DapperExample.csproj", "{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DapperTests", "test\DapperTests\DapperTests.csproj", "{80E322F5-5F5D-4670-A30F-02D33C2C7900}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCore.OpenApi", "src\JsonApiDotNetCore.OpenApi\JsonApiDotNetCore.OpenApi.csproj", "{71287D6F-6C3B-44B4-9FCA-E78FE3F02289}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenApiTests", "test\OpenApiTests\OpenApiTests.csproj", "{B693DE14-BB28-496F-AB39-B4E674ABCA80}"
Expand Down Expand Up @@ -294,6 +298,30 @@ Global
{24B0C12F-38CD-4245-8785-87BEFAD55B00}.Release|x64.Build.0 = Release|Any CPU
{24B0C12F-38CD-4245-8785-87BEFAD55B00}.Release|x86.ActiveCfg = Release|Any CPU
{24B0C12F-38CD-4245-8785-87BEFAD55B00}.Release|x86.Build.0 = Release|Any CPU
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Debug|x64.ActiveCfg = Debug|Any CPU
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Debug|x64.Build.0 = Debug|Any CPU
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Debug|x86.ActiveCfg = Debug|Any CPU
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Debug|x86.Build.0 = Debug|Any CPU
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Release|Any CPU.Build.0 = Release|Any CPU
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Release|x64.ActiveCfg = Release|Any CPU
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Release|x64.Build.0 = Release|Any CPU
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Release|x86.ActiveCfg = Release|Any CPU
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Release|x86.Build.0 = Release|Any CPU
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Debug|Any CPU.Build.0 = Debug|Any CPU
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Debug|x64.ActiveCfg = Debug|Any CPU
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Debug|x64.Build.0 = Debug|Any CPU
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Debug|x86.ActiveCfg = Debug|Any CPU
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Debug|x86.Build.0 = Debug|Any CPU
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Release|Any CPU.ActiveCfg = Release|Any CPU
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Release|Any CPU.Build.0 = Release|Any CPU
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Release|x64.ActiveCfg = Release|Any CPU
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Release|x64.Build.0 = Release|Any CPU
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Release|x86.ActiveCfg = Release|Any CPU
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Release|x86.Build.0 = Release|Any CPU
{71287D6F-6C3B-44B4-9FCA-E78FE3F02289}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{71287D6F-6C3B-44B4-9FCA-E78FE3F02289}.Debug|Any CPU.Build.0 = Debug|Any CPU
{71287D6F-6C3B-44B4-9FCA-E78FE3F02289}.Debug|x64.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -389,6 +417,8 @@ Global
{83FF097C-C8C6-477B-9FAB-DF99B84978B5} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}
{60334658-BE51-43B3-9C4D-F2BBF56C89CE} = {026FBC6C-AF76-4568-9B87-EC73457899FD}
{24B0C12F-38CD-4245-8785-87BEFAD55B00} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2} = {026FBC6C-AF76-4568-9B87-EC73457899FD}
{80E322F5-5F5D-4670-A30F-02D33C2C7900} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
{71287D6F-6C3B-44B4-9FCA-E78FE3F02289} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}
{B693DE14-BB28-496F-AB39-B4E674ABCA80} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}
Expand Down
4 changes: 4 additions & 0 deletions JsonApiDotNetCore.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -662,8 +662,12 @@ $left$ = $right$;</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=linebreaks/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Microservices/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=navigations/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Npgsql/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=parallelize/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=parameterless/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=playlists/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Pomelo/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Postgre/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Rewriter/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Startups/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=subdirectory/@EntryIndexedValue">True</s:Boolean>
Expand Down
14 changes: 10 additions & 4 deletions docs/getting-started/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,18 @@ Take a look at [JsonApiResourceService](https://github.com/json-api-dotnet/JsonA

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`.
That's because the built-in resource service translates all JSON:API query 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),
If your data store provides a LINQ provider, you can probably reuse [QueryableBuilder](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/JsonApiDotNetCore/Queries/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. 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).
Note however, that it also produces calls to `.Include("")`, which is an Entity Framework Core-specific extension method, so you'll need to
[prevent that from happening](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryLayerIncludeConverter.cs).

The example [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/Examples/NoEntityFrameworkExample/Repositories/InMemoryResourceRepository.cs) compiles and executes
the LINQ query against an in-memory list of resources.
For [MongoDB](https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb/blob/master/src/JsonApiDotNetCore.MongoDb/Repositories/MongoRepository.cs), we use the MongoDB LINQ provider.
If there's no LINQ provider available, the example [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/Examples/DapperExample/Repositories/DapperRepository.cs) may be of help,
which produces SQL and uses [Dapper](https://github.com/DapperLib/Dapper) for data access.

> [!TIP]
> [ExpressionTreeVisualizer](https://github.com/zspitz/ExpressionTreeVisualizer) is very helpful in trying to debug LINQ expression trees!
Expand Down
61 changes: 61 additions & 0 deletions src/Examples/DapperExample/AtomicOperations/AmbientTransaction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using System.Data.Common;
using JsonApiDotNetCore;
using JsonApiDotNetCore.AtomicOperations;

namespace DapperExample.AtomicOperations;

/// <summary>
/// Represents an ADO.NET transaction in a JSON:API atomic:operations request.
/// </summary>
internal sealed class AmbientTransaction : IOperationsTransaction
{
private readonly AmbientTransactionFactory _owner;

public DbTransaction Current { get; }

/// <inheritdoc />
public string TransactionId { get; }

public AmbientTransaction(AmbientTransactionFactory owner, DbTransaction current, Guid transactionId)
{
ArgumentGuard.NotNull(owner);
ArgumentGuard.NotNull(current);

_owner = owner;
Current = current;
TransactionId = transactionId.ToString();
}

/// <inheritdoc />
public Task BeforeProcessOperationAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}

/// <inheritdoc />
public Task AfterProcessOperationAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}

/// <inheritdoc />
public Task CommitAsync(CancellationToken cancellationToken)
{
return Current.CommitAsync(cancellationToken);
}

/// <inheritdoc />
public async ValueTask DisposeAsync()
{
DbConnection? connection = Current.Connection;

await Current.DisposeAsync();

if (connection != null)
{
await connection.DisposeAsync();
}

_owner.Detach(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Data.Common;
using DapperExample.TranslationToSql.DataModel;
using JsonApiDotNetCore;
using JsonApiDotNetCore.AtomicOperations;
using JsonApiDotNetCore.Configuration;

namespace DapperExample.AtomicOperations;

/// <summary>
/// Provides transaction support for JSON:API atomic:operation requests using ADO.NET.
/// </summary>
public sealed class AmbientTransactionFactory : IOperationsTransactionFactory
{
private readonly IJsonApiOptions _options;
private readonly IDataModelService _dataModelService;

internal AmbientTransaction? AmbientTransaction { get; private set; }

public AmbientTransactionFactory(IJsonApiOptions options, IDataModelService dataModelService)
{
ArgumentGuard.NotNull(options);
ArgumentGuard.NotNull(dataModelService);

_options = options;
_dataModelService = dataModelService;
}

internal async Task<AmbientTransaction> BeginTransactionAsync(CancellationToken cancellationToken)
{
var instance = (IOperationsTransactionFactory)this;

IOperationsTransaction transaction = await instance.BeginTransactionAsync(cancellationToken);
return (AmbientTransaction)transaction;
}

async Task<IOperationsTransaction> IOperationsTransactionFactory.BeginTransactionAsync(CancellationToken cancellationToken)
{
if (AmbientTransaction != null)
{
throw new InvalidOperationException("Cannot start transaction because another transaction is already active.");
}

DbConnection dbConnection = _dataModelService.CreateConnection();

try
{
await dbConnection.OpenAsync(cancellationToken);

DbTransaction transaction = _options.TransactionIsolationLevel != null
? await dbConnection.BeginTransactionAsync(_options.TransactionIsolationLevel.Value, cancellationToken)
: await dbConnection.BeginTransactionAsync(cancellationToken);

var transactionId = Guid.NewGuid();
AmbientTransaction = new AmbientTransaction(this, transaction, transactionId);

return AmbientTransaction;
}
catch (DbException)
{
await dbConnection.DisposeAsync();
throw;
}
}

internal void Detach(AmbientTransaction ambientTransaction)
{
ArgumentGuard.NotNull(ambientTransaction);

if (AmbientTransaction != null && AmbientTransaction == ambientTransaction)
{
AmbientTransaction = null;
}
else
{
throw new InvalidOperationException("Failed to detach ambient transaction.");
}
}
}
16 changes: 16 additions & 0 deletions src/Examples/DapperExample/Controllers/OperationsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using JsonApiDotNetCore.AtomicOperations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Controllers;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Resources;

namespace DapperExample.Controllers;

public sealed class OperationsController : JsonApiOperationsController
{
public OperationsController(IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor,
IJsonApiRequest request, ITargetedFields targetedFields)
: base(options, resourceGraph, loggerFactory, processor, request, targetedFields)
{
}
}
19 changes: 19 additions & 0 deletions src/Examples/DapperExample/DapperExample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>$(TargetFrameworkName)</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\JsonApiDotNetCore\JsonApiDotNetCore.csproj" />
<ProjectReference Include="..\..\JsonApiDotNetCore.SourceGenerators\JsonApiDotNetCore.SourceGenerators.csproj" OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Dapper" Version="$(DapperVersion)" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="$(EntityFrameworkCoreVersion)" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="$(EntityFrameworkCoreVersion)" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="$(NpgsqlVersion)" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="$(EntityFrameworkCoreVersion)" />
</ItemGroup>
</Project>
81 changes: 81 additions & 0 deletions src/Examples/DapperExample/Data/AppDbContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using DapperExample.Models;
using JetBrains.Annotations;
using JsonApiDotNetCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

// @formatter:wrap_chained_method_calls chop_always

namespace DapperExample.Data;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
public sealed class AppDbContext : DbContext
{
private readonly IConfiguration _configuration;

public DbSet<TodoItem> TodoItems => Set<TodoItem>();
public DbSet<Person> People => Set<Person>();
public DbSet<LoginAccount> LoginAccounts => Set<LoginAccount>();
public DbSet<AccountRecovery> AccountRecoveries => Set<AccountRecovery>();
public DbSet<Tag> Tags => Set<Tag>();
public DbSet<RgbColor> RgbColors => Set<RgbColor>();

public AppDbContext(DbContextOptions<AppDbContext> options, IConfiguration configuration)
: base(options)
{
ArgumentGuard.NotNull(configuration);

_configuration = configuration;
}

protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Person>()
.HasMany(person => person.AssignedTodoItems)
.WithOne(todoItem => todoItem.Assignee);

builder.Entity<Person>()
.HasMany(person => person.OwnedTodoItems)
.WithOne(todoItem => todoItem.Owner);

builder.Entity<Person>()
.HasOne(person => person.Account)
.WithOne(loginAccount => loginAccount.Person)
.HasForeignKey<Person>("AccountId");

builder.Entity<LoginAccount>()
.HasOne(loginAccount => loginAccount.Recovery)
.WithOne(accountRecovery => accountRecovery.Account)
.HasForeignKey<LoginAccount>("RecoveryId");

builder.Entity<Tag>()
.HasOne(tag => tag.Color)
.WithOne(rgbColor => rgbColor.Tag)
.HasForeignKey<RgbColor>("TagId");

var databaseProvider = _configuration.GetValue<DatabaseProvider>("DatabaseProvider");

if (databaseProvider != DatabaseProvider.SqlServer)
{
// In this example project, all cascades happen in the database, but SQL Server doesn't support that very well.
AdjustDeleteBehaviorForJsonApi(builder);
}
}

private static void AdjustDeleteBehaviorForJsonApi(ModelBuilder builder)
{
foreach (IMutableForeignKey foreignKey in builder.Model.GetEntityTypes()
.SelectMany(entityType => entityType.GetForeignKeys()))
{
if (foreignKey.DeleteBehavior == DeleteBehavior.ClientSetNull)
{
foreignKey.DeleteBehavior = DeleteBehavior.SetNull;
}

if (foreignKey.DeleteBehavior == DeleteBehavior.ClientCascade)
{
foreignKey.DeleteBehavior = DeleteBehavior.Cascade;
}
}
}
}
Loading

0 comments on commit a3975b7

Please sign in to comment.