I'm using my own middleware to capture exceptions thrown in my API to format the response to the client. This includes things like checking for the dev env to send additional information and logging. This all works great but the built-in validation middleware responds with a different response format. I want to keep the functionality and just change what data is sent to the client and how it's formatted.
Currently it returns the default
{
"message": "Validation error(s)",
"details": [
"The value '35353535353535353535353535353535353535353535' is not valid."
]
}
You can customize the default response by using a BadResultObject in the InvalidaModelStateResponseFactory of the ApiBehaviorOptions class. As an example:
apiBehaviorOptions.InvalidModelStateResponseFactory = actionContext => {
return new BadRequestObjectResult(new {
Code = 400,
Request_Id = "Someuniqueid",
Messages = actionContext.ModelState.Values.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage)
});
Configured:
serviceCollection.PostConfigure<ApiBehaviorOptions>(apiBehaviorOptions =>
apiBehaviorOptions.InvalidModelStateResponseFactory = ...
);
Or you can send the response directly from the action you are using as well with your own custom validation error result class. For example:
public class ValidationError
{
[JsonProperty(NullValueHandling=NullValueHandling.Ignore)]
public string Field { get; }
public string Message { get; }
public ValidationError(string field, string message)
{
Field = field != string.Empty ? field : null;
Message = message;
}
}
public class ValidationResultModel
{
public string Message { get; }
public List<ValidationError> Errors { get; }
public ValidationResultModel(ModelStateDictionary modelState)
{
Message = "Validation Failed";
Errors = modelState.Keys
.SelectMany(key => modelState[key].Errors.Select(x => new
ValidationError(key, x.ErrorMessage)))
.ToList();
}
}
Then we can create our own IActionResult. Here:
public class ValidationFailedResult : ObjectResult
{
public ValidationFailedResult(ModelStateDictionary modelState)
: base(new ValidationResultModel(modelState))
{
StatusCode = StatusCodes.Status404...;
}
}
And update our ValidateModelAttribute by overriding the OnActionExecuting to perform actions before they are taken.
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new ValidationFailedResult(context.ModelState);
}
}
}
Sources:
Customize automatic response on validation error
https://www.jerriepelser.com/blog/validation-response-aspnet-core-webapi/
Related
I'm trying to easily display errors in my View from anywhere in my code using :
#Html.ValidationSummary("", new { #class = "text-danger" })
Before MVC, I used :
ValidationError.Display("My error message");
And my ValidationError class looks like this:
public class ValidationError : IValidator
{
private ValidationError(string message)
{
ErrorMessage = message;
IsValid = false;
}
public string ErrorMessage { get; set; }
public bool IsValid { get; set; }
public void Validate()
{
// no action required
}
public static void Display(string message)
{
// here is the only part I would like to change ideally
var currentPage = HttpContext.Current.Handler as Page;
currentPage.Validators.Add(new ValidationError(message));
}
}
Now with MVC, to add errors, I can't use currentPage.Validators.
I need to use ModelState but my problem is that I can't access ModelState when I'm not in the Controller. I tried accessing the controller or the ModelState via HttpContext but I've not found a way to do it. Any idea ?
ModelState.AddModelError("", "My error message");
1. You can access it through ViewContext.ViewData.ModelState. Then use
#if (!ViewContext.ViewData.ModelState.IsValid)
{
<div>There are some errors</div>
}
OR
ViewData.ModelState.IsValidField("NameOfInput")
get a list of inputs:
var errors = ViewData.ModelState.Where(n => n.Value.Errors.Count > 0).ToList();
2. You can pass your model state around like this:
public class MyClass{
public static void errorMessage(ModelStateDictionary ModelState) {
if (something) ModelState.AddModelError("", "Error Message");
}
}
Use in controller:
MyClass.errorMessage(ModelState);
Use in view:
MyClass.errorMessage(ViewContext.ViewData.ModelState.IsValid);
3. ModelState via ActionFilter
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.Controller.ViewData.ModelState.IsValid)
{
//Do Something
}
}
}
You can get more help from this and this links.
I am trying to create an authentication provider in DotNetNuke 7.4 which supports LinkedId. I have used the source package for the Facebook provider from the DnnPlatform GIT as the base and have modified it for LinkedIn's oAuth. I am able to connect via LinkedIn and get the auth token but the code fails on
OAuthClient.GetCurrentUser<LinkedInUserData>();
due to LinkedInUserData being null. the specific logged error is
DotNetNuke.Services.Exceptions.Exceptions - ~/Default.aspx?tabid=55&error=An unexpected error has occurred
System.ArgumentNullException: Value cannot be null.
Parameter name: value
at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings)
at DotNetNuke.Services.Authentication.OAuth.OAuthClientBase.GetCurrentUser[TUserData]()
at DotNetNuke.Authentication.LinkedIn.Login.GetCurrentUser() in c:\Websites\dnndev74_2\DesktopModules\AuthenticationServices\LinkedIn\Login.ascx.cs:line 103
Below are the 3 classes that are in play, there is a lot of inheritance going on so I'm having trouble understanding the mechanism for how LinkedInUserData gets populated in the first place. On a note. when I take the facebook codebase from GIT and install it as a provider on my local, and try to register with facebook account, I get the same error. However, if I install the provider through the CMS it runs fine or use the dll that comes with the dnn 7.4 install, facebook works. So I am lead to believe there is something fundamentally flawed with the GIT code..
LinkedInClient.cs
namespace DotNetNuke.Authentication.LinkedIn.Components
{
public class LinkedInClient : OAuthClientBase
{
#region Constructors
public LinkedInClient(int portalId, AuthMode mode)
: base(portalId, mode, "LinkedIn")
{
base.AuthorizationEndpoint = new Uri("https://www.linkedin.com/uas/oauth2/authorization");
base.RequestTokenEndpoint = new Uri("https://api.linkedin.com/uas/oauth/requestToken?scope=r_emailaddress");
base.TokenMethod = HttpMethod.POST;
base.TokenEndpoint = new Uri("https://www.linkedin.com/uas/oauth2/accessToken");
base.MeGraphEndpoint = new Uri("https://api.linkedin.com/v1/people/~:(id,first-name,last-name,email-address,formatted-name,picture-url)?format=json");
base.AuthTokenName = "LinkedInUserToken";
base.OAuthVersion = "2.0";
base.LoadTokenCookie(string.Empty);
}
#endregion
protected override TimeSpan GetExpiry(string responseText)
{
var jsonSerializer = new JavaScriptSerializer();
var tokenDictionary = jsonSerializer.DeserializeObject(responseText) as Dictionary<string, object>;
return new TimeSpan(0, 0, Convert.ToInt32(tokenDictionary["expires_in"]));
}
protected override string GetToken(string responseText)
{
var jsonSerializer = new JavaScriptSerializer();
var tokenDictionary = jsonSerializer.DeserializeObject(responseText) as Dictionary<string, object>;
return Convert.ToString(tokenDictionary["access_token"]);
}
}
}
LinkedInUserData.cs
namespace DotNetNuke.Authentication.LinkedIn.Components
{
[DataContract]
[Serializable]
public class LinkedInUserData : UserData
{
#region Overrides
public override string FirstName
{
get { return LinkedInFirstName; }
set { }
}
public override string LastName
{
get { return LinkedInLastName; }
set { }
}
public override string Email
{
get { return emailAddress; }
set { }
}
public override string ProfileImage
{
get { return LinkedInPictureUrl; }
set { }
}
#endregion
[DataMember(Name = "first-name")]
public string LinkedInFirstName { get; set; }
[DataMember(Name = "last-name")]
public string LinkedInLastName { get; set; }
[DataMember(Name = "picture-url")]
public string LinkedInPictureUrl { get; set; }
[DataMember(Name = "email-address")]
public string emailAddress { set; get; }
}
Login.cs
namespace DotNetNuke.Authentication.LinkedIn
{
public partial class Login : OAuthLoginBase
{
protected override string AuthSystemApplicationName
{
get { return "LinkedIn"; }
}
public override bool SupportsRegistration
{
get { return true; }
}
protected override UserData GetCurrentUser()
{
return OAuthClient.GetCurrentUser<LinkedInUserData>();
}
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
loginButton.Click += loginButton_Click;
registerButton.Click += loginButton_Click;
OAuthClient = new LinkedInClient(PortalId, Mode);
loginItem.Visible = (Mode == AuthMode.Login);
registerItem.Visible = (Mode == AuthMode.Register);
}
protected override void AddCustomProperties(NameValueCollection properties)
{
base.AddCustomProperties(properties);
properties.Add("LinkedIn", OAuthClient.GetCurrentUser<LinkedInUserData>().Link.ToString());
}
private void loginButton_Click(object sender, EventArgs e)
{
AuthorisationResult result = OAuthClient.Authorize();
if (result == AuthorisationResult.Denied)
{
UI.Skins.Skin.AddModuleMessage(this, Localization.GetString("PrivateConfirmationMessage", Localization.SharedResourceFile), ModuleMessage.ModuleMessageType.YellowWarning);
}
}
}
}
Mark,
I wrote a DNN provider for Linkedin a couple years ago. Comparing my code to yours, the first thing mine does is redirect the user in order to get an access token. The base url for obtaining the access token and permission from the user is: www.linkedin.com/uas/oauth2. I must pass my linked API key, a redirect Url and a few other pieces of data which comes from the provider settings.
My redirect Url is the same as the default portal login page which loads my provider. Once the user allows LinkedIn to allow my application access and verifies he is logged in, the redirect back to my provider will look for the LinkedInAuthToken cookie in the Request.Cookies. Once I verify the token is valid, I can make an additional API call to the /v1/people/ API to get user data to complete any kind of auto-registration or auto profile updates to DNN.
It seems like your provider is immediately attempting the user data lookup API call before obtaining the oauth access cookie.
The code for my LinkedIn provider is not open source, but I suppose I could get permission from my work to make it public. Message me if you are interested in it.
I am trying to building custom validation with dynamic message in fluent validation library.
For example :
public class CreateProcessValidator : AbstractValidator<CreateProcessVM>
{
public CreateProcessValidator()
{
RuleFor(x => x.ProcessFile).Must((x,e) => IsProcessFileValid(x.ProcessFile))).WithMessage("Parse failed with error : {0}");
}
public bool IsProcessFileValid(HttpPostedFileBase file)
{
var errorMessage = "..." // pass result to validaton message ?
// logic
return false;
}
}
Is here any workaround how to pass validation result ?
Thanks
Have you tried something like this?
public class IsProcessFileValid : PropertyValidator
{
public IsProcessFileValid(): base("{ValidationMessage}") {}
protected override IsValid(PropertyValidatorContext context)
{
if (!IsProcessFileValid1(context))
context.MessageFormatter.AppendArgument("ValidationMessage",
"Custom validation message #1");
if (!IsProcessFileValid2(context))
context.MessageFormatter.AppendArgument("ValidationMessage",
"Custom validation message #2");
// ...etc
return true;
}
private bool IsProcessFileValid1(PropertyValidatorContext context)
{
// logic
return false;
}
private bool IsProcessFileValid2(PropertyValidatorContext context)
{
// logic
return false;
}
// ...etc
}
With extension method:
public static class IsProcessFileValidExtensions
{
public static IRuleBuilderOptions<T, object> MustBeValidProcessFile<T>
(this IRuleBuilder<T, object> ruleBuilder)
{
return ruleBuilder.SetValidator(new IsProcessFileValid());
}
}
... and then use it without a custom WithMessage:
public CreateProcessValidator()
{
RuleFor(x => x.ProcessFile).MustBeValidProcessFile();
}
By creating a custom PropertyValidator, you can encapsulate the default validation message within that class and make it dynamic. However you must not use the .WithMessage extension when declaring the RuleFor, because that would override the default validation message which you customized directly inside the PropertyValidator.
There's no way to do that. I would split the complex validation method you currently have into smaller methods (IsProcessFileValid1, IsProcessFileValid2, IsProcessFileValid3, ...) so that you could have more fine grained control over the error message. Also each method will be responsible for validating only once concern making them more reusable (single responsibility):
RuleFor(x => x.ProcessFile)
.Must(IsProcessFileValid1)
.WithMessage("Message 1")
.Must(IsProcessFileValid2)
.WithMessage("Message 2")
.Must(IsProcessFileValid3)
.WithMessage("Message 3");
Also notice how I simplified the lambda as the method could directly be passed to Must as argument.
Here is how I solved it. Tested with FluentValidation v8.5.0
class EmptyValidationMessage : IStringSource
{
public string ResourceName => null;
public Type ResourceType => null;
public string GetString(IValidationContext context)
{
return string.Empty;
}
public static readonly EmptyValidationMessage Instance = new EmptyValidationMessage();
}
public class MyPropValidator : PropertyValidator
{
public MyPropValidator() : base(EmptyValidationMessage.Instance)
{
}
protected override bool IsValid(PropertyValidatorContext context)
{
// if not valid
Options.ErrorMessageSource = new StaticStringSource("my message");
// you can do LanguageStringSource, LazyStringSource, LocalizedStringSource, etc
// example with localized string (https://github.com/clearwaterstream/LocalizedString.FluentValidation)
Options.ErrorMessageSource = new LocalizedStringSource("my message").InFrench("moi message");
return false;
}
}
Faced the same issue, while trying to insert exception message into WithMessage().
It worked with the method overload taking Func<T, string> messageProvider as parameter.
Here is the solution presented on the posters example (working code, FluentValidation v 9.1):
public class CreateProcessVM
{
public object ProcessFile { get; set; }
}
public class CreateProcessValidator : AbstractValidator<CreateProcessVM>
{
public CreateProcessValidator()
{
var message = "Something went wrong.";
RuleFor(x => x.ProcessFile)
.Must((x, e) => IsProcessFileValid(x.ProcessFile, out message))
// .WithMessage(message); will NOT work
.WithMessage(x => message); //Func<CreateProcessVM, string> as parameter
}
public bool IsProcessFileValid(object file, out string errorMessage)
{
errorMessage = string.Empty;
try
{
Validate(file);
return true;
}
catch (InvalidOperationException e)
{
errorMessage = e.Message;
return false;
}
}
private void Validate(object file)
{
throw new InvalidOperationException("File of type .custom is not allowed.");
}
}
And a test demonstrating that we really get the exception message in the error message:
[Fact]
public void Test()
{
var validator = new CreateProcessValidator();
var result = validator.Validate(new CreateProcessVM());
Assert.False(result.IsValid);
Assert.Equal("File of type .custom is not allowed.", result.Errors[0].ErrorMessage);
}
I'm trying to use RestSharp with MonoDroid and I'm running into some problems. A MethodMissingException is thrown with the message "Default constructor not found for type EmPubLite.Android.Tweet[]." I can only see the exception when I inspect the Task in the VS debugger, it doesn't show up in LogCat. I know that the exception is related to the JSON deserialization in RestSharp.
The linker is set to 'None' in MonoDroid options, and I also tried adding the [Preserve] attribute to my DTOs.
Beyond getting this to work, why doesn't this crash and show up in LogCat? I'd like any Tasks that fail with exceptions to rethrow the exception when I access the result.
[Activity(Label = "EmPubLite", MainLauncher = true, Icon = "#drawable/icon")]
public class EmPubLiteActivity : Activity
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.EmPubLight);
var client = new TwitterClient();
client.Search("bielema").ContinueWith(tweets =>
{
Debug.WriteLine("back");
Debug.WriteLine(tweets.Result[0].Text); // Exception not showing up in LogCat
});
}
}
public class TwitterClient
{
private RestClient client = new RestClient();
public Task<Tweet[]> Search(string query)
{
var request = new RestRequest("http://search.twitter.com/search.json?rpp=5&include_entities=true&result_type=mixed&q={q}", Method.GET);
request.AddUrlSegment("q", query);
var taskCompletionSource = new TaskCompletionSource<Tweet[]>();
client.ExecuteAsync<SearchResult>(request, response =>
{
if (response.ErrorException != null) taskCompletionSource.TrySetException(response.ErrorException);
else taskCompletionSource.TrySetResult(response.Data.Results);
// taskCompletionSource.TrySetResult(response.Data.Results);
});
return taskCompletionSource.Task;
}
}
public class SearchResult
{
public Tweet[] Results { get; set; }
}
public class Tweet
{
public string Text { get; set; }
}
I fixed this by using the ServiceStack JSON parser instead of the default RestSharp JSON parser.
I have a property on my Model Class that performs a type conversion as follows:
Model:
public class TimeClass
{
private int timeInSeconds;
[Required]
public int Id {get;set;}
[Required]
public string Timer
{
get {
TimeSpan ts = new TimeSpan(0,0,(int)timeInSeconds);
return ts.ToString(#"mm\:ss",CultureInfo.InvariantCulture);
}
set {
try {
var ts = TimeSpan.ParseExact(value, #"mm\:ss", CultureInfo.InvariantCulture);
timeInSeconds= Convert.ToInt32(ts.TotalSeconds);
}
catch {
//Is it possible to add a validation error to the modelstate here
}
}
}
}
Controller:
[HttpPost]
public ActionResult Create(FormCollection collection)
{
string[] whitelist = new[] {"Id", "Timer" };
if (TryUpdateModel(quiz, whitelist))
{
//Save to Repo
return RedirectToAction("Edit", new { Id = Id });
}
return View(tc);
}
What is the appropriate pattern to add an appropriate ModelError if the TimeSpan.Parse throws an exception?
Currently it will give the generic error "The value "xxx" is invalid". How can I customize this to return a specific error?
Rather than putting the try catch within your model, you could move the error handling to your controller code. For example if the tryupdatemodel throws an exception you could add a custom model error:
TimeClass model = new TimeClass();
string[] whitelist = new[] {"Id", "Timer" };
try
{
UpdateModel(model);
//Save to Repo
return RedirectToAction("Edit", new { Id = Id });
}
catch
{
// Generate error
ModelState.AddModelError(string.Empty, "Model Error Here");
return View(tc);
}