Skip to content
Open
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
44 changes: 22 additions & 22 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,40 @@
<ItemGroup>
<PackageVersion Include="AutoMapper" Version="13.0.1" />
<PackageVersion Include="AwesomeAssertions" Version="8.2.0" />
<PackageVersion Include="Azure.Identity" Version="1.13.1" />
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
<PackageVersion Include="FluentAssertions.Web" Version="1.8.0" />
<PackageVersion Include="LiteDB" Version="5.0.21" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.OData" Version="9.3.1" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.4" />
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.51.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Cosmos" Version="9.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.5" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageVersion Include="Microsoft.OData.Core" Version="8.2.3" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.OData" Version="9.4.1" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageVersion Include="Microsoft.Azure.Cosmos" Version="3.56.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Cosmos" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Proxies" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="Microsoft.OData.Core" Version="8.4.3" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="Microsoft.Spatial" Version="8.2.3" />
<PackageVersion Include="Microsoft.Spatial" Version="8.4.3" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="MongoDB.Driver" Version="3.3.0" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="NSwag.AspNetCore" Version="14.3.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-preview.3.efcore.9.0.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="8.1.1" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.0.1" />
<PackageVersion Include="System.Formats.Asn1" Version="9.0.4" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Linq.Async" Version="7.0.0" />
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Text.Json" Version="9.0.4" />
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.4" />
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="10.0.0" />
<PackageVersion Include="TestContainers.MongoDb" Version="4.4.0" />
<PackageVersion Include="TestContainers.MsSql" Version="4.4.0" />
<PackageVersion Include="TestContainers.MySql" Version="4.4.0" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>2fc55b72-4090-46ad-ae44-8b6a415339b8</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.7">
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>The client capabilities for developing applications using the Datasync Toolkit.</Description>
</PropertyGroup>
Expand All @@ -12,7 +12,6 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="System.Threading.Tasks.Dataflow" />
<PackageReference Include="Microsoft.Spatial" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi;
using System.Diagnostics.CodeAnalysis;
using System.Reflection.Metadata;
using System.Text.Json.Nodes;

namespace CommunityToolkit.Datasync.Server.OpenApi;

Expand Down Expand Up @@ -61,6 +64,54 @@ public async Task TransformAsync(OpenApiOperation operation, OpenApiOperationTra
return;
}

internal async Task<OpenApiSchemaReference> GetSchemaReferenceAsync(OpenApiOperationTransformerContext context, Type entityType)
{
if(context.Document is null)
{
throw new InvalidOperationException("The OpenAPI document is not available in the transformer context.");
}

if (!this.processedEntityNames.Contains(entityType.Name))
{
OpenApiSchema schema = await context.GetOrCreateSchemaAsync(entityType);

// This is a Datasync schema, so update the schema for the datasync attributes.
schema.MakeSystemPropertiesReadonly();

_ = context.Document.AddComponent(entityType.Name, schema);

Type pagedEntityType = typeof(Page<>).MakeGenericType(entityType);
string pagedEntitySchemaName = $"{entityType.Name}Page";

OpenApiSchema pagedEntitySchema = await context.GetOrCreateSchemaAsync(pagedEntityType);

OpenApiSchema itemsProp = (OpenApiSchema)pagedEntitySchema.Properties!["items"];
OpenApiSchema countProp = (OpenApiSchema)pagedEntitySchema.Properties!["count"];
OpenApiSchema nextLinkProp = (OpenApiSchema)pagedEntitySchema.Properties!["nextLink"];
itemsProp.ReadOnly = true;
countProp.ReadOnly = true;
//countProp.Type = JsonSchemaType.Integer;
//countProp.Format = "int64";
//countProp.Pattern = null;
nextLinkProp.ReadOnly = true;
//nextLinkProp.Type = JsonSchemaType.String;
//nextLinkProp.Format = "uri";
pagedEntitySchema.AdditionalPropertiesAllowed = false;
//_ = pagedEntitySchema.Required?.Remove("items");

itemsProp.Items = new OpenApiSchemaReference(entityType.Name, context.Document);

_ = context.Document.AddComponent(pagedEntitySchemaName, pagedEntitySchema);

this.processedEntityNames.Add(entityType.Name);
}

OpenApiSchemaReference schemaRef = new(entityType.Name, context.Document);

return schemaRef;
}
private readonly List<string> processedEntityNames = [];

/// <summary>
/// Determines if a controller presented is a datasync controller.
/// </summary>
Expand Down Expand Up @@ -103,21 +154,23 @@ internal static Type GetEntityType(OpenApiOperationTransformerContext context)
/// <param name="context">The operation transformer context.</param>
/// <param name="cancellationToken">A cancellation token to observe.</param>
/// <returns>A task that resolves when the operation is complete.</returns>
internal Task TransformCreateAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
internal async Task TransformCreateAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
Type entityType = GetEntityType(context);

operation.AddRequestBody(context.GetSchemaForType(entityType));
OpenApiSchemaReference schemaReference = await GetSchemaReferenceAsync(context, entityType);

operation.AddRequestBody(schemaReference);

operation.Responses ??= [];

operation.Responses.AddEntityResponse(StatusCodes.Status201Created,
context.GetSchemaForType(entityType), includeConditionalHeaders: true);
schemaReference, includeConditionalHeaders: true);
operation.Responses.AddStatusCode(StatusCodes.Status400BadRequest);
operation.Responses.AddEntityResponse(StatusCodes.Status409Conflict,
context.GetSchemaForType(entityType), includeConditionalHeaders: true);
schemaReference, includeConditionalHeaders: true);
operation.Responses.AddEntityResponse(StatusCodes.Status412PreconditionFailed,
context.GetSchemaForType(entityType), includeConditionalHeaders: true);

return Task.CompletedTask;
schemaReference, includeConditionalHeaders: true);
}

/// <summary>
Expand All @@ -127,22 +180,26 @@ internal Task TransformCreateAsync(OpenApiOperation operation, OpenApiOperationT
/// <param name="context">The operation transformer context.</param>
/// <param name="cancellationToken">A cancellation token to observe.</param>
/// <returns>A task that resolves when the operation is complete.</returns>
internal Task TransformDeleteAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
internal async Task TransformDeleteAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
Type entityType = GetEntityType(context);

OpenApiSchemaReference schemaReference = await GetSchemaReferenceAsync(context, entityType);

operation.Parameters ??= [];

operation.Parameters.AddIfMatchHeader();
operation.Parameters.AddIfUnmodifiedSinceHeader();

operation.Responses ??= [];

operation.Responses.AddStatusCode(StatusCodes.Status400BadRequest);
operation.Responses.AddStatusCode(StatusCodes.Status404NotFound);
operation.Responses.AddStatusCode(StatusCodes.Status410Gone);
operation.Responses.AddEntityResponse(StatusCodes.Status409Conflict,
context.GetSchemaForType(entityType), includeConditionalHeaders: true);
schemaReference, includeConditionalHeaders: true);
operation.Responses.AddEntityResponse(StatusCodes.Status412PreconditionFailed,
context.GetSchemaForType(entityType), includeConditionalHeaders: true);

return Task.CompletedTask;
schemaReference, includeConditionalHeaders: true);
}

/// <summary>
Expand All @@ -152,10 +209,13 @@ internal Task TransformDeleteAsync(OpenApiOperation operation, OpenApiOperationT
/// <param name="context">The operation transformer context.</param>
/// <param name="cancellationToken">A cancellation token to observe.</param>
/// <returns>A task that resolves when the operation is complete.</returns>
internal Task TransformQueryAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
internal async Task TransformQueryAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
Type entityType = GetEntityType(context);
Type pagedEntityType = typeof(PagedResult<>).MakeGenericType(entityType);

OpenApiSchemaReference pagedEntitySchemaRef = new($"{entityType.Name}Page", context.Document);

operation.Parameters ??= [];

operation.Parameters.AddBooleanQueryParameter("$count", "Whether to include the total count of items matching the query in the result");
operation.Parameters.AddStringQueryParameter("$filter", "The filter to apply to the query");
Expand All @@ -165,11 +225,11 @@ internal Task TransformQueryAsync(OpenApiOperation operation, OpenApiOperationTr
operation.Parameters.AddIntQueryParameter("$top", "The number of items to return", 1);
operation.Parameters.AddIncludeDeletedQuery();

operation.Responses ??= [];

operation.Responses.AddEntityResponse(StatusCodes.Status200OK,
context.GetSchemaForType(pagedEntityType), includeConditionalHeaders: false);
pagedEntitySchemaRef, includeConditionalHeaders: false);
operation.Responses.AddStatusCode(StatusCodes.Status400BadRequest);

return Task.CompletedTask;
}

/// <summary>
Expand All @@ -179,21 +239,25 @@ internal Task TransformQueryAsync(OpenApiOperation operation, OpenApiOperationTr
/// <param name="context">The operation transformer context.</param>
/// <param name="cancellationToken">A cancellation token to observe.</param>
/// <returns>A task that resolves when the operation is complete.</returns>
internal Task TransformReadAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
internal async Task TransformReadAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
Type entityType = GetEntityType(context);

OpenApiSchemaReference schemaReference = await GetSchemaReferenceAsync(context, entityType);

operation.Parameters ??= [];

operation.Parameters.AddIncludeDeletedQuery();
operation.Parameters.AddIfNoneMatchHeader();
operation.Parameters.AddIfModifiedSinceHeader();

operation.Responses ??= [];

operation.Responses.AddEntityResponse(StatusCodes.Status200OK,
context.GetSchemaForType(entityType), includeConditionalHeaders: true);
schemaReference, includeConditionalHeaders: true);
operation.Responses.AddStatusCode(StatusCodes.Status304NotModified);
operation.Responses.AddStatusCode(StatusCodes.Status404NotFound);
operation.Responses.AddStatusCode(StatusCodes.Status410Gone);

return Task.CompletedTask;
}

/// <summary>
Expand All @@ -203,25 +267,52 @@ internal Task TransformReadAsync(OpenApiOperation operation, OpenApiOperationTra
/// <param name="context">The operation transformer context.</param>
/// <param name="cancellationToken">A cancellation token to observe.</param>
/// <returns>A task that resolves when the operation is complete.</returns>
internal Task TransformReplaceAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
internal async Task TransformReplaceAsync(OpenApiOperation operation, OpenApiOperationTransformerContext context, CancellationToken cancellationToken)
{
Type entityType = GetEntityType(context);

operation.AddRequestBody(context.GetSchemaForType(entityType));
OpenApiSchemaReference schemaReference = await GetSchemaReferenceAsync(context, entityType);

operation.Parameters ??= [];

operation.AddRequestBody(schemaReference);
operation.Parameters.AddIncludeDeletedQuery();
operation.Parameters.AddIfMatchHeader();
operation.Parameters.AddIfUnmodifiedSinceHeader();

operation.Responses ??= [];

operation.Responses.AddEntityResponse(StatusCodes.Status200OK,
context.GetSchemaForType(entityType), includeConditionalHeaders: true);
schemaReference, includeConditionalHeaders: true);
operation.Responses.AddStatusCode(StatusCodes.Status400BadRequest);
operation.Responses.AddStatusCode(StatusCodes.Status404NotFound);
operation.Responses.AddStatusCode(StatusCodes.Status410Gone);
operation.Responses.AddEntityResponse(StatusCodes.Status409Conflict,
context.GetSchemaForType(entityType), includeConditionalHeaders: true);
schemaReference, includeConditionalHeaders: true);
operation.Responses.AddEntityResponse(StatusCodes.Status412PreconditionFailed,
context.GetSchemaForType(entityType), includeConditionalHeaders: true);
schemaReference, includeConditionalHeaders: true);
}

return Task.CompletedTask;
/// <summary>
/// A type representing a single page of entities.
/// </summary>
/// <typeparam name="T">The type of the entity.</typeparam>
[ExcludeFromCodeCoverage(Justification = "Model class - coverage not needed")]
internal class Page<T>
{
/// <summary>
/// The list of entities in this page of the results.
/// </summary>
public IEnumerable<T> Items { get; } = [];

/// <summary>
/// The count of all the entities in the result set.
/// </summary>
public long? Count { get; }

/// <summary>
/// The URI to the next page of entities.
/// </summary>
public Uri? NextLink { get; }
}
}
Loading