devexpress 21.2.5 CustomWebDocumentViewerController makes ambiguous contoller error in swagger - c#

i use asp.net boilerplate for my project. i updated devexpress version from 21.1.4 to 21.2.5 and made a custom WebDocumentViewerController.
public class CustomWebDocumentController :
WebDocumentViewerController
{
public
CustomWebDocumentController(IWebDocumentViewerMvcControllerService
controllerService) : base(controllerService)
{
}
}
i used this code to remove defualt DocumentViewerController in startup.sc:
services.AddMvc()
.ConfigureApplicationPartManager(x =>
{
var parts = x.ApplicationParts;
var aspNetCoreReportingAssemblyName =
typeof(WebDocumentViewerController).Assembly.GetName().Name;
var reportingPart = parts.FirstOrDefault(part => part.Name
== aspNetCoreReportingAssemblyName);
if (reportingPart != null)
{
parts.Remove(reportingPart);
}
});
the code is running but the defualtcontroller is still in list of controllers and makes swagger confiused.
how should i remove the defualt contoller?
thanks for your time.

The reason why this "ambiguous HTTP method for Action Error" pops up is because this controller 'CustomWebDocumentController' is missing the HTTP action decoration ([HttpGet],[HttpPost] etc) on top of it.
Simply decorate the controller with '[ApiExplorerSettings(IgnoreApi=true)]'. This will ultimately cause the entire controller or individual action to be omitted from the Swagger output.
Source: Exclude controllers methods from docs without using the Obsolete attribute
public class CustomWebDocumentController : WebDocumentViewerController
{
[ApiExplorerSettings(IgnoreApi = true)]
public
CustomWebDocumentController(IWebDocumentViewerMvcControllerService controllerService) : base(controllerService)
{
}
}

Related

.NET Core 3.1 custom model validation with fluentvalidation

Im trying to learn .net 3.1 by building a small test webapi and currently my objective is to validate dtos with fluentvalidation and in case it fails, present a custom json to the caller. The problems i have found and cant get over are two;
i cant seem to get the messages i write via fluentvalidation (they always are the - i assume .net core default ones)
i cant seem to modify the object type that is json-ified and then output to the caller.
My code is as follows:
1. The Controller
[ApiController]
[Route("[controller]")]
public class AdminController : ControllerBase
{
[HttpPost]
[ProducesResponseType(StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status202Accepted)]
public async Task<IActionResult> RegisterAccount(NewAccountInput dto)
{
return Ok();
}
}
2. The Dto and the custom validator
public class NewAccountInput
{
public string Username { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public AccountType Type { get; set; }
}
public class NewAccountInputValidator : AbstractValidator<NewAccountInput>
{
public NewAccountInputValidator()
{
RuleFor(o => o.Email).NotNull().NotEmpty().WithMessage("Email vazio");
RuleFor(o => o.Username).NotNull().NotEmpty().WithMessage("Username vazio");
}
}
3. The Filter im using for validation
public class ApiValidationFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
//the only output i want are the error descriptions, nothing else
var data = context.ModelState
.Values
.SelectMany(v => v.Errors.Select(b => b.ErrorMessage))
.ToList();
context.Result = new JsonResult(data) { StatusCode = 400 };
}
//base.OnActionExecuting(context);
}
}
finally, my configureservices
public void ConfigureServices(IServiceCollection services)
{
services
//tried both lines and it doesnt seem to work either way
.AddScoped<ApiValidationFilter>()
.AddControllers(//config=>
//config.Filters.Add(new ApiValidationFilter())
)
.AddFluentValidation(fv => {
fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false;//i was hoping this did the trick
fv.RegisterValidatorsFromAssemblyContaining<NewAccountInputValidator>();
});
}
Now, trying this with postman i get the result
which highlights both issues im having atm
This was done with asp.net core 3.15 and visualstudio 16.6.3
The message you are seeing is in fact coming from FluentValidation - see the source.
The reason you aren't seeing the custom message you are providing is that FluentValidation will show the validation message from the first validator that fails in the chain, in this case NotNull.
This question gives some options for specifying a single custom validation message for an entire chain of validators.
In this case the Action Filter you describe is never being hit, as the validation is failing first. To prevent this you can use:
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
which will stop the automatic return of BadRequest for an invalid model. This question provides some alternative solutions, including configuring an InvalidModelStateResponseFactory to do what you require.

How to register api controller from a library with configuration

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.

Binding source parameter inference in ASP.NET core

I'm currently upgrading a project from ASP.NET WebAPI 5.2.6 (OWIN) to ASP.NET Core 2.1.1 (Kestrel).
Our project is a single page application and we communicate via WebAPI with the client. Therefore I wanted to annotate the controllers wit the new ApiController attribute.
Unfortunately it seems that the binding source parameter inference isn't working as expected (at least for me). I assumed based on the docs, that complex types (e.g. my LoginRequest) are inferred as [FromBody].
Code (Controller & Startup)
// AccountController.cs
[Route("/account"), ApiController]
public class AccountController : ControllerBase
{
[HttpPost("backendLogin"), AllowAnonymous]
public async Task<ActionResult<LoginResponse>> BackendLogin(LoginRequest lr)
{
await Task.CompletedTask.ConfigureAwait(false); // do some business logic
return Ok(new LoginResponse {UserId = "123"});
}
// Models
public class LoginRequest {
public string Email { get; set; }
public string Password { get; set; }
}
public class LoginResponse {
public string UserId { get; set; }
}
}
// Startup.cs
public void ConfigureServices(IServiceCollection services) {
services.AddMvcCore()
.AddJsonFormatters(settings => {
settings.DateTimeZoneHandling = DateTimeZoneHandling.Utc;
settings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.Configure<ApiBehaviorOptions>(options => {
// options.SuppressConsumesConstraintForFormFileParameters = true;
// options.SuppressInferBindingSourcesForParameters = true;
// options.SuppressModelStateInvalidFilter = true;
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
}
app.UseMvc();
}
Problem
Calling the controller from the client via Ajax call (Content-Type: application/x-www-form-urlencoded;) results in a 400 (bad request) response, with content {"":["The input was not valid."]}. On the server I get the following trace output:
Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor: Information: Executing ObjectResult, writing value of type 'Microsoft.AspNetCore.Mvc.SerializableError'.
If I change the options.SuppressInferBindingSourcesForParameters in ConfigureServices to true, it seems to work. This is strange, since this setting should disable the binding inference or have I misconceived something? Is this a bug in ASP.NET core or am I missing something?
Btw. it also works if I ommit the ApiController attribute, but I guess this is not the real solution to this problem.
Furthermore I would be happy if I don't need to change anything on the client side (adding headers, change content types, ...), because there are a lot of Ajax calls out there and I just want to upgrade the server side components.
I also asked this question on the official ASP.NET Core MVC repo.
One of the members (pranavkm) came back with an answer, which I will just quote here:
ApiController is designed for REST-client specific scenarios and isn't designed towards browser based (form-urlencoded) requests. FromBody assumes JSON \ XML request bodies and it'll attempt to serialize it, which is not what you want with form url encoded content. Using a vanilla (non-ApiController) would be the way to go here.
So for now I will omit the [ApiController] attribute.
In a later step I may change the client calls to use a JSON body, so I can readd the attribute.

`SwaggerRequestExample` is being ignored

As I was adding swagger to my API, I wanted to get default values and response examples. I added the NuGet packages and tried to follow this tutorial. The SwaggerResponseExample attribute works properly but the SwaggerRequestExample seems to be simply ignored.
With my action defined as follow
[SwaggerRequestExample(typeof(int), typeof(PersonExamples.Request))]
[SwaggerResponseExample(200, typeof(PersonExamples.Response))]
/* more attribute & stuff */
public IActionResult Get(int id) { /* blabla */ }
The PersonExamples class being defined as follow (non-revelant code removed)
public class PersonExamples
{
public class Request : IExamplesProvider
{
public object GetExamples() { return _persons.List().First().Id; }
}
public class Response : IExamplesProvider
{
public object GetExamples() { return _persons.List().First(); }
}
}
Here is also the relevant part of the Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddSwaggerGen(conf =>
{
conf.SwaggerDoc(_documentationPrefix, new Info
{
Title = "Global",
Version = "v0",
});
conf.OperationFilter<ExamplesOperationFilter>();
var filePath = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, "Global.xml");
conf.IncludeXmlComments(filePath);
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseSwagger(opt =>
{
opt.RouteTemplate = "{documentName}/swagger.json";
});
if (env.IsDevelopment())
{
app.UseSwaggerUI(conf =>
{
conf.SwaggerEndpoint($"/{_documentationPrefix}/swagger.json", "DataService API");
conf.RoutePrefix = "doc/swagger";
});
}
}
When I run my project and go to the swagger.json page I notice that the response example is properly written, but the request example is nowhere to be found. After further debugging, I notice that a breakpoint placed in PersonExamples.Response.GetExamples will be hit when the page is called, but one placed in the PersonExamples.Request.GetExamples method won't. So i believe that the SwaggerRequestExample attribute never calls the method and may not even be called itself.
Did I improperly used the tag ? Why is it never called ?
I know this question is quite old, but Swagger Examples don't support GET request parameters (query parameters). It only works when the parameters are in the request body (eg: POST requests)

Simplifying configuration of WCF4 RESTful service routes

The default Global.asax.cs file from the "WCF REST Template 40(CS)" project template and every tutorial I've seen online include a variation of the following method:
private void RegisterRoutes()
{
// Edit the base address of Service1 by replacing the "Service1" string below
RouteTable.Routes.Add(new ServiceRoute("Service1", new WebServiceHostFactory(), typeof(Service1)));
}
Managing the service routing in this way seems needlessly cumbersome when the WebApplication itself should be able to discover which services should be available and apply routes based on convention or metadata.
QUESTIONS
Is there a built-in way beyond the default to define the service routes (either configurable in the web.config, or compiled onto the service itself)?
Do others that use this template always follow the model provided or has someone else come up with a better approach?
Proposed Solution
Migrated my proposed solution to an answer
I guess I have to assume that silence is acceptance. Here is my solution (originally from my question):
Assuming there is nothing better built in or otherwise available (because I didn't find anything), my attempt at doing this involves defining an attribute:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class ServiceRouteAttribute : Attribute
{
public string RoutePrefix { get; set; }
public Type ServiceFactoryType { get; set; }
public ServiceHostFactoryBase ServiceFactory
{
get
{
if (ServiceFactoryType == null || !ServiceFactoryType.IsRelated(typeof(ServiceHostFactoryBase)))
return null;
return Activator.CreateInstance(ServiceFactoryType) as ServiceHostFactoryBase;
}
}
public ServiceRouteAttribute() : this(string.empty) { }
public ServiceRouteAttribute(string routePrefix) : this(routePrefix, typeof(WebServiceHostFactory)) { }
public ServiceRouteAttribute(string routePrefix, Type serviceFactoryType)
{
RoutePrefix = routePrefix;
ServiceFactoryType = serviceFactoryType;
}
}
which is used to decorate each service contract that should be exposed, and changing the default RegisterRoutes to:
private void RegisterRoutes()
{
// `TypeHelper.GetTypes().FilterTypes<T>` will find all of the types in the
// current AppDomain that:
// - Implement T if T is an interface
// - Are decorated with T if T is an attribute
// - Are children of T if T is anything else
foreach (var type in TypeHelper.GetTypes()
.FilterTypes<ServiceRouteAttribute>())
{
// routeAttrs should never be null or empty because only types decorated
// with `ServiceRouteAttribute` should ever get here.
// `GetAttribute<T>` is my extension method for `MemberInfo` which returns all
// decorations of `type` that are T or children of T
var routeAttrs = type.GetAttributes<ServiceRouteAttribute>();
foreach (var routeAttr in routeAttrs)
{
// Some dupe and error checking
var routePrefix = routeAttr.RoutePrefix;
if (string.IsNullOrEmpty(routePrefix))
routePrefix = type.Name;
RouteTable.Routes.Add(new ServiceRoute(routePrefix,
routeAttr.ServiceFactory,
type));
}
}
}
This seems to work and isn't too intrusive because it only happens at Application_Start, but I'm new to building RESTful web services with WCF4 so I don't know what sort of problems it could cause.
If anyone comes up with a more elegant way of solving this, I'd gladly consider any alternative.

Categories

Resources