Search code examples
c#.net-corevalidationattribute

FileExtensions attribute - using Enumerator as a parameter


Long story short - I have an Entity Framework model which accepts Enum type property:

public class FileUploads {
    public AllowedFileTypes FileType { get; set; }
}

Enum is:

public enum AllowedFileTypes {
    jpg,
    png,
    gif,
    jpeg,
    bmp,
}

Then, in a Web API controller I set a validation attribute for the IFormFile like this:

[HttpPost]
public async Task<IActionResult> Upload(
    [Required]
    [FileExtensions(Extensions = "jpg,png,gif,jpeg,bmp")] // allowed filetypes
    IFormFile file)
{
    return Ok()
}

The method is used to upload files. Now, the problem is that I am basically setting FileExtensions attribute allowed formats manually. This means that whenever a new file format is added to enum in the future - I will need to go and update each FileExtensions attribute manually. This could be easily forgotten, or any other developer could not be aware of this fact..

So, I was thinking whether or not or How is it possible to pass Enum type parameter to the FileExtensions attribute?

My attempt was the following:

[FileExtensions(Extensions = string.Join(",", Enum.GetValues(typeof(FileExtensions))))]

Unfortunately, Extensions parameter must be a const type string, therefore an error is thrown by VS. I can of course write my own custom validation attribute such as this:

FileExtensions fileExtension;
bool fileExtensionParseResult = Enum.TryParse<FileExtensions>(Path.GetExtension(file.FileName), true, out fileExtension);

Any other ideas?


Solution

  • So, when I deal with white lists, I generally utilize a configuration file instead of hard coding this into the application. Also, I would utilize the Content-Type header to determine the content type of the request. They should send something like image/jpeg when uploading a jpg.

    If this doesn't give you enough information to get started, please comment, and I will work up a quick example.

    Edited:

    Here is an example from my own project. In appsettings.json, add the below:

    "AllowedFileUploadTypes": {
        "image/jpeg": "jpg",
        "video/quicktime": "mov"
      }
    

    I generally create a wrapper class for accessing settings, and below is an example of mine my .NET Core version:

    using System.Linq;
    using Microsoft.Extensions.Configuration;
    using System;
    using System.Collections.Generic;
    
    public class AppConfigurationManager
    {
        private IConfiguration _configuration;
    
        public AppConfigurationManager(IConfiguration configuration)
        {
            _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
        }
    
        public IDictionary<string, string> AllowedFileUploadTypes =>
                        _configuration.GetSection(nameof(AllowedFileUploadTypes)).GetChildren()
                            .Select(item => new KeyValuePair<string, string>(item.Key, item.Value))
                            .ToDictionary(x => x.Key, x => x.Value);
    
    }
    

    Of course you have to register this in Startup.cs

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {            
            Configuration = configuration;
                
        }
        public IConfiguration Configuration { get; }
    
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            //stuff...
            services.AddSingleton(Configuration);
            services.AddSingleton<AppConfigurationManager>();
            //other stuff...
        }
    }
    

    Then you can use the AppConfigurationManager.AllowedFileUploadTypes to evaluate the IFormFile.ContentType property to validate the content type of the file is valid. You can attempt to get the value from the dictionary and then validate against that property. Based on the documentation, I am assuming that the ContentType property will be populated by the Content-Type header. I generally upload files using chunks, so I have not used IFormFile.

    Edited: Wanting a way to apply to the action.

    Using an ActionFilterAttribute, you could do something like this:

    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.Filters;
    using System.Linq;
    using System.Reflection;
    using System.Threading.Tasks;
    
    public class ValidateFileExtensionsAttribute : ActionFilterAttribute
    {
    
        public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            var fileKeyValue = context.ActionArguments.FirstOrDefault(x => typeof(IFormFile).IsAssignableFrom(x.Value.GetType()));
    
            if (fileKeyValue.Value != null)
            {
                AppConfigurationManager sessionService = context.HttpContext.RequestServices.GetService(typeof(AppConfigurationManager)) as AppConfigurationManager;
                IFormFile fileArg = fileKeyValue.Value as IFormFile;
    
                if (!sessionService.AllowedFileUploadTypes.Keys.Any(x => x == fileArg.ContentType))
                {
                    context.Result = new ObjectResult(new { Error = $"The content-type '{fileArg.ContentType}' is not valid." }) { StatusCode = 400 };
    
                    //or you could set the modelstate
                    //context.ModelState.AddModelError(fileKeyValue.Key, $"The content-type '{fileArg.ContentType}' is not valid.");
                    return;
                }
            }
    
            await next();
        }
    }
    

    Then you could apply that to the action like this:

    [HttpPost]
    [ValidateFileExtensions]
    public async Task<IActionResult> Upload([Required]IFormFile file)
    {
        return Ok();
    }
    

    You could modify the ActionFilter to set the ModelState or you could just return the value.