Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom Model Binding in Asp .Net Core

i'm trying to bind a model with a IFormFile or IFormFileCollection property to my custom class CommonFile. i have not found so much documentation on internet about it using asp .net core, i tried to follow this link Custom Model Binding in ASP.Net Core 1.0 but it is binding a SimpleType property and i need to bind a complex type. Anyway i tried to make my version of this binding and i've got the following code:

FormFileModelBinderProvider.cs

public class FormFileModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null) throw new ArgumentNullException(nameof(context));

        if (!context.Metadata.IsComplexType) return null;

        var isIEnumerableFormFiles = context.Metadata.ModelType.GetInterfaces().Contains(typeof(IEnumerable<CommonFile>));

        var isFormFile = context.Metadata.ModelType.IsAssignableFrom(typeof(CommonFile));

        if (!isFormFile && !isIEnumerableFormFiles) return null;

        var propertyBinders = context.Metadata.Properties.ToDictionary(property => property,
            context.CreateBinder);
        return new FormFileModelBinder(propertyBinders);
    }
}

FromFileModelBinder.cs

the following code is incomplete because i'm not getting any result with bindingContext.ValueProvider.GetValue(bindingContext.ModelName); while i'm debugging everything is going well until bindingContext.ModelName has no a value and i can't bind my model From httpContext to Strongly typed Models.

public class FormFileModelBinder : IModelBinder
{
    private readonly ComplexTypeModelBinder _baseBinder;

    public FormFileModelBinder(IDictionary<ModelMetadata, IModelBinder> propertyBinders)
    {
        _baseBinder = new ComplexTypeModelBinder(propertyBinders);
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {

        if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));

        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        return Task.CompletedTask;

   }
}

Any suggestions?

like image 941
Jonathan Avatar asked Oct 18 '16 19:10

Jonathan


1 Answers

After 10 months i found a solution of that i wanted to do.

In Summary: I Want to replace IFormFile IFormFileCollection with my own classes not attached to Asp .Net because my view models are in different project with poco classes. My custom classes are ICommonFile, ICommonFileCollection, IFormFile (not Asp .net core class) and IFormFileCollection.

i will share it here:

ICommonFile.cs

/// <summary>
/// File with common Parameters including bytes
/// </summary>
public interface ICommonFile
{
    /// <summary>
    /// Stream File
    /// </summary>
    Stream File { get; }

    /// <summary>
    /// Name of the file
    /// </summary>
    string Name { get; }

    /// <summary>
    /// Gets the file name with extension.
    /// </summary>
    string FileName { get; }

    /// <summary>
    /// Gets the file length in bytes.
    /// </summary>
    long Length { get; }

    /// <summary>
    /// Copies the contents of the uploaded file to the <paramref name="target"/> stream.
    /// </summary>
    /// <param name="target">The stream to copy the file contents to.</param>
    void CopyTo(Stream target);

    /// <summary>
    /// Asynchronously copies the contents of the uploaded file to the <paramref name="target"/> stream.
    /// </summary>
    /// <param name="target">The stream to copy the file contents to.</param>
    /// <param name="cancellationToken">Enables cooperative cancellation between threads</param>
    Task CopyToAsync(Stream target, CancellationToken cancellationToken = default(CancellationToken));
}

ICommonFileCollection.cs

/// <inheritdoc />
/// <summary>
/// Represents the collection of files.
/// </summary>
public interface ICommonFileCollection : IReadOnlyList<ICommonFile>
{
    /// <summary>
    /// File Indexer by name
    /// </summary>
    /// <param name="name">File name index</param>
    /// <returns>File with related file name index</returns>
    ICommonFile this[string name] { get; }

    /// <summary>
    /// Gets file by name
    /// </summary>
    /// <param name="name">file name</param>
    /// <returns>File with related file name index</returns>
    ICommonFile GetFile(string name);

    /// <summary>
    /// Gets Files by name
    /// </summary>
    /// <param name="name"></param>>
    /// <returns>Files with related file name index</returns>
    IReadOnlyList<ICommonFile> GetFiles(string name);
}

IFormFile.cs

    /// <inheritdoc />
/// <summary>
/// File transferred by HttpProtocol, this is an independent
/// Asp.net core interface
/// </summary>
public interface IFormFile : ICommonFile
{
    /// <summary>
    /// Gets the raw Content-Type header of the uploaded file.
    /// </summary>
    string ContentType { get; }

    /// <summary>
    /// Gets the raw Content-Disposition header of the uploaded file.
    /// </summary>
    string ContentDisposition { get; }
}

IFormFileCollection.cs

/// <summary>
/// File Collection transferred by HttpProtocol, this is an independent
/// Asp.net core implementation
/// </summary>
public interface IFormFileCollection
{
    //Use it when you need to implement new features to Form File collection over HttpProtocol
}

I finally created my model binders successfully, i will share it too:

FormFileModelBinderProvider.cs

 /// <inheritdoc />
/// <summary>
/// Model Binder Provider, it inspects
/// any model when the request is triggered
/// </summary>
public class FormFileModelBinderProvider : IModelBinderProvider
{
    /// <inheritdoc />
    ///  <summary>
    ///  Inspects a Model for any CommonFile class or Collection with
    ///  same class if exist the FormFileModelBinder initiates
    ///  </summary>
    ///  <param name="context">Model provider context</param>
    ///  <returns>a new Instance o FormFileModelBinder if type is found otherwise null</returns>
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null) throw new ArgumentNullException(nameof(context));
        if (!context.Metadata.IsComplexType) return null;

        var isSingleCommonFile = IsSingleCommonFile(context.Metadata.ModelType);

        var isCommonFileCollection = IsCommonFileCollection(context.Metadata.ModelType);

        if (!isSingleCommonFile && !isCommonFileCollection) return null;

        return new FormFileModelBinder();
    }

    /// <summary>
    /// Checks if object type is a CommonFile Collection
    /// </summary>
    /// <param name="modelType">Context Meta data ModelType</param>
    /// <returns>If modelType is a collection of CommonFile returns true otherwise false</returns>
    private static bool IsCommonFileCollection(Type modelType)
    {
        if (typeof(ICommonFileCollection).IsAssignableFrom(modelType))
        {
            return true;
        }

        var hasCommonFileArguments = modelType.GetGenericArguments()
            .AsParallel().Any(t => typeof(ICommonFile).IsAssignableFrom(t));

        if (typeof(IEnumerable).IsAssignableFrom(modelType) && hasCommonFileArguments)
        {
            return true;
        }

        if (typeof(IAsyncEnumerable<object>).IsAssignableFrom(modelType) && hasCommonFileArguments)
        {
            return true;
        }

        return false;
    }

    /// <summary>
    /// Checks if object type is CommonFile or an implementation of ICommonFile
    /// </summary>
    /// <param name="modelType"></param>
    /// <returns></returns>
    private static bool IsSingleCommonFile(Type modelType)
    {
        if (modelType == typeof(ICommonFile) || modelType.GetInterfaces().Contains(typeof(ICommonFile)))
        {
            return true;
        }

        return false;
    }
}

FormFileModelBinder.cs

/// <inheritdoc />
/// <summary>
/// Form File Model binder
/// Parses the Form file object type to a commonFile
/// </summary>
public class FormFileModelBinder : IModelBinder
{
    /// <summary>
    /// Expression to map IFormFile object type to CommonFile
    /// </summary>
    private readonly Func<Microsoft.AspNetCore.Http.IFormFile, ICommonFile> _expression;

    /// <summary>
    /// FormFile Model binder constructor
    /// </summary>
    public FormFileModelBinder()
    {
        _expression = x => new CommonFile(x.OpenReadStream(), x.Length, x.Name, x.FileName);
    }

    /// <inheritdoc />
    ///  <summary>
    ///  It Binds IFormFile to Common file, getting the file
    ///  from the binding context
    ///  </summary>
    ///  <param name="bindingContext">Http Context</param>
    ///  <returns>Completed Task</returns>
    // TODO: Bind this context to ICommonFile or ICommonFileCollection object
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        dynamic model;
        if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));

        var formFiles = bindingContext.ActionContext.HttpContext.Request.Form.Files;

        if (!formFiles.Any()) return Task.CompletedTask;

        if (formFiles.Count > 1)
        {
            model = formFiles.AsParallel().Select(_expression);
        }
        else
        {
           model = new FormFileCollection();
           model.AddRange(filteredFiles.AsParallel().Select(_expression));
        }

        bindingContext.Result = ModelBindingResult.Success(model);
        return Task.CompletedTask;
    }
}

Actually Everything is working good except when i have Nested Models. I share an example of my models I'm using and I'll do some comments with working scenarios and which don't Test.cs

public class Test
{
    //It's Working
    public ICommonFileCollection Files { get; set; }

    //It's Working
    public ICommonFileCollection Files2 { get; set; }

    //This is a nested model
    public TestExtra TestExtra { get; set; }
}

TestExtra.cs

public class TestExtra
{
    //It's not working
    public ICommonFileCollection Files { get; set; }
}

Actually when i make a request to my API I've got the following (Screenshot): Visual Studio Debugging

I'm sharing a screenshot of my postman request too for clarifying my request is good. Postman request

If there is any subjection to make this work with nested model it would be great.

UPDATE Asp Net Core Model Binder won't bind model with one property only, if you have one property in a class, that property will be null but when you add two or more will bind it. my mistake i had one one property in a nested class. The entire code is correct.

like image 74
Jonathan Avatar answered Sep 20 '22 12:09

Jonathan