Wrapping results of ASP.NET Core WebAPI methods using IResultFilter - asp.net-core

I have implemented a result filter like this:
public class ResultWrapperFilter : IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
if (!(context.ActionDescriptor is ControllerActionDescriptor))
{
return;
}
var objectResult = context.Result as ObjectResult;
if (objectResult == null)
{
return;
}
if (!(objectResult.Value is WrappedResponseBase))
{
objectResult.Value = new WrappedResponse(objectResult.Value);
}
}
public void OnResultExecuted(ResultExecutedContext context)
{
}
}
The filter is used by configuring MvcOptions through ConfigureServices(IServiceCollection services) like this:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<MvcOptions>(
options => { options.Filters.AddService<ResultWrapperFilter>(); });
services.AddMvc();
// ... the rest is omitted for readability
}
The problem I'm experiencing is this filter is causing InvalidCastException: Unable to cast object of type 'WrappedResponse' to type 'System.String' (the method in question has string as the return value type).
Am I even allowed to do this using IResultFilter?
NOTE: I am aware of the possibility of using middleware to accomplish the response wrapping. I don't want to use the middleware to accomplish this because the middleware doesn't have access to context.Result as ObjectResult. Deserializing from the response stream, wrapping and serializing again seems so unnecessary.

An answer just came to me.
When setting objectResult.Value, objectResult.DeclaredType also needs to be set.
So in this case:
if (!(objectResult.Value is WrappedResponseBase))
{
objectResult.Value = new WrappedResponse(objectResult.Value);
objectResult.DeclaredType = typeof(WrappedResponse);
}

Related

Swashbuckle override response type

I have a generic Result<T> response type in my controllers, e.g.
public Result<T> GetSomething()
{
...
}
I also have a custom asp.net core filter that returns a Json representation of T
To have swashbuckle generate correct documentation, I have to decorate every method with:
[Produces(typeof(T))]
As this is cumbersome, easily forgotten and error prone, I was looking for a way to automate this.
Now in Swashbuckle you have a MapType, but I can't get a hold of the T in those methods:
services.AddSwaggerGen(c =>
{
...
c.MapType(typeof(Result<>), () => /*can't get T here*/);
};
I was looking at the IOperationFilter but I can't find a way to override the result type in there.
Then there are ISchemaFilter
public class ResultSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (!context.Type.IsGenericType || !context.Type.GetGenericTypeDefinition().IsAssignableFrom(typeof(Result<>)))
{
return;
}
var returnType = context.Type.GetGenericArguments()[0];
//How do I override the schema here ?
var newSchema = context.SchemaGenerator.GenerateSchema(returnType, context.SchemaRepository);
}
}
IOperationFilter is the correct choice. Here is an example that changes the response type for OData endpoints.
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
//EnableQueryAttribute refers to an OData endpoint.
if (context.ApiDescription.ActionDescriptor.EndpointMetadata.Any(em => em is EnableQueryAttribute))
{
//Fixing the swagger response for Controller style endpoints
if (context.ApiDescription.ActionDescriptor is ControllerActionDescriptor cad)
{
//If the return type is IQueryable<T>, use ODataResponseValue<T> as the Swagger response type.
var returnType = cad.MethodInfo.ReturnType;
if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(IQueryable<>))
{
var actualType = returnType.GetGenericArguments()[0];
var responseType = typeof(ODataResponseValue<>).MakeGenericType(actualType);
var schema = context.SchemaGenerator.GenerateSchema(responseType, context.SchemaRepository);
foreach (var item in operation.Responses["200"].Content)
item.Value.Schema = schema;
}
}
}
}
As you can see here, I'm looping through all of the items in operation.Responses["200"].Content, replacing their schema one by one using the GenerateSchema method that you found.

Autofac: ITenantIdentificationStrategy with RouteValues

I'm having issues making multitenancy work. I've tried to follow the sample here and can't see what my implementation is doing differently.
The tenants are identified by a routing parameter in the address field. This seems to work without issues (calling TryIdentifyTenant returns the correct one). I am using ASP.NET Core 3.1, together with Autofac.AspNetCore-Multitenant v3.0.1 and Autofac.Extensions.DependencyInjection v6.0.0.
I have made a simplification of the code (which is tested and still doesn't work). Two tenants are configured, "terminal1" and "terminal2". The output should differ depending on the tenant. However, it always returns the base implementation. In the example below, inputing "https://localhost/app/terminal1" returns "base : terminal1" and "https://localhost/app/terminal2" returns "base : terminal2". It should return "userhandler1 : terminal1" and "userhandler2 : terminal2".
HomeController:
public class HomeController : Controller
{
private readonly IUserHandler userHandler;
private readonly TerminalResolverStrategy terminalResolverStrategy;
public HomeController(IUserHandler userHandler, TerminalResolverStrategy terminalResolverStrategy)
{
this.userHandler = userHandler;
this.terminalResolverStrategy = terminalResolverStrategy;
}
public string Index()
{
terminalResolverStrategy.TryIdentifyTenant(out object tenant);
return userHandler.ControllingVncUser + " : " + (string)tenant;
}
}
UserHandler:
public interface IUserHandler
{
public string ControllingVncUser { get; set; }
}
public class UserHandler : IUserHandler
{
public UserHandler()
{
ControllingVncUser = "base";
}
public string ControllingVncUser { get; set; }
}
public class UserHandler1 : IUserHandler
{
public UserHandler1()
{
ControllingVncUser = "userhandler1";
}
public string ControllingVncUser { get; set; }
}
public class UserHandler2 : IUserHandler
{
public UserHandler2()
{
ControllingVncUser = "userhandler2";
}
public string ControllingVncUser { get; set; }
}
Startup:
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddControllersWithViews();
services.AddAutofacMultitenantRequestServices();
}
public void ConfigureContainer(ContainerBuilder builder)
{
builder.RegisterType<TerminalResolverStrategy>();
builder.RegisterType<UserHandler>().As<IUserHandler>();
}
public static MultitenantContainer ConfigureMultitenantContainer(IContainer container)
{
var strategy = new TerminalResolverStrategy(
container.Resolve<IOptions<TerminalAppSettings>>(),
container.Resolve<IHttpContextAccessor>());
var mtc = new MultitenantContainer(strategy, container);
mtc.ConfigureTenant("terminal1", b => b.RegisterType<UserHandler1>().As<IUserHandler>());
mtc.ConfigureTenant("terminal2", b => b.RegisterType<UserHandler2>().As<IUserHandler>());
return mtc;
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
loggerFactory.AddLog4Net();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{terminal}/{controller=Home}/{action=Index}/{id?}");
});
}
}
Program:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacMultitenantServiceProviderFactory(Startup.ConfigureMultitenantContainer))
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
ITenantIdentificationStrategy:
public class TerminalResolverStrategy : ITenantIdentificationStrategy
{
public IHttpContextAccessor Accessor { get; private set; }
private readonly TerminalAppSettings settings;
public TerminalResolverStrategy(
IOptions<TerminalAppSettings> options,
IHttpContextAccessor httpContextAccessor
)
{
Accessor = httpContextAccessor;
settings = options.Value;
}
public bool TryIdentifyTenant(out object terminal)
{
HttpContext httpCtx = Accessor.HttpContext;//
terminal = null;
try
{
if (httpCtx != null &&
httpCtx.Request != null &&
httpCtx.Request.RouteValues != null &&
httpCtx.Request.RouteValues.ContainsKey("terminal"))
{
string requestedTerminal = httpCtx.Request.RouteValues["terminal"].ToString();
bool terminalExists = settings.Terminals.ContainsKey(requestedTerminal);
if (terminalExists)
{
terminal = requestedTerminal;
}
}
}
catch (Exception) {}
return terminal != null;
}
}
}
What am i doing wrong? Thanks in advance.
"Multitenancy doesn't seem to work at all" is a somewhat ambiguous statement that's hard to address. Unfortunately, I don't personally have the time to download all of your example code and try to repro the whole thing and debug into it and see exactly what's wrong. Perhaps someone else does. However, I can offer some tips as to places I'd look and things I'd try to see what's up.
Tenant ID strategy set up twice. I see in Startup.ConfigureContainer that there's a builder.RegisterType<TerminalResolverStrategy>(); line - that's going to register your strategy type as instance-per-dependency, so every time it's needed it'll be resolved fresh. I also see in Startup.ConfigureMultitenantContainer that you're manually instantiating the strategy that gets used by the multitenant container. There's a non-zero possibility that something is getting messed up there. I would pick one way to get that done - either register the strategy or manually create it - and I'd make sure that thing is a stateless singleton. (It's not registered in the example.)
Route pattern possibly questionable. I see the route pattern you have registered looks like this: {terminal}/{controller=Home}/{action=Index}/{id?}. I also see your URLs look like this: https://localhost/app/terminal1 Have you stepped into your tenant ID strategy to make sure the route parsing mechanism works right? That is, app isn't being picked up as the terminal value? Route parsing/handling can be tricky.
Possibly bad settings. The tenant ID strategy only successfully identifies a tenant if there are options that specify that the specific terminal value exists. I don't see where any of those options are configured, which means in this repo there are no tenants defined. Your strategy won't identify anything without that.
If it was me, I'd probably start with a breakpoint in that tenant ID strategy and see what's getting resolved and what's not. It seems somewhat complex from an outside perspective and that's where I'd begin. If that is working, then I'd probably also look at cleaning up the registrations so the ID strategy isn't registered twice. Finally, I get the impression that this isn't all the code in the app; I'd probably look at making a super minimal reproduction that's about the size you actually have posted here. I'd then focus on making that minimal repro work; then once it works, I'd figure out what the difference is between the repro and my larger app.
Unfortunately, that's about all I can offer you. As I mentioned, with the current environment and my current workload, I won't be able to actually sit down and reproduce the whole thing with your code. I know the integration works because I have production apps using it; and there are a lot of tests (unit and integration) to validate it works; so the part that isn't working is likely in your code somewhere... and those are the places I'd start.
So after having identified the tenant identification as the problem, it seems like RouteValues isn't resolved with the HttpContext until later in the request chain. Thus, no tenant gets resolved. It seems to me like a bug in .NET Core. The problem got bypassed by using the request path instead:
public class TerminalResolverStrategy : ITenantIdentificationStrategy
{
private readonly TerminalAppSettings settings;
private readonly IHttpContextAccessor httpContextAccessor;
public TerminalResolverStrategy(
IOptions<TerminalAppSettings> options,
IHttpContextAccessor httpContextAccessor
)
{
this.httpContextAccessor = httpContextAccessor;
settings = options.Value;
}
public bool TryIdentifyTenant(out object terminal)
{
var httpCtx = httpContextAccessor.HttpContext;
terminal = null;
try
{
if (httpCtx != null)
{
string thisPath = httpCtx.Request.Path.Value;
var allTerminals = settings.Terminals.GetEnumerator();
while (allTerminals.MoveNext())
{
if (thisPath.Contains(allTerminals.Current.Key)) {
terminal = allTerminals.Current.Key;
return true;
}
}
}
}
catch (Exception) { }
return false;
}
}

Generic string router with DB in Asp.net Core

I am creating an internet store. And I want to add short URLs for products, categories and so on.
For example:
store.com/iphone-7-plus
This link should open the page with iPhone 7 plus product.
The logic is:
The server receives an URL
The server try it against existent routes
If there is no any route for this path - the server looks at a DB and try to find a product or category with such title.
Obvious solutions and why are they not applicable:
The first solution is a new route like that:
public class StringRouter : IRouter
{
private readonly IRouter _defaultRouter;
public StringRouter(IRouter defaultRouter)
{
_defaultRouter = defaultRouter;
}
public async Task RouteAsync(RouteContext context)
{
// special loggic
await _defaultRouter.RouteAsync(context);
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return _defaultRouter.GetVirtualPath(context);
}
}
The problem is I can't provide any access to my DB from StringRouter.
The second solution is:
public class MasterController : Controller
{
[Route("{path}")]
public IActionResult Map(string path)
{
// some logic
}
}
The problem is the server receive literally all callings like store.com/robots.txt
So the question is still open - could you please advise me some applicable solution?
For accessing DbContext, you could try :
using Microsoft.Extensions.DependencyInjection;
public async Task RouteAsync(RouteContext context)
{
var dbContext = context.HttpContext.RequestServices.GetRequiredService<RouterProContext>();
var products = dbContext.Product.ToList();
await _defaultRouter.RouteAsync(context);
}
You also could try Middleware to check whether the reuqest is not exist, and then return the expected response.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider)
{
app.Use(async (context,next) => {
await next.Invoke();
// add your own business logic to check this if statement
if (context.Response.StatusCode == 404)
{
var db = context.RequestServices.GetRequiredService<RouterProContext>();
var users = db.Users.ToList();
await context.Response.WriteAsync("Request From Middleware");
}
});
//your rest code
}

FluentValidation with IActionFilter in Asp.net Core 2.1 [duplicate]

I have a logic to apply in case the request received is a BadRequest, to do this I have created a filter:
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
// Apply logic
}
}
}
In Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options => { options.Filters.Add<ValidateModelAttribute>(); });
}
Controller:
[Route("api/[controller]")]
[ApiController]
public class VerifyController : ControllerBase
{
[Route("test")]
[HttpPost]
[ValidateModel]
public ActionResult<Guid> validationTest(PersonalInfo personalInfo)
{
return null;
}
}
Model:
public class PersonalInfo
{
public string FirstName { get; set; }
[RegularExpression("\\d{4}-?\\d{2}-?\\d{2}", ErrorMessage = "Date must be properly formatted according to ISO 8601")]
public string BirthDate { get; set; }
}
The thing is when I put a break point on the line:
if (!context.ModelState.IsValid)
execution reaches this line only if the request I send is valid. Why it is not passing the filter if I send a bad request?
The [ApiController] attribute that you've applied to your controller adds Automatic HTTP 400 Responses to the MVC pipeline, which means that your custom filter and action aren't executed if ModelState is invalid.
I see a few options for affecting how this works:
Remove the [ApiController] attribute
Although you can just remove the [ApiController] attribute, this would also cause the loss of some of the other features it provides, such as Binding source parameter inference.
Disable only the Automatic HTTP 400 Responses
Here's an example from the docs that shows how to disable just this feature:
services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
// ...
options.SuppressModelStateInvalidFilter = true;
// ...
}
This code goes inside of your Startup's ConfigureServices method.
Customise the automatic response that gets generated
If you just want to provide a custom response to the caller, you can customise what gets returned. I've already described how this works in another answer, here.
An example of intersection for logging is describe in Log automatic 400 responses
Add configuration in Startup.ConfigureServices.
services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
// To preserve the default behavior, capture the original delegate to call later.
var builtInFactory = options.InvalidModelStateResponseFactory;
options.InvalidModelStateResponseFactory = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Startup>>();
// Perform logging here.
//E.g. logger.LogError($”{context.ModelState}”);
logger.LogWarning(context.ModelState.ModelStateErrorsToString());
// Invoke the default behavior, which produces a ValidationProblemDetails response.
// To produce a custom response, return a different implementation of IActionResult instead.
return builtInFactory(context);
};
});
public static String ModelStateErrorsToString(this ModelStateDictionary modelState)
{
IEnumerable<ModelError> allErrors = modelState.Values.SelectMany(v => v.Errors);
StringBuilder sb = new StringBuilder();
foreach (ModelError error in allErrors)
{
sb.AppendLine($"error {error.ErrorMessage} {error.Exception}");
}
return sb.ToString();
}
As the attribute filter in the life cycle of the .Net Core you can’t handle it. The filter layer with ModelState will run after the model binding.
You can handle it with .Net Core middleware as the following https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-2.1&tabs=aspnetcore2x
If you want to SuppressModelStateInvalidFilter on individual action, consider to use custom attribute suggested on https://learn.microsoft.com/en-us/answers/questions/297568/how-to-suppress-suppressmodelstateinvalidfilter-at.html. (And similar answer https://github.com/aspnet/Mvc/issues/8575)
public class SuppressModelStateInvalidFilterAttribute : Attribute, IActionModelConvention
{
private const string FilterTypeName = "ModelStateInvalidFilterFactory";
public void Apply(ActionModel action)
{
for (var i = 0; i < action.Filters.Count; i++)
{
//if (action.Filters[i] is ModelStateInvalidFilter)
if (action.Filters[i].GetType().Name == FilterTypeName)
{
action.Filters.RemoveAt(i);
break;
}
}
}
}
Example of use
[ApiController]
public class PersonController
{
[SuppressModelStateInvalidFilter]
public ActionResult<Person> Get() => new Person();
}

Custom Model Binder Provider always null .net core

I'm having a problem trying to get custom model binders to work as a query parameter like I have gotten to work previously in .net framework 4.7.
To ensure this wasn't a scenario where my object was too complex, I reduced the model to a simple string but even then I cannot get this to work.
I have a simple model I would like to be binded from query parameters.
public class SearchModel {
public string SearchTerms { get; set; }
}
And I have configured the ModelBinder and ModelBinderProvider as shown here like so.
public class TestModelBinder : IModelBinder {
public Task BindModelAsync(ModelBindingContext bindingContext) {
if (bindingContext.ModelType != typeof(SearchModel)) {
throw new ArgumentException($"Invalid binding context supplied {bindingContext.ModelType}");
}
var model = (SearchModel)bindingContext.Model ?? new SearchModel();
var properties = model.GetType().GetProperties();
foreach(var p in properties) {
var value = this.GetValue(bindingContext, p.Name);
p.SetValue(model, Convert.ChangeType(value, p.PropertyType), null);
}
return Task.CompletedTask;
}
protected string GetValue(ModelBindingContext context, string key) {
var result = context.ValueProvider.GetValue(key);
return result.FirstValue;
}
}
public class TestModelBinderProvider : IModelBinderProvider {
public IModelBinder GetBinder(ModelBinderProviderContext context) {
if (context == null) {
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(SearchModel)) {
var returnType = new BinderTypeModelBinder(typeof(TestModelBinder));
return returnType;
}
return null;
}
}
As stated in the last step in Microsoft documentation I updated my ConfigureServices method in Startup.cs to include the BinderProvider.
services.AddMvc(options => {
options.ModelBinderProviders.Insert(0, new TestModelBinderProvider());
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
But when I call my Search endpoint with a url such as "https://localhost:44387/api/testbinding?searchTerms=newSearch" I am always seeing a return of "request == null True" even though I see it properly hit the custom binding and bind correctly if I step through debugging, can anyone please point me in the right direction as to what I am doing wrong?
[Route("api/[controller]")]
[ApiController]
public class TestBindingController : ControllerBase {
[HttpGet()]
public IActionResult GetResult([FromQuery] SearchModel request) {
return Ok($"request == null {request == null}");
}
}
I think what you're missing if the statement that sets the result of the model binding operation, as you can see in the AuthorEntityBinder code sample in this section of the docs:
bindingContext.Result = ModelBindingResult.Success(model);
Your implementation of the model binder does create an instance of SearchModel, but doesn't feed it back to the model binding context.
As a separate note, I don't think you need to add a custom model binder is the query string segments match the properties names of the model you're trying to bind.