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?
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.
Related
Currently using asp core to build a web service system
I hope to obtain the return data of the service method in a specific controller through attribute
The following are examples
[HttpPost, Route("list")]
[CustomAttribute]
public IActionResult GetList([FromBody] NewsClassDto request)
{
var data = newsClassService.GetList(model);
return OkResponse(data);
}
NewsClassService Examples
public NewsClassDto GetList(NewsClassDto dto)
{
var daoClassData = _newsClassDao.GetList(dto);
var daoData = _newsDataDao.GetList(dto);
/** logical processing **/
return daoClassData;
}
I want to record through
[CustomAttribute]
newsClassService.GetList(model);
data returns content and
_newsClassDao.GetList(dto);
_newsDataDao.GetList(dto);
daoClassData returns content and daoData returns content , but I don't know how to implement this in Attribute
Yes, this is very common. What you need to do is create an action filter attribute. You can read about filters in general here.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace aspnet6test;
public class CustomFilterAttribute : ActionFilterAttribute
{
public override void OnResultExecuted(ResultExecutedContext context)
{
if (context.Result is OkObjectResult okResult)
{
// Here, okResult.Value will contain the result data. You'll have to
// explore with a series of IF statements whether or not is the data you're
// looking for. Example:
if (okResult.Value is List<SomeDto> listOfDtos)
{
// So it is a list of wanted SomeDto objects. Do your magic.
}
}
}
}
The above example runs the code after the result has been returned by the controller action, which I believe satisfies the requirements in your question.
It would be used like this:
[HttpPost("list")] // <-- The HTTP verp attributes are also Route attributes
[CustomFilter]
public IActionResult GetList([FromBody] NewsClassDto request)
{
var data = newsClassService.GetList(model);
return Ok(data); // <-- I believe this is the correct way.
}
The filter may accept properties and even constructor values, but that's another topic.
I used to just post simple json objects to ASP.NET MVC controllers and the binding engine would parse the body out into the method's simple parameters:
{
"firstname" : "foo",
"lastname" : "bar"
}
and I could have a MVC controller like this:
public method Blah(string firstname, string lastname) {}
And firstname and lastname would automatically be pulled from the Json object and mapped to the simple parameters.
I've moved a backend piece to .NET Core 5.0 with the same method signatures, however, when I post the same simple JSON object, my parameters are null. I even tried doing [FromBody] on each parameter but they would still be null. It wasn't until I created an extra class that contained the parameter names would the model binding work:
public class BlahRequest
{
public string firstname { get; set;}
public string lastname { get; set; }
}
And then I have to update my method signature to look like this:
public method Blah([FromBody]BlahRequest request) { }
And now, the request has the properties firstname and lastname filled out from the post request.
Is there a model binder setting where I can go back to binding from a simple Json object to the method's parameters? Or do I have to update all my method signatures to contain a class with properties?
How the web api method is called
The original application is written in Angular but I can recreate it with a simple Fiddler request:
POST https://localhost:5001/Blah/Posted HTTP/1.1
Host: localhost:5001
Connection: keep-alive
Accept: application/json, text/plain, */*
Content-Type: application/x-www-form-urlencoded
{"firstname":"foo","lastname":"bar"}
In previous versions of the .Net framework, the controller's method would parse those values automatically. Now, on core, it requires a model to be passed in. I've tried application/json, multipart/form-data, and application/x-www-form-urlencoded as the Content-type and they all end up with null values.
Smallest .Net Core project
public class Startup {
public Startup(IConfiguration configuration) {
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services){
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
app.UseHttpsRedirection();
app.UseRouting();
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}
[Route("[controller]/[action]")]
public class BlahController : ControllerBase {
public object Posted(string firstname, string lastname) {
Console.WriteLine(firstname);
Console.WriteLine(lastname);
return true;
}
}
Looking into the guts of the .Net core source code, I was able to find an example of how the form values provider worked and then reverse engineered a solution for this project. I think if one was starting fresh, they wouldn't have to solve this problem, but we are moving an existing UI built on Angular onto this new backend on .Net core and didn't want to rewrite all the server side calls as models with the parameters of the methods on the controllers.
So, just to review, in previous versions of .Net, you could post a Json object to an Mvc controller with multiple parameters in the method signature.
public object GetSalesOrders(string dashboardName, int fillableState = 0){}
and a simple method to call this would like the following:
POST https://localhost:5001/api/ShippingDashboard/GetSalesOrders HTTP/1.1
Content-Type: application/json
{"dashboardName":"/shippingdashboard/bigshipping","fillableState":1}
Looking into value providers, I created my own value provider and pushed it to the top of the list during the configuration of the Startup class.
services
.AddControllers( options =>{
options.ValueProviderFactories.Insert(0, new JsonBodyValueProviderFactory());
})
JsonBodyValueProviderFactory is a factory class I wrote. It inspects the request and if the content type is application/json, it will add a provider to the content:
public class JsonBodyValueProviderFactory : IValueProviderFactory{
public Task CreateValueProviderAsync(ValueProviderFactoryContext context) {
if (context == null) {
throw new ArgumentNullException(nameof(context));
}
var request = context.ActionContext.HttpContext.Request;
if (request.HasJsonContentType()) {
// Allocating a Task only when the body is json data
return AddValueProviderAsync(context);
}
return Task.CompletedTask;
}
private static async Task AddValueProviderAsync(ValueProviderFactoryContext context) {
var request = context.ActionContext.HttpContext.Request;
var body = "";
Dictionary<string,object> asDict = new Dictionary<string, object>();
try {
using (StreamReader stream = new StreamReader(request.Body)){
body = await stream.ReadToEndAsync();
}
var obj = JObject.Parse(body);
foreach(var item in obj.Children()){
asDict.Add(item.Path, item.Values().First());
}
} catch (InvalidDataException ex) {
Console.WriteLine(ex.ToString());
} catch (IOException ex) {
Console.WriteLine(ex.ToString());
}
var valueProvider = new JsonBodyValueProvider(BindingSource.Form, asDict);
context.ValueProviders.Add(valueProvider);
}
}
Since I don't always know the shape of the data, I utilized Json.Net's JObject and suck the Json body into a JObject. I then use the top level properties and add them to a dictionary for easy lookup.
The actual class that takes the values and responds to the parameter name is JsonBodyValueProvider:
public class JsonBodyValueProvider : BindingSourceValueProvider, IEnumerableValueProvider {
private readonly Dictionary<string, object> values;
private PrefixContainer? _prefixContainer;
public JsonBodyValueProvider( BindingSource bindingSource, Dictionary<string,object> values) : base(bindingSource) {
if (bindingSource == null) {
throw new ArgumentNullException(nameof(bindingSource));
}
if (values == null) {
throw new ArgumentNullException(nameof(values));
}
this.values = values;
}
protected PrefixContainer PrefixContainer {
get {
if (_prefixContainer == null) {
_prefixContainer = new PrefixContainer(values.Keys);
}
return _prefixContainer;
}
}
public override bool ContainsPrefix(string prefix) {
return PrefixContainer.ContainsPrefix(prefix);
}
public virtual IDictionary<string, string> GetKeysFromPrefix(string prefix) {
if (prefix == null) {
throw new ArgumentNullException(nameof(prefix));
}
return PrefixContainer.GetKeysFromPrefix(prefix);
}
public override ValueProviderResult GetValue(string key){
if (key == null) {
throw new ArgumentNullException(nameof(key));
}
if (key.Length == 0) {
return ValueProviderResult.None;
}
var _values = values[key];
if (!values.ContainsKey(key)) {
return ValueProviderResult.None;
} else {
return new ValueProviderResult(_values.ToString());
}
}
}
This is pretty much a carbon copy of the FormValueProvider in .Net Core, I just adjusted it to work with a dictionary of input values.
Now my controllers can stay the same from prior versions of .Net without changing method signatures.
It depends on the content type. You must be using application/json content type. This is why you have to create a viewmodel and to add [FromBody] attribute
public method Blah([FromBody]BlahRequest request) { }
but if you use application/x-www-form-urlencoded or multipart/form-data form enctype or ajax content-type then this will be working
public method Blah(string firstname, string lastname) {}
if you still have some problems, it means that you are using ApiController. In this case add this code to startup
using Microsoft.AspNetCore.Mvc;
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressInferBindingSourcesForParameters = true;
});
We're using Microsoft.AspNet.OData, and our controllers look like this:
namespace Backend.api.Process
{
public class OdataProcessController: ODataController
{
private readonly IProcessService _service;
public OdataProcessController(IProcessService service)
{
_service = service;
}
[EnableQuery]
public IHttpActionResult Get()
{
return Ok(_service.GetAllProcesses());
}
}
}
However, some of our processes have names that include special characters (like "&"), which have to be URLencoded before being sent backend when searching. It does not seem like the ASP.Net Odata framework automatically handles this. Therefore, my task is now to ensure the search string is decoded backend.
I've looked into extending the endpoint to accept ODataQueryOptions (see below), but I can't figure out how to change the filter string and rewrapping it back into an ODataQueryOptions object:
public IHttpActionResult Get(ODataQueryOptions<ProcessModel> queryOptions)
{
var allProcesses= _service.GetAllProcesses();
queryOptions.ApplyTo(allProcesses);
return Ok(allProcesses);
}
Is my only option to rewrite my service function to accept the filter parameter and implement the filtering myself?
The link Erdem provided gave me the answer, which was to create an attribute class to override the OnActionExecuting function:
using Microsoft.AspNet.OData;
using System;
using System.Web;
using System.Web.Http.Controllers;
namespace Backend.api.Process
{
public class DecodeFilter : EnableQueryAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
var url = actionContext.Request.RequestUri.OriginalString;
var newUrl = HttpUtility.UrlDecode(url);
actionContext.Request.RequestUri = new Uri(newUrl);
base.OnActionExecuting(actionContext);
}
}
}
and then using it as follows:
[EnableQuery]
[DecodeFilter]
public IHttpActionResult Get()
{
return Ok(_service.GetAllProcesses());
}
What I have done is created a small API in a class library. This API would be used by other sites. Think of it as a standard endpoint that all of our websites will contain.
[Route("api/[controller]")]
[ApiController]
public class CustomController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
}
The above is in a class library. Now what i would like to do is be able to add this to the projects in a simple manner.
app.UseCustomAPI("/api/crap");
I am not exactly sure how i should handle routing to the api controllers in the library. I created a CustomAPIMiddleware which is able to catch that i called "/api/crap" however i am not sure how i should forward the request over to CustomController in the library
public async Task Invoke(HttpContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
PathString matched;
PathString remaining;
if (context.Request.Path.StartsWithSegments(_options.PathMatch, out matched, out remaining))
{
PathString path = context.Request.Path;
PathString pathBase = context.Request.PathBase;
context.Request.PathBase = pathBase.Add(matched);
context.Request.Path = remaining;
try
{
await this._options.Branch(context);
}
finally
{
context.Request.PathBase = pathBase;
context.Request.Path = path;
}
path = new PathString();
pathBase = new PathString();
}
else
await this._next(context);
}
After having done that i am starting to think i may have approached this in the wrong manner and should actually be trying to add it directly to the routing tables somehow. That being said i would like it if they could customize the endpoint that the custom controller reads from.
Update
The following does work. Loading and registering API Controllers From Class Library in ASP.NET core
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddApplicationPart(Assembly.Load(new AssemblyName("WebAPI")));
However i am really looking for a middlewere type solution so that users can simply add it and i can configure the default settings or they can change some of the settings. The above example would not allow for altering the settings.
app.UseCustomAPI("/api/crap");
Update from comment without Assembly
If i dont add the .AddApplicationPart(Assembly.Load(new AssemblyName("WebAPI")));
This localhost page can’t be found No webpage was found for the web address:
https://localhost:44368/api/Custom
To customise the routing for a controller at runtime, you can use an Application Model Convention. This can be achieved with a custom implementation of IControllerModelConvention:
public class CustomControllerConvention : IControllerModelConvention
{
private readonly string newEndpoint;
public CustomControllerConvention(string newEndpoint)
{
this.newEndpoint = newEndpoint;
}
public void Apply(ControllerModel controllerModel)
{
if (controllerModel.ControllerType.AsType() != typeof(CustomController))
return;
foreach (var selectorModel in controllerModel.Selectors)
selectorModel.AttributeRouteModel.Template = newEndpoint;
}
}
This example just replaces the existing template (api/[controller]) with whatever is provided in the CustomControllerConvention constructor. The next step is to register this new convention, which can be done via the call to AddMvc. Here's an example of how that works:
services.AddMvc(o =>
{
o.Conventions.Add(new CustomControllerConvention("api/whatever"));
});
That's all that's needed to make things work here, but as you're offering this up from another assembly, I'd suggest an extension method based approach. Here's an example of that:
public static class MvcBuilderExtensions
{
public static IMvcBuilder SetCustomControllerRoute(
this IMvcBuilder mvcBuilder, string newEndpoint)
{
return mvcBuilder.AddMvcOptions(o =>
{
o.Conventions.Add(new CustomControllerConvention(newEndpoint));
});
}
}
Here's how that would be called:
services.AddMvc()
.SetCustomControllerRoute("api/whatever");
This whole approach means that without a call to SetCustomControllerRoute, api/Custom will still be used as a default.
I am currently using swagger in my project and i have more than 100 controllers there. I guess due to the large number of controller, swagger UI documentation page takes more than 5 min to load its controller. Is it possible to select specific controllers at the UI page and load options for them only?
Or else there are other methods to load UI page faster?
Help me!
you can use ApiExplorerSettings on either controller to ignore a controller completely or on a method.
[ApiExplorerSettings(IgnoreApi = true)]
public class MyController
{
[ApiExplorerSettings(IgnoreApi = true)]
public string MyMethod
{
...
}
}
Using swashbuckle's document filter you can remove some elements of the generated specification after the fact, and they would then not be included on the integrated swagger-ui. Create a class such as the below:
using System;
using System.Web.Http.Description;
using Swashbuckle.Swagger;
internal class SwaggerFilterOutControllers : IDocumentFilter
{
void IDocumentFilter.Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer)
{
foreach (ApiDescription apiDescription in apiExplorer.ApiDescriptions)
{
Console.WriteLine(apiDescription.Route.RouteTemplate);
if ((apiDescription.RelativePathSansQueryString().StartsWith("api/System/"))
|| (apiDescription.RelativePath.StartsWith("api/Internal/"))
|| (apiDescription.Route.RouteTemplate.StartsWith("api/OtherStuff/"))
)
{
swaggerDoc.paths.Remove("/" + apiDescription.Route.RouteTemplate.TrimEnd('/'));
}
}
}
}
and then edit your SwaggerConfig.cs file to include the filter:
GlobalConfiguration.Configuration
.EnableSwagger(c =>
c.DocumentFilter<SwaggerFilterOutControllers>();
Note that while the controllers have been removed from the specification, other items such as the result models will still be included in the specification and might still be slowing down the page load.
It could also be slow simply due to enumerating all of the controllers/models etc in the first place, in which case this might not help.
Edit: I noticed it would regenerate the whole definition every time the UI page was viewed (which could be crippling in your scenario). Fortunately it's super easy to cache this (which should be fine as it shouldn't change at runtime for the majority of people).
Add this to your config:
c.CustomProvider((defaultProvider) => new CachingSwaggerProvider(defaultProvider));
and use this class shamelessly copied from https://github.com/domaindrivendev/Swashbuckle/blob/master/Swashbuckle.Dummy.Core/App_Start/CachingSwaggerProvider.cs
using Swashbuckle.Swagger;
using System.Collections.Concurrent;
namespace <your namespace>
{
public class CachingSwaggerProvider : ISwaggerProvider
{
private static ConcurrentDictionary<string, SwaggerDocument> _cache =
new ConcurrentDictionary<string, SwaggerDocument>();
private readonly ISwaggerProvider _swaggerProvider;
public CachingSwaggerProvider(ISwaggerProvider swaggerProvider)
{
_swaggerProvider = swaggerProvider;
}
public SwaggerDocument GetSwagger(string rootUrl, string apiVersion)
{
string cacheKey = string.Format("{0}_{1}", rootUrl, apiVersion);
return _cache.GetOrAdd(cacheKey, (key) => _swaggerProvider.GetSwagger(rootUrl, apiVersion));
}
}
}
In response to the previous answer, this is the updated code for ASP.NET Core. I also added the feature to remove models.
using System;
using System.Linq;
using System.Web.Http;
using Swashbuckle.AspNetCore.SwaggerGen;
using Swashbuckle.AspNetCore.Swagger;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
internal class SwaggerFilterOutControllers : IDocumentFilter
{
void IDocumentFilter.Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
{
foreach (var item in swaggerDoc.Paths.ToList())
{
if (!(item.Key.ToLower().Contains("/api/endpoint1") ||
item.Key.ToLower().Contains("/api/endpoint2")))
{
swaggerDoc.Paths.Remove(item.Key);
}
}
swaggerDoc.Definitions.Remove("Model1");
swaggerDoc.Definitions.Remove("Model2");
}
}
swaggerDoc.paths.Remove("/" + apiDescription.Route.RouteTemplate.TrimEnd('/'));did not remove anything for me. So,
internal class SwaggerFilterOutControllers : IDocumentFilter
{
void IDocumentFilter.Apply(SwaggerDocument swaggerDoc, SchemaRegistry schemaRegistry, IApiExplorer apiExplorer)
{
foreach (var item in swaggerDoc.Paths.ToList())
{
if (!(item.Key.ToLower().Contains("/api/v1/xxxx") ||
item.Key.ToLower().Contains("/api/v1/yyyy")))
{
swaggerDoc.Paths.Remove(item.Key);
}
}
}
}
You can try this. You APIExplorerSetting to specify APIs to be included in a particular group.
Start by defining multiple Swagger docs in Startup.cs:
services.AddSwaggerGen(c => {
c.SwaggerDoc("v1", new OpenApiInfo {
Title = "My API - V1",
Version = "v1"
});
c.SwaggerDoc("v2", new OpenApiInfo {
Title = "My API - V2",
Version = "v2"
});
});
Then decorate the individual controller with the above groups:
[ApiExplorerSettings(GroupName = "v2")]
Reference: https://github.com/domaindrivendev/Swashbuckle.AspNetCore#generate-multiple-swagger-documents
You can use DocInclusionPredicate to Customize the Action Selection Process:
When selecting actions for a given Swagger document, the generator invokes a DocInclusionPredicate against every ApiDescription that's surfaced by the framework. The default implementation inspects ApiDescription.GroupName and returns true if the value is either null OR equal to the requested document name. However, you can also provide a custom inclusion predicate.
services.AddSwaggerGen(c =>
{
c.DocInclusionPredicate((string title, ApiDescription apiDesc) =>
{
// filter the ApiDescription
});
});