webentwicklung-frage-antwort-db.com.de

ASP.NET Core MVC-Modellbindung und -validierung für gemischte Routen / Ausgangsmodelle

Ich verwende ASP.NET Core 1.1 MVC, um eine JSON-API zu erstellen. Unter Berücksichtigung des folgenden Modells und der folgenden Aktionsmethode:

public class TestModel
{
    public int Id { get; set; }

    [Range(100, 999)]
    public int RootId { get; set; }

    [Required, MaxLength(200)]
    public string Name { get; set; }

    public string Description { get; set; }
}

[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho([FromBody] TestModel data)
{
    return Json(new
    {
        data.Id,
        data.RootId,
        data.Name,
        data.Description,
        Errors = ModelState.IsValid ? null : ModelState.SelectMany(x => x.Value.Errors)
    });
}

Das [FromBody] in meinem Aktionsmethodenparameter bewirkt, dass das Modell an die JSON-Nutzdaten gebunden wird, die an den Endpunkt gesendet werden, verhindert jedoch auch, dass die Eigenschaften Id und RootId über die Route gebunden werden Parameter.

Ich könnte dies in separate Modelle aufteilen, eines von der Route und eines vom Körper, oder ich könnte jeden Kunden zwingen, die id & rootId als Teil der Nutzlast zu senden, aber Beide Lösungen scheinen die Dinge mehr zu komplizieren, als ich möchte, und erlauben es mir nicht, die Validierungslogik an einem einzigen Ort zu belassen. Gibt es eine Möglichkeit, diese Situation zum Laufen zu bringen, in der das Modell ordnungsgemäß gebunden werden kann und ich meine Modell- und Validierungslogik zusammenhalten kann?

15
heavyd

Sie können den Dekorator [FromBody] In Ihrer Eingabe entfernen und die MVC-Bindung die Eigenschaften zuordnen lassen:

[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(TestModel data)
{
    return Json(new
    {
        data.Id,
        data.RootId,
        data.Name,
        data.Description,
        Errors = ModelState.IsValid ? null : ModelState.SelectMany(x => x.Value.Errors)
    });
}

Weitere Informationen: Modellbindung in ASP.NET Core MVC

[~ # ~] Update [~ # ~]

Testen

enter image description here

enter image description here

UPDATE 2

@heavyd, Sie haben Recht, dass für JSON-Daten das Attribut [FromBody] erforderlich ist, um Ihr Modell zu binden. Was ich oben gesagt habe, funktioniert also mit Formulardaten, aber nicht mit JSON-Daten.

Alternativ können Sie einen benutzerdefinierten Modellordner erstellen, der die Eigenschaften Id und RootId aus der URL bindet, während der Rest der Eigenschaften aus dem Anforderungshauptteil gebunden wird.

public class TestModelBinder : IModelBinder
{
    private BodyModelBinder defaultBinder;

    public TestModelBinder(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory) // : base(formatters, readerFactory)
    {
        defaultBinder = new BodyModelBinder(formatters, readerFactory);
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        // callinng the default body binder
        await defaultBinder.BindModelAsync(bindingContext);

        if (bindingContext.Result.IsModelSet)
        {
            var data = bindingContext.Result.Model as TestModel;
            if (data != null)
            {
                var value = bindingContext.ValueProvider.GetValue("Id").FirstValue;
                int intValue = 0;
                if (int.TryParse(value, out intValue))
                {
                    // Override the Id property
                    data.Id = intValue;
                }
                value = bindingContext.ValueProvider.GetValue("RootId").FirstValue;
                if (int.TryParse(value, out intValue))
                {
                    // Override the RootId property
                    data.RootId = intValue;
                }
                bindingContext.Result = ModelBindingResult.Success(data);
            }

        }

    }
}

Erstellen Sie einen Sammelmappenanbieter:

public class TestModelBinderProvider : IModelBinderProvider
{
    private readonly IList<IInputFormatter> formatters;
    private readonly IHttpRequestStreamReaderFactory readerFactory;

    public TestModelBinderProvider(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory)
    {
        this.formatters = formatters;
        this.readerFactory = readerFactory;
    }

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType == typeof(TestModel))
            return new TestModelBinder(formatters, readerFactory);

        return null;
    }
}

Und teilen Sie MVC mit, dass es verwendet werden soll:

services.AddMvc()
  .AddMvcOptions(options =>
  {
     IHttpRequestStreamReaderFactory readerFactory = services.BuildServiceProvider().GetRequiredService<IHttpRequestStreamReaderFactory>();
     options.ModelBinderProviders.Insert(0, new TestModelBinderProvider(options.InputFormatters, readerFactory));
  });

Dann hat Ihr Controller:

[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(TestModel data)
{...}

Testen

enter image description hereenter image description here

Sie können Ihrem JSON ein Id und ein RootId hinzufügen, diese werden jedoch ignoriert, da sie in unserem Modellordner überschrieben werden.

UPDATE 3

Oben können Sie Ihre Datenmodellanmerkungen zum Validieren von Id und RootId verwenden. Aber ich denke, es könnte andere Entwickler verwirren, die sich Ihren API-Code ansehen würden. Ich würde vorschlagen, nur die API-Signatur zu vereinfachen, um ein anderes Modell für die Verwendung mit [FromBody] Zu akzeptieren und die beiden anderen Eigenschaften, die vom uri stammen, zu trennen.

[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(int id, int rootId, [FromBody]TestModelNameAndAddress testModelNameAndAddress)

Und Sie könnten einen Prüfer für all Ihre Eingaben genau richtig machen, wie zum Beispiel:

// This would return a list of tuples of property and error message.
var errors = validator.Validate(id, rootId, testModelNameAndAddress); 
if (errors.Count() > 0)
{
    foreach (var error in errors)
    {
        ModelState.AddModelError(error.Property, error.Message);
    }
}
10
Frank Fajardo

Nach der Recherche fand ich eine Lösung zum Erstellen eines neuen Modellbinders + Bindungsquelle + Attributs, die die Funktionalität von BodyModelBinder und ComplexTypeModelBinder kombiniert. Er liest zuerst mit BodyModelBinder aus dem Text und füllt dann mit ComplexModelBinder andere Felder aus. Code hier:

public class BodyAndRouteBindingSource : BindingSource
{
    public static readonly BindingSource BodyAndRoute = new BodyAndRouteBindingSource(
        "BodyAndRoute",
        "BodyAndRoute",
        true,
        true
        );

    public BodyAndRouteBindingSource(string id, string displayName, bool isGreedy, bool isFromRequest) : base(id, displayName, isGreedy, isFromRequest)
    {
    }

    public override bool CanAcceptDataFrom(BindingSource bindingSource)
    {
        return bindingSource == Body || bindingSource == this;
    }
}

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromBodyAndRouteAttribute : Attribute, IBindingSourceMetadata
{
    public BindingSource BindingSource => BodyAndRouteBindingSource.BodyAndRoute;
}

public class BodyAndRouteModelBinder : IModelBinder
{
    private readonly IModelBinder _bodyBinder;
    private readonly IModelBinder _complexBinder;

    public BodyAndRouteModelBinder(IModelBinder bodyBinder, IModelBinder complexBinder)
    {
        _bodyBinder = bodyBinder;
        _complexBinder = complexBinder;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        await _bodyBinder.BindModelAsync(bindingContext);

        if (bindingContext.Result.IsModelSet)
        {
            bindingContext.Model = bindingContext.Result.Model;
        }

        await _complexBinder.BindModelAsync(bindingContext);
    }
}

public class BodyAndRouteModelBinderProvider : IModelBinderProvider
{
    private BodyModelBinderProvider _bodyModelBinderProvider;
    private ComplexTypeModelBinderProvider _complexTypeModelBinderProvider;

    public BodyAndRouteModelBinderProvider(BodyModelBinderProvider bodyModelBinderProvider, ComplexTypeModelBinderProvider complexTypeModelBinderProvider)
    {
        _bodyModelBinderProvider = bodyModelBinderProvider;
        _complexTypeModelBinderProvider = complexTypeModelBinderProvider;
    }

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        var bodyBinder = _bodyModelBinderProvider.GetBinder(context);
        var complexBinder = _complexTypeModelBinderProvider.GetBinder(context);

        if (context.BindingInfo.BindingSource != null
            && context.BindingInfo.BindingSource.CanAcceptDataFrom(BodyAndRouteBindingSource.BodyAndRoute))
        {
            return new BodyAndRouteModelBinder(bodyBinder, complexBinder);
        }
        else
        {
            return null;
        }
    }
}

public static class BodyAndRouteModelBinderProviderSetup
{
    public static void InsertBodyAndRouteBinding(this IList<IModelBinderProvider> providers)
    {
        var bodyProvider = providers.Single(provider => provider.GetType() == typeof(BodyModelBinderProvider)) as BodyModelBinderProvider;
        var complexProvider = providers.Single(provider => provider.GetType() == typeof(ComplexTypeModelBinderProvider)) as ComplexTypeModelBinderProvider;

        var bodyAndRouteProvider = new BodyAndRouteModelBinderProvider(bodyProvider, complexProvider);

        providers.Insert(0, bodyAndRouteProvider);
    }
}
14
Matiszak
  1. Installationspaket HybridModelBinding

  2. Hinzufügen zu Statrup:

    services.AddMvc()
        .AddHybridModelBinder();
    
  3. Modell:

    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string FavoriteColor { get; set; }
    }
    
  4. Regler:

    [HttpPost]
    [Route("people/{id}")]
    public IActionResult Post([FromHybrid]Person model)
    { }
    
  5. Anfrage:

    curl -X POST -H "Accept: application/json" -H "Content-Type:application/json" -d '{
        "id": 999,
        "name": "Bill Boga",
        "favoriteColor": "Blue"
    }' "https://localhost/people/123?name=William%20Boga"
    
  6. Ergebnis:

    {
        "Id": 123,
        "Name": "William Boga",
        "FavoriteColor": "Blue"
    }
    
  7. Es gibt noch weitere erweiterte Funktionen.

12
Mentor

Ich habe dies nicht für Ihr Beispiel ausprobiert, aber es sollte wie folgt als Kernunterstützungsmodellbindung von asp.net funktionieren.

Sie können ein solches Modell erstellen.

public class TestModel
{
    [FromRoute]
    public int Id { get; set; }

    [FromRoute]
    [Range(100, 999)]
    public int RootId { get; set; }

    [FromBody]
    [Required, MaxLength(200)]
    public string Name { get; set; }

    [FromBody]
    public string Description { get; set; }
}

Update 1: Oben funktioniert nicht, wenn der Stream nicht zurückgespult werden kann. Hauptsächlich in Ihrem Fall, wenn Sie JSON-Daten posten.

Der Ordner "Benutzerdefiniertes Modell" ist eine Lösung. Wenn Sie diesen Ordner jedoch immer noch nicht erstellen und nur mit "Modell" verwalten möchten, können Sie zwei Modelle erstellen.

public class TestModel
    {
        [FromRoute]
        public int Id { get; set; }

        [FromRoute]
        [Range(100, 999)]
        public int RootId { get; set; }        

        [FromBody]
        public ChildModel OtherData { get; set; }        
    }


    public class ChildModel
    {            
        [Required, MaxLength(200)]
        public string Name { get; set; }

        public string Description { get; set; }
    }

Hinweis: Dies funktioniert perfekt mit der Anwendung/Json-Bindung, da es etwas anders funktioniert als andere Inhaltstypen.

2
dotnetstep