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

Feature/generated patch objects #1059

Merged
merged 3 commits into from
Apr 13, 2022
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
2 changes: 1 addition & 1 deletion .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"packageRules": [
{
"description": "dotnet monorepo",
"enabled": false,
"enabled": true,
"matchSourceUrlPrefixes": [
"https://github.com/dotnet/aspnetcore",
"https://github.com/dotnet/efcore",
Expand Down
37 changes: 29 additions & 8 deletions sample/Sample.Core/Operations/LaunchRecords/EditLaunchRecord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Rocket.Surgery.LaunchPad.Foundation;
using Sample.Core.Domain;
using Sample.Core.Models;
using Sample.Core.Operations.Rockets;

namespace Sample.Core.Operations.LaunchRecords;

Expand Down Expand Up @@ -53,6 +54,14 @@ public partial record Request : IRequest<LaunchRecordModel>
public RocketId RocketId { get; set; } // TODO: Make generator that can be used to create a writable view model
}

public partial record PatchRequest : IRequest<LaunchRecordModel>, IPropertyTracking<Request>
{
/// <summary>
/// The rocket id
/// </summary>
public LaunchRecordId Id { get; init; }
}

private class Mapper : Profile
{
public Mapper()
Expand Down Expand Up @@ -94,29 +103,41 @@ public Validator()
}
}

private class Handler : IRequestHandler<Request, LaunchRecordModel>
private class Handler : PatchRequestHandler<Request, PatchRequest, LaunchRecordModel>, IRequestHandler<Request, LaunchRecordModel>
{
private readonly RocketDbContext _dbContext;
private readonly IMapper _mapper;

public Handler(RocketDbContext dbContext, IMapper mapper)
public Handler(RocketDbContext dbContext, IMapper mapper, IMediator mediator) : base(mediator)
{
_dbContext = dbContext;
_mapper = mapper;
}

public async Task<LaunchRecordModel> Handle(Request request, CancellationToken cancellationToken)
private async Task<LaunchRecord> GetLaunchRecord(LaunchRecordId id, CancellationToken cancellationToken)
{
var rocket
= await _dbContext.LaunchRecords
.Include(z => z.Rocket)
.FirstOrDefaultAsync(z => z.Id == request.Id, cancellationToken)
.ConfigureAwait(false);
var rocket = await _dbContext.LaunchRecords
.Include(z => z.Rocket)
.FirstOrDefaultAsync(z => z.Id == id, cancellationToken)
.ConfigureAwait(false);
if (rocket == null)
{
throw new NotFoundException();
}

return rocket;
}

protected override async Task<Request> GetRequest(PatchRequest patchRequest, CancellationToken cancellationToken)
{
var rocket = await GetLaunchRecord(patchRequest.Id, cancellationToken);
return _mapper.Map<Request>(_mapper.Map<LaunchRecordModel>(rocket));
}

public async Task<LaunchRecordModel> Handle(Request request, CancellationToken cancellationToken)
{
var rocket = await GetLaunchRecord(request.Id, cancellationToken);

_mapper.Map(request, rocket);
_dbContext.Update(rocket);
await _dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
Expand Down
30 changes: 26 additions & 4 deletions sample/Sample.Core/Operations/Rockets/EditRocket.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ public record Request : IRequest<RocketModel>
public RocketType Type { get; set; } // TODO: Make generator that can be used to create a writable view model
}

public partial record PatchRequest : IRequest<RocketModel>, IPropertyTracking<Request>
{
/// <summary>
/// The rocket id
/// </summary>
public RocketId Id { get; init; }
}

private class Mapper : Profile
{
public Mapper()
Expand Down Expand Up @@ -64,25 +72,39 @@ public RequestValidator()
}
}

private class Handler : IRequestHandler<Request, RocketModel>
private class RequestHandler : PatchRequestHandler<Request, PatchRequest, RocketModel>, IRequestHandler<Request, RocketModel>
{
private readonly RocketDbContext _dbContext;
private readonly IMapper _mapper;

public Handler(RocketDbContext dbContext, IMapper mapper)
public RequestHandler(RocketDbContext dbContext, IMapper mapper, IMediator mediator) : base(mediator)
{
_dbContext = dbContext;
_mapper = mapper;
}

public async Task<RocketModel> Handle(Request request, CancellationToken cancellationToken)
private async Task<ReadyRocket?> GetRocket(RocketId id, CancellationToken cancellationToken)
{
var rocket = await _dbContext.Rockets.FindAsync(new object[] { request.Id }, cancellationToken).ConfigureAwait(false);
var rocket = await _dbContext.Rockets.FindAsync(new object[] { id }, cancellationToken)
.ConfigureAwait(false);
if (rocket == null)
{
throw new NotFoundException();
}

return rocket;
}

protected override async Task<Request> GetRequest(PatchRequest patchRequest, CancellationToken cancellationToken)
{
var rocket = await GetRocket(patchRequest.Id, cancellationToken);
return _mapper.Map<Request>(_mapper.Map<RocketModel>(rocket));
}

public async Task<RocketModel> Handle(Request request, CancellationToken cancellationToken)
{
var rocket = await GetRocket(request.Id, cancellationToken);

_mapper.Map(request, rocket);
_dbContext.Update(rocket);
await _dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
Expand Down
14 changes: 1 addition & 13 deletions sample/Sample.Graphql/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,7 @@ public static IHostBuilder CreateHostBuilder(string[] args)
.LaunchWith(
RocketBooster.ForDependencyContext(DependencyContext.Default),
z => z
.WithConventionsFrom(GetConventions)
.Set(
new RocketChocolateOptions
{
RequestPredicate = type =>
type is { IsNested: true, DeclaringType: { } }
&& !( type.Name.StartsWith("Get", StringComparison.Ordinal)
|| type.Name.StartsWith(
"List", StringComparison.Ordinal
) ),
IncludeAssemblyInfoQuery = true
}
)
.WithConventionsFrom(GetConventions)
)
.ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
}
Expand Down
153 changes: 135 additions & 18 deletions sample/Sample.Graphql/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
using HotChocolate;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using HotChocolate;
using HotChocolate.Configuration;
using HotChocolate.Data.Filters;
using HotChocolate.Data.Sorting;
using HotChocolate.Types;
using HotChocolate.Types.Descriptors;
using HotChocolate.Types.Descriptors.Definitions;
using HotChocolate.Types.Pagination;
using HotChocolate.Utilities;
using MediatR;
using Rocket.Surgery.LaunchPad.AspNetCore;
using Rocket.Surgery.LaunchPad.Foundation;
using Rocket.Surgery.LaunchPad.HotChocolate;
using Sample.Core.Domain;
using Sample.Core.Models;
using Sample.Core.Operations.LaunchRecords;
using Sample.Core.Operations.Rockets;
using Serilog;

namespace Sample.Graphql;
Expand All @@ -18,33 +31,25 @@ public void ConfigureServices(IServiceCollection services)
{
services
.AddGraphQLServer()
.ConfigureStronglyTypedId<RocketId, UuidType>()
.ConfigureStronglyTypedId<LaunchRecordId, UuidType>()
// .AddDefaultTransactionScopeHandler()
.AddQueryType()
.AddMutationType()
.ModifyRequestOptions(
options => { options.IncludeExceptionDetails = true; }
)
.AddTypeConverter<RocketId, Guid>(source => source.Value)
.AddTypeConverter<Guid, RocketId>(source => new RocketId(source))
.AddTypeConverter<LaunchRecordId, Guid>(source => source.Value)
.AddTypeConverter<Guid, LaunchRecordId>(source => new LaunchRecordId(source))
.ModifyRequestOptions(options => options.IncludeExceptionDetails = true)
.ConfigureSchema(
s =>
{
s.AddType<QueryType>();
s.AddType<RocketMutation>();
s.AddType<LaunchRecordMutation>();
s.AddType<ReadyRocketType>();
s.AddType<LaunchRecordType>();

s.BindClrType<RocketId, UuidType>();
s.BindClrType<LaunchRecordId, UuidType>();
s.BindRuntimeType<RocketId>(ScalarNames.UUID);
s.BindRuntimeType<LaunchRecordId>(ScalarNames.UUID);
}
)
.AddSorting()
.AddFiltering()
.AddProjections()
.AddConvention<IFilterConvention, CustomFilterConventionExtension>();
.AddProjections();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
Expand All @@ -62,9 +67,121 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

app.UseRouting();

app.UseEndpoints(
endpoints => { endpoints.MapGraphQL(); }
);
app.UseEndpoints(endpoints => endpoints.MapGraphQL());
}
}

internal class StronglyTypedIdChangeTypeProvider : IChangeTypeProvider
{
private readonly Dictionary<(Type, Type), ChangeType> _typeMap = new();

public void AddTypeConversion(Type strongIdType)
{
var underlyingType = strongIdType.GetProperty("Value")!.PropertyType;
var value = Expression.Parameter(typeof(object), "value");
// .AddTypeConverter<LaunchRecordId, Guid>(source => source.Value)

if (!_typeMap.ContainsKey(( strongIdType, underlyingType )))
{
_typeMap.Add(
( strongIdType, underlyingType ),
Expression.Lambda<ChangeType>(
Expression.Convert(Expression.Property(Expression.Convert(value, strongIdType), "Value"), typeof(object)), false, value
).Compile()
);
}

if (!_typeMap.ContainsKey(( underlyingType, strongIdType )))
{
_typeMap.Add(
( underlyingType, strongIdType ),
Expression.Lambda<ChangeType>(
Expression.Convert(
Expression.New(strongIdType.GetConstructor(new[] { underlyingType })!, Expression.Convert(value, underlyingType)), typeof(object)
), false, value
).Compile()
);
}
}

public bool TryCreateConverter(Type source, Type target, ChangeTypeProvider root, [NotNullWhen(true)] out ChangeType? converter)
{
if (_typeMap.TryGetValue(( source, target ), out var @delegate))
{
converter = input => input is null ? default : @delegate(input);
return true;
}

converter = null;
return false;
}
}

public partial record EditRocketPatchRequest : IOptionalTracking<EditRocket.PatchRequest>
{
public RocketId Id { get; init; }
}

public partial record EditLaunchRecordPatchRequest : IOptionalTracking<EditLaunchRecord.PatchRequest>
{
public LaunchRecordId Id { get; init; }
}

[ExtendObjectType(OperationTypeNames.Mutation)]
public class RocketMutation
{
[UseServiceScope]
public Task<CreateRocket.Response> CreateRocket([Service] IMediator mediator, CancellationToken cancellationToken, CreateRocket.Request request)
{
return mediator.Send(request, cancellationToken);
}

[UseServiceScope]
public Task<RocketModel> EditRocket([Service] IMediator mediator, CancellationToken cancellationToken, EditRocket.Request request)
{
return mediator.Send(request, cancellationToken);
}

[UseServiceScope]
public Task<RocketModel> PatchRocket([Service] IMediator mediator, CancellationToken cancellationToken, EditRocketPatchRequest request)
{
return mediator.Send(request.Create(), cancellationToken);
}

[UseServiceScope]
public Task<Unit> DeleteRocket([Service] IMediator mediator, CancellationToken cancellationToken, DeleteRocket.Request request)
{
return mediator.Send(request, cancellationToken);
}
}

[ExtendObjectType(OperationTypeNames.Mutation)]
public class LaunchRecordMutation
{
[UseServiceScope]
public Task<CreateLaunchRecord.Response> CreateLaunchRecord(
[Service] IMediator mediator, CancellationToken cancellationToken, CreateLaunchRecord.Request request
)
{
return mediator.Send(request, cancellationToken);
}

[UseServiceScope]
public Task<LaunchRecordModel> EditLaunchRecord([Service] IMediator mediator, CancellationToken cancellationToken, EditLaunchRecord.Request request)
{
return mediator.Send(request, cancellationToken);
}

[UseServiceScope]
public Task<LaunchRecordModel> PatchLaunchRecord([Service] IMediator mediator, CancellationToken cancellationToken, EditLaunchRecordPatchRequest request)
{
return mediator.Send(request.Create(), cancellationToken);
}

[UseServiceScope]
public Task<Unit> DeleteLaunchRecord([Service] IMediator mediator, CancellationToken cancellationToken, DeleteLaunchRecord.Request request)
{
return mediator.Send(request, cancellationToken);
}
}

Expand Down
2 changes: 1 addition & 1 deletion sample/Sample.Restful.Client/Sample.Restful.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
ReferenceOutputAssembly="false"
>
<Options
>/generateClientClasses:true /generateClientInterfaces:true /injectHttpClient:true /disposeHttpClient:false /generateExceptionClasses:true /wrapDtoExceptions:true /useBaseUrl:false /generateBaseUrlProperty:false /operationGenerationMode:"MultipleClientsFromFirstTagAndOperationId" /generateOptionalParameters:true /generateJsonMethods:false /enforceFlagEnums:true /parameterArrayType:"System.Collections.Generic.IEnumerable" /parameterDictionaryType:"System.Collections.Generic.IDictionary" /responseArrayType:"System.Collections.Generic.ICollection" /responseDictionaryType:"System.Collections.Generic.IDictionary" /wrapResponses:true /generateResponseClasses:true /responseClass:"Response" /requiredPropertiesMustBeDefined:true /dateType:"System.DateTimeOffset" /dateTimeType:"System.DateTimeOffset" /timeType:"System.TimeSpan" /timeSpanType:"System.TimeSpan" /arrayType:"System.Collections.ObjectModel.Collection" /arrayInstanceType:"System.Collections.ObjectModel.Collection" /dictionaryType:"System.Collections.Generic.IDictionary" /dictionaryInstanceType:"System.Collections.Generic.Dictionary" /arrayBaseType:"System.Collections.ObjectModel.Collection" /dictionaryBaseType:"System.Collections.Generic.Dictionary" /classStyle:"Poco" /generateDefaultValues:true /generateDataAnnotations:true /generateImmutableArrayProperties:true /generateImmutableDictionaryProperties:true /generateDtoTypes:true /generateOptionalPropertiesAsNullable:true</Options>
>/generateClientClasses:true /generateClientInterfaces:true /injectHttpClient:true /disposeHttpClient:false /generateExceptionClasses:true /wrapDtoExceptions:true /useBaseUrl:false /generateBaseUrlProperty:false /operationGenerationMode:"MultipleClientsFromFirstTagAndOperationId" /generateOptionalParameters:true /generateJsonMethods:false /enforceFlagEnums:true /parameterArrayType:"System.Collections.Generic.IEnumerable" /parameterDictionaryType:"System.Collections.Generic.IDictionary" /responseArrayType:"System.Collections.Generic.ICollection" /responseDictionaryType:"System.Collections.Generic.IDictionary" /wrapResponses:true /generateResponseClasses:true /responseClass:"Response" /requiredPropertiesMustBeDefined:false /dateType:"System.DateTimeOffset" /dateTimeType:"System.DateTimeOffset" /timeType:"System.TimeSpan" /timeSpanType:"System.TimeSpan" /arrayType:"System.Collections.ObjectModel.Collection" /arrayInstanceType:"System.Collections.ObjectModel.Collection" /dictionaryType:"System.Collections.Generic.IDictionary" /dictionaryInstanceType:"System.Collections.Generic.Dictionary" /arrayBaseType:"System.Collections.ObjectModel.Collection" /dictionaryBaseType:"System.Collections.Generic.Dictionary" /classStyle:"Poco" /generateDefaultValues:true /generateDataAnnotations:true /generateImmutableArrayProperties:true /generateImmutableDictionaryProperties:true /generateDtoTypes:true /generateOptionalPropertiesAsNullable:true</Options>
</OpenApiProjectReference>
</ItemGroup>
<ItemGroup>
Expand Down
11 changes: 9 additions & 2 deletions sample/Sample.Restful.Client/Test1.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
namespace JetBrains.Annotations;
using Newtonsoft.Json;

public class Test1
namespace Sample.Restful.Client;

public partial class RocketClient
{
partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings)
{
// This is required for patching to work as expected
// settings.NullValueHandling = NullValueHandling.Ignore;
}
}
10 changes: 10 additions & 0 deletions sample/Sample.Restful/Controllers/LaunchRecordController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ public partial class LaunchRecordController : RestfulApiController
// ReSharper disable once RouteTemplates.ParameterTypeAndConstraintsMismatch
public partial Task<ActionResult> EditLaunchRecord([BindRequired] [FromRoute] LaunchRecordId id, EditLaunchRecord.Request model);

/// <summary>
/// Update a given launch record
/// </summary>
/// <param name="id">The id of the launch record</param>
/// <param name="model">The request details</param>
/// <returns></returns>
[HttpPatch("{id:guid}")]
// ReSharper disable once RouteTemplates.ParameterTypeAndConstraintsMismatch
public partial Task<ActionResult> PatchLaunchRecord([BindRequired] [FromRoute] LaunchRecordId id, EditLaunchRecord.PatchRequest model);

/// <summary>
/// Remove a launch record
/// </summary>
Expand Down
Loading