Skip to content

Commit 697c295

Browse files
committed
Add example that produces SQL without Entity Framework Core
1 parent 9830302 commit 697c295

File tree

131 files changed

+14001
-64
lines changed

Some content is hidden

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

131 files changed

+14001
-64
lines changed

Directory.Build.props

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
<CSharpGuidelinesAnalyzerVersion>3.8.*</CSharpGuidelinesAnalyzerVersion>
2929
<CodeAnalysisVersion>4.7.*</CodeAnalysisVersion>
3030
<CoverletVersion>6.0.*</CoverletVersion>
31+
<DapperVersion>2.1.*</DapperVersion>
3132
<DateOnlyTimeOnlyVersion>2.1.*</DateOnlyTimeOnlyVersion>
3233
<EntityFrameworkCoreVersion>7.0.*</EntityFrameworkCoreVersion>
3334
<FluentAssertionsVersion>6.12.*</FluentAssertionsVersion>

JsonApiDotNetCore.sln

+30
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatabasePerTenantExample",
5656
EndProject
5757
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnnotationTests", "test\AnnotationTests\AnnotationTests.csproj", "{24B0C12F-38CD-4245-8785-87BEFAD55B00}"
5858
EndProject
59+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DapperExample", "src\Examples\DapperExample\DapperExample.csproj", "{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}"
60+
EndProject
61+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DapperTests", "test\DapperTests\DapperTests.csproj", "{80E322F5-5F5D-4670-A30F-02D33C2C7900}"
62+
EndProject
5963
Global
6064
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6165
Debug|Any CPU = Debug|Any CPU
@@ -282,6 +286,30 @@ Global
282286
{24B0C12F-38CD-4245-8785-87BEFAD55B00}.Release|x64.Build.0 = Release|Any CPU
283287
{24B0C12F-38CD-4245-8785-87BEFAD55B00}.Release|x86.ActiveCfg = Release|Any CPU
284288
{24B0C12F-38CD-4245-8785-87BEFAD55B00}.Release|x86.Build.0 = Release|Any CPU
289+
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
290+
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
291+
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Debug|x64.ActiveCfg = Debug|Any CPU
292+
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Debug|x64.Build.0 = Debug|Any CPU
293+
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Debug|x86.ActiveCfg = Debug|Any CPU
294+
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Debug|x86.Build.0 = Debug|Any CPU
295+
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
296+
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Release|Any CPU.Build.0 = Release|Any CPU
297+
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Release|x64.ActiveCfg = Release|Any CPU
298+
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Release|x64.Build.0 = Release|Any CPU
299+
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Release|x86.ActiveCfg = Release|Any CPU
300+
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2}.Release|x86.Build.0 = Release|Any CPU
301+
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
302+
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Debug|Any CPU.Build.0 = Debug|Any CPU
303+
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Debug|x64.ActiveCfg = Debug|Any CPU
304+
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Debug|x64.Build.0 = Debug|Any CPU
305+
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Debug|x86.ActiveCfg = Debug|Any CPU
306+
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Debug|x86.Build.0 = Debug|Any CPU
307+
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Release|Any CPU.ActiveCfg = Release|Any CPU
308+
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Release|Any CPU.Build.0 = Release|Any CPU
309+
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Release|x64.ActiveCfg = Release|Any CPU
310+
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Release|x64.Build.0 = Release|Any CPU
311+
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Release|x86.ActiveCfg = Release|Any CPU
312+
{80E322F5-5F5D-4670-A30F-02D33C2C7900}.Release|x86.Build.0 = Release|Any CPU
285313
EndGlobalSection
286314
GlobalSection(SolutionProperties) = preSolution
287315
HideSolutionNode = FALSE
@@ -305,6 +333,8 @@ Global
305333
{83FF097C-C8C6-477B-9FAB-DF99B84978B5} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}
306334
{60334658-BE51-43B3-9C4D-F2BBF56C89CE} = {026FBC6C-AF76-4568-9B87-EC73457899FD}
307335
{24B0C12F-38CD-4245-8785-87BEFAD55B00} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
336+
{C1774117-5073-4DF8-B5BE-BF7B538BD1C2} = {026FBC6C-AF76-4568-9B87-EC73457899FD}
337+
{80E322F5-5F5D-4670-A30F-02D33C2C7900} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
308338
EndGlobalSection
309339
GlobalSection(ExtensibilityGlobals) = postSolution
310340
SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4}

JsonApiDotNetCore.sln.DotSettings

+4
Original file line numberDiff line numberDiff line change
@@ -659,8 +659,12 @@ $left$ = $right$;</s:String>
659659
<s:Boolean x:Key="/Default/UserDictionary/Words/=linebreaks/@EntryIndexedValue">True</s:Boolean>
660660
<s:Boolean x:Key="/Default/UserDictionary/Words/=Microservices/@EntryIndexedValue">True</s:Boolean>
661661
<s:Boolean x:Key="/Default/UserDictionary/Words/=navigations/@EntryIndexedValue">True</s:Boolean>
662+
<s:Boolean x:Key="/Default/UserDictionary/Words/=Npgsql/@EntryIndexedValue">True</s:Boolean>
662663
<s:Boolean x:Key="/Default/UserDictionary/Words/=parallelize/@EntryIndexedValue">True</s:Boolean>
664+
<s:Boolean x:Key="/Default/UserDictionary/Words/=parameterless/@EntryIndexedValue">True</s:Boolean>
663665
<s:Boolean x:Key="/Default/UserDictionary/Words/=playlists/@EntryIndexedValue">True</s:Boolean>
666+
<s:Boolean x:Key="/Default/UserDictionary/Words/=Pomelo/@EntryIndexedValue">True</s:Boolean>
667+
<s:Boolean x:Key="/Default/UserDictionary/Words/=Postgre/@EntryIndexedValue">True</s:Boolean>
664668
<s:Boolean x:Key="/Default/UserDictionary/Words/=Rewriter/@EntryIndexedValue">True</s:Boolean>
665669
<s:Boolean x:Key="/Default/UserDictionary/Words/=Startups/@EntryIndexedValue">True</s:Boolean>
666670
<s:Boolean x:Key="/Default/UserDictionary/Words/=subdirectory/@EntryIndexedValue">True</s:Boolean>

docs/getting-started/faq.md

+10-4
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,18 @@ Take a look at [JsonApiResourceService](https://github.com/json-api-dotnet/JsonA
145145

146146
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.
148-
That's because the built-in resource service translates all JSON:API aspects of the request into a database-agnostic data structure called `QueryLayer`.
148+
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`.
149149
Now the hard part for you becomes reading that data structure and producing data access calls from that.
150-
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),
150+
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),
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. 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).
152+
Note however, that it also produces calls to `.Include("")`, which is an Entity Framework Core-specific extension method, so you'll need to
153+
[prevent that from happening](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryLayerIncludeConverter.cs).
154+
155+
The example [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/Examples/NoEntityFrameworkExample/Repositories/InMemoryResourceRepository.cs) compiles and executes
156+
the LINQ query against an in-memory list of resources.
157+
For [MongoDB](https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb/blob/master/src/JsonApiDotNetCore.MongoDb/Repositories/MongoRepository.cs), we use the MongoDB LINQ provider.
158+
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,
159+
which produces SQL and uses [Dapper](https://github.com/DapperLib/Dapper) for data access.
154160

155161
> [!TIP]
156162
> [ExpressionTreeVisualizer](https://github.com/zspitz/ExpressionTreeVisualizer) is very helpful in trying to debug LINQ expression trees!
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using System.Data.Common;
2+
using JsonApiDotNetCore;
3+
using JsonApiDotNetCore.AtomicOperations;
4+
5+
namespace DapperExample.AtomicOperations;
6+
7+
/// <summary>
8+
/// Represents an ADO.NET transaction in a JSON:API atomic:operations request.
9+
/// </summary>
10+
internal sealed class AmbientTransaction : IOperationsTransaction
11+
{
12+
private readonly AmbientTransactionFactory _owner;
13+
14+
public DbTransaction Current { get; }
15+
16+
/// <inheritdoc />
17+
public string TransactionId { get; }
18+
19+
public AmbientTransaction(AmbientTransactionFactory owner, DbTransaction current, Guid transactionId)
20+
{
21+
ArgumentGuard.NotNull(owner);
22+
ArgumentGuard.NotNull(current);
23+
24+
_owner = owner;
25+
Current = current;
26+
TransactionId = transactionId.ToString();
27+
}
28+
29+
/// <inheritdoc />
30+
public Task BeforeProcessOperationAsync(CancellationToken cancellationToken)
31+
{
32+
return Task.CompletedTask;
33+
}
34+
35+
/// <inheritdoc />
36+
public Task AfterProcessOperationAsync(CancellationToken cancellationToken)
37+
{
38+
return Task.CompletedTask;
39+
}
40+
41+
/// <inheritdoc />
42+
public Task CommitAsync(CancellationToken cancellationToken)
43+
{
44+
return Current.CommitAsync(cancellationToken);
45+
}
46+
47+
/// <inheritdoc />
48+
public async ValueTask DisposeAsync()
49+
{
50+
DbConnection? connection = Current.Connection;
51+
52+
await Current.DisposeAsync();
53+
54+
if (connection != null)
55+
{
56+
await connection.DisposeAsync();
57+
}
58+
59+
_owner.Detach(this);
60+
}
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using System.Data.Common;
2+
using DapperExample.TranslationToSql.DataModel;
3+
using JsonApiDotNetCore;
4+
using JsonApiDotNetCore.AtomicOperations;
5+
using JsonApiDotNetCore.Configuration;
6+
7+
namespace DapperExample.AtomicOperations;
8+
9+
/// <summary>
10+
/// Provides transaction support for JSON:API atomic:operation requests using ADO.NET.
11+
/// </summary>
12+
public sealed class AmbientTransactionFactory : IOperationsTransactionFactory
13+
{
14+
private readonly IJsonApiOptions _options;
15+
private readonly IDataModelService _dataModelService;
16+
17+
internal AmbientTransaction? AmbientTransaction { get; private set; }
18+
19+
public AmbientTransactionFactory(IJsonApiOptions options, IDataModelService dataModelService)
20+
{
21+
ArgumentGuard.NotNull(options);
22+
ArgumentGuard.NotNull(dataModelService);
23+
24+
_options = options;
25+
_dataModelService = dataModelService;
26+
}
27+
28+
internal async Task<AmbientTransaction> BeginTransactionAsync(CancellationToken cancellationToken)
29+
{
30+
var instance = (IOperationsTransactionFactory)this;
31+
32+
IOperationsTransaction transaction = await instance.BeginTransactionAsync(cancellationToken);
33+
return (AmbientTransaction)transaction;
34+
}
35+
36+
async Task<IOperationsTransaction> IOperationsTransactionFactory.BeginTransactionAsync(CancellationToken cancellationToken)
37+
{
38+
if (AmbientTransaction != null)
39+
{
40+
throw new InvalidOperationException("Cannot start transaction because another transaction is already active.");
41+
}
42+
43+
DbConnection dbConnection = _dataModelService.CreateConnection();
44+
45+
try
46+
{
47+
await dbConnection.OpenAsync(cancellationToken);
48+
49+
DbTransaction transaction = _options.TransactionIsolationLevel != null
50+
? await dbConnection.BeginTransactionAsync(_options.TransactionIsolationLevel.Value, cancellationToken)
51+
: await dbConnection.BeginTransactionAsync(cancellationToken);
52+
53+
var transactionId = Guid.NewGuid();
54+
AmbientTransaction = new AmbientTransaction(this, transaction, transactionId);
55+
56+
return AmbientTransaction;
57+
}
58+
catch (DbException)
59+
{
60+
await dbConnection.DisposeAsync();
61+
throw;
62+
}
63+
}
64+
65+
internal void Detach(AmbientTransaction ambientTransaction)
66+
{
67+
ArgumentGuard.NotNull(ambientTransaction);
68+
69+
if (AmbientTransaction != null && AmbientTransaction == ambientTransaction)
70+
{
71+
AmbientTransaction = null;
72+
}
73+
else
74+
{
75+
throw new InvalidOperationException("Failed to detach ambient transaction.");
76+
}
77+
}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using JsonApiDotNetCore.AtomicOperations;
2+
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.Controllers;
4+
using JsonApiDotNetCore.Middleware;
5+
using JsonApiDotNetCore.Resources;
6+
7+
namespace DapperExample.Controllers;
8+
9+
public sealed class OperationsController : JsonApiOperationsController
10+
{
11+
public OperationsController(IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor,
12+
IJsonApiRequest request, ITargetedFields targetedFields)
13+
: base(options, resourceGraph, loggerFactory, processor, request, targetedFields)
14+
{
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
<PropertyGroup>
3+
<TargetFramework>$(TargetFrameworkName)</TargetFramework>
4+
</PropertyGroup>
5+
6+
<ItemGroup>
7+
<ProjectReference Include="..\..\JsonApiDotNetCore\JsonApiDotNetCore.csproj" />
8+
<ProjectReference Include="..\..\JsonApiDotNetCore.SourceGenerators\JsonApiDotNetCore.SourceGenerators.csproj" OutputItemType="Analyzer"
9+
ReferenceOutputAssembly="false" />
10+
</ItemGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="Dapper" Version="$(DapperVersion)" />
14+
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="$(EntityFrameworkCoreVersion)" />
15+
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="$(EntityFrameworkCoreVersion)" />
16+
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="$(NpgsqlVersion)" />
17+
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="$(EntityFrameworkCoreVersion)" />
18+
</ItemGroup>
19+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using DapperExample.Models;
2+
using JetBrains.Annotations;
3+
using JsonApiDotNetCore;
4+
using Microsoft.EntityFrameworkCore;
5+
using Microsoft.EntityFrameworkCore.Metadata;
6+
7+
// @formatter:wrap_chained_method_calls chop_always
8+
9+
namespace DapperExample.Data;
10+
11+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
12+
public sealed class AppDbContext : DbContext
13+
{
14+
private readonly IConfiguration _configuration;
15+
16+
public DbSet<TodoItem> TodoItems => Set<TodoItem>();
17+
public DbSet<Person> People => Set<Person>();
18+
public DbSet<LoginAccount> LoginAccounts => Set<LoginAccount>();
19+
public DbSet<AccountRecovery> AccountRecoveries => Set<AccountRecovery>();
20+
public DbSet<Tag> Tags => Set<Tag>();
21+
public DbSet<RgbColor> RgbColors => Set<RgbColor>();
22+
23+
public AppDbContext(DbContextOptions<AppDbContext> options, IConfiguration configuration)
24+
: base(options)
25+
{
26+
ArgumentGuard.NotNull(configuration);
27+
28+
_configuration = configuration;
29+
}
30+
31+
protected override void OnModelCreating(ModelBuilder builder)
32+
{
33+
builder.Entity<Person>()
34+
.HasMany(person => person.AssignedTodoItems)
35+
.WithOne(todoItem => todoItem.Assignee);
36+
37+
builder.Entity<Person>()
38+
.HasMany(person => person.OwnedTodoItems)
39+
.WithOne(todoItem => todoItem.Owner);
40+
41+
builder.Entity<Person>()
42+
.HasOne(person => person.Account)
43+
.WithOne(loginAccount => loginAccount.Person)
44+
.HasForeignKey<Person>("AccountId");
45+
46+
builder.Entity<LoginAccount>()
47+
.HasOne(loginAccount => loginAccount.Recovery)
48+
.WithOne(accountRecovery => accountRecovery.Account)
49+
.HasForeignKey<LoginAccount>("RecoveryId");
50+
51+
builder.Entity<Tag>()
52+
.HasOne(tag => tag.Color)
53+
.WithOne(rgbColor => rgbColor.Tag)
54+
.HasForeignKey<RgbColor>("TagId");
55+
56+
var databaseProvider = _configuration.GetValue<DatabaseProvider>("DatabaseProvider");
57+
58+
if (databaseProvider != DatabaseProvider.SqlServer)
59+
{
60+
// In this example project, all cascades happen in the database, but SQL Server doesn't support that very well.
61+
AdjustDeleteBehaviorForJsonApi(builder);
62+
}
63+
}
64+
65+
private static void AdjustDeleteBehaviorForJsonApi(ModelBuilder builder)
66+
{
67+
foreach (IMutableForeignKey foreignKey in builder.Model.GetEntityTypes()
68+
.SelectMany(entityType => entityType.GetForeignKeys()))
69+
{
70+
if (foreignKey.DeleteBehavior == DeleteBehavior.ClientSetNull)
71+
{
72+
foreignKey.DeleteBehavior = DeleteBehavior.SetNull;
73+
}
74+
75+
if (foreignKey.DeleteBehavior == DeleteBehavior.ClientCascade)
76+
{
77+
foreignKey.DeleteBehavior = DeleteBehavior.Cascade;
78+
}
79+
}
80+
}
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
namespace DapperExample.Data;
2+
3+
internal abstract class RotatingList
4+
{
5+
public static RotatingList<T> Create<T>(int count, Func<int, T> createElement)
6+
{
7+
List<T> elements = new();
8+
9+
for (int index = 0; index < count; index++)
10+
{
11+
T element = createElement(index);
12+
elements.Add(element);
13+
}
14+
15+
return new RotatingList<T>(elements);
16+
}
17+
}
18+
19+
internal sealed class RotatingList<T>
20+
{
21+
private int _index = -1;
22+
23+
public IList<T> Elements { get; }
24+
25+
public RotatingList(IList<T> elements)
26+
{
27+
Elements = elements;
28+
}
29+
30+
public T GetNext()
31+
{
32+
_index++;
33+
return Elements[_index % Elements.Count];
34+
}
35+
}

0 commit comments

Comments
 (0)