How to add an Action with a DateTime parameter? - asp.net-core

I want to add an Action to my Invoices controller which has a DateTime parameter:
Here is my Controller with my action:
[Route("api/[controller]")]
public class InvoicesController : Controller
{
private readonly IInvoiceRepository _repository;
private readonly ILogger<InvoicesController> _logger;
public InvoicesController(IInvoiceRepository repository, ILogger<InvoicesController> logger)
{
_repository = repository;
_logger = logger;
}
[HttpGet]
public IActionResult FilterBy(DateTime date)
{
try
{
return Ok(_repository.GetInvoicesByDate(date));
}
catch (Exception ex)
{
string errorMessage = "Failed to get invoices by date";
_logger.LogError("{0} {1}", errorMessage, ex);
return BadRequest(errorMessage);
}
}
}
I had in the startup.cs file this line:
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc(routes =>
routes.MapRoute(
name: "getInvoicesByDate",
template: "api/Invoices/{action}/{date:DateTime}",
defaults: new { controller = "Invoices", action = "FilterBy" }));
}
What's wrong in my code ? Thank you
UPDATE
I would like an URL this: format http://domain/api/invoices/filterby/2017-01-01

hm..
I think it is not typical route and parameter with ASP.NET WebAPI, I am also not sure it's WebSite or WebAPI.
It works for me (url format like: domain/api/invoices/filterby/2017-01-01):
[Route("api/[controller]")]
public class InvoicesController : Controller
{
[HttpGet("[action]/{date}")]
public IActionResult FilterBy(DateTime date)
{
try
{
return Ok(date);
}
catch (Exception ex)
{
string errorMessage = "Failed to get invoices by date";
return BadRequest(errorMessage);
}
}
}
--
In my opinion, I would like to make it simple: only use route attribute with WebAPI.
only use app.UseMvc(); in Startup.cs
add route attribute in controller:
Finally, make sure your WebAPI url with parameter(HTTP GET)
http://localhost:xxxx/api/Invoices/FilterBy?date=2017-01-01

Related

Differ IOutputFormatter per endpoint in ASP.NET Core 6

I have a legacy ASP.NET Web API 2 app which must be ported to ASP.NET Core 6 and it has the following behaviour:
Some controllers return responses in Pascal-case Json
Some controllers return responses in camel-case Json
All controllers have the same authentication/authorization, but they return different objects using different serializers for 401/403 cases.
In ASP.NET Web API 2 it was easily solved with IControllerConfiguration (to set the formatter for a controller), AuthorizeAttribute (to throw exceptions for 401/403), ExceptionFilterAttribute to set 401/403 status code and response which will be serialized using correct formatter.
In ASP.NET Core, it seems that IOutputFormatter collection is global for all controllers and it is not available during UseAuthentication + UseAuthorization pipeline where it terminates in case of failure.
Best I could come up with is to always "succeed" in authentication / authorization with some failing flag in claims and add IActionFilter as first filter checking those flags, but it looks very hacky.
Is there some better approach?
Update1:
Implementing different output formatters for IActionResult from controller or IFilter (including IExceptionFilter) is not very difficult.
What I want is to be able to either set IActionResult or use IOutputFormatter related to Action identified by UseRouting for Authentication/Authorization error or IAuthorizationHandler, but looks like all those auth steps are invoked before either ActionContext or IOutputFormatter is invoked.
So 2 approaches I see now:
hack auth code to "always pass" and handle HttpContext.Items["MyRealAuthResult"] object in IActionFilter
expose V1OutputFormatter/V2OutputFormatter in a static field and duplicate selection logic in HandleChallengeAsync/HandleForbiddenAsync based on to what controller/action it was routed from UseRouting step.
Here is sample app that uses auth and has 2 endpoints:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IConfigureOptions<MvcOptions>, MvcOptionsSetup>();
builder.Services.AddAuthentication(options =>
{
options.AddScheme<DefAuthHandler>("defscheme", "defscheme");
});
builder.Services.AddAuthorization(options =>
options.DefaultPolicy = new AuthorizationPolicyBuilder("defscheme")
.RequireAssertion(context =>
// false here should result in Pascal case POCO for WeatherForecastV1Controller
// and camel case POCO for WeatherForecastV2Controller
context.User.Identities.Any(c => c.AuthenticationType == "secretheader"))
.Build())
.AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizationResultHandler>();
builder.Services.AddControllers();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
public class AuthorizationResultHandler : IAuthorizationMiddlewareResultHandler
{
private readonly AuthorizationMiddlewareResultHandler _handler;
public AuthorizationResultHandler()
{
_handler = new AuthorizationMiddlewareResultHandler();
}
public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
{
// Can't set ActionContext.Response here or use IOutputFormatter
await _handler.HandleAsync(next, context, policy, authorizeResult);
}
}
public class DefAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public DefAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock) { }
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new List<ClaimsIdentity>();
if (Request.Headers.ContainsKey("secretheader")) claims.Add(new ClaimsIdentity("secretheader"));
return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(claims), "defscheme"));
}
}
public class MvcOptionsSetup : IConfigureOptions<MvcOptions>
{
private readonly ArrayPool<char> arrayPool;
private readonly MvcNewtonsoftJsonOptions mvcNewtonsoftJsonOptions;
public MvcOptionsSetup(ArrayPool<char> arrayPool, IOptions<MvcNewtonsoftJsonOptions> mvcNewtonsoftJsonOptions)
{
this.arrayPool = arrayPool;
this.mvcNewtonsoftJsonOptions = mvcNewtonsoftJsonOptions.Value;
}
public void Configure(MvcOptions options)
{
options.OutputFormatters.Insert(0, new V1OutputFormatter(arrayPool, options, mvcNewtonsoftJsonOptions));
options.OutputFormatters.Insert(0, new V2OutputFormatter(arrayPool, options, mvcNewtonsoftJsonOptions));
}
}
public class V1OutputFormatter : NewtonsoftJsonOutputFormatter
{
public V1OutputFormatter(ArrayPool<char> charPool, MvcOptions mvcOptions, MvcNewtonsoftJsonOptions? jsonOptions)
: base(new JsonSerializerSettings { ContractResolver = new DefaultContractResolver() }, charPool, mvcOptions, jsonOptions) { }
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{
var controllerDescriptor = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor>();
return controllerDescriptor?.ControllerName == "WeatherForecastV1";
}
}
public class V2OutputFormatter : NewtonsoftJsonOutputFormatter
{
public V2OutputFormatter(ArrayPool<char> charPool, MvcOptions mvcOptions, MvcNewtonsoftJsonOptions? jsonOptions)
: base(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }, charPool, mvcOptions, jsonOptions) { }
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{
var controllerDescriptor = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata<Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor>();
return controllerDescriptor?.ControllerName == "WeatherForecastV2";
}
}
[ApiController]
[Authorize]
[Route("v1/weatherforecast")]
public class WeatherForecastV1Controller : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
// This must be Pascal case
return Ok(new WeatherForecast() { Summary = "summary" });
}
}
[ApiController]
[Authorize]
[Route("v2/weatherforecast")]
public class WeatherForecastV2Controller : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
// This must be camel case
return Ok(new WeatherForecast() { Summary = "summary" });
}
}
If there is no way to configure controllers independently, then you could use some middleware to convert output from selected controllers that meet a path-based predicate.
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapWhen(ctx => ctx.Request.Path.Containes("v2/"), cfg =>
{
app.UseMiddleware<JsonCapitalizer>();
});
app.Run();
And then create a JsonCapitalizer class to convert output from any path that contains "v2/". Note, this middleware will not run if the predicate in MapWhen is not satisfied.
public class JsonCapitalizer
{
readonly RequestDelegate _nextRequestDelegate;
public RequestLoggingMiddleware(
RequestDelegate nextRequestDelegate)
{
_nextRequestDelegate = nextRequestDelegate;
}
public async Task Invoke(HttpContext httpContext)
{
await _nextRequestDelegate(httpContext);
// Get the httpContext.Response
// Capitalize it
// Rewrite the response
}
}
There may be better ways, but that's the first that comes to mind.
The following link will help with manipulation of the response body:
https://itecnote.com/tecnote/c-how-to-read-asp-net-core-response-body/
I also faced such a problem in ASP Core 7 and ended up with writing an attribute.
So the attribute will be applied on each Action where the response type has to be converted. You can write many an attribute for camelcase response and another attribute for pascalcase. The attribute will look like below for CamelCase
public class CamelCaseAttribute : ActionFilterAttribute
{
private static readonly SystemTextJsonOutputFormatter formatter = new SystemTextJsonOutputFormatter(new()
{
ReferenceHandler = ReferenceHandler.IgnoreCycles,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
public override void OnActionExecuted(ActionExecutedContext context)
{
if (context.Result is ObjectResult objectResult)
{
objectResult.Formatters
.RemoveType<NewtonsoftJsonOutputFormatter>();
objectResult.Formatters.Add(formatter);
}
else
{
base.OnActionExecuted(context);
}
}
}
And on the Contoller Action you can use it like below
[CamelCase]
public async IAsyncEnumerable<ResponseResult<IReadOnlyList<VendorBalanceReportDto>>> VendorBalanceReport([FromQuery] Paginator paginator, [FromQuery] VendorBalanceReportFilter filter, [EnumeratorCancellation] CancellationToken token)
{
var response = _reportService.VendorBalanceReport(paginator, filter, token);
await foreach (var emailMessage in response)
{
yield return emailMessage;
}
}

ASP.NetCore API controller does not call my method in my Controller

I create an API Controller in my ASP.NET core web api project.
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
public MyData Method1(string Id)
{
Console.WriteLine("here");
return null;
}
public MyData Method2()
{
Console.WriteLine("here");
return null;
}
}
I set breakpoints in each method and load these urls in my browser:
https://localhost:44357/test/Method1/1343a
https://localhost:44357/test/Method2
And in my Startup.cs, I have
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
Can you please help me why my API controller does not get invoked when I load ^ urls in browser?
I read https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-6.0 , I think those method should get called.
Thank you for your help.
First check by adding verb over method [HTTPPOST] or [HTTPGET].
Also your starup.cs code looks like as written below:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
You controller Code:
[ApiController]
[Route("api/[controller]")]
public class TestController : ControllerBase
{
[HttpGet]
[Route("method1")]
public MyData Method1(string Id)
{
Console.WriteLine("here");
return null;
}
[HttpGet]
[Route("method2")]
public MyData Method2()
{
Console.WriteLine("here");
return null;
}
}
Now call you API like :
https://localhost:portno/api/test/method1/abc
https://localhost:portno/api/test/method2
Please change your route to include the action like below
[ApiController]
[Route("[controller]/[action]")]
public class TestController : ControllerBase
{
public MyData Method1(string Id)
{
Console.WriteLine("here");
return null;
}
public MyData Method2()
{
Console.WriteLine("here");
return null;
}
}
in Asp.net Api you need route for each Action:
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
[HttpGet("[Action]/{Id}")]
public MyData Method1(string Id)
{
Console.WriteLine("here");
return null;
}
[HttpGet("[Action]")]
public MyData Method2()
{
Console.WriteLine("here");
return null;
}
}
For Net Core 6.0 change this in your Program.cs
`var services = builder.Services;
services.AddControllers();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.Run();`
and [Route("methodname")] use this attribute above your action method.

LinkGenerator.GetUriByAction not following the custom route

That's the controller:
[ApiController]
[Route("api/[controller]")
public class MarketController : ControllerBase
{
[HttpGet("{id}/picture")
public async Task<IActionResult> GetPictureAsync(Guid id)
{
...
}
}
I'm using LinkGenerator to create a Absolute URI from GetPictureAsync. And set the Startup class to start HttpContextAccessor as DI.
// Startup.cs
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddHttpContextAccessor();
...
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapDefaultControllerRoute();
});
}
And in my custom class I use that way:
public class CustomClass
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly LinkGenerator _linkGenerator;
public CustomClass(IHttpContextAccessor httpContextAccessor,
LinkGenerator linkGenerator)
{
_httpContextAccessor = httpContextAccessor;
_linkGenerator = linkGenerator;
}
public void SomeMethod()
{
var uri = _linkGenerator.GetUriByAction(_httpContextAccessor.HttpContext, "GetPicture", "Markets", new { id = id });
}
}
The problem is because LinkGenerator is not following the custom route sample that I set in GetPicture method.
The LinkGenerator generates the following value:
https://localhost:5051/Markets/GetPicture/00748d23-afa7-4efb-b67b-77f68fdc44d5
But it should generate:
https://localhost:5051/api/Markets/00748d23-afa7-4efb-b67b-77f68fdc44d5/picture
The reason is you use wrong controller name in SomeMethod. Follow the steps you provided, I reproduced your issue.
You should use Market, not Markets.
Because your controller name is MarketController.
After test it,it works for me.

How to return indented json content from an OData controller in asp core web api?

I can retrieve intended json result from normal WebApi using following way.
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddJsonOptions(x=>
{
x.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;
});
But I cannot find a way to output json like this when using ODataController as opposed to ControllerBase when web api is used. ODataController always sends a minified json.
public class EmployeeController : ODataController
{
[EnableQuery()]
public IActionResult Get()
{
return Ok(new BOContext().Employees.ToList());
}
}
Also, startup.cs
public class Startup
{
private static IEdmModel GetModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Employee>("Employee");
return builder.GetEdmModel();
}
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)
{
services.AddOData();
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddJsonOptions(x=>
{
x.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.None;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapODataServiceRoute("odata", "odata", GetModel());
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
The route is working and I am receiving correct data.
Is there a way to control and output indented json from an OData controller?
I suggest you to make the transfer with minified jsonm, but use json beutifier to show formatted json. dont do this on the data flow phase.
If you are using javascript on the front-end side. You can simple use
JSON.stringify(jsObj, null, "\t"); // stringify with tabs inserted at each level
JSON.stringify(jsObj, null, 2); // stringify with 2 spaces at each level
Not sure if this is still actual, but you can specify formatter when returning the data
// [...]
public IActionResult Get()
{
var res = Ok(_db.Employees);
res.Formatters.Add(new Microsoft.AspNetCore.Mvc.Formatters.JsonOutputFormatter(
new Newtonsoft.Json.JsonSerializerSettings() { Formatting = Newtonsoft.Json.Formatting.Indented },
System.Buffers.ArrayPool<char>.Create()));
return res;
}
And of course, if you want more generalized solution (or you just have a lot of code that is already written), you can create interim abstract class and inherit from that class instead of just ODataController:
public abstract class AbstractFormattedOdataController : ODataController
{
public override OkObjectResult Ok(object value)
{
var res = base.Ok(value);
res.Formatters.Add(new Microsoft.AspNetCore.Mvc.Formatters.JsonOutputFormatter(
new Newtonsoft.Json.JsonSerializerSettings() { Formatting = Newtonsoft.Json.Formatting.Indented },
System.Buffers.ArrayPool<char>.Create()));
return res;
}
}
// [...]
public class EmployeesController : AbstractFormattedOdataController
{
[EnableQuery()]
public IActionResult Get()
{
return Ok(new BOContext().Employees.ToList());
}
}

ASP.NET Core: Many routes -> always only one controller

Similar to SO ASP.NET MVC: Many routes -> always only one controller:
O have a .net 4.7 MVC project project
my config route are as follows (following from the above post)
config.Routes.MapHttpRoute(
name: "AllRoutes",
routeTemplate: "{*url}",
defaults: new
{
controller = "base",
});
my base controller in my .net 4.7 project
public class BaseController : ApiController
{
[HttpGet]
public IHttpActionResult Get(HttpRequestMessage request)
{
return Ok();
}
[HttpPost]
public IHttpActionResult Post(HttpRequestMessage request)
{
return Ok();
}
[HttpPut]
public IHttpActionResult Put(HttpRequestMessage request)
{
return Ok();
}
[HttpDelete]
public IHttpActionResult Delete(HttpRequestMessage request)
{
return Ok();
}
}
now I'm porting my project into a .NET Core 2.0
I can't seem to setup the same thing
my config in the .net core project is as follows
app.UseMvc(routes =>
{
routes.MapRoute(
name: "AllRoutes",
template: "{*url}",
defaults: new
{
controller = "Base"
}
);
my base controller for my .net core project
//[Route("api/[controller]")]
public class BaseController : Controller
{
[HttpGet]
public IActionResult Get()
{
return Ok("get success");
}
// POST api/values
[HttpPost]
public IActionResult Post([FromBody]string value)
{
return Ok("post success");
}
[HttpPut]
public IActionResult Put([FromBody]string value)
{
return Ok("put success");
}
[HttpDelete]
public IActionResult Delete()
{
return Ok("delete success");
}
}
any ideas?
Why do you even want to use MVC, when you have no controllers or routes?
Just use a custom middleware:
// Startup configure
app.Use(async (context, next) =>
{
var service = context.RequestServices.GetRequiredServce<MyService>();
var service.Execute();
async next();
});
Update
Just in case it's not clear, you can inject IHttpContextAccessor in your service, where you can directly access the request stream and do whatever you need to do with it.
public class BaseContoller : Controller {
[HttpGet("/base/get")]
public IActionResult Get() {
return Ok("get success");
}
[HttpPost("/base/post")]
public IActionResult Post() {
return Ok("post success");
}
}
you looking for something like this?
or if you want to route this links you need add something like this
public class BaseController : Controller {
[Route("/get")]
public IActionResult Get() {
return Ok("get success");
}
}