You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET中能否以Fluent方式在目标模型外定义绑定属性?

如何通过Fluent方式在模型外部定义ASP.NET Core绑定源属性

可以实现。我们可以通过自定义模型绑定器、绑定配置接口以及绑定提供器,实现类似FluentValidation的外部绑定源配置,避免在模型属性上直接标记[FromRoute]、[FromBody]等特性。

实现步骤

  1. 定义绑定配置接口与配置器
    创建接口和配置类,用来以Fluent风格声明模型属性的绑定源:
public interface IModelBindingConfiguration<TModel>
{
    void ConfigureBinding(BindingConfigurator<TModel> configurator);
}

public class BindingConfigurator<TModel>
{
    private Dictionary<Expression<Func<TModel, object>>, BindingSource> _propertyBindings = new();

    public void FromRoute(Expression<Func<TModel, object>> propertyExpression)
    {
        _propertyBindings[propertyExpression] = BindingSource.Path;
    }

    public void FromBody(Expression<Func<TModel, object>> propertyExpression)
    {
        _propertyBindings[propertyExpression] = BindingSource.Body;
    }

    public void FromQuery(Expression<Func<TModel, object>> propertyExpression)
    {
        _propertyBindings[propertyExpression] = BindingSource.Query;
    }

    internal Dictionary<Expression<Func<TModel, object>>, BindingSource> GetBindings() => _propertyBindings;
}
  1. 自定义Fluent模型绑定器
    编写模型绑定器,根据外部配置从对应源获取属性值,未配置的属性默认使用Body绑定:
public class FluentModelBinder : IModelBinder
{
    private readonly IModelBinderFactory _modelBinderFactory;
    private readonly IServiceProvider _serviceProvider;

    public FluentModelBinder(IModelBinderFactory modelBinderFactory, IServiceProvider serviceProvider)
    {
        _modelBinderFactory = modelBinderFactory;
        _serviceProvider = serviceProvider;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelType = bindingContext.ModelType;
        var configType = typeof(IModelBindingConfiguration<>).MakeGenericType(modelType);
        var config = _serviceProvider.GetService(configType) as dynamic;

        // 无配置则使用默认绑定逻辑
        if (config == null)
        {
            var defaultBinder = _modelBinderFactory.CreateBinder(new ModelBinderFactoryContext
            {
                Metadata = bindingContext.ModelMetadata,
                BindingInfo = bindingContext.BindingInfo
            });
            await defaultBinder.BindModelAsync(bindingContext);
            return;
        }

        var configurator = new BindingConfigurator<dynamic>();
        config.ConfigureBinding(configurator);
        var propertyBindings = configurator.GetBindings();

        var model = Activator.CreateInstance(modelType);

        // 处理配置了绑定源的属性
        foreach (var (propertyExpr, bindingSource) in propertyBindings)
        {
            var propertyName = ExtractPropertyName(propertyExpr);
            var property = modelType.GetProperty(propertyName);
            if (property == null) continue;

            var valueProvider = bindingContext.ValueProviders.FirstOrDefault(vp => 
                vp.CanProvideValue(propertyName) && vp.BindingSource == bindingSource);
            
            if (valueProvider != null)
            {
                var valueResult = valueProvider.GetValue(propertyName);
                if (valueResult != ValueProviderResult.None)
                {
                    var parsedValue = Convert.ChangeType(valueResult.FirstValue, property.PropertyType);
                    property.SetValue(model, parsedValue);
                }
            }
        }

        // 处理未配置的属性,默认从Body绑定
        var bodyBinder = _modelBinderFactory.CreateBinder(new ModelBinderFactoryContext
        {
            Metadata = bindingContext.ModelMetadata,
            BindingInfo = new BindingInfo { BindingSource = BindingSource.Body }
        });

        var tempContext = DefaultModelBindingContext.CreateBindingContext(
            bindingContext.ActionContext,
            bindingContext.ValueProviders,
            bindingContext.ModelMetadata,
            bindingContext.BindingInfo,
            model);

        await bodyBinder.BindModelAsync(tempContext);

        bindingContext.Result = tempContext.Result.IsModelSet 
            ? ModelBindingResult.Success(tempContext.Result.Model) 
            : ModelBindingResult.Failed();
    }

    private string ExtractPropertyName(Expression<Func<dynamic, object>> propertyExpr)
    {
        if (propertyExpr.Body is MemberExpression memberExpr)
            return memberExpr.Member.Name;
        
        if (propertyExpr.Body is UnaryExpression unaryExpr && unaryExpr.Operand is MemberExpression innerMember)
            return innerMember.Member.Name;
        
        throw new ArgumentException("无效的属性表达式");
    }
}
  1. 注册绑定提供器
    创建绑定提供器,让ASP.NET Core识别并使用我们的自定义绑定器:
public class FluentModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        var modelType = context.Metadata.ModelType;
        var configType = typeof(IModelBindingConfiguration<>).MakeGenericType(modelType);
        
        // 只有注册了绑定配置的模型才使用自定义绑定器
        if (context.Services.Any(s => s.ServiceType == configType))
        {
            return new FluentModelBinder(
                context.Services.GetRequiredService<IModelBinderFactory>(), 
                context.Services);
        }
        
        return null;
    }
}

在Program.cs中注册服务:

builder.Services.AddControllers(options =>
{
    // 将自定义提供器插入到最前面,优先使用
    options.ModelBinderProviders.Insert(0, new FluentModelBinderProvider());
});
  1. 为命令模型编写绑定配置
    为UpdateTodoListCommand创建外部绑定配置:
public class UpdateTodoListCommandBindingConfig : IModelBindingConfiguration<UpdateTodoListCommand>
{
    public void ConfigureBinding(BindingConfigurator<UpdateTodoListCommand> configurator)
    {
        // 指定Id从路由绑定,其余属性默认从Body绑定
        configurator.FromRoute(c => c.Id);
    }
}

注册该配置到服务容器:

builder.Services.AddSingleton<IModelBindingConfiguration<UpdateTodoListCommand>, UpdateTodoListCommandBindingConfig>();
  1. 控制器中使用模型
    命令模型无需任何绑定特性:
public class UpdateTodoListCommand
{
    public int Id { get; set; }
    public string Title { get; set; }
    public bool IsCompleted { get; set; }
}

控制器动作直接使用模型:

[HttpPut("{id}")]
public async Task<ActionResult> Update(UpdateTodoListCommand command)
{
    // command.Id 自动从路由获取,Title和IsCompleted从请求体获取
    await Mediator.Send(command);
    return Ok();
}

内容的提问来源于stack exchange,提问作者gius

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 18:05:25