From 79199d45ce8d5890d4ee752d8674e734e1ff509f Mon Sep 17 00:00:00 2001 From: jrozac Date: Tue, 9 Feb 2021 13:54:15 +0100 Subject: [PATCH 1/9] AspNetCore Grpc permissions validation. --- .../CoreSharp.Cqrs.Grpc.AspNetCore.csproj | 3 +- .../GrpcCqrsEndpointRouteBuilderExtensions.cs | 17 ++++ .../GrpcCqrsServicesExtensions.cs | 1 + ...viceEndpointConventionBuilderExtensions.cs | 90 +++++++++++++++++++ .../Common/CqrsChannelInfo.cs | 5 +- .../Common/CqrsInfoExtensions.cs | 5 +- CoreSharp.Cqrs.Resolver/CqrsInfo.cs | 9 +- CoreSharp.Cqrs.Resolver/CqrsInfoExtensions.cs | 5 ++ .../CqrsInfoResolverUtil.cs | 15 +++- 9 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 CoreSharp.Cqrs.Grpc.AspNetCore/GrpcServiceEndpointConventionBuilderExtensions.cs diff --git a/CoreSharp.Cqrs.Grpc.AspNetCore/CoreSharp.Cqrs.Grpc.AspNetCore.csproj b/CoreSharp.Cqrs.Grpc.AspNetCore/CoreSharp.Cqrs.Grpc.AspNetCore.csproj index 1a9b5ce..3620e94 100644 --- a/CoreSharp.Cqrs.Grpc.AspNetCore/CoreSharp.Cqrs.Grpc.AspNetCore.csproj +++ b/CoreSharp.Cqrs.Grpc.AspNetCore/CoreSharp.Cqrs.Grpc.AspNetCore.csproj @@ -1,4 +1,4 @@ - + Library @@ -12,6 +12,7 @@ + diff --git a/CoreSharp.Cqrs.Grpc.AspNetCore/GrpcCqrsEndpointRouteBuilderExtensions.cs b/CoreSharp.Cqrs.Grpc.AspNetCore/GrpcCqrsEndpointRouteBuilderExtensions.cs index 7c201b5..275123a 100644 --- a/CoreSharp.Cqrs.Grpc.AspNetCore/GrpcCqrsEndpointRouteBuilderExtensions.cs +++ b/CoreSharp.Cqrs.Grpc.AspNetCore/GrpcCqrsEndpointRouteBuilderExtensions.cs @@ -4,6 +4,7 @@ using CoreSharp.Cqrs.Grpc.AspNetCore; using CoreSharp.Cqrs.Grpc.Contracts; using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Logging; using SimpleInjector; namespace Microsoft.AspNetCore.Routing @@ -11,6 +12,22 @@ namespace Microsoft.AspNetCore.Routing public static class GrpcCqrsEndpointRouteBuilderExtensions { + public static GrpcServiceEndpointConventionBuilder MapCqrsGrpcWithAuthorization(this IEndpointRouteBuilder endpoints) + { + + // get channel info + var cqrsAdapter = (CqrsContractsAdapter)endpoints.ServiceProvider.GetService(typeof(CqrsContractsAdapter)); + var channelInfo = cqrsAdapter.ToCqrsChannelInfo(); + + // get logger for auth + ILogger logger = (endpoints.ServiceProvider.GetService(typeof(ILoggerFactory)) as ILoggerFactory)?.CreateLogger("CqrsGrpcServer"); + + // map and return + var map = endpoints.MapCqrsGrpc(); + map = map.AddCqrsAuthorization(channelInfo, logger); + return map; + } + public static GrpcServiceEndpointConventionBuilder MapCqrsGrpc(this IEndpointRouteBuilder endpoints) { // map proto list diff --git a/CoreSharp.Cqrs.Grpc.AspNetCore/GrpcCqrsServicesExtensions.cs b/CoreSharp.Cqrs.Grpc.AspNetCore/GrpcCqrsServicesExtensions.cs index 63d02e1..96ce67f 100644 --- a/CoreSharp.Cqrs.Grpc.AspNetCore/GrpcCqrsServicesExtensions.cs +++ b/CoreSharp.Cqrs.Grpc.AspNetCore/GrpcCqrsServicesExtensions.cs @@ -30,6 +30,7 @@ public static IServiceCollection AddCqrsGrpc(this IServiceCollection services, // register grpc services.AddGrpc(); services.AddSingleton(svc => container.GetInstance>()); + services.AddSingleton(svc => container.GetInstance()); // return return services; diff --git a/CoreSharp.Cqrs.Grpc.AspNetCore/GrpcServiceEndpointConventionBuilderExtensions.cs b/CoreSharp.Cqrs.Grpc.AspNetCore/GrpcServiceEndpointConventionBuilderExtensions.cs new file mode 100644 index 0000000..f033b78 --- /dev/null +++ b/CoreSharp.Cqrs.Grpc.AspNetCore/GrpcServiceEndpointConventionBuilderExtensions.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Security.Claims; +using System.Threading.Tasks; +using CoreSharp.Cqrs.Grpc.Common; +using CoreSharp.Cqrs.Resolver; +using CoreSharp.Identity.Extensions; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Logging; + +namespace CoreSharp.Cqrs.Grpc.AspNetCore +{ + public static class GrpcServiceEndpointConventionBuilderExtensions + { + + public static GrpcServiceEndpointConventionBuilder AddCqrsAuthorization(this GrpcServiceEndpointConventionBuilder builder, IEnumerable cqrs, ILogger logger = null) + { + + builder.Add(cnv => { + + var execDelegate = cnv.RequestDelegate; + cnv.RequestDelegate = ctx => { + + // no path set, this should not happen + if(string.IsNullOrWhiteSpace(ctx.Request.Path)) + { + logger?.LogWarning("Request path is not set."); + ctx.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + return Task.CompletedTask; + } + + // get path definition + var path = ctx.Request.Path.Value.ToLowerInvariant(); + var definition = cqrs.Where(x => ((CqrsInfo)x).GetPath().ToLowerInvariant().StartsWith(path)) + .OrderByDescending(x => ((CqrsInfo)x).GetPath().Length).FirstOrDefault(); + + // definition not found, this should not be the case + if(definition == null) + { + logger?.LogWarning("No CQRS found for request path."); + ctx.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + return Task.CompletedTask; + } + + // log required permissions + logger?.LogDebug("Authorization required: permissions={permissions}", + string.Join(',', definition.Permissions?.ToList() ?? new List())); + + // authorization required + if (definition.IsAuthorize) { + + // user required for authorization + var identity = ctx.User?.Identity as ClaimsIdentity; + if (identity == null || !identity.IsAuthenticated) + { + ctx.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + return Task.CompletedTask; + } + + // log required permissions + logger?.LogDebug("Authorization user: permissions={permissions}, system={systemUser}", + string.Join(',', identity.GetPermissions()?.ToList() ?? new List()), + identity.IsSystemUser()); + + // permissions check + if (definition.Permissions != null && definition.Permissions.Any() && !identity.IsSystemUser() + && !identity.HasAnyPermission(definition.Permissions.ToArray())) + { + ctx.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + return Task.CompletedTask; + } + + } + + // log access + logger?.LogDebug("Authorization passed for path {path}.", ctx.Request.Path); + + // execute action + return execDelegate(ctx); + }; + + }); + + return builder; + } + + + } +} diff --git a/CoreSharp.Cqrs.Grpc.Shared/Common/CqrsChannelInfo.cs b/CoreSharp.Cqrs.Grpc.Shared/Common/CqrsChannelInfo.cs index 4775c89..da70e4c 100644 --- a/CoreSharp.Cqrs.Grpc.Shared/Common/CqrsChannelInfo.cs +++ b/CoreSharp.Cqrs.Grpc.Shared/Common/CqrsChannelInfo.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using CoreSharp.Cqrs.Resolver; namespace CoreSharp.Cqrs.Grpc.Common @@ -11,8 +12,8 @@ public class CqrsChannelInfo : CqrsInfo public Type ChRspEnvType { get; } - internal CqrsChannelInfo(Type reqType, string serviceName, string methodName, string formatter, bool isQuery, bool isCommand, bool isAsync, Type rspType, Type chReqType, Type chRspType, Type chRspEnvType) - : base(reqType, serviceName, methodName, formatter, isQuery, isCommand, isAsync, rspType) + internal CqrsChannelInfo(Type reqType, string serviceName, string methodName, string formatter, bool isQuery, bool isCommand, bool isAsync, bool isAuthorize, Type rspType, Type chReqType, Type chRspType, Type chRspEnvType, IEnumerable permissions) + : base(reqType, serviceName, methodName, formatter, isQuery, isCommand, isAsync, isAuthorize, rspType, permissions) { ChReqType = chReqType; ChRspType = chRspType; diff --git a/CoreSharp.Cqrs.Grpc.Shared/Common/CqrsInfoExtensions.cs b/CoreSharp.Cqrs.Grpc.Shared/Common/CqrsInfoExtensions.cs index b1d55e6..6f81327 100644 --- a/CoreSharp.Cqrs.Grpc.Shared/Common/CqrsInfoExtensions.cs +++ b/CoreSharp.Cqrs.Grpc.Shared/Common/CqrsInfoExtensions.cs @@ -43,10 +43,13 @@ public static IEnumerable ToCqrsChannelInfo(this IEnumerable).MakeGenericType(x.RspType)] : null)).ToList(); + x.RspType != null ? channelContracts[typeof(GrpcResponseEnvelope<>).MakeGenericType(x.RspType)] : null, + x.Permissions + )).ToList(); return channelCqrs; } diff --git a/CoreSharp.Cqrs.Resolver/CqrsInfo.cs b/CoreSharp.Cqrs.Resolver/CqrsInfo.cs index 265d67c..cbded0f 100644 --- a/CoreSharp.Cqrs.Resolver/CqrsInfo.cs +++ b/CoreSharp.Cqrs.Resolver/CqrsInfo.cs @@ -1,20 +1,23 @@ using System; +using System.Collections.Generic; namespace CoreSharp.Cqrs.Resolver { public class CqrsInfo { - public CqrsInfo(Type reqType, string serviceName, string methodName, string formatter, bool isQuery, bool isCommand, bool isAsync, Type rspType) + public CqrsInfo(Type reqType, string serviceName, string methodName, string formatter, bool isQuery, bool isCommand, bool isAsync, bool isAuthorize, Type rspType, IEnumerable permissions) { ReqType = reqType; RspType = rspType; IsAsync = isAsync; IsQuery = isQuery; IsCommand = isCommand; + IsAuthorize = isAuthorize; ServiceName = serviceName; MethodName = methodName; Formatter = formatter; + Permissions = permissions; } public Type ReqType { get; } @@ -27,10 +30,14 @@ public CqrsInfo(Type reqType, string serviceName, string methodName, string form public bool IsCommand { get; } + public bool IsAuthorize { get; } + public string ServiceName { get; } public string MethodName { get; } public string Formatter { get; } + + public IEnumerable Permissions { get; } } } diff --git a/CoreSharp.Cqrs.Resolver/CqrsInfoExtensions.cs b/CoreSharp.Cqrs.Resolver/CqrsInfoExtensions.cs index 0032714..4b52760 100644 --- a/CoreSharp.Cqrs.Resolver/CqrsInfoExtensions.cs +++ b/CoreSharp.Cqrs.Resolver/CqrsInfoExtensions.cs @@ -67,5 +67,10 @@ public static Type GetCommandHandlerType(this CqrsInfo info) } } } + + public static string GetPath(this CqrsInfo info) + { + return $"/{info.ServiceName}/{info.MethodName}"; + } } } diff --git a/CoreSharp.Cqrs.Resolver/CqrsInfoResolverUtil.cs b/CoreSharp.Cqrs.Resolver/CqrsInfoResolverUtil.cs index f9fb1b9..2447fa5 100644 --- a/CoreSharp.Cqrs.Resolver/CqrsInfoResolverUtil.cs +++ b/CoreSharp.Cqrs.Resolver/CqrsInfoResolverUtil.cs @@ -19,7 +19,7 @@ public static IEnumerable GetCqrsDefinitions(Assembly assembly) var serviceName = string.Join("_", x.FullName.Split('.').Take(x.FullName.Split('.').Length - 1)); var methodName = x.IsQuery() ? CqrsExposeUtil.GetQueryKey(x) : CqrsExposeUtil.GetCommandKey(x); var formatter = CqrsExposeUtil.GetFormatter(x); - return new CqrsInfo(x, serviceName, methodName, formatter, x.IsQuery(), x.IsCommand(), x.IsCqrsAsync(), x.GetResultType()); + return new CqrsInfo(x, serviceName, methodName, formatter, x.IsQuery(), x.IsCommand(), x.IsCqrsAsync(), x.IsAuthorize(), x.GetResultType(), x.GetPermissions()); }).ToList(); return infos; } @@ -61,5 +61,18 @@ private static bool Implements(this Type type, Type interfaceType) return type.GetAllInterfaces().Any(x => (x.IsGenericType && x.GetGenericTypeDefinition() == interfaceType) || x == interfaceType); } + private static IEnumerable GetPermissions(this Type type) + { + var permissions = type.GetCustomAttributes() + ?.SelectMany(y => y.Permissions ?? new string[0]).Distinct().ToList() ?? new List(); + return permissions; + } + + private static bool IsAuthorize(this Type type) + { + var count = type.GetCustomAttributes()?.Count() ?? 0; + return count > 0; + } + } } From 99fdc147aee73d451804f7b35bebd2dff7386dad Mon Sep 17 00:00:00 2001 From: jrozac Date: Tue, 16 Feb 2021 16:41:01 +0100 Subject: [PATCH 2/9] Grpc internal bearer authentication. --- .../GrpcCqrsAspNetCoreBearerConfiguration.cs | 16 ++++ .../CoreSharp.Cqrs.Grpc.AspNetCore.Jwt.csproj | 13 ++++ ...GrpcCqrsAuthenticationBuilderExtensions.cs | 75 +++++++++++++++++++ .../GrpcCqrsAspNetCoreRawConfiguration.cs | 6 ++ ...qrsAspNetCoreRawConfigurationExtensions.cs | 8 +- .../CommandDispatcherRouteDecorator.cs | 71 ++++++++++++++++-- .../EnumChannelAuthorizationType.cs | 8 ++ .../Configuration/GrpcCqrsCallOptions.cs | 7 ++ .../GrpcCqrsClientConfiguration.cs | 12 +++ .../GrpcCqrsClientRawConfiguration.cs | 12 +++ ...rpcCqrsClientRawConfigurationExtensions.cs | 6 +- .../CoreSharp.Cqrs.Grpc.Client.csproj | 1 + .../GrpcClientCommandDispatcher.cs | 46 +++++++++--- .../GrpcClientQueryProcessor.cs | 26 +++++-- CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs | 49 ++++++++++-- .../IGrpcCommandDispatcher.cs | 8 +- .../IGrpcQueryProcessor.cs | 7 +- .../QueryProcessorRouteDecorator.cs | 43 +++++++++-- .../CoreSharp.Identity.Jwt.csproj | 11 +++ CoreSharp.Identity.Jwt/TokenConfiguration.cs | 19 +++++ .../TokenConfigurationUser.cs | 11 +++ CoreSharp.Identity.Jwt/TokenData.cs | 11 +++ CoreSharp.Identity.Jwt/TokenFactory.cs | 42 +++++++++++ CoreSharp.Identity.Jwt/TokenService.cs | 73 ++++++++++++++++++ CoreSharp.sln | 24 ++++-- 25 files changed, 555 insertions(+), 50 deletions(-) create mode 100644 CoreSharp.Cqrs.Grpc.AspNetCore.Jwt/Configuration/GrpcCqrsAspNetCoreBearerConfiguration.cs create mode 100644 CoreSharp.Cqrs.Grpc.AspNetCore.Jwt/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt.csproj create mode 100644 CoreSharp.Cqrs.Grpc.AspNetCore.Jwt/GrpcCqrsAuthenticationBuilderExtensions.cs create mode 100644 CoreSharp.Cqrs.Grpc.Client/Configuration/EnumChannelAuthorizationType.cs create mode 100644 CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsCallOptions.cs create mode 100644 CoreSharp.Identity.Jwt/CoreSharp.Identity.Jwt.csproj create mode 100644 CoreSharp.Identity.Jwt/TokenConfiguration.cs create mode 100644 CoreSharp.Identity.Jwt/TokenConfigurationUser.cs create mode 100644 CoreSharp.Identity.Jwt/TokenData.cs create mode 100644 CoreSharp.Identity.Jwt/TokenFactory.cs create mode 100644 CoreSharp.Identity.Jwt/TokenService.cs diff --git a/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt/Configuration/GrpcCqrsAspNetCoreBearerConfiguration.cs b/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt/Configuration/GrpcCqrsAspNetCoreBearerConfiguration.cs new file mode 100644 index 0000000..2d7d95b --- /dev/null +++ b/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt/Configuration/GrpcCqrsAspNetCoreBearerConfiguration.cs @@ -0,0 +1,16 @@ +namespace CoreSharp.Cqrs.Grpc.AspNetCore +{ + public class GrpcCqrsAspNetCoreBearerConfiguration + { + public string Secret { get; set; } + + public string JwtIssuer { get; set; } = "coresharp-grpc"; + + public int ClockSkewSeconds { get; set; } = 30; + + public bool RequireHttpsMetadata { get; set; } + + public bool SaveToken { get; set; } + + } +} diff --git a/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt.csproj b/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt.csproj new file mode 100644 index 0000000..90ee9a8 --- /dev/null +++ b/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt.csproj @@ -0,0 +1,13 @@ + + + + Library + netcoreapp3.1 + true + + + + + + + diff --git a/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt/GrpcCqrsAuthenticationBuilderExtensions.cs b/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt/GrpcCqrsAuthenticationBuilderExtensions.cs new file mode 100644 index 0000000..bd35bce --- /dev/null +++ b/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt/GrpcCqrsAuthenticationBuilderExtensions.cs @@ -0,0 +1,75 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Text; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; + +namespace CoreSharp.Cqrs.Grpc.AspNetCore +{ + public static class GrpcCqrsAuthenticationBuilderExtensions + { + public static AuthenticationBuilder AddCoreSharpGrpc(this AuthenticationBuilder builder, GrpcCqrsAspNetCoreBearerConfiguration configuration) + { + // register confiuration + builder.Services.AddSingleton(x => configuration); + + builder.AddJwtBearer($"Bearer-{configuration.JwtIssuer}", x => { + x.RequireHttpsMetadata = configuration.RequireHttpsMetadata; + x.SaveToken = configuration.SaveToken; + x.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.Default.GetBytes(configuration.Secret)), + ValidateIssuer = false, + ValidateAudience = false, + ClockSkew = TimeSpan.FromSeconds(configuration.ClockSkewSeconds) + }; + }); + + return builder; + } + + public static IApplicationBuilder UseCoreSharpAuthentication(this IApplicationBuilder builder) + { + return builder.Use(async (context, next) => + { + var cfg = context.RequestServices.GetService(); + + if (!context.User.Identity.IsAuthenticated) + { + var key = "Authorization"; + var authParts = (context.Request.Headers.TryGetValue(key, out var sv) ? sv.ToString() : null)?.Split(" "); + var token = authParts?.Length > 1 ? authParts[1] : null; + string scheme = null; + if (!string.IsNullOrWhiteSpace(token)) + { + try + { + var jwtToken = new JwtSecurityToken(token); + scheme = jwtToken.Issuer == cfg.JwtIssuer ? $"Bearer-{jwtToken.Issuer}" : null; + } + catch (Exception) + { + } + } + + // validate against selected scheme + if (!string.IsNullOrWhiteSpace(scheme)) + { + var result = await context.AuthenticateAsync(scheme); + if (result.Succeeded) + { + context.User = result.Principal; + } + } + } + + // continue + await next(); + }); + + } + } +} diff --git a/CoreSharp.Cqrs.Grpc.AspNetCore/Configuration/GrpcCqrsAspNetCoreRawConfiguration.cs b/CoreSharp.Cqrs.Grpc.AspNetCore/Configuration/GrpcCqrsAspNetCoreRawConfiguration.cs index 27b5473..684254f 100644 --- a/CoreSharp.Cqrs.Grpc.AspNetCore/Configuration/GrpcCqrsAspNetCoreRawConfiguration.cs +++ b/CoreSharp.Cqrs.Grpc.AspNetCore/Configuration/GrpcCqrsAspNetCoreRawConfiguration.cs @@ -9,5 +9,11 @@ public class GrpcCqrsAspNetCoreRawConfiguration public int TimeoutMs { get; set; } = 10000; public IEnumerable ContractsAssemblies { get; set; } + + public string ServerId { get; set; } + + public bool ExposeProto { get; set; } = true; + + public string MapperValidator { get; set; } } } diff --git a/CoreSharp.Cqrs.Grpc.AspNetCore/Configuration/GrpcCqrsAspNetCoreRawConfigurationExtensions.cs b/CoreSharp.Cqrs.Grpc.AspNetCore/Configuration/GrpcCqrsAspNetCoreRawConfigurationExtensions.cs index be68ce3..ca908a0 100644 --- a/CoreSharp.Cqrs.Grpc.AspNetCore/Configuration/GrpcCqrsAspNetCoreRawConfigurationExtensions.cs +++ b/CoreSharp.Cqrs.Grpc.AspNetCore/Configuration/GrpcCqrsAspNetCoreRawConfigurationExtensions.cs @@ -1,4 +1,5 @@ -using CoreSharp.Common.Extensions; +using System; +using CoreSharp.Common.Extensions; namespace CoreSharp.Cqrs.Grpc.AspNetCore { @@ -10,7 +11,10 @@ public static GrpcCqrsAspNetCoreConfiguration ToConfiguration(this GrpcCqrsAspNe { ContractsAssemblies = raw.ContractsAssemblies.ToAssemblies(), ServiceNamePrefix = raw.ServiceNamePrefix, - TimeoutMs = raw.TimeoutMs + TimeoutMs = raw.TimeoutMs, + ServerId = raw.ServerId, + ExposeProto = raw.ExposeProto, + MapperValidator = !string.IsNullOrWhiteSpace(raw.MapperValidator) ? Type.GetType(raw.MapperValidator) : null }; return cfg; } diff --git a/CoreSharp.Cqrs.Grpc.Client/CommandDispatcherRouteDecorator.cs b/CoreSharp.Cqrs.Grpc.Client/CommandDispatcherRouteDecorator.cs index eb35420..6b7ad0c 100644 --- a/CoreSharp.Cqrs.Grpc.Client/CommandDispatcherRouteDecorator.cs +++ b/CoreSharp.Cqrs.Grpc.Client/CommandDispatcherRouteDecorator.cs @@ -17,39 +17,94 @@ public CommandDispatcherRouteDecorator(IGrpcClientManager clientManager, IComman _grpcDispatcher = grpcDispatcher; } + public void Dispatch(ICommand command, GrpcCqrsCallOptions options) + { + var remoteDispatcher = GetRemoteDispatcher(command); + if (remoteDispatcher != null) + { + remoteDispatcher.Dispatch(command, options); + } + else + { + _dispatcher.Dispatch(command); + } + } + + public TResult Dispatch(ICommand command, GrpcCqrsCallOptions options) + { + var remoteDispatcher = GetRemoteDispatcher(command); + if (remoteDispatcher != null) + { + return remoteDispatcher.Dispatch(command, options); + } + else + { + return _dispatcher.Dispatch(command); + } + } + + public async Task DispatchAsync(IAsyncCommand command, GrpcCqrsCallOptions options, CancellationToken cancellationToken) + { + var remoteDispatcher = GetRemoteDispatcher(command); + if(remoteDispatcher != null) + { + await remoteDispatcher.DispatchAsync(command, options, cancellationToken); + } else + { + await _dispatcher.DispatchAsync(command, cancellationToken); + } + } + + public async Task DispatchAsync(IAsyncCommand command, GrpcCqrsCallOptions options, CancellationToken cancellationToken) + { + var remoteDispatcher = GetRemoteDispatcher(command); + if (remoteDispatcher != null) + { + return await remoteDispatcher.DispatchAsync(command, options, cancellationToken); + } + else + { + return await _dispatcher.DispatchAsync(command, cancellationToken); + } + } + + #region IGrpcCommandDispatcher + public void Dispatch(ICommand command) { - GetDispatcher(command).Dispatch(command); + Dispatch(command, null); } public TResult Dispatch(ICommand command) { - return GetDispatcher(command).Dispatch(command); + return Dispatch(command, null); } public async Task DispatchAsync(IAsyncCommand command) { - await GetDispatcher(command).DispatchAsync(command); + await DispatchAsync(command, null, default); } public async Task DispatchAsync(IAsyncCommand command) { - return await GetDispatcher(command).DispatchAsync(command); + return await DispatchAsync(command, null, default); } public async Task DispatchAsync(IAsyncCommand command, CancellationToken cancellationToken) { - await GetDispatcher(command).DispatchAsync(command, cancellationToken); + await DispatchAsync(command, null, cancellationToken); } public async Task DispatchAsync(IAsyncCommand command, CancellationToken cancellationToken) { - return await GetDispatcher(command).DispatchAsync(command, cancellationToken); + return await DispatchAsync(command, null, cancellationToken); } - private ICommandDispatcher GetDispatcher(T command) + #endregion + + private IGrpcCommandDispatcher GetRemoteDispatcher(T command) { - var dispatcher = _clientManager.ExistsClientFor(command) ? _grpcDispatcher : _dispatcher; + var dispatcher = _clientManager.ExistsClientFor(command) ? _grpcDispatcher : null; return dispatcher; } } diff --git a/CoreSharp.Cqrs.Grpc.Client/Configuration/EnumChannelAuthorizationType.cs b/CoreSharp.Cqrs.Grpc.Client/Configuration/EnumChannelAuthorizationType.cs new file mode 100644 index 0000000..d6f0214 --- /dev/null +++ b/CoreSharp.Cqrs.Grpc.Client/Configuration/EnumChannelAuthorizationType.cs @@ -0,0 +1,8 @@ +namespace CoreSharp.Cqrs.Grpc.Client +{ + public enum EnumChannelAuthorizationType + { + None, + Token + } +} diff --git a/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsCallOptions.cs b/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsCallOptions.cs new file mode 100644 index 0000000..d8b1be6 --- /dev/null +++ b/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsCallOptions.cs @@ -0,0 +1,7 @@ +namespace CoreSharp.Cqrs.Grpc.Client +{ + public class GrpcCqrsCallOptions + { + public bool AddInternalAuthorization { get; set; } + } +} diff --git a/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientConfiguration.cs b/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientConfiguration.cs index d4ba479..8b10cd8 100644 --- a/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientConfiguration.cs +++ b/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientConfiguration.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Reflection; +using CoreSharp.Identity.Jwt; namespace CoreSharp.Cqrs.Grpc.Client { @@ -15,8 +16,19 @@ public class GrpcCqrsClientConfiguration public bool HandleExceptions { get; set; } + public bool HandleUnauthenticated { get; set; } = true; + public IEnumerable ContractsAssemblies { get; set; } public string ClientId { get; set; } + + public EnumChannelAuthorizationType AuthorizationType { get; set; } + + public TokenConfiguration TokenConfiguration { get; set; } + + public GrpcCqrsCallOptions DefaultCallOptions { get; set; } = new GrpcCqrsCallOptions + { + AddInternalAuthorization = true + }; } } diff --git a/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientRawConfiguration.cs b/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientRawConfiguration.cs index ee44ab3..786a261 100644 --- a/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientRawConfiguration.cs +++ b/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientRawConfiguration.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using CoreSharp.Identity.Jwt; namespace CoreSharp.Cqrs.Grpc.Client { @@ -14,8 +15,19 @@ public class GrpcCqrsClientRawConfiguration public bool HandleExceptions { get; set; } + public bool HandleUnauthenticated { get; set; } = true; + public IEnumerable ContractsAssemblies { get; set; } public string ClientId { get; set; } + + public EnumChannelAuthorizationType AuthorizationType { get; set; } + + public TokenConfiguration TokenConfiguration { get; set; } + + public GrpcCqrsCallOptions DefaultCallOptions { get; set; } = new GrpcCqrsCallOptions + { + AddInternalAuthorization = true + }; } } diff --git a/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientRawConfigurationExtensions.cs b/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientRawConfigurationExtensions.cs index e469593..d1fc0f8 100644 --- a/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientRawConfigurationExtensions.cs +++ b/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientRawConfigurationExtensions.cs @@ -11,11 +11,15 @@ public static GrpcCqrsClientConfiguration ToConfiguration(this GrpcCqrsClientRaw { Url = raw.Url, HandleExceptions = raw.HandleExceptions, + HandleUnauthenticated = raw.HandleUnauthenticated, Port = raw.Port, TimeoutMs = raw.TimeoutMs, ServiceNamePrefix = raw.ServiceNamePrefix, ContractsAssemblies = raw.ContractsAssemblies.ToAssemblies(), - ClientId = raw.ClientId + ClientId = raw.ClientId, + AuthorizationType = raw.AuthorizationType, + TokenConfiguration = raw.TokenConfiguration, + DefaultCallOptions = raw.DefaultCallOptions }; return cfg; } diff --git a/CoreSharp.Cqrs.Grpc.Client/CoreSharp.Cqrs.Grpc.Client.csproj b/CoreSharp.Cqrs.Grpc.Client/CoreSharp.Cqrs.Grpc.Client.csproj index 5509981..5683a16 100644 --- a/CoreSharp.Cqrs.Grpc.Client/CoreSharp.Cqrs.Grpc.Client.csproj +++ b/CoreSharp.Cqrs.Grpc.Client/CoreSharp.Cqrs.Grpc.Client.csproj @@ -11,6 +11,7 @@ + diff --git a/CoreSharp.Cqrs.Grpc.Client/GrpcClientCommandDispatcher.cs b/CoreSharp.Cqrs.Grpc.Client/GrpcClientCommandDispatcher.cs index 9f08be6..f9ddc91 100644 --- a/CoreSharp.Cqrs.Grpc.Client/GrpcClientCommandDispatcher.cs +++ b/CoreSharp.Cqrs.Grpc.Client/GrpcClientCommandDispatcher.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using CoreSharp.Cqrs.Command; @@ -17,37 +15,61 @@ public GrpcClientCommandDispatcher(IGrpcClientManager clientManager) _clientManager = clientManager; } - public void Dispatch(ICommand command) + public void Dispatch(ICommand command, GrpcCqrsCallOptions options) { throw new NotImplementedException(); } - public Task DispatchAsync(IAsyncCommand command) + public TResult Dispatch(ICommand command, GrpcCqrsCallOptions options) { - throw new NotImplementedException(); + var rsp = _clientManager.GetClientFor(command).Execute, TResult>(command, options, default).Result; + return rsp.Value; } - public Task DispatchAsync(IAsyncCommand command, CancellationToken cancellationToken) + public Task DispatchAsync(IAsyncCommand command, GrpcCqrsCallOptions options, CancellationToken cancellationToken) { throw new NotImplementedException(); } - public TResult Dispatch(ICommand command) + public async Task DispatchAsync(IAsyncCommand command, GrpcCqrsCallOptions options, CancellationToken cancellationToken) { - var rsp = _clientManager.GetClientFor(command).Execute, TResult>(command, default).Result; + var rsp = await _clientManager.GetClientFor(command).Execute, TResult>(command, options, cancellationToken); return rsp.Value; } + #region ICommandDispatcher + + public void Dispatch(ICommand command) + { + Dispatch(command, null); + } + + public TResult Dispatch(ICommand command) + { + return Dispatch(command, null); + } + + public async Task DispatchAsync(IAsyncCommand command) + { + await DispatchAsync(command, null, default); + } + public async Task DispatchAsync(IAsyncCommand command) { - var rsp = await _clientManager.GetClientFor(command).Execute, TResult>(command, default); - return rsp.Value; + return await DispatchAsync(command, null, default); + } + + public async Task DispatchAsync(IAsyncCommand command, CancellationToken cancellationToken) + { + await DispatchAsync(command, null, cancellationToken); } public async Task DispatchAsync(IAsyncCommand command, CancellationToken cancellationToken) { - var rsp = await _clientManager.GetClientFor(command).Execute, TResult>(command, cancellationToken); - return rsp.Value; + return await DispatchAsync(command, null, cancellationToken); } + + #endregion + } } diff --git a/CoreSharp.Cqrs.Grpc.Client/GrpcClientQueryProcessor.cs b/CoreSharp.Cqrs.Grpc.Client/GrpcClientQueryProcessor.cs index 4cc9207..d5b54e9 100644 --- a/CoreSharp.Cqrs.Grpc.Client/GrpcClientQueryProcessor.cs +++ b/CoreSharp.Cqrs.Grpc.Client/GrpcClientQueryProcessor.cs @@ -15,29 +15,41 @@ public GrpcClientQueryProcessor(IEnumerable clients) _clients = clients; } - public TResult Handle(IQuery query) + public TResult Handle(IQuery query, GrpcCqrsCallOptions callOptions) { - var rsp = GetClientForQuery(query).Execute, TResult>(query, default).Result; + var rsp = GetClientForQuery(query).Execute, TResult>(query, callOptions, default).Result; return rsp.Value; } - public async Task HandleAsync(IAsyncQuery query) + public async Task HandleAsync(IAsyncQuery query, GrpcCqrsCallOptions callOptions, CancellationToken cancellationToken) { - var rsp = await GetClientForQuery(query).Execute, TResult>(query, default); + var rsp = await GetClientForQuery(query).Execute, TResult>(query, callOptions, cancellationToken); return rsp.Value; } + #region IQueryProcessor + + public TResult Handle(IQuery query) + { + return Handle(query, null); + } + + public async Task HandleAsync(IAsyncQuery query) + { + return await HandleAsync(query, null, default); + } + public async Task HandleAsync(IAsyncQuery query, CancellationToken cancellationToken) { - var rsp = await GetClientForQuery(query).Execute, TResult>(query, cancellationToken); - return rsp.Value; + return await HandleAsync(query, null, cancellationToken); } + #endregion + private GrpcCqrsClient GetClientForQuery(object query) { var assemlby = query.GetType().Assembly; return _clients.First(x => x.ContractsAssemblies.Contains(assemlby)); } - } } diff --git a/CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs b/CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs index 0781ef5..08d8ceb 100644 --- a/CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs +++ b/CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs @@ -10,6 +10,7 @@ using CoreSharp.Cqrs.Grpc.Common; using CoreSharp.Cqrs.Grpc.Contracts; using CoreSharp.Cqrs.Resolver; +using CoreSharp.Identity.Jwt; using CoreSharp.Validation; using Grpc.Core; using Microsoft.Extensions.Logging; @@ -24,6 +25,8 @@ public class GrpcCqrsClient private readonly IMapper _mapper; + private readonly TokenService _tokenService; + private readonly Dictionary _grpcMethods; private readonly CqrsContractsAdapter _cqrsAdapter; @@ -58,8 +61,12 @@ public GrpcCqrsClient(GrpcCqrsClientConfiguration configuration, // create mapper _mapper = _cqrsAdapter.CreateMapper(); + // token service + _tokenService = (_configuration.TokenConfiguration != null && _configuration.AuthorizationType == EnumChannelAuthorizationType.Token) + ? new TokenService(_configuration.TokenConfiguration) : null; + // create client - var ch = new Channel(configuration.Url, configuration.Port, ChannelCredentials.Insecure); + var ch = CreateChannel(configuration); _invoker = new DefaultCallInvoker(ch); } @@ -74,7 +81,7 @@ public string GetProto() return _cqrsAdapter.GetProto(); } - public async Task> Execute(TRequest request, CancellationToken cancellationToken) + public async Task> Execute(TRequest request, GrpcCqrsCallOptions callOptions = null, CancellationToken cancellationToken = default) { if (request == null) { @@ -95,12 +102,12 @@ public async Task> Execute( var callMethod = GetType().GetMethods(BindingFlags.Instance | BindingFlags.NonPublic) .First(x => x.Name == nameof(GrpcCqrsClient.CallUnaryMethodAsync)) .MakeGenericMethod(chInfo.ReqType, chInfo.RspType, chInfo.ChReqType, chInfo.ChRspType, chInfo.ChRspEnvType); - var execTask = callMethod.Invoke(this, new object[] { request, default(CancellationToken) }) as Task>; + var execTask = callMethod.Invoke(this, new object[] { request, callOptions, cancellationToken }) as Task>; var result = await execTask; return result; } - private async Task> CallUnaryMethodAsync(TRequest req, CancellationToken ct) + private async Task> CallUnaryMethodAsync(TRequest req, GrpcCqrsCallOptions options, CancellationToken ct) where TRequest : class where TChRequest : class where TChResponseEnvelope : class @@ -116,7 +123,7 @@ private async Task> CallUnaryMethodAsync(req); _clientAspect?.BeforeExecution(req); - var chRsp = await CallUnaryMethodChannelAsync(chReq, ct); + var chRsp = await CallUnaryMethodChannelAsync(chReq, options, ct); rsp = _mapper.Map>(chRsp); watch.Stop(); @@ -147,6 +154,12 @@ private async Task> CallUnaryMethodAsync> CallUnaryMethodAsync CallUnaryMethodChannelAsync(TChRequest request, CancellationToken ct) + private async Task CallUnaryMethodChannelAsync(TChRequest request, GrpcCqrsCallOptions options, CancellationToken ct) where TChRequest : class where TChResponseEnvelope : class { // set call options var callOptions = new CallOptions(cancellationToken: ct, headers: new Metadata()); + + // use default options if not provided + if(options == null && _configuration.DefaultCallOptions != null) + { + options = _configuration.DefaultCallOptions; + } + + // add local token + if(options != null && options.AddInternalAuthorization) + { + var token = _tokenService?.GetDefaultUserToken(); + if (!string.IsNullOrWhiteSpace(token)) + { + callOptions.Headers.Add("authorization", "Bearer " + token); + } + } + + // aspect options _clientAspect?.OnCall(callOptions, request); // invoke @@ -197,6 +228,12 @@ private Method GetGrpcMethodDefinition; } + private Channel CreateChannel(GrpcCqrsClientConfiguration configuration) + { + _logger?.LogInformation("Creating insecure client."); + return new Channel(configuration.Url, configuration.Port, ChannelCredentials.Insecure); + } + private static object CreateGrpcMethodForCqrsChannel(CqrsChannelInfo info) { var grpcMethodFnc = typeof(GrpcMethodFactoryUtil).GetMethod(nameof(GrpcMethodFactoryUtil.CreateGrpcMethod)).MakeGenericMethod(info.ChReqType, info.ChRspEnvType); diff --git a/CoreSharp.Cqrs.Grpc.Client/IGrpcCommandDispatcher.cs b/CoreSharp.Cqrs.Grpc.Client/IGrpcCommandDispatcher.cs index 156b55e..5cd1406 100644 --- a/CoreSharp.Cqrs.Grpc.Client/IGrpcCommandDispatcher.cs +++ b/CoreSharp.Cqrs.Grpc.Client/IGrpcCommandDispatcher.cs @@ -1,8 +1,14 @@ -using CoreSharp.Cqrs.Command; +using System.Threading; +using System.Threading.Tasks; +using CoreSharp.Cqrs.Command; namespace CoreSharp.Cqrs.Grpc.Client { public interface IGrpcCommandDispatcher : ICommandDispatcher { + void Dispatch(ICommand command, GrpcCqrsCallOptions options); + TResult Dispatch(ICommand command, GrpcCqrsCallOptions options); + Task DispatchAsync(IAsyncCommand command, GrpcCqrsCallOptions options, CancellationToken cancellationToken = default); + Task DispatchAsync(IAsyncCommand command, GrpcCqrsCallOptions options, CancellationToken cancellationToken = default); } } diff --git a/CoreSharp.Cqrs.Grpc.Client/IGrpcQueryProcessor.cs b/CoreSharp.Cqrs.Grpc.Client/IGrpcQueryProcessor.cs index f262a4a..210a2fe 100644 --- a/CoreSharp.Cqrs.Grpc.Client/IGrpcQueryProcessor.cs +++ b/CoreSharp.Cqrs.Grpc.Client/IGrpcQueryProcessor.cs @@ -1,8 +1,13 @@ -using CoreSharp.Cqrs.Query; +using System.Threading; +using System.Threading.Tasks; +using CoreSharp.Cqrs.Query; namespace CoreSharp.Cqrs.Grpc.Client { public interface IGrpcQueryProcessor : IQueryProcessor { + TResult Handle(IQuery query, GrpcCqrsCallOptions callOptions); + + Task HandleAsync(IAsyncQuery query, GrpcCqrsCallOptions callOptions, CancellationToken cancellationToken); } } diff --git a/CoreSharp.Cqrs.Grpc.Client/QueryProcessorRouteDecorator.cs b/CoreSharp.Cqrs.Grpc.Client/QueryProcessorRouteDecorator.cs index aa39d77..130f759 100644 --- a/CoreSharp.Cqrs.Grpc.Client/QueryProcessorRouteDecorator.cs +++ b/CoreSharp.Cqrs.Grpc.Client/QueryProcessorRouteDecorator.cs @@ -4,7 +4,7 @@ namespace CoreSharp.Cqrs.Grpc.Client { - public class QueryProcessorRouteDecorator : IQueryProcessor + public class QueryProcessorRouteDecorator : IGrpcQueryProcessor { private readonly IGrpcClientManager _clientManager; @@ -18,25 +18,54 @@ public QueryProcessorRouteDecorator(IGrpcClientManager clientManager, IQueryProc _grpcProcessor = grpcProcessor; } + public TResult Handle(IQuery query, GrpcCqrsCallOptions callOptions) + { + var remoteProcessor = GetRemoteProcessor(query); + if (remoteProcessor != null) + { + return remoteProcessor.Handle(query, callOptions); + } + else + { + return _processor.Handle(query); + } + } + + public async Task HandleAsync(IAsyncQuery query, GrpcCqrsCallOptions callOptions, CancellationToken cancellationToken) + { + var remoteProcessor = GetRemoteProcessor(query); + if (remoteProcessor != null) + { + return await remoteProcessor.HandleAsync(query, callOptions, cancellationToken); + } + else + { + return await _processor.HandleAsync(query, cancellationToken); + } + } + + #region IGrpcQueryProcessor + public TResult Handle(IQuery query) { - return GetProcessor(query).Handle(query); + return Handle(query, null); } public async Task HandleAsync(IAsyncQuery query) { - return await GetProcessor(query).HandleAsync(query); + return await HandleAsync(query, null, default); } public async Task HandleAsync(IAsyncQuery query, CancellationToken cancellationToken) { - return await GetProcessor(query).HandleAsync(query, cancellationToken); + return await HandleAsync(query, null, cancellationToken); } - private IQueryProcessor GetProcessor(T command) + #endregion + + private IGrpcQueryProcessor GetRemoteProcessor(T query) { - var dispatcher = _clientManager.ExistsClientFor(command) ? _grpcProcessor : _processor; - return dispatcher; + return _clientManager.ExistsClientFor(query) ? _grpcProcessor : null; } } } diff --git a/CoreSharp.Identity.Jwt/CoreSharp.Identity.Jwt.csproj b/CoreSharp.Identity.Jwt/CoreSharp.Identity.Jwt.csproj new file mode 100644 index 0000000..80f5806 --- /dev/null +++ b/CoreSharp.Identity.Jwt/CoreSharp.Identity.Jwt.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/CoreSharp.Identity.Jwt/TokenConfiguration.cs b/CoreSharp.Identity.Jwt/TokenConfiguration.cs new file mode 100644 index 0000000..7315222 --- /dev/null +++ b/CoreSharp.Identity.Jwt/TokenConfiguration.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace CoreSharp.Identity.Jwt +{ + public class TokenConfiguration + { + public string Secret { get; set; } + + public string JwtIssuer { get; set; } = "coresharp-grpc"; + + public string JwtAudience { get; set; } = "coresharp-grpc"; + + public string AuthMethod { get; set; } = "coresharp-grpc"; + + public float JwtExpireSeconds { get; set; } = 120; + + public IEnumerable Users { get; set; } + } +} diff --git a/CoreSharp.Identity.Jwt/TokenConfigurationUser.cs b/CoreSharp.Identity.Jwt/TokenConfigurationUser.cs new file mode 100644 index 0000000..f7dbca8 --- /dev/null +++ b/CoreSharp.Identity.Jwt/TokenConfigurationUser.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace CoreSharp.Identity.Jwt +{ + public class TokenConfigurationUser + { + public string Name { get; set; } + + public IDictionary Claims { get; set; } + } +} diff --git a/CoreSharp.Identity.Jwt/TokenData.cs b/CoreSharp.Identity.Jwt/TokenData.cs new file mode 100644 index 0000000..f29d5d4 --- /dev/null +++ b/CoreSharp.Identity.Jwt/TokenData.cs @@ -0,0 +1,11 @@ +using System; + +namespace CoreSharp.Identity.Jwt +{ + internal class TokenData + { + public string Payload { get; set; } + + public DateTime ValidUntil { get; set; } + } +} diff --git a/CoreSharp.Identity.Jwt/TokenFactory.cs b/CoreSharp.Identity.Jwt/TokenFactory.cs new file mode 100644 index 0000000..93b0214 --- /dev/null +++ b/CoreSharp.Identity.Jwt/TokenFactory.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace CoreSharp.Identity.Jwt +{ + public class TokenFactory + { + + private readonly SecurityTokenHandler _tokenHandler; + private readonly string _algorithm; + private readonly SymmetricSecurityKey _securityKey; + private readonly TokenConfiguration _configuration; + + public TokenFactory(TokenConfiguration configuration) + { + _tokenHandler = new JwtSecurityTokenHandler(); + _algorithm = SecurityAlgorithms.HmacSha256; // TODO: configurable + _securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration.Secret)); + _configuration = configuration; + } + + public string GenerateTokenForClaims(IEnumerable claims) + { + + var token = _tokenHandler.CreateToken(new SecurityTokenDescriptor + { + Issuer = _configuration.JwtIssuer, + Audience = _configuration.JwtAudience, + Subject = new ClaimsIdentity(claims), + NotBefore = DateTime.UtcNow, + Expires = DateTime.UtcNow.AddSeconds(_configuration.JwtExpireSeconds), + SigningCredentials = new SigningCredentials(_securityKey, _algorithm) + }); + + return _tokenHandler.WriteToken(token); + } + } +} diff --git a/CoreSharp.Identity.Jwt/TokenService.cs b/CoreSharp.Identity.Jwt/TokenService.cs new file mode 100644 index 0000000..d156ce5 --- /dev/null +++ b/CoreSharp.Identity.Jwt/TokenService.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Security.Claims; + +namespace CoreSharp.Identity.Jwt +{ + public class TokenService + { + private readonly TokenFactory _factory; + private readonly TokenConfiguration _configuration; + private readonly ConcurrentDictionary _tokens = new ConcurrentDictionary(); + + public TokenService(TokenConfiguration configuration) + { + _factory = new TokenFactory(configuration); + _configuration = configuration; + } + + public string GetUserToken(string user) + { + + var key = "token-" + user; + + // get existing token + var tokenData = _tokens.GetOrAdd(key, x => CreateNewtoken(user)); + + // token not valid + if(tokenData == null || tokenData.ValidUntil < DateTime.UtcNow) + { + // create new one + tokenData = _tokens.AddOrUpdate(key, x => CreateNewtoken(user), (x, old) => CreateNewtoken(user)); + } + + return tokenData?.Payload; + } + + public string GetDefaultUserToken() + { + var user = _configuration.Users?.FirstOrDefault()?.Name; + if(string.IsNullOrWhiteSpace(user)) + { + return null; + } + + return GetUserToken(user); + } + + private TokenData CreateNewtoken(string user) + { + + var claims = _configuration.Users?.FirstOrDefault(x => x.Name == user)?.Claims + .Select(x => new Claim(x.Key, x.Value)); + if (claims == null || !claims.Any()) + { + return null; + } + + var targetClaims = claims.ToList(); + targetClaims.Add(new Claim(ClaimTypes.Name, user)); + targetClaims.Add(new Claim(ClaimTypes.AuthenticationMethod, _configuration.AuthMethod)); + + var payload = _factory.GenerateTokenForClaims(targetClaims); + var validUnit = DateTime.UtcNow.AddSeconds(_configuration.JwtExpireSeconds / 2); + return new TokenData + { + Payload = payload, + ValidUntil = validUnit + }; + + } + } +} diff --git a/CoreSharp.sln b/CoreSharp.sln index 2c35958..1843b7a 100644 --- a/CoreSharp.sln +++ b/CoreSharp.sln @@ -87,24 +87,26 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreSharp.Cqrs.Grpc.AspNetC EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreSharp.Cqrs.Grpc.Server.Host", "CoreSharp.Cqrs.Grpc.Server.Host\CoreSharp.Cqrs.Grpc.Server.Host.csproj", "{10FEEC7F-EE7C-4A09-9FB9-9AD539FB6F52}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreSharp.Cqrs.Mvc", "CoreSharp.Cqrs.Mvc\CoreSharp.Cqrs.Mvc.csproj", "{98A3FA7E-B70D-48E4-B5A3-CDC8395DE114}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreSharp.Cqrs.Mvc", "CoreSharp.Cqrs.Mvc\CoreSharp.Cqrs.Mvc.csproj", "{98A3FA7E-B70D-48E4-B5A3-CDC8395DE114}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreSharp.Cqrs.Validation", "CoreSharp.Cqrs.Validation\CoreSharp.Cqrs.Validation.csproj", "{C9448B9D-93E8-4391-AF70-6D5DD954EDAF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreSharp.Cqrs.Validation", "CoreSharp.Cqrs.Validation\CoreSharp.Cqrs.Validation.csproj", "{C9448B9D-93E8-4391-AF70-6D5DD954EDAF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreSharp.Mvc.Formatters", "CoreSharp.Mvc.Formatters\CoreSharp.Mvc.Formatters.csproj", "{A6D76085-63EF-4B2C-A3D6-B096392E98CC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreSharp.Mvc.Formatters", "CoreSharp.Mvc.Formatters\CoreSharp.Mvc.Formatters.csproj", "{A6D76085-63EF-4B2C-A3D6-B096392E98CC}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{FBF7815A-2F16-43C1-8D21-EF4E6616446A}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreSharp.NHibernate.SourceGenerator", "CoreSharp.NHibernate.SourceGenerator\CoreSharp.NHibernate.SourceGenerator.csproj", "{19881C1A-D1CA-40BD-8579-7543C4315902}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreSharp.Identity.Jwt", "CoreSharp.Identity.Jwt\CoreSharp.Identity.Jwt.csproj", "{396EE263-834E-4B86-AFDE-06D72525A78B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreSharp.Cqrs.Grpc.AspNetCore.Jwt", "CoreSharp.Cqrs.Grpc.AspNetCore.Jwt\CoreSharp.Cqrs.Grpc.AspNetCore.Jwt.csproj", "{D7BEB632-2975-4330-904B-AB590FF0B2F8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {FBF7815A-2F16-43C1-8D21-EF4E6616446A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FBF7815A-2F16-43C1-8D21-EF4E6616446A}.Release|Any CPU.ActiveCfg = Release|Any CPU {BDE123B6-E1D3-4EC8-AA9A-400B4E186141}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BDE123B6-E1D3-4EC8-AA9A-400B4E186141}.Debug|Any CPU.Build.0 = Debug|Any CPU {BDE123B6-E1D3-4EC8-AA9A-400B4E186141}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -265,6 +267,16 @@ Global {19881C1A-D1CA-40BD-8579-7543C4315902}.Debug|Any CPU.Build.0 = Debug|Any CPU {19881C1A-D1CA-40BD-8579-7543C4315902}.Release|Any CPU.ActiveCfg = Release|Any CPU {19881C1A-D1CA-40BD-8579-7543C4315902}.Release|Any CPU.Build.0 = Release|Any CPU + {FBF7815A-2F16-43C1-8D21-EF4E6616446A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBF7815A-2F16-43C1-8D21-EF4E6616446A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {396EE263-834E-4B86-AFDE-06D72525A78B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {396EE263-834E-4B86-AFDE-06D72525A78B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {396EE263-834E-4B86-AFDE-06D72525A78B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {396EE263-834E-4B86-AFDE-06D72525A78B}.Release|Any CPU.Build.0 = Release|Any CPU + {D7BEB632-2975-4330-904B-AB590FF0B2F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7BEB632-2975-4330-904B-AB590FF0B2F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7BEB632-2975-4330-904B-AB590FF0B2F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7BEB632-2975-4330-904B-AB590FF0B2F8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -297,6 +309,8 @@ Global {10FEEC7F-EE7C-4A09-9FB9-9AD539FB6F52} = {F6105F72-43B0-4CBE-AB3A-CA3758B4CD35} {98A3FA7E-B70D-48E4-B5A3-CDC8395DE114} = {F6105F72-43B0-4CBE-AB3A-CA3758B4CD35} {C9448B9D-93E8-4391-AF70-6D5DD954EDAF} = {F6105F72-43B0-4CBE-AB3A-CA3758B4CD35} + {396EE263-834E-4B86-AFDE-06D72525A78B} = {D62C732A-084D-4EBF-811E-0B22C6BB4863} + {D7BEB632-2975-4330-904B-AB590FF0B2F8} = {F6105F72-43B0-4CBE-AB3A-CA3758B4CD35} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AC5FB6CC-0977-47B3-8BD1-FCFE1EEC64F2} From ca6ab028ee75bb7cf3f618099dfb7563a5188f70 Mon Sep 17 00:00:00 2001 From: jrozac Date: Wed, 17 Feb 2021 15:20:59 +0100 Subject: [PATCH 3/9] Grpc client auth fix, cqrs options aspect. --- .../CommandDispatcherRouteDecorator.cs | 1 + .../Configuration/GrpcCqrsClientConfiguration.cs | 1 + .../GrpcCqrsClientRawConfiguration.cs | 1 + .../GrpcClientCommandDispatcher.cs | 1 + .../GrpcClientQueryProcessor.cs | 1 + CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs | 15 +++++++++++++-- .../IGrpcCommandDispatcher.cs | 1 + CoreSharp.Cqrs.Grpc.Client/IGrpcQueryProcessor.cs | 1 + .../QueryProcessorRouteDecorator.cs | 1 + .../Aspects/IGrpcClientAspect.cs | 3 ++- .../Common}/GrpcCqrsCallOptions.cs | 2 +- 11 files changed, 24 insertions(+), 4 deletions(-) rename {CoreSharp.Cqrs.Grpc.Client/Configuration => CoreSharp.Cqrs.Grpc.Shared/Common}/GrpcCqrsCallOptions.cs (73%) diff --git a/CoreSharp.Cqrs.Grpc.Client/CommandDispatcherRouteDecorator.cs b/CoreSharp.Cqrs.Grpc.Client/CommandDispatcherRouteDecorator.cs index 6b7ad0c..6993ca9 100644 --- a/CoreSharp.Cqrs.Grpc.Client/CommandDispatcherRouteDecorator.cs +++ b/CoreSharp.Cqrs.Grpc.Client/CommandDispatcherRouteDecorator.cs @@ -1,6 +1,7 @@ using System.Threading; using System.Threading.Tasks; using CoreSharp.Cqrs.Command; +using CoreSharp.Cqrs.Grpc.Common; namespace CoreSharp.Cqrs.Grpc.Client { diff --git a/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientConfiguration.cs b/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientConfiguration.cs index 8b10cd8..23e529b 100644 --- a/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientConfiguration.cs +++ b/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientConfiguration.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Reflection; +using CoreSharp.Cqrs.Grpc.Common; using CoreSharp.Identity.Jwt; namespace CoreSharp.Cqrs.Grpc.Client diff --git a/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientRawConfiguration.cs b/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientRawConfiguration.cs index 786a261..50e0e82 100644 --- a/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientRawConfiguration.cs +++ b/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsClientRawConfiguration.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using CoreSharp.Cqrs.Grpc.Common; using CoreSharp.Identity.Jwt; namespace CoreSharp.Cqrs.Grpc.Client diff --git a/CoreSharp.Cqrs.Grpc.Client/GrpcClientCommandDispatcher.cs b/CoreSharp.Cqrs.Grpc.Client/GrpcClientCommandDispatcher.cs index f9ddc91..06b1b3e 100644 --- a/CoreSharp.Cqrs.Grpc.Client/GrpcClientCommandDispatcher.cs +++ b/CoreSharp.Cqrs.Grpc.Client/GrpcClientCommandDispatcher.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using CoreSharp.Cqrs.Command; +using CoreSharp.Cqrs.Grpc.Common; namespace CoreSharp.Cqrs.Grpc.Client { diff --git a/CoreSharp.Cqrs.Grpc.Client/GrpcClientQueryProcessor.cs b/CoreSharp.Cqrs.Grpc.Client/GrpcClientQueryProcessor.cs index d5b54e9..df50d18 100644 --- a/CoreSharp.Cqrs.Grpc.Client/GrpcClientQueryProcessor.cs +++ b/CoreSharp.Cqrs.Grpc.Client/GrpcClientQueryProcessor.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using CoreSharp.Cqrs.Grpc.Common; using CoreSharp.Cqrs.Query; namespace CoreSharp.Cqrs.Grpc.Client diff --git a/CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs b/CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs index 08d8ceb..90d1cd6 100644 --- a/CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs +++ b/CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs @@ -200,12 +200,23 @@ private async Task CallUnaryMethodChannelAsync x.Key == key); + if (existingAuth != null) + { + callOptions.Headers.Remove(existingAuth); + } + + // add token + callOptions.Headers.Add(key, "Bearer " + token); } } // aspect options - _clientAspect?.OnCall(callOptions, request); + _clientAspect?.OnCall(callOptions, request, options); // invoke var method = GetGrpcMethodDefinition(); diff --git a/CoreSharp.Cqrs.Grpc.Client/IGrpcCommandDispatcher.cs b/CoreSharp.Cqrs.Grpc.Client/IGrpcCommandDispatcher.cs index 5cd1406..baca3ea 100644 --- a/CoreSharp.Cqrs.Grpc.Client/IGrpcCommandDispatcher.cs +++ b/CoreSharp.Cqrs.Grpc.Client/IGrpcCommandDispatcher.cs @@ -1,6 +1,7 @@ using System.Threading; using System.Threading.Tasks; using CoreSharp.Cqrs.Command; +using CoreSharp.Cqrs.Grpc.Common; namespace CoreSharp.Cqrs.Grpc.Client { diff --git a/CoreSharp.Cqrs.Grpc.Client/IGrpcQueryProcessor.cs b/CoreSharp.Cqrs.Grpc.Client/IGrpcQueryProcessor.cs index 210a2fe..cf34eaa 100644 --- a/CoreSharp.Cqrs.Grpc.Client/IGrpcQueryProcessor.cs +++ b/CoreSharp.Cqrs.Grpc.Client/IGrpcQueryProcessor.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using CoreSharp.Cqrs.Grpc.Common; using CoreSharp.Cqrs.Query; namespace CoreSharp.Cqrs.Grpc.Client diff --git a/CoreSharp.Cqrs.Grpc.Client/QueryProcessorRouteDecorator.cs b/CoreSharp.Cqrs.Grpc.Client/QueryProcessorRouteDecorator.cs index 130f759..2f16bbf 100644 --- a/CoreSharp.Cqrs.Grpc.Client/QueryProcessorRouteDecorator.cs +++ b/CoreSharp.Cqrs.Grpc.Client/QueryProcessorRouteDecorator.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using CoreSharp.Cqrs.Grpc.Common; using CoreSharp.Cqrs.Query; namespace CoreSharp.Cqrs.Grpc.Client diff --git a/CoreSharp.Cqrs.Grpc.Shared/Aspects/IGrpcClientAspect.cs b/CoreSharp.Cqrs.Grpc.Shared/Aspects/IGrpcClientAspect.cs index 085fec5..f7dad16 100644 --- a/CoreSharp.Cqrs.Grpc.Shared/Aspects/IGrpcClientAspect.cs +++ b/CoreSharp.Cqrs.Grpc.Shared/Aspects/IGrpcClientAspect.cs @@ -1,11 +1,12 @@ using System; +using CoreSharp.Cqrs.Grpc.Common; using Grpc.Core; namespace CoreSharp.Cqrs.Grpc.Aspects { public interface IGrpcClientAspect { - void OnCall(CallOptions callOptions, object channelRequest); + void OnCall(CallOptions callOptions, object channelRequest, GrpcCqrsCallOptions cqrsCallOptions); void BeforeExecution(object req); diff --git a/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsCallOptions.cs b/CoreSharp.Cqrs.Grpc.Shared/Common/GrpcCqrsCallOptions.cs similarity index 73% rename from CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsCallOptions.cs rename to CoreSharp.Cqrs.Grpc.Shared/Common/GrpcCqrsCallOptions.cs index d8b1be6..ddbd34a 100644 --- a/CoreSharp.Cqrs.Grpc.Client/Configuration/GrpcCqrsCallOptions.cs +++ b/CoreSharp.Cqrs.Grpc.Shared/Common/GrpcCqrsCallOptions.cs @@ -1,4 +1,4 @@ -namespace CoreSharp.Cqrs.Grpc.Client +namespace CoreSharp.Cqrs.Grpc.Common { public class GrpcCqrsCallOptions { From cfcc96b20060d49b11ba82be3198eeee8e9de282 Mon Sep 17 00:00:00 2001 From: jrozac Date: Tue, 23 Feb 2021 12:02:20 +0100 Subject: [PATCH 4/9] Grpc client multiple aspects support. --- .../GrpcClientContainerExtensions.cs | 10 +++++++++- CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs | 12 ++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CoreSharp.Cqrs.Grpc.Client/GrpcClientContainerExtensions.cs b/CoreSharp.Cqrs.Grpc.Client/GrpcClientContainerExtensions.cs index 72299ca..b156cf7 100644 --- a/CoreSharp.Cqrs.Grpc.Client/GrpcClientContainerExtensions.cs +++ b/CoreSharp.Cqrs.Grpc.Client/GrpcClientContainerExtensions.cs @@ -25,9 +25,17 @@ public static Container RegisterCqrsGrpcClients(this Container container, IEnume // client container.Collection.Register(configurations.Select(x => Lifestyle.Singleton.CreateRegistration( () =>{ + var clientAspectList = container.TryGetAllInstances(typeof(IGrpcClientAspect)) + ?.Where(a => a is IGrpcClientAspect) + .Select(a => a as IGrpcClientAspect)?.ToList() + ?? new List(); var clientAspect = container.TryGetInstance(typeof(IGrpcClientAspect)) as IGrpcClientAspect; + if(clientAspect != null) + { + clientAspectList.Add(clientAspect); + } var logger = container.TryGetInstance(typeof(ILogger)) as ILogger; - return new GrpcCqrsClient(x, logger, clientAspect); + return new GrpcCqrsClient(x, logger, clientAspectList); }, container )).ToArray()); container.Register(); diff --git a/CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs b/CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs index 90d1cd6..cfa627c 100644 --- a/CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs +++ b/CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs @@ -35,16 +35,16 @@ public class GrpcCqrsClient private ILogger _logger; - private readonly IGrpcClientAspect _clientAspect; + private readonly IEnumerable _clientAspects; public GrpcCqrsClient(GrpcCqrsClientConfiguration configuration, ILogger logger, - IGrpcClientAspect clientAspect = null) + IEnumerable clientAspects = null) { _configuration = configuration; _logger = logger; - _clientAspect = clientAspect; + _clientAspects = clientAspects?.ToList(); // client id definition Id = !string.IsNullOrWhiteSpace(_configuration.ClientId) ? _configuration.ClientId : Assembly.GetEntryAssembly().FullName.Split(',')[0]; @@ -122,7 +122,7 @@ private async Task> CallUnaryMethodAsync(req); - _clientAspect?.BeforeExecution(req); + _clientAspects?.ForEach(x => x.BeforeExecution(req)); var chRsp = await CallUnaryMethodChannelAsync(chReq, options, ct); rsp = _mapper.Map>(chRsp); watch.Stop(); @@ -176,7 +176,7 @@ private async Task> CallUnaryMethodAsync x.AfterExecution(rsp, exception)); } } @@ -216,7 +216,7 @@ private async Task CallUnaryMethodChannelAsync x.OnCall(callOptions, request, options)); // invoke var method = GetGrpcMethodDefinition(); From 9eac526538ba63c5ec2d1ce00559ee149fd78cb1 Mon Sep 17 00:00:00 2001 From: jrozac Date: Tue, 23 Feb 2021 13:22:00 +0100 Subject: [PATCH 5/9] Client proxy to forward http auth header. --- .../GrpcClientAuthorizationForwardAspect.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 CoreSharp.Cqrs.Grpc.AspNetCore/GrpcClientAuthorizationForwardAspect.cs diff --git a/CoreSharp.Cqrs.Grpc.AspNetCore/GrpcClientAuthorizationForwardAspect.cs b/CoreSharp.Cqrs.Grpc.AspNetCore/GrpcClientAuthorizationForwardAspect.cs new file mode 100644 index 0000000..1247f9c --- /dev/null +++ b/CoreSharp.Cqrs.Grpc.AspNetCore/GrpcClientAuthorizationForwardAspect.cs @@ -0,0 +1,53 @@ +using System; +using System.Linq; +using CoreSharp.Cqrs.Grpc.Aspects; +using CoreSharp.Cqrs.Grpc.Common; +using Grpc.Core; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace CoreSharp.Cqrs.Grpc.AspNetCore +{ + public class GrpcClientAuthorizationForwardAspect : IGrpcClientAspect + { + + private readonly IHttpContextAccessor _httpContextAccessor; + + public GrpcClientAuthorizationForwardAspect(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public void AfterExecution(object rsp, Exception e) + { + } + + public void BeforeExecution(object req) + { + } + + public void OnCall(CallOptions callOptions, object channelRequest, GrpcCqrsCallOptions cqrsCallOptions) + { + // auth header passthrough + if ((cqrsCallOptions == null || !cqrsCallOptions.AddInternalAuthorization) + && _httpContextAccessor?.HttpContext?.Request?.Headers?.TryGetValue("authorization", out StringValues vs) == true + && !string.IsNullOrWhiteSpace(vs.ToString())) + { + var key = "authorization"; + + // remove existing header + var existingAuth = callOptions.Headers.FirstOrDefault(x => x.Key == key); + if (existingAuth != null) + { + callOptions.Headers.Remove(existingAuth); + } + + // add new header + // note: avoid very long headers, it will result in an error + var authHeader = vs.ToString(); + callOptions.Headers.Add(key, authHeader); + } + } + + } +} From 39751f7a576bf5a481d3150f05a8eea42b92295e Mon Sep 17 00:00:00 2001 From: jrozac Date: Fri, 5 Mar 2021 14:13:58 +0100 Subject: [PATCH 6/9] Grpc net client instead grpc.core for client. --- .../CoreSharp.Cqrs.Grpc.Client.csproj | 8 +++--- CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs | 26 +++++++++++++++---- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/CoreSharp.Cqrs.Grpc.Client/CoreSharp.Cqrs.Grpc.Client.csproj b/CoreSharp.Cqrs.Grpc.Client/CoreSharp.Cqrs.Grpc.Client.csproj index 5683a16..125af08 100644 --- a/CoreSharp.Cqrs.Grpc.Client/CoreSharp.Cqrs.Grpc.Client.csproj +++ b/CoreSharp.Cqrs.Grpc.Client/CoreSharp.Cqrs.Grpc.Client.csproj @@ -1,12 +1,14 @@ - + - netstandard2.0 + netstandard2.0;netstandard2.1;net5.0 - + + + diff --git a/CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs b/CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs index cfa627c..923b820 100644 --- a/CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs +++ b/CoreSharp.Cqrs.Grpc.Client/GrpcCqrsClient.cs @@ -13,6 +13,9 @@ using CoreSharp.Identity.Jwt; using CoreSharp.Validation; using Grpc.Core; +#if NETSTANDARD2_1 || NET5_0 +using Grpc.Net.Client; +#endif using Microsoft.Extensions.Logging; namespace CoreSharp.Cqrs.Grpc.Client @@ -65,9 +68,8 @@ public GrpcCqrsClient(GrpcCqrsClientConfiguration configuration, _tokenService = (_configuration.TokenConfiguration != null && _configuration.AuthorizationType == EnumChannelAuthorizationType.Token) ? new TokenService(_configuration.TokenConfiguration) : null; - // create client - var ch = CreateChannel(configuration); - _invoker = new DefaultCallInvoker(ch); + // create client invoker + _invoker = CreateCallInvoker(configuration); } public string Id { get; } @@ -239,12 +241,26 @@ private Method GetGrpcMethodDefinition; } - private Channel CreateChannel(GrpcCqrsClientConfiguration configuration) +#if NETSTANDARD2_1 || NET5_0 + + private CallInvoker CreateCallInvoker(GrpcCqrsClientConfiguration configuration) + { + _logger?.LogInformation("Creating insecure net client."); + var ch = GrpcChannel.ForAddress($"http://{configuration.Url}:{configuration.Port}"); + return ch.CreateCallInvoker(); + } + +#else + + private CallInvoker CreateCallInvoker(GrpcCqrsClientConfiguration configuration) { _logger?.LogInformation("Creating insecure client."); - return new Channel(configuration.Url, configuration.Port, ChannelCredentials.Insecure); + var ch = new Channel(configuration.Url, configuration.Port, ChannelCredentials.Insecure); + return new DefaultCallInvoker(ch); } +#endif + private static object CreateGrpcMethodForCqrsChannel(CqrsChannelInfo info) { var grpcMethodFnc = typeof(GrpcMethodFactoryUtil).GetMethod(nameof(GrpcMethodFactoryUtil.CreateGrpcMethod)).MakeGenericMethod(info.ChReqType, info.ChRspEnvType); From 6f53e4c70707a162db86a5572c64b57b58b6fb25 Mon Sep 17 00:00:00 2001 From: jernejr Date: Wed, 10 Mar 2021 08:47:40 +0100 Subject: [PATCH 7/9] Framework references fixes. --- .../CoreSharp.Cqrs.Grpc.AspNetCore.Jwt.csproj | 2 +- .../CoreSharp.Cqrs.Grpc.AspNetCore.csproj | 2 +- .../GrpcClientAuthorizationForwardAspect.cs | 3 ++- .../CoreSharp.Cqrs.Grpc.Server.Host.csproj | 2 +- CoreSharp.Cqrs.Mvc/CoreSharp.Cqrs.Mvc.csproj | 13 ++++++++++--- .../CoreSharp.Mvc.Formatters.csproj | 10 +++++++--- 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt.csproj b/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt.csproj index 90ee9a8..25a481b 100644 --- a/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt.csproj +++ b/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt/CoreSharp.Cqrs.Grpc.AspNetCore.Jwt.csproj @@ -2,7 +2,7 @@ Library - netcoreapp3.1 + net5.0;netcoreapp3.1 true diff --git a/CoreSharp.Cqrs.Grpc.AspNetCore/CoreSharp.Cqrs.Grpc.AspNetCore.csproj b/CoreSharp.Cqrs.Grpc.AspNetCore/CoreSharp.Cqrs.Grpc.AspNetCore.csproj index 3620e94..82aa2f9 100644 --- a/CoreSharp.Cqrs.Grpc.AspNetCore/CoreSharp.Cqrs.Grpc.AspNetCore.csproj +++ b/CoreSharp.Cqrs.Grpc.AspNetCore/CoreSharp.Cqrs.Grpc.AspNetCore.csproj @@ -2,7 +2,7 @@ Library - netcoreapp3.1 + net5.0;netcoreapp3.1 true diff --git a/CoreSharp.Cqrs.Grpc.AspNetCore/GrpcClientAuthorizationForwardAspect.cs b/CoreSharp.Cqrs.Grpc.AspNetCore/GrpcClientAuthorizationForwardAspect.cs index 1247f9c..b6817de 100644 --- a/CoreSharp.Cqrs.Grpc.AspNetCore/GrpcClientAuthorizationForwardAspect.cs +++ b/CoreSharp.Cqrs.Grpc.AspNetCore/GrpcClientAuthorizationForwardAspect.cs @@ -29,8 +29,9 @@ public void BeforeExecution(object req) public void OnCall(CallOptions callOptions, object channelRequest, GrpcCqrsCallOptions cqrsCallOptions) { // auth header passthrough + StringValues vs = default; if ((cqrsCallOptions == null || !cqrsCallOptions.AddInternalAuthorization) - && _httpContextAccessor?.HttpContext?.Request?.Headers?.TryGetValue("authorization", out StringValues vs) == true + && _httpContextAccessor?.HttpContext?.Request?.Headers?.TryGetValue("authorization", out vs) == true && !string.IsNullOrWhiteSpace(vs.ToString())) { var key = "authorization"; diff --git a/CoreSharp.Cqrs.Grpc.Server.Host/CoreSharp.Cqrs.Grpc.Server.Host.csproj b/CoreSharp.Cqrs.Grpc.Server.Host/CoreSharp.Cqrs.Grpc.Server.Host.csproj index 4732653..959d52d 100644 --- a/CoreSharp.Cqrs.Grpc.Server.Host/CoreSharp.Cqrs.Grpc.Server.Host.csproj +++ b/CoreSharp.Cqrs.Grpc.Server.Host/CoreSharp.Cqrs.Grpc.Server.Host.csproj @@ -2,7 +2,7 @@ Library - netcoreapp3.1 + net5.0;netcoreapp3.1 true diff --git a/CoreSharp.Cqrs.Mvc/CoreSharp.Cqrs.Mvc.csproj b/CoreSharp.Cqrs.Mvc/CoreSharp.Cqrs.Mvc.csproj index 5e6b4e4..320956e 100644 --- a/CoreSharp.Cqrs.Mvc/CoreSharp.Cqrs.Mvc.csproj +++ b/CoreSharp.Cqrs.Mvc/CoreSharp.Cqrs.Mvc.csproj @@ -1,12 +1,19 @@  - netstandard2.0 + net5.0;netcoreapp3.1 - + + + + + + + + - + diff --git a/CoreSharp.Mvc.Formatters/CoreSharp.Mvc.Formatters.csproj b/CoreSharp.Mvc.Formatters/CoreSharp.Mvc.Formatters.csproj index eac4ad2..439fefd 100644 --- a/CoreSharp.Mvc.Formatters/CoreSharp.Mvc.Formatters.csproj +++ b/CoreSharp.Mvc.Formatters/CoreSharp.Mvc.Formatters.csproj @@ -1,11 +1,15 @@ - netstandard2.0 + net5.0;netcoreapp3.1 - - + + + + + + From d8757b0ebb31159217d261f2af6aeb5f4ec1c75a Mon Sep 17 00:00:00 2001 From: jernejr Date: Mon, 15 Mar 2021 11:42:18 +0100 Subject: [PATCH 8/9] Grpc dictionary type channel support. --- .../Contracts/ContractsBuilder.cs | 41 ++++++++++++++++++- CoreSharp.Cqrs.Tests/Grpc/DataMappingTest.cs | 7 +++- .../Grpc/Samples/Models/Application.cs | 9 ++++ .../Grpc/Samples/Models/User.cs | 3 ++ 4 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 CoreSharp.Cqrs.Tests/Grpc/Samples/Models/Application.cs diff --git a/CoreSharp.Cqrs.Grpc.Shared/Contracts/ContractsBuilder.cs b/CoreSharp.Cqrs.Grpc.Shared/Contracts/ContractsBuilder.cs index 67810b3..5a67c8c 100644 --- a/CoreSharp.Cqrs.Grpc.Shared/Contracts/ContractsBuilder.cs +++ b/CoreSharp.Cqrs.Grpc.Shared/Contracts/ContractsBuilder.cs @@ -140,7 +140,6 @@ private void AddProperty(TypeBuilder typeBuilder, PropertyInfo prop, List return; } - var name = recordType.Name; var recordChType = recordType; if (IsComplexType(recordType)) { @@ -151,6 +150,7 @@ private void AddProperty(TypeBuilder typeBuilder, PropertyInfo prop, List { recordChType = GetReplaceType(recordType); } + var propType = typeof(IEnumerable<>).MakeGenericType(recordChType); var propBuilder = typeBuilder.AddProperty(propType, prop.Name, true, true); propBuilder.AddAttribute(typeof(DataMemberAttribute), null, new Dictionary { { "Order", order } }); @@ -169,7 +169,44 @@ private void AddProperty(TypeBuilder typeBuilder, PropertyInfo prop, List // dictionary if (dictionaryInterface != null) { - // todo: implement + + var keyType = dictionaryInterface.GetGenericArguments().ElementAt(0); + var valType = dictionaryInterface.GetGenericArguments().ElementAt(1); + if (!IsMappableType(keyType) || !IsMappableType(valType)) + { + order = order - 1; + return; + } + + // key type + var chKeyType = keyType; + if (IsComplexType(keyType)) + { + AddType(keyType, recursionBag); + chKeyType = _dic[keyType]; + } + else if (GetReplaceType(keyType) != null) + { + chKeyType = GetReplaceType(keyType); + } + + // value type + var chValueType = valType; + if (IsComplexType(valType)) + { + AddType(valType, recursionBag); + chValueType = _dic[valType]; + } + else if (GetReplaceType(valType) != null) + { + chValueType = GetReplaceType(valType); + } + + // set property + var propType = typeof(IDictionary<,>).MakeGenericType(chKeyType, chValueType); + var propBuilder = typeBuilder.AddProperty(propType, prop.Name, true, true); + propBuilder.AddAttribute(typeof(DataMemberAttribute), null, new Dictionary { { "Order", order } }); + return; } // no mapping found - revert counter diff --git a/CoreSharp.Cqrs.Tests/Grpc/DataMappingTest.cs b/CoreSharp.Cqrs.Tests/Grpc/DataMappingTest.cs index 3633eb9..7cf45d4 100644 --- a/CoreSharp.Cqrs.Tests/Grpc/DataMappingTest.cs +++ b/CoreSharp.Cqrs.Tests/Grpc/DataMappingTest.cs @@ -30,7 +30,12 @@ public void TestDateTimeMap() LocationTime = DateTimeOffset.Parse("2021-01-01 20:22:23.232 +01:00"), LastLogin = null, UserCreated = DateTime.Parse("2021-01-01 20:22:23.232"), - UserActivated = null + UserActivated = null, + Applications = new Dictionary { + { "app1", new Application{ Id = "app1", Name ="Application 1"} }, + { "app2", new Application{ Id = "app2", Name ="Application 2"} }, + { "app3", new Application{ Id = "app3", Name ="Application 3"} } + } }; // map through channel diff --git a/CoreSharp.Cqrs.Tests/Grpc/Samples/Models/Application.cs b/CoreSharp.Cqrs.Tests/Grpc/Samples/Models/Application.cs new file mode 100644 index 0000000..8c2b676 --- /dev/null +++ b/CoreSharp.Cqrs.Tests/Grpc/Samples/Models/Application.cs @@ -0,0 +1,9 @@ +namespace CoreSharp.Cqrs.Tests.Grpc.Samples.Models +{ + public class Application + { + public string Name { get; set; } + + public string Id { get; set; } + } +} diff --git a/CoreSharp.Cqrs.Tests/Grpc/Samples/Models/User.cs b/CoreSharp.Cqrs.Tests/Grpc/Samples/Models/User.cs index e504915..77a68dc 100644 --- a/CoreSharp.Cqrs.Tests/Grpc/Samples/Models/User.cs +++ b/CoreSharp.Cqrs.Tests/Grpc/Samples/Models/User.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace CoreSharp.Cqrs.Tests.Grpc.Samples.Models { @@ -19,5 +20,7 @@ public class User public DateTime UserCreated { get; set; } public DateTime? UserActivated { get; set; } + + public IDictionary Applications { get; set; } } } From 317fb8c75bcaa546ffc4fc2ef13bfab2f3243703 Mon Sep 17 00:00:00 2001 From: jernejr Date: Tue, 23 Mar 2021 08:15:03 +0100 Subject: [PATCH 9/9] Cam case mvc formatter. --- .../CamelCase/JsonCamelCaseInputFormatter.cs | 89 +++++++++++++++++++ .../CamelCase/JsonCamelCaseOutputFormatter.cs | 60 +++++++++++++ .../CamelCase/MvcExtensionsJsonCamelCase.cs | 14 +++ .../CamelCase/SerializationHelpers.cs | 44 +++++++++ .../CoreSharp.Mvc.Formatters.Json.csproj | 23 +++++ CoreSharp.sln | 6 ++ 6 files changed, 236 insertions(+) create mode 100644 CoreSharp.Mvc.Formatters.Json/CamelCase/JsonCamelCaseInputFormatter.cs create mode 100644 CoreSharp.Mvc.Formatters.Json/CamelCase/JsonCamelCaseOutputFormatter.cs create mode 100644 CoreSharp.Mvc.Formatters.Json/CamelCase/MvcExtensionsJsonCamelCase.cs create mode 100644 CoreSharp.Mvc.Formatters.Json/CamelCase/SerializationHelpers.cs create mode 100644 CoreSharp.Mvc.Formatters.Json/CoreSharp.Mvc.Formatters.Json.csproj diff --git a/CoreSharp.Mvc.Formatters.Json/CamelCase/JsonCamelCaseInputFormatter.cs b/CoreSharp.Mvc.Formatters.Json/CamelCase/JsonCamelCaseInputFormatter.cs new file mode 100644 index 0000000..0d57fdc --- /dev/null +++ b/CoreSharp.Mvc.Formatters.Json/CamelCase/JsonCamelCaseInputFormatter.cs @@ -0,0 +1,89 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Formatters; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; + +namespace CoreSharp.Mvc.Formatters +{ + + public class JsonCamelCaseInputFormatter : JsonInputFormatterBase + { + + private readonly JsonSerializerSettings _jsonSerializerSettings; + + public JsonCamelCaseInputFormatter() : base("CamelCase") + { + _jsonSerializerSettings = CreateJsonSerializerSettings(); + } + + private static JsonSerializerSettings CreateJsonSerializerSettings() + { + var jsonSerializerSettings = new JsonSerializerSettings + { + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy() + }, + NullValueHandling = NullValueHandling.Include, + PreserveReferencesHandling = PreserveReferencesHandling.None, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + TypeNameHandling = TypeNameHandling.None, + TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple + }; + + // Default is DateTimeZoneHandling.RoundtripKind - you can change that here. + // jsonSerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; + + // Hack is for the issue described in this post: + // http://stackoverflow.com/questions/11789114/internet-explorer-json-net-javascript-date-and-milliseconds-issue + jsonSerializerSettings.Converters.Add(new IsoDateTimeConverter + { + DateTimeFormat = "yyyy-MM-dd\\THH:mm:ss.fffK" + // DateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK" + }); + + // Needed because JSON.NET does not natively support I8601 Duration formats for TimeSpan + //jsonSerializerSettings.Converters.Add(new TimeSpanConverter()); + + // enum as string + jsonSerializerSettings.Converters.Add(new StringEnumConverter()); + + // return + return jsonSerializerSettings; + } + + public override async Task ReadAsync(InputFormatterContext context) + { + + try + { + var request = context.HttpContext.Request; + string body; + if (request.Method == HttpMethod.Get.Method || request.HasFormContentType) + { + body = SerializationHelpers.ConvertQueryStringToJson(request.QueryString.Value); + } + else + { + using (var stream = new StreamReader(request.Body)) + { + body = await stream.ReadToEndAsync(); + } + } + + var obj = JsonConvert.DeserializeObject(body, context.ModelType, _jsonSerializerSettings); + return await InputFormatterResult.SuccessAsync(obj); + } + catch (Exception) + { + return await InputFormatterResult.FailureAsync(); + } + } + + } + +} diff --git a/CoreSharp.Mvc.Formatters.Json/CamelCase/JsonCamelCaseOutputFormatter.cs b/CoreSharp.Mvc.Formatters.Json/CamelCase/JsonCamelCaseOutputFormatter.cs new file mode 100644 index 0000000..996b702 --- /dev/null +++ b/CoreSharp.Mvc.Formatters.Json/CamelCase/JsonCamelCaseOutputFormatter.cs @@ -0,0 +1,60 @@ +using Microsoft.AspNetCore.Mvc.Formatters; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; + +namespace CoreSharp.Mvc.Formatters +{ + public class JsonCamelCaseOutputFormatter : JsonOutputFormatterBase + { + + private readonly JsonSerializerSettings _jsonSerializerSettings; + + public JsonCamelCaseOutputFormatter() : base("CamelCase") + { + _jsonSerializerSettings = CreateJsonSerializerSettings(); + } + + private static JsonSerializerSettings CreateJsonSerializerSettings() + { + var jsonSerializerSettings = new JsonSerializerSettings + { + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy() + }, + NullValueHandling = NullValueHandling.Include, + PreserveReferencesHandling = PreserveReferencesHandling.None, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + TypeNameHandling = TypeNameHandling.None, + TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple + }; + + // Default is DateTimeZoneHandling.RoundtripKind - you can change that here. + // jsonSerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; + + // Hack is for the issue described in this post: + // http://stackoverflow.com/questions/11789114/internet-explorer-json-net-javascript-date-and-milliseconds-issue + jsonSerializerSettings.Converters.Add(new IsoDateTimeConverter + { + DateTimeFormat = "yyyy-MM-dd\\THH:mm:ss.fffK" + // DateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK" + }); + + // Needed because JSON.NET does not natively support I8601 Duration formats for TimeSpan + // jsonSerializerSettings.Converters.Add(new TimeSpanConverter()); + + // enum as string + jsonSerializerSettings.Converters.Add(new StringEnumConverter()); + + // return + return jsonSerializerSettings; + } + + public override string GetPayload(OutputFormatterWriteContext context) + { + var payload = JsonConvert.SerializeObject(context.Object, _jsonSerializerSettings); + return payload; + } + } +} diff --git a/CoreSharp.Mvc.Formatters.Json/CamelCase/MvcExtensionsJsonCamelCase.cs b/CoreSharp.Mvc.Formatters.Json/CamelCase/MvcExtensionsJsonCamelCase.cs new file mode 100644 index 0000000..307e046 --- /dev/null +++ b/CoreSharp.Mvc.Formatters.Json/CamelCase/MvcExtensionsJsonCamelCase.cs @@ -0,0 +1,14 @@ +using CoreSharp.Mvc.Formatters; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class MvcExtensionsJsonCamelCase + { + public static IMvcBuilder AddJsonCamelCaseFormatters(this IMvcBuilder mvc) + { + mvc.AddMvcOptions(opt => opt.InputFormatters.Insert(0, new JsonCamelCaseInputFormatter())); + mvc.AddMvcOptions(opt => opt.OutputFormatters.Insert(0, new JsonCamelCaseOutputFormatter())); + return mvc; + } + } +} diff --git a/CoreSharp.Mvc.Formatters.Json/CamelCase/SerializationHelpers.cs b/CoreSharp.Mvc.Formatters.Json/CamelCase/SerializationHelpers.cs new file mode 100644 index 0000000..fb9dfe9 --- /dev/null +++ b/CoreSharp.Mvc.Formatters.Json/CamelCase/SerializationHelpers.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace CoreSharp.Mvc.Formatters +{ + internal static class SerializationHelpers + { + public static string ConvertQueryStringToJson(string query) + { + var collection = HttpUtility.ParseQueryString(query); + var dictionary = collection.AllKeys.Where(x => !string.IsNullOrWhiteSpace(x)).ToDictionary(key => key, key => collection[key]); + + return ConvertDictionaryToJson(dictionary); + } + + private static string ConvertDictionaryToJson(Dictionary dictionary) + { + var propertyNames = + from key in dictionary.Keys + let index = key.IndexOf('.') + select index < 0 ? key : key.Substring(0, index); + + var data = + from propertyName in propertyNames.Distinct() + let json = dictionary.ContainsKey(propertyName) + ? HttpUtility.JavaScriptStringEncode(dictionary[propertyName], true) + : ConvertDictionaryToJson(FilterByPropertyName(dictionary, propertyName)) + select HttpUtility.JavaScriptStringEncode(propertyName, true) + ": " + json; + + return "{ " + string.Join(", ", data) + " }"; + } + + private static Dictionary FilterByPropertyName(Dictionary dictionary, string propertyName) + { + var prefix = propertyName + "."; + + return dictionary.Keys + .Where(key => key.StartsWith(prefix)) + .ToDictionary(key => key.Substring(prefix.Length), key => dictionary[key]); + } + + } +} diff --git a/CoreSharp.Mvc.Formatters.Json/CoreSharp.Mvc.Formatters.Json.csproj b/CoreSharp.Mvc.Formatters.Json/CoreSharp.Mvc.Formatters.Json.csproj new file mode 100644 index 0000000..ac319dd --- /dev/null +++ b/CoreSharp.Mvc.Formatters.Json/CoreSharp.Mvc.Formatters.Json.csproj @@ -0,0 +1,23 @@ + + + + net5.0;netcoreapp3.1 + + + + + + + + + + + + + + + + + + + diff --git a/CoreSharp.sln b/CoreSharp.sln index 1843b7a..35cd00e 100644 --- a/CoreSharp.sln +++ b/CoreSharp.sln @@ -101,6 +101,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreSharp.Identity.Jwt", "C EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoreSharp.Cqrs.Grpc.AspNetCore.Jwt", "CoreSharp.Cqrs.Grpc.AspNetCore.Jwt\CoreSharp.Cqrs.Grpc.AspNetCore.Jwt.csproj", "{D7BEB632-2975-4330-904B-AB590FF0B2F8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreSharp.Mvc.Formatters.Json", "CoreSharp.Mvc.Formatters.Json\CoreSharp.Mvc.Formatters.Json.csproj", "{E037D487-90EC-4187-8306-808205C8C18F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -277,6 +279,10 @@ Global {D7BEB632-2975-4330-904B-AB590FF0B2F8}.Debug|Any CPU.Build.0 = Debug|Any CPU {D7BEB632-2975-4330-904B-AB590FF0B2F8}.Release|Any CPU.ActiveCfg = Release|Any CPU {D7BEB632-2975-4330-904B-AB590FF0B2F8}.Release|Any CPU.Build.0 = Release|Any CPU + {E037D487-90EC-4187-8306-808205C8C18F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E037D487-90EC-4187-8306-808205C8C18F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E037D487-90EC-4187-8306-808205C8C18F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E037D487-90EC-4187-8306-808205C8C18F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE