How to validate requests with FluentValdation instead of data annotations? - c#

I created a new .Net 5 Web API with the following setup
public class MyController : ControllerBase
{
[HttpGet("{Title}")]
public ActionResult<string> Get([FromRoute] RouteModel routeModel)
{
return Ok(routeModel);
}
}
public class RouteModel
{
[MinLength(3)]
public string Title { get; set; }
}
The request validation works fine. Since I'm using FluentValidation I installed the package
FluentValidation.AspNetCore v10.3.0
and updated the Startup.ConfigureServices method to
public void ConfigureServices(IServiceCollection services)
{
services.AddValidatorsFromAssemblies(AppDomain.CurrentDomain.GetAssemblies());
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication", Version = "v1" });
});
}
Instead of using the data annotations I removed the data annotations from RouteModel.Title and created this validator
public class RouteModelValidator : AbstractValidator<RouteModel>
{
public RouteModelValidator()
{
RuleFor(model => model.Title).MinimumLength(3);
}
}
I would expect a ValidationException and this would result into a 500 status code if the title has a length less than 3. But it seems there is no validation trigger anymore, the request always passes.
Does someone know what's missing?

Register FluentValidation as
services
.AddFluentValidation(x => x.RegisterValidatorsFromAssemblies(AppDomain.CurrentDomain.GetAssemblies()));
Mark your Controller with ApiController attribute
[ApiController]
public class MyController : ControllerBase
Otherwise, you should manually validate ModelState, something like:
[HttpGet("{Title}")]
public ActionResult<string> Get([FromRoute] RouteModel routeModel)
{
if (!ModelState.IsValid)
{
return BadRequest("bad");
}
return Ok(routeModel);
}
Some info:
It depends on the type of controller - if you're using the [ApiController] attribute, then ASP.NET will generate a Bad Request result automatically. If you don't use the ApiController attribute (ie for non-api controllers, such as where you return a view), then you're expected to handle the ModelState.IsValid check manually (see the asp.net doc link above). But again, this is a convention of ASP.NET, it isn't a feature of FluentValidation.

Related

Can I use Fluent Validation with Dependency Injection without having the automatic validation?

I'm using "FluentValidation.DependencyInjectionExtensions" library (Version="11.4.0") for a .Net Core 6 project.
I have my validator registered in my program file:
services.AddScoped<IValidator<SomeModel>, SomeValidator>();
My code is something like this:
[ApiController]
[Route("[controller]")]
public class SomeController : ControllerBase
{
private readonly IValidator<SomeModel> _someValidator;
public SomeController(IValidator<SomeModel> validator)
{
_someValidator = validator ?? throw new ArgumentNullException(nameof(validator));
}
[HttpPost]
public async Task<IActionResult> SetSome([FromBody] SomeModel model)
{
//The SomeValidator is triggered before this, while being injected into the constructure.
ValidationResult validationResult = await _someValidator.ValidateAsync(model);
}
}
public class SomeModel
{
public string Name { get; private set; }
}
public class SomeValidator : AbstractValidator<SomeModel>
{
public SomeValidator(/* Need to use DI here */)
{
RuleFor(x => x.Name).NotEmpty();
}
}
How can I disable the automatic validation?
I was looking for a solution where i didn't have to use FluentValidation.AspNetCore because supposedly is going to be deprecated. The only way I was able to disable the automatic validation was to remove the validator injection and do something like this:
ValidationResult validationResult = await new SomeValidator.ValidateAsync(model);
But has mentioned earlier I wanted to use DI inside my validator without the need to pass the parameters.

Using IResult as return value for "classic" api instead of Minimal API

I have multiple services that return IResult and donĀ“t want to use minimal API structure, but I want to use the same return type IResult, but when I use that in the controller it always returns Ok:
Minimal API example that returns 400 Bad request:
app.MapPut("test", async () =>
{
return Results.BadRequest("hello");
});
The classic controller that returns 200 Ok:
[ApiController]
public class TestController : ControllerBase
[HttpPut("test")]
public async Task<IResult> Test()
{
return Results.BadRequest();
}
Is there an easy way to fix this or do I need a mapping function?
I am using .NET 6.
Technically you can call IResult.ExecuteAsync on the ControllerBase's context:
public class SomeController : ControllerBase
{
public async Task SomeAction()
{
IResult result = Results.BadRequest(); // dummy result, use one from the service
await result.ExecuteAsync(HttpContext);
}
}
But in general it is better to follow standard patterns and in this case make your service return some custom app-specific response which is "platform-agnostic" i.e. it should not depend on web framework used and represent some business/domain related entity/data. Potentially you can look into some library which can represent "result" abstraction. For example FluentResults or go a bit more functional with some kind of Either (many options here CSharpFunctionalExtensions, language-ext, Optional, etc.).
This is supported in .NET 7 now.
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
public class Home : ControllerBase
{
[HttpGet("/")]
public IResult Get() => Results.Ok("Okies");
}
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
[HttpPut]
public IActionResult Test()
{
return BadRequest("...from Test...");
}
}
The URL to hit it is /test

.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.

Getting OData Count in ASP.NET Core WebAPI

Using the sample code from Hassan Habib's Supercharging ASP.NET Core API with OData blog post, I am able to get the record count using an OData query of $count=true:
What needs to be configured to get the response object to be wrapped in an OData context so that the #odata.count property will show?
In my own ASP.NET Core web API project, I cannot get the simple $count parameter to work and I have no idea why.
With Hassan's sample code, the response JSON is wrapped in an OData context and the payload (an IEnumerable<Student> object) is in the value property of the JSON response. In my project, the OData context wrapper does not exist; my code never returns OData context, it only returns the payload object of type IEnumerable<T>:
I've also noticed that the Content-Type in the response header is application/json; odata.metadata=minimal; odata.streaming=true; charset=utf-8 in the sample project, where as it is simply application/json; charset=utf-8 in my project. I don't see any setting that controls this in either project, so I'm assuming the Microsoft.AspNetCore.Odata NuGet package is magically changing the response when it's configured properly.
My project is also using .NET Core 2.2 (Upgraded from 2.1), all the same versions of NuGet packages as Hassan's sample projects, and all the same settings in the StartUp.cs class... although my StartUp.cs is way more complicated (hence the reason I'm not posting it's content here.)
I could reproduce your issue when i use [Route("api/[controller]")]and [ApiController] with the startup.cs like below:
app.UseMvc(routeBuilder =>
{
routeBuilder.Expand().Select().Count().OrderBy().Filter();
routeBuilder.EnableDependencyInjection();
});
To fix it,be sure you have built a private method to do a handshake between your existing data models (OData model in this case) and EDM.
Here is a simple demo:
1.Controller(comment on Route attribute and ApiController attribute):
//[Route("api/[controller]")]
//[ApiController]
public class StudentsController : ControllerBase
{
private readonly WSDbContext _context;
public StudentsController(WSDbContext context)
{
_context = context;
}
// GET: api/Students
[HttpGet]
[EnableQuery()]
public IEnumerable<Student> Get()
{
return _context.Students;
}
}
//[Route("api/[controller]")]
//[ApiController]
public class SchoolsController : ControllerBase
{
private readonly WSDbContext _context;
public SchoolsController(WSDbContext context)
{
_context = context;
}
// GET: api/Schools
[HttpGet]
[EnableQuery()]
public IEnumerable<School> Get()
{
return _context.Schools;
}
2.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)
{
services.AddMvcCore(action => action.EnableEndpointRouting = false);
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
var connection = #"Server=(localdb)\mssqllocaldb;Database=WSDB;Trusted_Connection=True;ConnectRetryCount=0";
services.AddDbContext<WSDbContext>(options => options.UseSqlServer(connection));
services.AddOData();
}
// 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.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc(routeBuilder =>
{
routeBuilder.Expand().Select().Count().OrderBy().Filter();
routeBuilder.MapODataServiceRoute("api", "api", GetEdmModel());
});
}
private static IEdmModel GetEdmModel()
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Student>("Students");
builder.EntitySet<Student>("Schools");
return builder.GetEdmModel();
}
}
Just been battling this.
I found that if I request my controller at /api/Things that most of the OData options work but $count doesn't.
However, $count does work if I request the same method via /odata/Things.
In my case I wanted to extend existing Api methods with [EnableQuery] but have it include the count metadata.
I ended up extending the EnableQuery attribute to return a different reponse, it worked perfectly.
public class EnableQueryWithMetadataAttribute : EnableQueryAttribute
{
public override void OnActionExecuted(ActionExecutedContext actionExecutedContext)
{
base.OnActionExecuted(actionExecutedContext);
if (actionExecutedContext.Result is ObjectResult obj && obj.Value is IQueryable qry)
{
obj.Value = new ODataResponse
{
Count = actionExecutedContext.HttpContext.Request.ODataFeature().TotalCount,
Value = qry
};
}
}
public class ODataResponse
{
[JsonPropertyName("#odata.count")]
public long? Count { get; set; }
[JsonPropertyName("value")]
public IQueryable Value { get; set; }
}
}
You can just set an empty preffix route when you map OData, and you will receive OData with your request your endpoint.
routeBuilder.MapODataServiceRoute("ODataEdmModel", "", GetEdmModel());
In my case I've created a special action named $count that users the OData Filter query my collection (EF Database) and return the Count;
[HttpGet("$count")]
public async Task<int> Count(ODataQueryOptions<MyBookEntity> odataQueryOptions)
{
var queryable = this.dataContext.MyBooks;
return await odataQueryOptions.Filter
.ApplyTo(queryable, new ODataQuerySettings())
.Cast<MyBookEntity>()
.CountAsync();
}

I've added breeze to my webapi but it still not adding the custom $id and $type properties

I'm trying to add Breeze to my .Net Core 2.2 webapi and I can't figure out what I'm missing. To troubleshoot I've created a very simple webapi that returns 1 item. This works but breeze isn't adding it's custom properties to my entities.
I've added [BreezeQueryFilter] to my controller but the $id and $type properties are not being added to my entities.
I've create a simple repository with what I have so far.
https://github.com/wstow/SimpleBreeze
Thanks
My Controller
[Route("api/[controller]/[action]")]
[BreezeQueryFilter]
public class OrderController : Controller
{
private OrderContext _context;
private OrderManager PersistenceManager;
public OrderController(OrderContext context)
{
this._context = context;
PersistenceManager = new OrderManager(context);
}
[HttpGet]
public IActionResult Metadata()
{
return Ok(PersistenceManager.Metadata());
}
[HttpGet]
public IQueryable<ReqStatus> Status()
{
return PersistenceManager.Context.ReqStatus;
}
}
My Manager
public class OrderManager : EFPersistenceManager<OrderContext>
{
public OrderManager(OrderContext orderContext) : base(orderContext) { }
}
My Context
public class OrderContext : DbContext
{
public OrderContext()
{
//Configuration.ProxyCreationEnabled = false;
// Configuration.LazyLoadingEnabled = false;
}
public OrderContext(DbContextOptions<OrderContext> options)
: base(options)
{ }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{ }
public DbSet<ReqStatus> ReqStatus { get; set; }
}
The problem is in the JSON serialization settings. Newtonsoft.Json is highly configurable, and you need to use the right settings to communicate properly with the Breeze client.
To make this easy, Breeze has a configuration function to change the settings to good defaults. You call it from your Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
var mvcBuilder = services.AddMvc();
mvcBuilder.AddJsonOptions(opt => {
var ss = JsonSerializationFns.UpdateWithDefaults(opt.SerializerSettings);
});
mvcBuilder.AddMvcOptions(o => { o.Filters.Add(new GlobalExceptionFilter()); });
...
The documentation is lacking, but you can see what JsonSerializationFns does by looking in the Breeze code.
The last line adds an exception filter that wraps server-side validation errors so that the Breeze client can handle them. You can see what it does here.

Categories

Resources