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

Example to produce SQL without Entity Framework Core! #1361

Merged
merged 1 commit into from
Oct 27, 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
1 change: 1 addition & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<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>
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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -282,6 +286,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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -305,6 +333,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}
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 @@ -659,8 +659,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;
}
}
}
}
35 changes: 35 additions & 0 deletions src/Examples/DapperExample/Data/RotatingList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace DapperExample.Data;

internal abstract class RotatingList
{
public static RotatingList<T> Create<T>(int count, Func<int, T> createElement)
{
List<T> elements = new();

for (int index = 0; index < count; index++)
{
T element = createElement(index);
elements.Add(element);
}

return new RotatingList<T>(elements);
}
}

internal sealed class RotatingList<T>
{
private int _index = -1;

public IList<T> Elements { get; }

public RotatingList(IList<T> elements)
{
Elements = elements;
}

public T GetNext()
{
_index++;
return Elements[_index % Elements.Count];
}
}
Loading