gpt4 book ai didi

c# - ASP.net 中的流畅模型绑定(bind)

转载 作者:行者123 更新时间:2023-12-05 04:21:54 25 4
gpt4 key购买 nike

有没有办法在目标模型之外以流畅的方式定义绑定(bind)属性(FromBodyFromQuery 等)?类似于 FluentValidation vs [Required][MaxLength] 等属性。


背景故事:

我想使用命令模型作为 Controller Action 参数:

[HttpPost]
public async Task<ActionResult<int>> Create(UpdateTodoListCommand command)
{
return await Mediator.Send(command);
}

更重要的是,我希望模型从多个来源(路线、 body 、atc.)绑定(bind):

[HttpPut("{id}")]
public async Task<ActionResult> Update(UpdateTodoListCommand command)
{
// command.Id is bound from the route, the rest is from the request body
}

这应该是可能的(https://josef.codes/model-bind-multiple-sources-to-a-single-class-in-asp-net-core/https://github.com/ardalis/RouteAndBodyModelBinding),但需要在命令的属性上绑定(bind)属性,这应该避免。

最佳答案

AspNetCore modelBinding 过程可以在没有属性的情况下使用自定义 IModelBinderProvider 进行自定义。

对于这样的请求,我将解释一种实现以下结果的方法:

Header: PUT
URL: /TodoList/testid?Title=mytitle&Index=2
BODY: { "Description": "mydesc" }

预期的响应主体:

{"Id":"testid","Title":"mytitle","Description":"mydesc","Index":2}

因此 Controller 应该将来自路由、查询和正文的所有数据混合为一个模型,然后返回序列化模型(我们只是想检查示例中的自定义绑定(bind)结果)。

C# POCO 可以是:

public class UpdateTodoListCommand
{
public string Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
// string properties are too easy to bind, so we add an extra property of another type for the demo
public int Index { get; set; }
}

Controller :

[Route("[controller]")]
public class TodoListController : Controller
{
[HttpPut("{id}")]
public IActionResult Update(UpdateTodoListCommand command
{
// return the serialized model so we can check all query and route data are merged as expected in the command instance
return Ok(JsonSerializer.Serialize(command));
}
}

我们需要一些样板代码来声明关于我们的命令的元数据,并定义应该绑定(bind)哪个属性来查询或路由数据。我说得很简单,因为这不是主题的目的:

public class CommandBindingModel
{
public HashSet<string> FromQuery { get; } = new HashSet<string>();
public HashSet<string> FromPath { get; } = new HashSet<string>();
}

public class CommandBindingModelStore
{
private readonly Dictionary<Type, CommandBindingModel> _inner = new ();

public CommandBindingModel? Get(Type type, bool createIfNotExists)
{
if (_inner.TryGetValue(type, out var model))
return model;
if (createIfNotExists)
{
model = new CommandBindingModel();
_inner.Add(type, model);
}
return model;
}
}

存储将包含您希望与自定义进程绑定(bind)的所有命令的元数据快照。

store 的 fluent builder 可能是这样的(我再次尝试简单):

public class CommandBindingModelBuilder
{
public CommandBindingModelStore Store { get; } = new CommandBindingModelStore();

public CommandBindingModelBuilder Configure<TModel>(Action<Step<TModel>> configure)
{
var model = Store.Get(typeof(TModel), true);
configure(new Step<TModel>(model ?? throw new Exception()));
return this;
}

public class Step<TModel>
{
private readonly CommandBindingModel _model;

public Step(CommandBindingModel model)
{
_model = model;
}

public Step<TModel> FromQuery<TProperty>(Expression<Func<TModel, TProperty>> property
{
if (property.Body is not MemberExpression me)
throw new NotImplementedException();
_model.FromQuery.Add(me.Member.Name);
return this;
}

public Step<TModel> FromPath<TProperty>(Expression<Func<TModel, TProperty>> property)
{
if (property.Body is not MemberExpression me)
throw new NotImplementedException();
_model.FromPath.Add(me.Member.Name);
return this;
}
}
}

现在我们可以创建一个自定义实现 IModelBinderProvider .这个有责任给定制IModelBinder为我们商店的每个命令添加 MVC。我们的命令是复杂类型,因此我们必须获取一些元数据(来自 MVC api)以简化属性绑定(bind):

public class CommandModelBinderProvider : IModelBinderProvider
{
private readonly CommandBindingModelStore _store;

public CommandModelBinderProvider(CommandBindingModelStore store)
{
_store = store;
}

public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
var model = _store.Get(context.Metadata.ModelType, false);
if (model != null)
{
var binders = new Dictionary<ModelMetadata, IModelBinder>();
foreach(var property in model.FromQuery.Concat(model.FromPath))
{
var metadata = context.Metadata.GetMetadataForProperty(context.Metadata.ModelType, property);
var binder = context.CreateBinder(metadata);
binders.Add(metadata, binder);
}
return new CommandModelBinder(model, binders);
}
return null;
}
}

自定义 Binder 将读取请求正文(示例中为 JSON,但您可以以任何需要的格式读取和解析):

public class CommandModelBinder : IModelBinder
{
private readonly CommandBindingModel _commandBindingModel;
private readonly Dictionary<ModelMetadata, IModelBinder> _binders;

public CommandModelBinder(CommandBindingModel commandBindingModel, Dictionary<ModelMetadata, IModelBinder> binders)
{
_commandBindingModel = commandBindingModel;
_binders = binders;
}

public async Task BindModelAsync(ModelBindingContext bindingContext)
{
var value = await bindingContext.HttpContext.Request.ReadFromJsonAsync(bindingContext.ModelType);
if (value == null)
{
bindingContext.Result = ModelBindingResult.Failed();
return;
}
bindingContext.Model = value;

/* CUSTOM BINDING HERE */

bindingContext.Result = ModelBindingResult.Success(value);
}
}

如果我们现在执行代码(假设 MVC 知道自定义提供程序,在我的示例的这个阶段这不是真的),只有属性 Description 按预期绑定(bind)。所以我们必须绑定(bind) QueryString 的属性。在 MVC 绑定(bind)哲学中,ValueProvider 负责从请求中获取原始值:QueryStringValueProvider是 QueryString 的那个。所以我们可以使用它:

var queryStringValueProvider = new QueryStringValueProvider(BindingSource.Query, bindingContext.HttpContext.Request.Query, CultureInfo.CurrentCulture);
foreach (var fq in _commandBindingModel.FromQuery)
{
var r = queryStringValueProvider.GetValue(fq);
bindingContext.ModelState.SetModelValue(fq, r);
if (r == ValueProviderResult.None) continue;
/* we have to bind the value to our command */
}

这里很容易使用反射来设置我们的命令的属性,但是 MVC 给了我们一些工具,所以我认为最好使用它们。此外,我们只得到一个类型为 StringValues 的原始值。 ,因此将其转换为预期的属性类型可能会很痛苦(想想我们的 Index 的属性 UpdateTodoListCommand)。现在是使用在自定义 IModelProvider 中创建的绑定(bind)器的时候了:

var m = bindingContext.ModelMetadata.GetMetadataForProperty(bindingContext.ModelType, fq);
using (bindingContext.EnterNestedScope(m, fq, fq, m.PropertyGetter(value)))
{
bindingContext.Result = ModelBindingResult.Success(r);
var binder = _binders[m];
await binder.BindModelAsync(bindingContext);
var result = bindingContext.Result;
m.PropertySetter(value, result.Model);
}

现在,在我们的示例中,Title 和 Index 将按预期绑定(bind)。 Id 属性可以与 RouteValueProvider 绑定(bind):

var routeValueProvider = new RouteValueProvider(BindingSource.Path, bindingContext.ActionContext.RouteData.Values);
foreach (var fp in _commandBindingModel.FromPath)
{
var r = routeValueProvider.GetValue(fp);
bindingContext.ModelState.SetModelValue(fp, r);
if (r == ValueProviderResult.None) continue;
var m = bindingContext.ModelMetadata.GetMetadataForProperty(bindingContext.ModelType, fp);
using (bindingContext.EnterNestedScope(m, fp, fp, m.PropertyGetter(value)))
{
bindingContext.Result = ModelBindingResult.Success(r);
var binder = _binders[m];
await binder.BindModelAsync(bindingContext);
var result = bindingContext.Result;
m.PropertySetter(value, result.Model);
}
}

最后要做的是告诉 MVC 我们的自定义 IModelBinderProvider , 应该在 MvcOptions 中完成:

var store = new CommandBindingModelBuilder()
.Configure<UpdateTodoListCommand>(e => e
.FromPath(c => c.Id)
.FromQuery(c => c.Title)
.FromQuery(c => c.Index)
)
.Store;

builder.Services.AddControllers().AddMvcOptions(options =>
{
options.ModelBinderProviders.Insert(0, new CommandModelBinderProvider(store));
});

这里有一个完整的要点:https://gist.github.com/thomasouvre/e5438816af1a0ad81bddf106432cfa7d

编辑:当然,您可以使用自定义 IOperationProcessor 自定义 NSwag 操作生成像这样:

public class NSwagCommandOperationProcessor : IOperationProcessor
{
private readonly CommandBindingModelStore _store;

public NSwagCommandOperationProcessor(CommandBindingModelStore store)
{
_store = store;
}

public bool Process(OperationProcessorContext context)
{
ParameterInfo? pinfo = null;
CommandBindingModel? model = null;

// check if there is a command parameter in the action
foreach (var p in context.MethodInfo.GetParameters())
{
pinfo = p;
model = _store.Get(pinfo.ParameterType, false);
if (model != null) break;
}

if (model == null || pinfo == null) return true; // false will exclude the action

var jsonSchema = JsonSchema.FromType(pinfo.ParameterType); // create a full schema from the command type
if (jsonSchema.Type != JsonObjectType.Object) return false;
var bodyParameter = new OpenApiParameter() { IsRequired = true, Kind = OpenApiParameterKind.Body, Schema = jsonSchema, Name = pinfo.Name };
foreach (var prop in jsonSchema.Properties.Keys.ToList())
{
if (model.FromQuery.Contains(prop) || model.FromPath.Contains(prop))
{
// then excludes some properties from the schema
jsonSchema.Properties.Remove(prop);
continue;
}
bodyParameter.Properties.Add(prop, jsonSchema.Properties[prop]);

// if the property is not excluded, the property should be binded from the body
// so we have to delete existing parameters generated by NSwag (probably binded as from query)
var operationParameter = context.OperationDescription.Operation.Parameters.FirstOrDefault(p => p.Name == prop);
if (operationParameter != null)
context.OperationDescription.Operation.Parameters.Remove(operationParameter);
}
if (bodyParameter.Properties.Count > 0)
context.OperationDescription.Operation.Parameters.Add(bodyParameter);

return true;
}
}

Swagger UI 的实际结果: enter image description here

关于c# - ASP.net 中的流畅模型绑定(bind),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/74136993/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com