Skip to content
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

Merge master (dapper example) into openapi #1382

Closed
wants to merge 10 commits into from
Closed
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
45 changes: 45 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 All @@ -66,6 +70,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCoreExampleCli
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenApiClientTests", "test\OpenApiClientTests\OpenApiClientTests.csproj", "{77F98215-3085-422E-B99D-4C404C2114CF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenApiEndToEndTests", "test\OpenApiEndToEndTests\OpenApiEndToEndTests.csproj", "{3BA4F9B9-3D90-44B5-B09C-28D98E0B4225}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -292,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 @@ -352,6 +382,18 @@ Global
{77F98215-3085-422E-B99D-4C404C2114CF}.Release|x64.Build.0 = Release|Any CPU
{77F98215-3085-422E-B99D-4C404C2114CF}.Release|x86.ActiveCfg = Release|Any CPU
{77F98215-3085-422E-B99D-4C404C2114CF}.Release|x86.Build.0 = Release|Any CPU
{3BA4F9B9-3D90-44B5-B09C-28D98E0B4225}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3BA4F9B9-3D90-44B5-B09C-28D98E0B4225}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3BA4F9B9-3D90-44B5-B09C-28D98E0B4225}.Debug|x64.ActiveCfg = Debug|Any CPU
{3BA4F9B9-3D90-44B5-B09C-28D98E0B4225}.Debug|x64.Build.0 = Debug|Any CPU
{3BA4F9B9-3D90-44B5-B09C-28D98E0B4225}.Debug|x86.ActiveCfg = Debug|Any CPU
{3BA4F9B9-3D90-44B5-B09C-28D98E0B4225}.Debug|x86.Build.0 = Debug|Any CPU
{3BA4F9B9-3D90-44B5-B09C-28D98E0B4225}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3BA4F9B9-3D90-44B5-B09C-28D98E0B4225}.Release|Any CPU.Build.0 = Release|Any CPU
{3BA4F9B9-3D90-44B5-B09C-28D98E0B4225}.Release|x64.ActiveCfg = Release|Any CPU
{3BA4F9B9-3D90-44B5-B09C-28D98E0B4225}.Release|x64.Build.0 = Release|Any CPU
{3BA4F9B9-3D90-44B5-B09C-28D98E0B4225}.Release|x86.ActiveCfg = Release|Any CPU
{3BA4F9B9-3D90-44B5-B09C-28D98E0B4225}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -375,11 +417,14 @@ 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}
{7FC5DFA3-6F66-4FD8-820D-81E93856F252} = {026FBC6C-AF76-4568-9B87-EC73457899FD}
{77F98215-3085-422E-B99D-4C404C2114CF} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
{3BA4F9B9-3D90-44B5-B09C-28D98E0B4225} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4}
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>
Loading