Skip to content

Commit

Permalink
控制器Action以接口作为入参,默认FromServices能够从DI得到正确类型的对象,但是内部属性为空,没有读取参数;如果使用Fro…
Browse files Browse the repository at this point in the history
…mBody,则提示接口无法反序列化。
  • Loading branch information
nnhy committed Jun 17, 2024
1 parent 19ffcf8 commit 904e351
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ protected override Boolean OnAuthorize(String token)
/// <returns></returns>
[AllowAnonymous]
[HttpPost(nameof(Login))]
public virtual ILoginResponse Login(ILoginRequest request)
public virtual ILoginResponse Login([FromServices][FromBody] ILoginRequest request)
{
// 先查一次,后续即使登录失败,也可以写设备历史
_device = _deviceService.QueryDevice(request.Code);
Expand Down
21 changes: 19 additions & 2 deletions NewLife.Remoting.Extensions/RemotingExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NewLife.Caching;
using NewLife.Remoting.Extensions.Models;
using NewLife.Remoting.Extensions.Services;
using NewLife.Remoting.Models;
using NewLife.Security;

namespace NewLife.Remoting.Extensions;
Expand All @@ -18,15 +21,29 @@ public static IServiceCollection AddRemoting(this IServiceCollection services, I
{
if (setting == null) throw new ArgumentNullException(nameof(setting));

services.TryAddTransient<ILoginRequest, LoginRequest>();
services.TryAddTransient<ILoginResponse, LoginResponse>();
services.TryAddTransient<ILogoutResponse, LogoutResponse>();
services.TryAddTransient<IPingRequest, PingRequest>();
services.TryAddTransient<IPingResponse, PingResponse>();

// 注册Remoting所必须的服务
services.TryAddSingleton<TokenService>();
services.TryAddSingleton(setting);

// 注册密码提供者,用于通信过程中保护密钥,避免明文传输
services.TryAddSingleton<IPasswordProvider>(new SaltPasswordProvider { Algorithm = "md5", SaltTime = 60 });

services.TryAddSingleton<ICache, MemoryCache>();

// 添加模型绑定器
//var binderProvider = new ServiceModelBinderProvider();
//services.Configure<MvcOptions>(MvcOptions =>
//{
// //MvcOptions.ModelBinderProviders.Insert(0, binderProvider);
//});
//services.AddSingleton<IModelMetadataProvider, ServicModelMetadataProvider>();

return services;
}
}
144 changes: 144 additions & 0 deletions NewLife.Remoting.Extensions/Services/ServicModelMetadataProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Options;
using NewLife.Remoting.Models;

namespace NewLife.Remoting.Extensions.Services;

class ServiceModelBinder : IModelBinder
{
private readonly IServiceProvider _serviceProvider;
private readonly IList<IInputFormatter> _formatters;
private readonly Func<Stream, Encoding, TextReader> _readerFactory;
private readonly MvcOptions _options;

public ServiceModelBinder(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_formatters = serviceProvider.GetServices<IInputFormatter>().ToList();
_readerFactory = _serviceProvider.GetRequiredService<IHttpRequestStreamReaderFactory>().CreateReader;
_options = serviceProvider.GetRequiredService<IOptions<MvcOptions>>().Value;
}

public async Task BindModelAsync(ModelBindingContext bindingContext)
{
ArgumentNullException.ThrowIfNull(bindingContext, nameof(bindingContext));
var modelBindingKey = (!bindingContext.IsTopLevelObject) ? bindingContext.ModelName : (bindingContext.BinderModelName ?? String.Empty);
var httpContext = bindingContext.HttpContext;
var inputFormatterContext = new InputFormatterContext(httpContext, modelBindingKey, bindingContext.ModelState, bindingContext.ModelMetadata, _readerFactory, false);
IInputFormatter formatter = null;
for (var i = 0; i < _formatters.Count; i++)
{
if (_formatters[i].CanRead(inputFormatterContext))
{
formatter = _formatters[i];
break;
}
}
if (formatter == null)
{
var exception = new UnsupportedContentTypeException(httpContext.Request.ContentType);
bindingContext.ModelState.AddModelError(modelBindingKey, exception, bindingContext.ModelMetadata);
return;
}
try
{
var inputFormatterResult = await formatter.ReadAsync(inputFormatterContext);
if (inputFormatterResult.HasError)
{
return;
}
if (inputFormatterResult.IsModelSet)
{
var model = inputFormatterResult.Model;
bindingContext.Result = ModelBindingResult.Success(model);
}
else
{
var errorMessage = bindingContext.ModelMetadata.ModelBindingMessageProvider.MissingRequestBodyRequiredValueAccessor();
bindingContext.ModelState.AddModelError(modelBindingKey, errorMessage);
}
}
catch (Exception ex) when (ex is InputFormatterException || ShouldHandleException(formatter))
{
bindingContext.ModelState.AddModelError(modelBindingKey, ex, bindingContext.ModelMetadata);
}
}

private static Boolean ShouldHandleException(IInputFormatter formatter)
{
return ((formatter as IInputFormatterExceptionPolicy)?.ExceptionPolicy ?? InputFormatterExceptionPolicy.MalformedInputExceptions) == InputFormatterExceptionPolicy.AllExceptions;
}
}

class ServiceModelBinderProvider : IModelBinderProvider
{
public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
if (!context.Metadata.IsComplexType) return null;

var type = context.Metadata.ModelType;
if (type.IsInterface && context.Services?.GetService(type) != null)
{
return new ServiceModelBinder(context.Services);
}

return null;
}
}

class ServicModelMetadataProvider : DefaultModelMetadataProvider
{
private readonly IServiceProvider _serviceProvider;

public ServicModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider, IServiceProvider serviceProvider)
: base(detailsProvider)
{
_serviceProvider = serviceProvider;
}

public override ModelMetadata GetMetadataForType(Type modelType)
{
if (modelType.IsInterface)
{
var momdel = _serviceProvider.GetService(modelType);
if (momdel != null)
{
modelType = momdel.GetType();
}
}

return base.GetMetadataForType(modelType);
}

//protected override ModelMetadata CreateModelMetadata(DefaultMetadataDetails entry) => base.CreateModelMetadata(entry);

protected override DefaultMetadataDetails CreateParameterDetails(ModelMetadataIdentity key) => base.CreateParameterDetails(key);

protected override DefaultMetadataDetails[] CreatePropertyDetails(ModelMetadataIdentity key) => base.CreatePropertyDetails(key);

protected override DefaultMetadataDetails CreateTypeDetails(ModelMetadataIdentity key) => base.CreateTypeDetails(key);

public override IEnumerable<ModelMetadata> GetMetadataForProperties(Type modelType) => base.GetMetadataForProperties(modelType);

protected override ModelMetadata CreateModelMetadata(DefaultMetadataDetails entry)
{
var modelType = entry.Key.ModelType;

if (modelType.IsInterface && (modelType == typeof(ILoginRequest) || modelType == typeof(IPingRequest)))
{
var model = _serviceProvider.GetService(modelType);
if (model != null)
{
var key = ModelMetadataIdentity.ForType(model.GetType());
entry = new DefaultMetadataDetails(key, entry.ModelAttributes);
}
}

return base.CreateModelMetadata(entry);
}
}
11 changes: 9 additions & 2 deletions NewLife.Remoting/Clients/ClientBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,16 +162,23 @@ class MyApiClient : ApiClient
/// <returns></returns>
public virtual async Task<TResult> OnInvokeAsync<TResult>(String action, Object? args, CancellationToken cancellationToken)
{
if (Log != null && Log.Level <= LogLevel.Debug) WriteLog("[{0}]=>{1}", action, args?.ToJson());

TResult? rs = default;
if (_client is ApiHttpClient http)
{
var method = System.Net.Http.HttpMethod.Post;
if (args == null || action.StartsWithIgnoreCase("Get") || action.ToLower().Contains("/get"))
method = System.Net.Http.HttpMethod.Get;

return await http.InvokeAsync<TResult>(method, action, args, null, cancellationToken);
rs = await http.InvokeAsync<TResult>(method, action, args, null, cancellationToken);
}

return await _client.InvokeAsync<TResult>(action, args, cancellationToken);
rs = await _client!.InvokeAsync<TResult>(action, args, cancellationToken);

if (Log != null && Log.Level <= LogLevel.Debug) WriteLog("[{0}]<={1}", action, rs?.ToJson());

return rs!;
}

/// <summary>远程调用拦截,支持重新登录</summary>
Expand Down
8 changes: 4 additions & 4 deletions Samples/IoTZero/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
using IoTZero.Services;
using NewLife.Caching;
using NewLife.Cube;
using NewLife.IoT.Models;
using NewLife.Log;
using NewLife.Reflection;
using NewLife.Remoting.Extensions;
using NewLife.Remoting.Extensions.Services;
using NewLife.Remoting.Models;
using XCode;

// 日志输出到控制台,并拦截全局异常
XTrace.UseConsole();

#if DEBUG
XTrace.Log.Level = NewLife.Log.LogLevel.Debug;
#endif

var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;

Expand Down

0 comments on commit 904e351

Please sign in to comment.