Getting ActionContext of an action from another - asp.net-core

Can I get an ActionContext or ActionDescriptor or something that can describe a specific action based on a route name ?
Having the following controller.
public class Ctrl : ControllerBase
{
[HttpGet]
public ActionResult Get() { ... }
[HttpGet("{id}", Name = "GetUser")]
public ActionResult Get(int id) { ... }
}
What I want to do is when "Get" is invoked, to be able to have access to "GetUser" metadata like verb, route parameters , etc
Something like
ActionContext/Description/Metadata info = somerService.Get(routeName : "GetUser")
or
ActionContext/Description/Metadata info = somerService["GetUser"];
something in this idea.

There is a nuget package, AspNetCore.RouteAnalyzer, that may provide what you want. It exposes strings for the HTTP verb, mvc area, path and invocation.
Internally it uses ActionDescriptorCollectionProvider to get at that information:
List<RouteInformation> ret = new List<RouteInformation>();
var routes = m_actionDescriptorCollectionProvider.ActionDescriptors.Items;
foreach (ActionDescriptor _e in routes)
{
RouteInformation info = new RouteInformation();
// Area
if (_e.RouteValues.ContainsKey("area"))
{
info.Area = _e.RouteValues["area"];
}
// Path and Invocation of Razor Pages
if (_e is PageActionDescriptor)
{
var e = _e as PageActionDescriptor;
info.Path = e.ViewEnginePath;
info.Invocation = e.RelativePath;
}
// Path of Route Attribute
if (_e.AttributeRouteInfo != null)
{
var e = _e;
info.Path = $"/{e.AttributeRouteInfo.Template}";
}
// Path and Invocation of Controller/Action
if (_e is ControllerActionDescriptor)
{
var e = _e as ControllerActionDescriptor;
if (info.Path == "")
{
info.Path = $"/{e.ControllerName}/{e.ActionName}";
}
info.Invocation = $"{e.ControllerName}Controller.{e.ActionName}";
}
// Extract HTTP Verb
if (_e.ActionConstraints != null && _e.ActionConstraints.Select(t => t.GetType()).Contains(typeof(HttpMethodActionConstraint)))
{
HttpMethodActionConstraint httpMethodAction =
_e.ActionConstraints.FirstOrDefault(a => a.GetType() == typeof(HttpMethodActionConstraint)) as HttpMethodActionConstraint;
if(httpMethodAction != null)
{
info.HttpMethod = string.Join(",", httpMethodAction.HttpMethods);
}
}
// Special controller path
if (info.Path == "/RouteAnalyzer_Main/ShowAllRoutes")
{
info.Path = RouteAnalyzerRouteBuilderExtensions.RouteAnalyzerUrlPath;
}
// Additional information of invocation
info.Invocation += $" ({_e.DisplayName})";
// Generating List
ret.Add(info);
}
// Result
return ret;
}

Try this:
// Initialize via constructor dependency injection
private readonly IActionDescriptorCollectionProvider _provider;
var info = _provider.ActionDescriptors.Items.Where(x => x.AttributeRouteInfo.Name == "GetUser");

Related

What is the best possible way to send custom error responses in .net core web api

I'm making a .net Core WebApi using .Net Core 2.2. The API is ready but the failure message and response is where I'm stuck at.
Right now, I'm getting respose like below
json
{
"empId":1999,
"empName":"Conroy, Deborah",
"enrollmentStatus":true,
"primaryFingerprintScore":65,
"secondaryFingerprintScore":60,
"primaryFingerprint":null,
"secondaryFingerprint":null,
"primaryFingerprintType":null,
"secondaryFingerprintType":null}
}
I created a json formatter class and wrote the below code
public class SuperJsonOutputFormatter : JsonOutputFormatter
{
public SuperJsonOutputFormatter(
JsonSerializerSettings serializerSettings,
ArrayPool<char> charPool) : base(serializerSettings, charPool)
{
}
public override async Task WriteResponseBodyAsync(
OutputFormatterWriteContext context,
Encoding selectedEncoding)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
if (selectedEncoding == null)
throw new ArgumentNullException(nameof(selectedEncoding));
using (TextWriter writer =
context.WriterFactory(
context.HttpContext.Response.Body,
selectedEncoding))
{
var rewrittenValue = new
{
resultCode = context.HttpContext.Response.StatusCode,
resultMessage =
((HttpStatusCode)context.HttpContext.Response.StatusCode)
.ToString(),
result = context.Object
};
this.WriteObject(writer, rewrittenValue);
await writer.FlushAsync();
}
}
I expect all the error codes to be sent as generic error messages like the JSON below.
FOR STATUS OKAY:
{
"status" : True,
"error" : null,
"data" : {
{
"empId":1999,
"empName":"Conroy, Deborah",
"enrollmentStatus":true,
"primaryFingerprintScore":65,
"secondaryFingerprintScore":60,
"primaryFingerprint":null,
"secondaryFingerprint":null,
"primaryFingerprintType":null,
"secondaryFingerprintType":null}
}
}
}
FOR OTHER STATUS LIKE 404, 500, 400, 204
{
"status" : False,
"error" : {
"error code" : 404,
"error description" : Not Found
},
"data" : null
}
I expect all the error codes to be sent as generic error messages like the JSON below
You're almost there. What you need to do is enabling your SuperJsonOutputFormatter.
A Little Change to Your Formatter
Firstly, your formatter didn't return a json with the same schema as you want. So I create a dummy class to hold the information for error code and error description:
public class ErrorDescription{
public ErrorDescription(HttpStatusCode statusCode)
{
this.Code = (int)statusCode;
this.Description = statusCode.ToString();
}
[JsonProperty("error code")]
public int Code {get;set;}
[JsonProperty("error description")]
public string Description {get;set;}
}
And change your WriteResponseBodyAsync() method as below:
...
using (TextWriter writer = context.WriterFactory(context.HttpContext.Response.Body, selectedEncoding)) {
var statusCode = context.HttpContext.Response.StatusCode;
var rewrittenValue = new {
status = IsSucceeded(statusCode),
error = IsSucceeded(statusCode) ? null : new ErrorDescription((HttpStatusCode)statusCode),
data = context.Object,
};
this.WriteObject(writer, rewrittenValue);
await writer.FlushAsync();
}
Here the IsSucceeded(statusCode) is a simple helper method that you can custom as you need:
private bool IsSucceeded(int statusCode){
// I don't think 204 indicates that's an error.
// However, you could comment out it if you like
if(statusCode >= 400 /* || statusCode==204 */ ) { return false; }
return true;
}
Enable your Formatter
Secondly, to enable your custom Formatter, you have two approaches: One way is to register it as an global Formatter, the other way is to enable it for particular Controller or Action. Personally, I believe the 2nd way is better. So I create a Action Filter to enable your formatter.
Here's an implementation of the Filter that enables your custom formatter dynamically:
public class SuperJsonOutputFormatterFilter : IAsyncActionFilter{
private readonly SuperJsonOutputFormatter _formatter;
// inject your SuperJsonOutputFormatter service
public SuperJsonOutputFormatterFilter(SuperJsonOutputFormatter formatter){
this._formatter = formatter;
}
// a helper method that provides an ObjectResult wrapper over the raw object
private ObjectResult WrapObjectResult(ActionExecutedContext context, object obj){
var wrapper = new ObjectResult(obj);
wrapper.Formatters.Add(this._formatter);
context.Result= wrapper;
return wrapper;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
ActionExecutedContext resultContext = await next();
// in case we get a 500
if(resultContext.Exception != null && ! resultContext.ExceptionHandled){
var ewrapper = this.WrapObjectResult(resultContext, new {});
ewrapper.StatusCode = (int) HttpStatusCode.InternalServerError;
resultContext.ExceptionHandled = true;
return;
}
else {
switch(resultContext.Result){
case BadRequestObjectResult b : // 400 with an object
var bwrapper=this.WrapObjectResult(resultContext,b.Value);
bwrapper.StatusCode = b.StatusCode;
break;
case NotFoundObjectResult n : // 404 with an object
var nwrapper=this.WrapObjectResult(resultContext,n.Value);
nwrapper.StatusCode = n.StatusCode;
break;
case ObjectResult o : // plain object
this.WrapObjectResult(resultContext,o.Value);
break;
case JsonResult j : // plain json
this.WrapObjectResult(resultContext,j.Value);
break;
case StatusCodeResult s: // other statusCodeResult(including NotFound,NoContent,...), you might want to custom this case
var swrapper = this.WrapObjectResult(resultContext, new {});
swrapper.StatusCode = s.StatusCode;
break;
}
}
}
}
And don't forget to register your formatter as a service :
services.AddScoped<SuperJsonOutputFormatter>();
Finally, when you want to enable your formatter, just add a [TypeFilter(typeof(SuperJsonOutputFormatterFilter))] annotation for the controller or action.
Demo
Let's create an action method for Test:
[TypeFilter(typeof(SuperJsonOutputFormatterFilter))]
public IActionResult Test(int status)
{
// test json result(200)
if(status == 200){ return Json(new { Id = 1, }); }
// test 400 object result
else if(status == 400){ return BadRequest( new {}); }
// test 404 object result
else if(status == 404){ return NotFound(new { Id = 1, }); }
// test exception
else if(status == 500){ throw new Exception("unexpected exception"); }
// test status code result
else if(status == 204){ return new StatusCodeResult(204); }
// test normal object result(200)
var raw = new ObjectResult(new XModel{
empId=1999,
empName = "Conroy, Deborah",
enrollmentStatus=true,
primaryFingerprintScore=65,
secondaryFingerprintScore=60,
primaryFingerprint = null,
secondaryFingerprint= null,
primaryFingerprintType=null,
secondaryFingerprintType=null
});
return raw;
}
Screenshot:

Razor page routing based on different domains

I'm trying to setup a single ASP.NET Core Razor Web app localized for use on multi domains. I have the localization working, with one different language for each domain. But right now I want to have the .com domain accepting a routing parameter, to make the URL path decide with language to show.
Something like:
www.mysite.pt - no custom routing - www.mysite.pt/PageA works, localized in Portuguese.
www.mysite.com - custom routing - www.mysite.com/us/PageA goes to PageA, localized in en-US. But www.mysite.com/PageA should return a 404, as for this domain every page needs the country parameter.
For MVC this could be achieved by using the MapRoute with a custom IRouteConstraint to filter by domain.
However with Razor pages, I only see the option to go with the conventions and add a class derived from IPageRouteModelConvention.
But I don't see a way on the IPageRouteModelConvention methodology to use a IRouteConstraint.
Is there a way to do this?
Not exactly the best solution... but worked this out:
On ConfigureServices added a custom convention that takes a country parameter only with two country codes US and CA:
options.Conventions.Add(new CountryTemplateRouteModelConvention());
wethe this class being:
public class CountryTemplateRouteModelConvention : IPageRouteModelConvention
{
public void Apply(PageRouteModel model)
{
var selectorCount = model.Selectors.Count;
for (var i = 0; i < selectorCount; i++)
{
var selector = model.Selectors[i];
// selector.AttributeRouteModel.SuppressLinkGeneration = false;
//we are not adding the selector, but replacing the existing one
model.Selectors.Add(new SelectorModel
{
AttributeRouteModel = new AttributeRouteModel
{
Order = -1,
Template = AttributeRouteModel.CombineTemplates(#"{country:length(2):regex(^(us|ca)$)}", selector.AttributeRouteModel.Template),
}
});
}
}
}
Then, before the UseMvc on Configure, I used two types of Rewrite rules:
var options = new RewriteOptions();
options.Add(new CountryBasedOnDomainRewriteRule(domains: GetDomainsWhereCountryComesFromDomain(Configuration)));
options.Add(new CountryBasedOnPathRewriteRule(domains: GetDomainsWhereCountryComesFromPath(Configuration)));
app.UseRewriter(options);
The methods GetDomainsWhereCountryComesFromDomain and GetDomainsWhereCountryComesFromPath just read from the appsettings the domains where I want to have a single language, and the domains where I want the language to be obtained from the URL path.
Now, the two IRule classes:
public class CountryBasedOnPathRewriteRule : IRule
{
private readonly string[] domains;
public CountryBasedOnPathRewriteRule(string[] domains)
{
this.domains = domains;
}
public void ApplyRule(RewriteContext context)
{
string hostname = context.HttpContext.Request.Host.Host.ToLower();
if (!domains.Contains(hostname)) return;
//only traffic that has the country on the path is valid. examples:
// www.mysite.com/ -> www.mysite.com/US/
// www.mysite.com/Cart -> www.mysite.com/US/Cart
var path = context.HttpContext.Request.Path.ToString().ToLower();
/* let's exclude the error page, as method UseExceptionHandler doesn't accept the country parameter */
if (path == "/" || path == "/error")
{
//redirect to language default
var response = context.HttpContext.Response;
response.StatusCode = (int)HttpStatusCode.Moved;
response.Headers[HeaderNames.Location] = "/us/"; //default language/country
context.Result = RuleResult.EndResponse;
}
string pathFirst = path.Split('/')?[1];
if (pathFirst.Length != 2) /* US and CA country parameter is already enforced by the routing */
{
var response = context.HttpContext.Response;
response.StatusCode = (int)HttpStatusCode.NotFound;
context.Result = RuleResult.EndResponse;
}
}
}
public class CountryBasedOnDomainRewriteRule : IRule
{
private readonly string[] domains;
public CountryBasedOnDomainRewriteRule(string[] domains)
{
this.domains = domains;
}
public void ApplyRule(RewriteContext context)
{
string hostname = context.HttpContext.Request.Host.Host.ToLower();
if (!domains.Contains(hostname)) return;
var path = context.HttpContext.Request.Path.ToString().ToLower();
string pathFirst = path.Split('/')?[1];
if (pathFirst.Length == 2) //we are trying to use www.mysite.co.uk/us which is not allowed
{
var response = context.HttpContext.Response;
response.StatusCode = (int)HttpStatusCode.NotFound;
context.Result = RuleResult.EndResponse;
}
}
}
And that's it.

Custom serializer not working in Web API 2 for oData 4 when the URL contains $select

I implemented a custom serializer by inheriting ODataEntityTypeSerializer. The serializer sets the value of "MessageStateName" by getting the name of BayStateEnum from the value of "MessageState".
It works well only except when the URL contains "$select". I debugged the code and found it was executed and entityInstanceContext.EntityInstance had the correct value, but entityInstanceContext.EdmModel, which was of type System.Web.OData.Query.Expressions.SelectExpandBinder.SelectSome, still had an empty "MessageStateName".
public class CustomEntitySerializer : ODataEntityTypeSerializer
{
public CustomEntitySerializer(ODataSerializerProvider serializerProvider)
: base(serializerProvider)
{
}
public override ODataEntry CreateEntry(SelectExpandNode selectExpandNode, EntityInstanceContext entityInstanceContext)
{
if (entityInstanceContext.EntityInstance is SmartLinkInfoModel)
{
var smartLinkInfo = entityInstanceContext.EntityInstance as SmartLinkInfoModel;
if (smartLinkInfo.ModemIMEI != null)
{
smartLinkInfo.ModemIMEIString = "0x" + string.Join(string.Empty, smartLinkInfo.ModemIMEI.Select(b => (b - 48).ToString()));
}
if (smartLinkInfo.SmartLinkHardwareId != null)
{
smartLinkInfo.SmartLinkHardwareIdString = "0x" + string.Join(string.Empty, smartLinkInfo.SmartLinkHardwareId.Select(b => b.ToString()));
}
if (smartLinkInfo.XbeeSourceId != null)
{
smartLinkInfo.XbeeSourceIdString = "0x" + string.Join(string.Empty, smartLinkInfo.XbeeSourceId.Select(b => b.ToString()));
}
}
else if (entityInstanceContext.EntityInstance is BayMessageModel)
{
var bayMessage = entityInstanceContext.EntityInstance as BayMessageModel;
bayMessage.MessageStateName = Enum.GetName(typeof(BayStateEnum), bayMessage.MessageState);
}
return base.CreateEntry(selectExpandNode, entityInstanceContext);
}
}
Your code to change the entityInstanceContext.EntityInstance is right, but it won't change the result of select, you can see
object propertyValue = entityInstanceContext.GetPropertyValue(structuralProperty.Name);
in ODataEntityTypeSerializer 's CreateStructuralProperty method, you should override this method, if the structuralProperty.Name is MessageStateName, then use (entityInstanceContext.EntityInstance as BayMessageModel).MessageStateName

Issue with Web Api Custom Model Binder in MVC4

I am using Mvc4 with WebApi.
I am using Dto objects for the webApi.
I am having enum as below.
public enum Status
{
[FlexinumDefault]
Unknown = -1,
Active = 0,
Inactive = 100,
}
Dto structure is as follows.
[DataContract]
public class abc()
{
[DataMemebr]
[Required]
int Id{get;set;}
[DataMember]
[Required]
Status status{get;set}
}
I have created Custom Model Binder which will validate the enum(status) property in the dto object and return false if the enum value is not passed.
if the status enum property is not passed in the dto object,we should throw exception
public bool BindModel(System.Web.Http.Controllers.HttpActionContext actionContext, System.Web.Http.ModelBinding.ModelBindingContext bindingContext)
{
var input = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (input != null && !string.IsNullOrEmpty(input.AttemptedValue))
{
if (bindingContext.ModelType == typeof(Enum))
{
//var actualValue = null;
var value = input.RawValue;
in the api controller,i have action method like
public void Create([FromUri(BinderType = typeof(EnumCustomModelBinder))]abcdto abc)
{
In global.asax.cs
i have set like
GlobalConfiguration.Configuration.BindParameter(typeof(Enum), new EnumCustomModelBinder());
the issue i am facing is the custombinder
var input = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
,the input value is coming as null.
Please sugggest
I found the solution
This works fine,but the default implementation of model binder is missing.
public bool BindModel(System.Web.Http.Controllers.HttpActionContext actionContext, ModelBindingContext bindingContext)
{
var json = actionContext.Request.Content.ReadAsStringAsync().Result;
if (!string.IsNullOrEmpty(json))
{
var jsonObject = (JObject) Newtonsoft.Json.JsonConvert.DeserializeObject(json);
var jsonPropertyNames = jsonObject.Properties().Select(p => p.Name).ToList();
var requiredProperties = bindingContext.ModelType.GetProperties().Where(p =>p.GetCustomAttributes(typeof(RequiredAttribute),
false).Any()).ToList();
var missingProperties = requiredProperties.Where(bindingProperty => !jsonPropertyNames.Contains(bindingProperty.Name)).ToList();
if (missingProperties.Count > 0)
{
missingProperties.ForEach(
prop =>
{
if (prop.PropertyType.IsEnum)
actionContext.ModelState.AddModelError(prop.Name, prop.Name + " is Required");
});
}
var nullProperties = requiredProperties.Except(missingProperties).ToList();
if (nullProperties.Count > 0)
{
nullProperties.ForEach(p =>
{
var jsonvalue = JObject.Parse(json);
var value = (JValue)jsonvalue[p.Name];
if (value.Value == null)
{
actionContext.ModelState.AddModelError(p.Name, p.Name + " is Required");
}
});
}
}
// Now we can try to eval the object's properties using reflection.
return true;
}

Web API Help pages - customizing Property documentation

I have my web api and I added the web api help pages to auto-generate my documentation. It's working great for methods where my parameters are listed out, but I have a method like this:
public SessionResult PostLogin(CreateSessionCommand request)
And, on my help page, it is only listing the command parameter in the properties section. However, in the sample request section, it lists out all of the properties of my CreateSessionCommand class.
Parameters
Name | Description | Additional information
request | No documentation available. | Define this parameter in the request body.
I would like it instead to list all of the properties in my CreateSessionCommand class. Is there an easy way to do this?
So, I managed to devise a workaround for this problem, in case anyone is interested.
In HelpPageConfigurationExtensions.cs I added the following extension method:
public static void AlterApiDescription(this ApiDescription apiDescription, HttpConfiguration config)
{
var docProvider = config.Services.GetDocumentationProvider();
var addParams = new List<ApiParameterDescription>();
var removeParams = new List<ApiParameterDescription>();
foreach (var param in apiDescription.ParameterDescriptions)
{
var type = param.ParameterDescriptor.ParameterType;
//string is some special case that is not a primitive type
//also, compare by full name because the type returned does not seem to match the types generated by typeof
bool isPrimitive = type.IsPrimitive || String.Compare(type.FullName, typeof(string).FullName) == 0;
if (!isPrimitive)
{
var properties = from p in param.ParameterDescriptor.ParameterType.GetProperties()
let s = p.SetMethod
where s.IsPublic
select p;
foreach (var property in properties)
{
var documentation = docProvider.GetDocumentation(new System.Web.Http.Controllers.ReflectedHttpParameterDescriptor()
{
ActionDescriptor = param.ParameterDescriptor.ActionDescriptor,
ParameterInfo = new CustomParameterInfo(property)
});
addParams.Add(new ApiParameterDescription()
{
Documentation = documentation,
Name = property.Name,
Source = ApiParameterSource.FromBody,
ParameterDescriptor = param.ParameterDescriptor
});
}
//since this is a complex type, select it to be removed from the api description
removeParams.Add(param);
}
}
//add in our new items
foreach (var item in addParams)
{
apiDescription.ParameterDescriptions.Add(item);
}
//remove the complex types
foreach (var item in removeParams)
{
apiDescription.ParameterDescriptions.Remove(item);
}
}
And here is the Parameter info instanced class I use
internal class CustomParameterInfo : ParameterInfo
{
public CustomParameterInfo(PropertyInfo prop)
{
base.NameImpl = prop.Name;
}
}
Then, we call the extension in another method inside the extensions class
public static HelpPageApiModel GetHelpPageApiModel(this HttpConfiguration config, string apiDescriptionId)
{
object model;
string modelId = ApiModelPrefix + apiDescriptionId;
if (!config.Properties.TryGetValue(modelId, out model))
{
Collection<ApiDescription> apiDescriptions = config.Services.GetApiExplorer().ApiDescriptions;
ApiDescription apiDescription = apiDescriptions.FirstOrDefault(api => String.Equals(api.GetFriendlyId(), apiDescriptionId, StringComparison.OrdinalIgnoreCase));
if (apiDescription != null)
{
apiDescription.AlterApiDescription(config);
HelpPageSampleGenerator sampleGenerator = config.GetHelpPageSampleGenerator();
model = GenerateApiModel(apiDescription, sampleGenerator);
config.Properties.TryAdd(modelId, model);
}
}
return (HelpPageApiModel)model;
}
The comments that are used for this must be added to the controller method and not the properties of the class object. This might be because my object is part of an outside library
this should go as an addition to #Josh answer. If you want not only to list properties from the model class, but also include documentation for each property, Areas/HelpPage/XmlDocumentationProvider.cs file should be modified as follows:
public virtual string GetDocumentation(HttpParameterDescriptor parameterDescriptor)
{
ReflectedHttpParameterDescriptor reflectedParameterDescriptor = parameterDescriptor as ReflectedHttpParameterDescriptor;
if (reflectedParameterDescriptor != null)
{
if (reflectedParameterDescriptor.ParameterInfo is CustomParameterInfo)
{
const string PropertyExpression = "/doc/members/member[#name='P:{0}']";
var pi = (CustomParameterInfo) reflectedParameterDescriptor.ParameterInfo;
string selectExpression = String.Format(CultureInfo.InvariantCulture, PropertyExpression, pi.Prop.DeclaringType.FullName + "." + pi.Prop.Name);
XPathNavigator methodNode = _documentNavigator.SelectSingleNode(selectExpression);
if (methodNode != null)
{
return methodNode.Value.Trim();
}
}
else
{
XPathNavigator methodNode = GetMethodNode(reflectedParameterDescriptor.ActionDescriptor);
if (methodNode != null)
{
string parameterName = reflectedParameterDescriptor.ParameterInfo.Name;
XPathNavigator parameterNode = methodNode.SelectSingleNode(String.Format(CultureInfo.InvariantCulture, ParameterExpression, parameterName));
if (parameterNode != null)
{
return parameterNode.Value.Trim();
}
}
}
}
return null;
}
and CustomParameterInfo class should keep property info as well:
internal class CustomParameterInfo : ParameterInfo
{
public PropertyInfo Prop { get; private set; }
public CustomParameterInfo(PropertyInfo prop)
{
Prop = prop;
base.NameImpl = prop.Name;
}
}
This is currently not supported out of the box. Following bug is kind of related to that:
http://aspnetwebstack.codeplex.com/workitem/877