I am putting together a talk for a local Code Camp and am trying to understand the nuances of HTTP Verbs in ApiController. Several things about ApiController significantly changed between Beta, RC and final release, and the advice on how you can set this up is conflicting and sometimes wrong.
Assuming I am just leaving the standard routing in WebApiConfig:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
(since you can really put things on their head if you add in an {action} parameter here)
I understand how the convention is working for simple Crud calls like:
// GET api/values
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
public string Get(int id)
{
return "value";
}
// POST api/values
public void Post([FromBody]string value)
{
}
Or that you can change these as long as they start with the verb name:
// GET api/values/5
public string GetMyStuff(int id)
{
return "value";
}
However, the initial spec says that ApiController supports Get, Put, Post and Delete.
Yet I can add methods for:
public void HeadOfTheClass()
{
}
Which works for a Head verb, but I can’t add methods for the obscure or non-existent verbs:
public void MKCOL()
{
}
public void Bubba()
{
}
What is the full list of Native “Supported” verbs?
I can however add support for these methods by using the AcceptVerb attribute:
[AcceptVerbs("MKCOL")]
public void MKCOL()
{
}
[AcceptVerbs("Bubba")]
public void Bubba()
{
}
This also works, or for any “defined” verb use the Http attributes:
[HttpHead]
public void HeadOfTheClass()
{
}
[HttpGet]
public void Bubba()
{
}
Which is correct or preferred? (There was also attributes like [GET] and [POST] as well, are these deprecated?)
Are [HttpBindNever] and [NonAction] equivalent?
I love open source. :)
From ReflectedHttpActionDescriptor:
private static readonly HttpMethod[] _supportedHttpMethodsByConvention =
{
HttpMethod.Get,
HttpMethod.Post,
HttpMethod.Put,
HttpMethod.Delete,
HttpMethod.Head,
HttpMethod.Options,
new HttpMethod("PATCH")
};
Related
Does anyone know how I can mark an argument on ActionDescriptor.Parameters to behave in a similar way the [BindNever] is behaving?
I want to always exclude a specific argument from a specific type without keep decorating it on the Controller.
Essentially I would like to be able to add my injected to my functions somehow how similar to the way its done with CancellationToken
public class TestController : ControllerBase
{
[HttpGet(Name = "Get")]
public IActionResult Get([BindNever] IInjectedInterface injected)
{
//Injected can be used in this method
return Ok();
}
[HttpPost(Name = "Post")]
public IActionResult Post([BindNever] IInjectedInterface injected, FormModel formModel)
{
//Injected doesn't work here. There is an error that
/*System.InvalidOperationException: 'Action 'WebApplication3.Controllers.TestController.Post (WebApplication3)'
has more than one parameter that was specified or inferred as bound from request body. Only one parameter per action may be bound from body.
Inspect the following parameters, and use 'FromQueryAttribute' to specify bound from query, 'FromRouteAttribute' to specify bound from route,
and 'FromBodyAttribute' for parameters to be bound from body:
IInjectedInterface injected
FormModel formModel'
*/
return Ok();
}
}
public class ActionExecutionFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var injectedParam = context.ActionDescriptor.Parameters.SingleOrDefault(x => x.ParameterType == typeof(IInjectedInterface));
if (injectedParam != null)
{
context.ActionArguments[injectedParam.Name] = new Injected(99);
}
await next.Invoke();
}
private class Injected : IInjectedInterface
{
public Injected(int someData)
{
SomeData = someData;
}
public int SomeData { get; }
}
}
I was able to solve it. Apparently you need to add the following lines on your program.cs to avoid the model binder related errors.
options.ModelMetadataDetailsProviders.Add(
new ExcludeBindingMetadataProvider(typeof(IInjectedInterface)));
options.ModelMetadataDetailsProviders.Add(
new BindingSourceMetadataProvider(typeof(IInjectedInterface), BindingSource.Special));
My project references the following packages;
Swashbuckle.AspNetCore.Filters v6.0.0
Swashbuckle.AspNetCore.Swagger v5.6.3
Swashbuckle.AspNetCore.SwaggerGen v5.6.3
Swashbuckle.AspNetCore.SwaggerNewtonSoft v5.6.3
Microsoft.AspNetCore.OData v7.5.0
Here's the issue:
I have a controller called "TestController". In it, there is a single [HttpGet] method called Test.
The method is decorated as follows;
[HttpGet]
[SwaggerOperation(OperationId = nameof(Test))]
public IActionResult Test([FromQuery] string id, [FromQuery] ODataQueryOptions<SearchOptions> oData)
{
// ...
}
Since I'm using Swashbuckle, the expected results should be that there is a get method named Test with a bunch of query parameters returned to the documentation UI.
However, instead I see an exception. The exception says;
Failed to generate Scheme for type - ODataQueryOptions<`T>. See inner exception
Inspecting the inner exception shows that swagger is attempting to build what-looks to be a scheme of a bunch of system types (eg. HttpContext, response, request, etc...).
I believe this is happening b/c the ODataQueryOption<`T> class comes with a number of contextual properties to help facilitate URI parsing.
See more about that class here: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnet.odata.query.odataqueryoptions?view=odata-aspnetcore-7.0
The exceptions and random google searches have lead me down the path of adding custom IOperationFilter, ISchemeFilters, and IParameterFilters (these all are Swagger specific configuration 'filters').
I've tried to remove the operation all-together by setting properties to null. I've attempted the same with Scheme and Parameter filters... All with no luck. And no documentation to help...
Example of my attempts:
class ParamFilter : IParameterFilter {
public void Apply(OpenApiParameter parameter, ParameterFilterContext context) {
parameter.Scheme = null;
parameter.Reference = null;
}
}
class SchemeFilter : ISchemeFilter {
public void Apply(OpenApiScheme scheme, SchemeFilterContext context) {
scheme.Items = null;
scheme.Reference = null;
scheme.Reference = null;
}
}
// Note: this never gets hit by the debugger. App throws exception before invocation.
class OperationFilter : IOperationFilter {
public void Apply(OpenApiOperation operation, OperationFilterContext context) {
operation.Parameters.clear()
}
}
Nothing worked. Same exception.
At this point my question is fairly simple;
How can I remove the ODataQueryOption parameter from swagger documentation generation?
EDIT: Adding exception messages
Failed to generate Schema for type - Microsoft.AspNet.OData.Query.ODataQueryOptions`1[SearchOptions].
See inner exception
Failed to generate Schema for type -
Microsoft.AspNetCore.Http.HttpRequest. See inner exception
Failed to generate Schema for type -
Microsoft.AspNetCore.Http.HttpContext. See inner exception
Failed to generate Schema for type -
Microsoft.AspNetCore.Http.Authentication.AuthenticationManager. See
inner exception
Could not load type
'Microsoft.AspNetCore.Http.Features.Authentication.AuthenticateContext'
from assembly 'Microsoft.AspNetCore.Http.Features, Version=3.1.8.0,
Culture=neutral, PublicKeyToken=adb9793829ddae60'.
It could work well in my project:
Action(Be sure remove [FromQuery] on ODataQueryOptions):
[HttpGet]
[EnableQuery]
public IActionResult Test Get([FromQuery] string id, ODataQueryOptions<SearchOptions> ODataQueryOptions)
{
//...
}
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddOData();
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
});
SetOutputFormatters(services);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.EnableDependencyInjection();
endpoints.Select().Filter().Expand().MaxTop(10);
endpoints.MapODataRoute("odata", "odata", GetEdmModel());
});
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
}
IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<WeatherForecast>("WeatherForecast");
return builder.GetEdmModel();
}
private static void SetOutputFormatters(IServiceCollection services)
{
services.AddMvcCore(options =>
{
IEnumerable<ODataOutputFormatter> outputFormatters =
options.OutputFormatters.OfType<ODataOutputFormatter>()
.Where(foramtter => foramtter.SupportedMediaTypes.Count == 0);
foreach (var outputFormatter in outputFormatters)
{
outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/odata"));
}
});
}
Try setting the mapping for ODataQueryOptions with options.MapType(typeof(ODataQueryOptions<>), () => new ());
My controllers return unified RequestResult:
public Task<RequestResult> SomeAction()
{
...
return new RequestResult(RequestResultType.NotFound);
}
public class RequestResult
{
public RequestResultType Type { get;set; }
... //actual data
}
public enum RequestResultType
{
Success = 1,
NotFound = 2
}
So basically RequestResult combines actual Action data and error type (if it happened). Now I need to specify Response Type at some point in case if Action returned Error. My best guess here is to use Middleware:
public class ResponseTypeMiddleware
{
private readonly RequestDelegate next;
public ResponseTypeMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context)
{
await next(context);
var response = context.Response.Body; //how to access object?
}
}
but I can't figure out what to do with it. What I'd perfectly like to do is to check if response is of type RequestResult, then specify ResponseType equal BadRequest. But I don't see how I can do it here as what I have is just a stream. May be I can hijack into pipeline earlier, before result was serialized (Controller?).
P. S. The reason why I don't use Controller.BadRequest directly in Action is that my Action's logic is implemented via CQRS command/query handlers, so I don't have direct access to Controller.
As you are going to process controller's action result (MVC), the best way is to use ActionFilter or ResultFilter here, instead of Middleware. Filters in ASP.NET Core are a part of MVC and so know about controllers, actions and so on. Middleware is a more common conception - it is an additional chain in application request-response pipeline.
public class SampleActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
// do something before the action executes
}
public void OnActionExecuted(ActionExecutedContext context)
{
// do something after the action executes
// get or set controller action result here
var result = context.Result as RequestResult;
}
}
I've been trying to get Web Api to work with Sitecore 8.1.
I installed this package: https://www.nuget.org/packages/Krusen.Sitecore.WebApi.Custom/ and I modified the ConfigureWebApi to the following:
public class ConfigureWebApi
{
public void Process(PipelineArgs args)
{
GlobalConfiguration.Configure(config => config.Routes.MapHttpRoute("API Default", "api/{controller}/{id}",
new { id = RouteParameter.Optional }));
GlobalConfiguration.Configure(config => config.MapHttpAttributeRoutes());
GlobalConfiguration.Configure(ReplaceControllerSelector);
}
private static void ReplaceControllerSelector(HttpConfiguration config)
{
config.Services.Replace(typeof (IHttpControllerSelector),
new CustomHttpControllerSelector(config, new NamespaceQualifiedUniqueNameGenerator()));
}
}
However, whenever I use post requests, I get the following error:
{"Message":"The requested resource does not support http method
'POST'."}. Get requests work.
This is the implementation of the controller:
[RoutePrefix("api/authentication")]
public class AuthenticationController : ApiController
{
[Route("email")]
[HttpPost]
public bool Login([FromBody] string email)
{
return true;
}
}
I figured out what the error was. When my controller was called AuthenticationController it gave the following error:
The requested document was not found
If I called it something else, the web api worked as a charm e.g.
public TestController : ApiController {
//Code goes here
}
In a console application, I would like to use a service that would normally need the current http context to be passed to its constructor. I am using Ninject, and I think I can simply fake an http context and define the proper binding, but I have been struggling with this for a few hours without success.
The details:
The service is actually a mailing service that comes from an ASP.Net MVC project. I am also using Ninject for IoC. The mail service needs the current http context to be passed to its constructor. I do the binding as follows:
kernel.Bind<IMyEmailService>().To<MyEmailService>()
.WithConstructorArgument("httpContext", ninjectContext => new HttpContextWrapper(HttpContext.Current));
However, I would like now to use this mailing service in a console application that will be used to run automated tasks at night. In order to do this, I think I can simply fake an http context, but I have been struggling for a few hours with this.
All the mailing service needs from the context are these two properties:
httpContext.Request.UserHostAddress
httpContext.Request.RawUrl
I thought I could do something like this, but:
Define my own fake request class:
public class AutomatedTaskHttpRequest : SimpleWorkerRequest
{
public string UserHostAddress;
public string RawUrl;
public AutomatedTaskHttpRequest(string appVirtualDir, string appPhysicalDir, string page, string query, TextWriter output)
: base(appVirtualDir, appPhysicalDir, page, query, output)
{
this.UserHostAddress = "127.0.0.1";
this.RawUrl = null;
}
}
Define my own context class:
public class AutomatedTasksHttpContext
{
public AutomatedTaskHttpRequest Request;
public AutomatedTasksHttpContext()
{
this.Request = new AutomatedTaskHttpRequest("", "", "", null, new StringWriter());
}
}
and bind it as follows in my console application:
kernel.Bind<IUpDirEmailService>().To<UpDirEmailService>()
.WithConstructorArgument("httpContext", ninjectContext => new AutomatedTasksHttpContext());
Unfortunately, this is not working out. I tried various variants, but none was working. Please bear with me. All that IoC stuff is quite new to me.
I'd answered recently about using a HttpContextFactory for testing, which takes a different approach equally to a console application.
public static class HttpContextFactory
{
[ThreadStatic]
private static HttpContextBase _serviceHttpContext;
public static void SetHttpContext(HttpContextBase httpContextBase)
{
_serviceHttpContext = httpContextBase;
}
public static HttpContextBase GetHttpContext()
{
if (_serviceHttpContext!= null)
{
return _serviceHttpContext;
}
if (HttpContext.Current != null)
{
return new HttpContextWrapper(HttpContext.Current);
}
return null;
}
}
then in your code to this:
var rawUrl = HttpContextFactory.GetHttpContext().Request.RawUrl;
then in your tests use the property as a seam
HttpContextFactory.SetHttpContext(HttpMocks.HttpContext());
where HttpMocks has the following and would be adjusted for your tests:
public static HttpContextBase HttpContext()
{
var context = MockRepository.GenerateMock<HttpContextBase>();
context.Stub(r => r.Request).Return(HttpRequest());
// and stub out whatever else you need to, like session etc
return context;
}
public static HttpRequestBase HttpRequest()
{
var httpRequest = MockRepository.GenerateMock<HttpRequestBase>();
httpRequest.Stub(r => r.UserHostAddress).Return("127.0.0.1");
httpRequest.Stub(r => r.RawUrl).Return(null);
return httpRequest;
}