Skip to content

Commit

Permalink
feat(grpc): drop inheritance, use composition
Browse files Browse the repository at this point in the history
  • Loading branch information
SonicGD committed Jan 7, 2023
1 parent d2fed71 commit 65f327a
Show file tree
Hide file tree
Showing 18 changed files with 297 additions and 238 deletions.
1 change: 1 addition & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<PackageVersion Include="IL.FluentValidation.Extensions.Options" Version="10.0.1"/>
<PackageVersion Include="JetBrains.Annotations" Version="2022.3.1"/>
<PackageVersion Include="Newtonsoft.Json" Version="13.0.2"/>
<PackageVersion Include="PeanutButter.DuckTyping" Version="3.0.96"/>
<PackageVersion Include="Tempus" Version="1.0.0"/>
<PackageVersion Include="Scrutor" Version="4.2.0"/>
<PackageVersion Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0"/>
Expand Down
2 changes: 1 addition & 1 deletion src/Sitko.Core.Grpc.Server/BaseGrpcServerModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public abstract class BaseGrpcServerModule<TConfig> : BaseApplicationModule<TCon
TConfig startupOptions)
{
base.ConfigureServices(applicationContext, services, startupOptions);
services.AddScoped(typeof(IGrpcCallProcessor<>), typeof(GrpcCallProcessor<>));
services.AddGrpc(options =>
{
options.EnableDetailedErrors = startupOptions.EnableDetailedErrors;
Expand Down Expand Up @@ -63,4 +64,3 @@ public void ConfigureHostBuilder(IApplicationContext context, IHostBuilder hostB
}
}
}

226 changes: 226 additions & 0 deletions src/Sitko.Core.Grpc.Server/GrpcCallProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
using Google.Protobuf;
using Grpc.Core;
using Microsoft.Extensions.Logging;
using PeanutButter.DuckTyping.Extensions;
using Sitko.Core.Grpc.Extensions;
using Sitko.FluentValidation.Graph;

namespace Sitko.Core.Grpc.Server;

public class GrpcCallProcessor
{
protected static readonly GraphValidationContextOptions ValidationOptions = new()
{
NeedToValidate = o =>
o.GetType().Namespace?.StartsWith("Google.Protobuf",
StringComparison.InvariantCultureIgnoreCase) != true
};
}

public class GrpcCallProcessor<TService> : GrpcCallProcessor, IGrpcCallProcessor<TService>
{
private readonly IFluentGraphValidator graphValidator;
private readonly ILogger<TService> logger;

public GrpcCallProcessor(ILogger<TService> logger, IFluentGraphValidator graphValidator)
{
this.logger = logger;
this.graphValidator = graphValidator;
}

public async Task<TResponse> ProcessCall<TResponse>(IMessage request, ServerCallContext context,
Func<TResponse, GrpcCallResult> execute) where TResponse : class, IMessage, new()
{
var response = CreateResponse<TResponse>();
if (!await ValidateRequestAsync(request, response, context))
{
return response;
}

try
{
var result = execute(response);
ProcessResult(result, request, response, context.Method);
}
catch (Exception ex)
{
ProcessResult(new GrpcCallResult(ex), request, response, context.Method);
}

return response;
}

public async Task<TResponse> ProcessCallAsync<TResponse>(IMessage request, ServerCallContext context,
Func<TResponse, Task<GrpcCallResult>> executeAsync) where TResponse : class, IMessage, new()
{
var response = CreateResponse<TResponse>();
if (!await ValidateRequestAsync(request, response, context))
{
return response;
}

try
{
var result = await executeAsync(response);
ProcessResult(result, request, response, context.Method);
}
catch (RpcException)
{
throw;
}
catch (Exception ex)
{
ProcessResult(new GrpcCallResult(ex), request, response, context.Method);
}

return response;
}

public Task<TResponse> ProcessCallAsync<TResponse>(IMessage request, ServerCallContext context,
Func<TResponse, Task> executeAsync) where TResponse : class, IMessage, new() => ProcessCallAsync<TResponse>(
request, context, async response =>
{
await executeAsync(response);
return GrpcCallResult.Ok();
});

public async Task ProcessStreamAsync<TResponse>(IMessage request, IServerStreamWriter<TResponse> responseStream,
ServerCallContext context,
Func<Func<Action<TResponse>, Task>, Task> executeAsync) where TResponse : class, IMessage, new()
{
var errorResponse = CreateResponse<TResponse>();

if (!await ValidateRequestAsync(request, errorResponse, context))
{
await responseStream.WriteAsync(errorResponse);
return;
}

try
{
await executeAsync(async fillResponse =>
{
var response = CreateResponse<TResponse>();
fillResponse(response);
await responseStream.WriteAsync(response);
});
}
catch (Exception ex)
{
var response = CreateResponse<TResponse>();
ProcessResult(new GrpcCallResult(ex), request, response, context.Method);
await responseStream.WriteAsync(response);
}
}

public async Task<TResponse> ProcessStreamAsync<TResponse>(IAsyncStreamReader<IMessage> requestStream,
ServerCallContext context, Func<TResponse, Task<GrpcCallResult>> executeAsync)
where TResponse : class, IMessage, new()
{
var response = CreateResponse<TResponse>();
try
{
var result = await executeAsync(response);
ProcessResult(result, null, response, context.Method);
}
catch (Exception ex)
{
ProcessResult(new GrpcCallResult(ex), null, response, context.Method);
}

return response;
}

public async Task ProcessStreamAsync<TResponse>(IAsyncStreamReader<IMessage> requestStream,
IServerStreamWriter<TResponse> responseStream,
ServerCallContext context, Func<Func<Action<TResponse>, Task>, Task> executeAsync)
where TResponse : class, IMessage, new()
{
try
{
await executeAsync(async fillResponse =>
{
var response = CreateResponse<TResponse>();
fillResponse(response);
await responseStream.WriteAsync(response);
});
}
catch (Exception ex)
{
var response = CreateResponse<TResponse>();
ProcessResult(new GrpcCallResult(ex), null, response, context.Method);
await responseStream.WriteAsync(response);
}
}

public async Task<bool> ValidateRequestAsync(IMessage request, IMessage response, ServerCallContext context)
{
var validationResult = await graphValidator.TryValidateModelAsync(
new ModelGraphValidationContext(request, ValidationOptions), context.CancellationToken);
if (!validationResult.IsValid)
{
ProcessResult(
new GrpcCallResult(validationResult.Results.SelectMany(result =>
result.Errors.Select(failure => $"{result.Path}{failure.PropertyName}: {failure.ErrorMessage}"))),
request,
response, context.Method);
return false;
}

return true;
}

private static TResponse CreateResponse<TResponse>() where TResponse : class, IMessage, new()
{
var response = new TResponse();
#pragma warning disable CS0618
if (response.DuckAs<IGrpcResponse>() is { } grpcResponse)
#pragma warning restore CS0618
{
grpcResponse.ResponseInfo = new ApiResponseInfo { IsSuccess = true };
}

return response;
}

private void ProcessResult<TResponse>(GrpcCallResult result, IMessage? request, TResponse response,
string methodName)
where TResponse : class, IMessage
{
if (!result.IsSuccess)
{
FillErrors(result, request, response, methodName);
}
}

private void FillErrors<TResponse>(GrpcCallResult result, IMessage? request, TResponse response,
string methodName)
where TResponse : class, IMessage
{
if (result.Exception is not null)
{
logger.LogError(result.Exception,
"Error in method {MethodName}. Request: {@Request}. Error: {ErrorText}",
methodName,
request, result.Exception.ToString());
#pragma warning disable CS0618
if (response.DuckAs<IGrpcResponse>() is { } grpcResponse)
{
grpcResponse.SetException(result.Exception);
}
#pragma warning restore CS0618
}
else
{
if (result.Error.Length > 0)
{
#pragma warning disable CS0618
if (response.DuckAs<IGrpcResponse>() is { } grpcResponse)
{
grpcResponse.SetErrors(result.Error);
}
#pragma warning restore CS0618
}
}
}
}
42 changes: 42 additions & 0 deletions src/Sitko.Core.Grpc.Server/IGrpcCallProcessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Google.Protobuf;
using Grpc.Core;

namespace Sitko.Core.Grpc.Server;

// ReSharper disable once UnusedTypeParameter
public interface IGrpcCallProcessor<TService>
{
Task<TResponse> ProcessCall<TResponse>(IMessage request,
ServerCallContext context,
Func<TResponse, GrpcCallResult> execute)
where TResponse : class, IMessage, new();

Task<TResponse> ProcessCallAsync<TResponse>(IMessage request,
ServerCallContext context,
Func<TResponse, Task<GrpcCallResult>> executeAsync)
where TResponse : class, IMessage, new();

Task<TResponse> ProcessCallAsync<TResponse>(IMessage request,
ServerCallContext context,
Func<TResponse, Task> executeAsync)
where TResponse : class, IMessage, new();

Task ProcessStreamAsync<TResponse>(IMessage request,
IServerStreamWriter<TResponse> responseStream,
ServerCallContext context,
Func<Func<Action<TResponse>, Task>, Task> executeAsync)
where TResponse : class, IMessage, new();

Task<TResponse> ProcessStreamAsync<TResponse>(IAsyncStreamReader<IMessage> requestStream,
ServerCallContext context,
Func<TResponse, Task<GrpcCallResult>> executeAsync)
where TResponse : class, IMessage, new();

Task ProcessStreamAsync<TResponse>(IAsyncStreamReader<IMessage> requestStream,
IServerStreamWriter<TResponse> responseStream,
ServerCallContext context,
Func<Func<Action<TResponse>, Task>, Task> executeAsync)
where TResponse : class, IMessage, new();

Task<bool> ValidateRequestAsync(IMessage request, IMessage response, ServerCallContext context);
}
5 changes: 4 additions & 1 deletion src/Sitko.Core.Grpc/Extensions/GrpcExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,27 @@ public static class GrpcExtensions
{
public static string PrepareString(string? s) => s ?? string.Empty;

[Obsolete("Do not use this method in new code")]
public static void SetError(this IGrpcResponse response, string error, int code = 500)
{
response.ResponseInfo.IsSuccess = false;
response.ResponseInfo.Error = new ApiResponseError { Code = code, Errors = { error } };
}

[Obsolete("Do not use this method in new code")]
public static void SetErrors(this IGrpcResponse response, IEnumerable<string> errors, int code = 500)
{
response.ResponseInfo.IsSuccess = false;
response.ResponseInfo.Error = new ApiResponseError { Code = code, Errors = { errors } };
}

[Obsolete("Do not use this method in new code")]
public static void SetException(this IGrpcResponse response, Exception ex, int code = 500)
{
response.ResponseInfo.IsSuccess = false;
response.ResponseInfo.Error = new ApiResponseError { Code = code, Errors = { ex.ToString() } };
}

[Obsolete("Do not use this method in new code")]
public static bool IsSuccess(this IGrpcResponse response) => response.ResponseInfo.IsSuccess;
}

3 changes: 2 additions & 1 deletion src/Sitko.Core.Grpc/GrpcCallResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ public GrpcCallResult(IEnumerable<string> errors)
public bool IsSuccess { get; }
public string[] Error => errors.ToArray();
public Exception? Exception { get; }
}

public static GrpcCallResult Ok() => new();
}
Loading

0 comments on commit 65f327a

Please sign in to comment.