GraphQL-dotnet fields Validation Prior Resolving - c#

I'm building GraphQL API in Asp.Net Core 2.2 using GraphQL.Net 2.4.0
I've created Controller to handle GraphQL Queries:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class GraphQLController : Controller
{
private readonly ISchema _schema;
private readonly IDocumentExecuter _executer;
private readonly ILogger _logger;
IEnumerable<IValidationRule> _validationRules;
private readonly DataLoaderDocumentListener _dataLoaderDocumentListener;
//private readonly IDocumentWriter _writer;
//private readonly IHttpContextAccessor _accessor;
public GraphQLController(
ILogger<GraphQLController> logger,
IEnumerable<IValidationRule> validationRules,
IDocumentExecuter executer,
DataLoaderDocumentListener dataLoaderDocumentListener,
//IDocumentWriter writer,
ISchema schema)
{
_executer = executer;
_schema = schema;
_logger = logger;
_validationRules = validationRules;
_dataLoaderDocumentListener = dataLoaderDocumentListener;
//_writer = writer;
}
[Route("graphql")]
[HttpPost]
public async Task<IActionResult> PostAsync([FromBody]GraphQLQuery query)
{
if (!ModelState.IsValid || query == null)
{
return Json("Проблем в формата на изпратената на заявка!");
}
var inputs = query.Variables.ToInputs();
var result = await _executer.ExecuteAsync(o =>
{
o.Schema = _schema;
o.Query = query.Query;
o.OperationName = query.OperationName;
o.Inputs = inputs;
o.ExposeExceptions = true;
o.EnableMetrics = true;
o.ComplexityConfiguration = new GraphQL.Validation.Complexity.ComplexityConfiguration { MaxDepth = 15 };
//o.FieldMiddleware.Use<InstrumentFieldsMiddleware>();
o.UserContext = new GraphQLUserContext
{
// this is the User on your controller
// which is populated from your jwt
User = User
};
o.ValidationRules = DocumentValidator.CoreRules().Concat(_validationRules).ToList();
o.Listeners.Add(_dataLoaderDocumentListener);
}).ConfigureAwait(false);
if (result.Errors?.Count > 0)
{
_logger.LogWarning($"Errors: {JsonConvert.SerializeObject(result.Errors)}");
return BadRequest(result);
}
return Ok(result);
}
}
The problem arises when i want to validate some of the Fields in my InputObjectGraphType , I've implemented IValidationRule interface.
Prior this im adding Metadata to the field i want to validate so i can easily find it. I;m getting the fieldType but cant fetch the Value to Validate it.
This is the Implementation of the IValidationRule i Use:
public class PhoneNumberValidationRule : IValidationRule
{
public INodeVisitor Validate(ValidationContext context)
{
return new EnterLeaveListener(_ =>
{
_.Match<Argument>(argAst =>
{
var inputType = context.TypeInfo.GetInputType().GetNamedType() as InputObjectGraphType;
var argDef = context.TypeInfo.GetArgument();
if (argDef == null) return;
var type = argDef.ResolvedType;
if (type.IsInputType())
{
var fields = ((type as NonNullGraphType)?.ResolvedType as IComplexGraphType)?.Fields;
if (fields != null)
{
foreach (var field in fields)
{
if (field.ResolvedType is NonNullGraphType)
{
if ((field.ResolvedType as NonNullGraphType).ResolvedType is IComplexGraphType)
{
fields = fields.Union(((field.ResolvedType as NonNullGraphType).ResolvedType as IComplexGraphType)?.Fields);
}
}
if (field.ResolvedType is IComplexGraphType)
{
fields = fields.Union((field.ResolvedType as IComplexGraphType)?.Fields);
}
}
//let's look for fields that have a specific metadata
foreach (var fieldType in fields.Where(f => f.HasMetadata(nameof(EmailAddressValidationRule))))
{
//now it's time to get the value
context.Inputs.GetValue(argAst.Name, fieldType.Name);
if (value != null)
{
if (!value.IsValidPhoneNumber())
{
context.ReportError(new ValidationError(context.OriginalQuery
, "Invalid Phone Number"
, "The supplied phone number is not valid."
, argAst
));
}
}
}
}
}
});
});
}
}
But here the context.Inputs property is always null.
In the controller this line var inputs = query.Variables.ToInputs();
also produces null. Is this query Variable field and Document Executer's Input Field anything to do with this ?

You probably figured it out by now, but for anyone finding this question in the future:
context.Inputs is only used with variables.
As far as I could understand argAst.GetValue could be either VariableReference or ObjectValue depending on whether the query was using a variable or not.
In case variables were not used context.Inputs.GetValue will return null.

Related

C# ASP.NET Core ModelBinder not Updating Model

I have created a ModelBinder, which only gets triggered if an object has a [Decimal] attribute assigned, yet for some reason, despite it actually sanitising the data it does not seem to update the posted model.
I wonder if someone could see from my code below, where I maybe going wrong.
Startup.cs
public void ConfigureServices(IServiceCollection serviceCollection)
{
serviceCollection.AddMvc(config => config.ModelBinderProviders.Insert(0, new DecimalModelBinderProvider()));
}
DecimalModelBinderProvider.cs
public class DecimalModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext modelBinderProviderContext)
{
if (modelBinderProviderContext == null)
{
throw new ArgumentNullException(nameof(modelBinderProviderContext));
}
if (!modelBinderProviderContext.Metadata.IsComplexType)
{
try
{
var propertyName = modelBinderProviderContext.Metadata.PropertyName;
var property = modelBinderProviderContext.Metadata.ContainerType.GetProperty(propertyName);
if (property != null)
{
var attribute = property.GetCustomAttributes(typeof(DecimalAttribute), false).FirstOrDefault();
if (attribute != null)
{
return new DecimalModelBinder(modelBinderProviderContext.Metadata.ModelType, attribute as IDecimalAttribute);
}
}
}
catch (Exception exception)
{
var message = exception.Message;
return null;
}
}
return null;
}
}
DecimalModelBinder.cs
public class DecimalModelBinder : IModelBinder
{
private readonly IDecimalAttribute _decimalAttribute;
private readonly SimpleTypeModelBinder _simpleTypeModelBinder;
public DecimalModelBinder(Type type, IDecimalAttribute decimalAttribute)
{
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}
_decimalAttribute = decimalAttribute;
_simpleTypeModelBinder = new SimpleTypeModelBinder(type);
}
public Task BindModelAsync(ModelBindingContext modelBindingContext)
{
if (modelBindingContext == null)
{
throw new ArgumentNullException(nameof(modelBindingContext));
}
var valueProviderResult = modelBindingContext.ValueProvider.GetValue(modelBindingContext.ModelName);
if (valueProviderResult != ValueProviderResult.None)
{
modelBindingContext.ModelState.SetModelValue(modelBindingContext.ModelName, valueProviderResult);
var value = valueProviderResult.FirstValue;
bool success;
var result = _decimalAttribute.Decimal(value, out success);
if (success)
{
modelBindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
return _simpleTypeModelBinder.BindModelAsync(modelBindingContext);
}
}
IDecimalAttribute.cs
public interface IDecimalAttribute
{
object Decimal(string value, out bool success);
}
DecimalAttribute.cs
[AttributeUsage(AttributeTargets.Property)]
public class DecimalAttribute : Attribute, IDecimalAttribute
{
public object Decimal(string value, out bool success)
{
var tryModelValue = string.IsNullOrEmpty(value) ? "0.00" : value.Replace("£", "").Replace("%", "");
decimal #decimal;
success = decimal.TryParse(tryModelValue, out #decimal);
return #decimal;
}
}
Test.cs
public class Test
{
[Display(Name = "Name", Description = "Name")]
public string Name { get; set; }
[Decimal]
[Display(Name = "Amount", Description = "Amount")]
public double Amount { get; set; }
}
HomeController
[ValidateAntiForgeryToken]
[HttpPost]
public IActionResult Index(Test test)
{
if (ModelState.IsValid)
{
}
return View(test);
}
For the purpose of testing I will enter the value £252.83 into the Amount field and submit the form.
If I then place a brakepoint on the line var value = valueProviderResult.FirstValue; I can see that value is £252.83 and if I place a breakpoint on the line modelBindingContext.Result = ModelBindingResult.Success(result); I can see that the result is 252.83M.
However if I step through the code further and place a breakpoint on the line if (ModelState.IsValid) the valid state is false and if I inspect the model test the object Amount is 0.
If anyone can help it would be much appreciated :-)
Try inspect further on the ModelState Error, the Amount property should be invalid and there must be an exception.
I am guessing it should be InvalidCastException. I notice that the Amount property in Test class is Double while you are producing Decimal in your DecimalAttribute.
So the build-in Model Binder that processing the Test class (should be ComplexTypeModelBinder) unable to set the Amount property as it is different type.

Flushing MemoryCache ASP.NET Core 2

I am trying to flush my cache after I update an item, and I have tried a few different options and none are working as expected
public class PostApiController : Controller
{
private readonly IPostService _postService;
private readonly IPostTagService _postTagService;
private IMemoryCache _cache;
private MemoryCacheEntryOptions cacheEntryOptions;
public PostApiController(IPostService postService, IPostTagService postTagService, IMemoryCache cache)
{
_postService = postService;
_postTagService = postTagService;
_cache = cache;
cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromDays(1));
}
[HttpGet("{url}", Name = "GetPost")]
public IActionResult GetById(string url, bool includeExcerpt)
{
Post cacheEntry;
if (!_cache.TryGetValue($"GetById{url}{includeExcerpt}", out cacheEntry))
{
cacheEntry = _postService.GetByUrl(url, includeExcerpt);
_cache.Set($"GetById{url}{includeExcerpt}", cacheEntry, cacheEntryOptions);
}
if (cacheEntry == null)
{
return NotFound();
}
return new ObjectResult(cacheEntry);
}
[HttpPut("{id}")]
public IActionResult Update(int id, [FromBody] Post item)
{
if (item == null)
{
return BadRequest();
}
var todo = _postService.GetById(id);
if (todo == null)
{
return NotFound();
}
_postService.Update(item);
_postTagService.Sync(item.Tags.Select(a => new PostTag { PostId = item.Id, TagId = a.Id }).ToList());
//Want to flush entire cache here
return new NoContentResult();
}
I have tried to Dispose() MemoryCache here but on next Api call, it is still disposed. Since the keys are somewhat dynamic, I can't just get the keys. How can I go about doing this?
You can store the dictionary instead. This way you can use the dynamic keys for entries and one single static key for the dictionary container, which can be stored in the cache instead of storing each entry separately.
Something like that:
private const string CachedEntriesKey = "SOME-STATIC-KEY";
[HttpGet("{url}", Name = "GetPost")]
public IActionResult GetById(string url, bool includeExcerpt)
{
Dictionary<string, Post> cacheEntries;
if (!_cache.TryGetValue(CachedEntriesKey, out cacheEntries))
{
cacheEntries = new Dictionary<string, Post>();
_cache.Set(CachedEntriesKey, cacheEntries);
}
var entryKey = $"GetById{url}{includeExcerpt}";
if (!cacheEntries.ContainsKey(entryKey))
{
return NotFound(); // by the way, why you do that instead of adding to the cache?
}
return new ObjectResult(cacheEntries[entryKey]);
}

500 Internal Server Error for Store Method

I am gettin an null reference exception inside the Controller on the line:
return await Store.GetSearchDTO();
The error in the console reads:
POST http://localhost:55471/api/GetSearchDTO 500 (Internal Server Error)
Error: Resolving failed with a reason [object Object], but no resolveFailed provided for segment Search
Any insight on why this may be happening would be great.
Controller
namespace Api.Controllers
{
[Authorize]
[RoutePrefix("api/Search")]
public class Controller : ApiController
{
private Store _store;
public Store Store
{
get
{
return _store ?? Request.GetOwinContext().Get<Store>();
}
private set
{
_store = value;
}
}
public Controller()
{
}
public Controller(Store store)
{
Store = store;
}
[HttpPost]
[Route("GetSearchDTO")]
public async Task<SearchDTO> GetSearchDTO()
{
return await Store.GetSearchDTO();
}
}
}
Store
public async Task<SearchDTO> GetSearchDTO()
{
var toReturn = new SearchDTO();
var assessment = await Db.Definitions.Where(x => x.IsActive == true).ToListAsync();
var Types = await Db.Types.ToListAsync();
int i = 0;
int j = 0;
foreach(var assess in assessment)
{
var courseName = await Db.Courses.Where(x => x.Id == assess.CourseId).FirstOrDefaultAsync();
toReturn.CourseIds[i] = courseName.Id;
toReturn.CourseNames[i] = courseName.Name;
toReturn.Names[i] = assess.Name;
i++;
}
foreach(var type in Types)
{
toReturn.TypeIds[j] = type.Id;
toReturn.Types[j] = type.Name;
}
toReturn.SectionFlag = true;
return toReturn;
}
}
}

PageFactory pattern - extend IWebElement C#

I am starting to implement the PageFactory design pattern on Selenium .NET driver. I have a Page object class called example "ButtonControl" that I want to be treated as an IWebElement.
Default it looks like :
[FindsBy(How = How.CssSelector, Using = "someSelector")]
public IWebElement button1;
What I really want is this:
// in the page object:
[FindsBy(How = How.CssSelector, Using = "someSelector")]
public ButtonControl button1;
// in test code:
page.button1.Click();
So what I need is... I don't know. Maybe custom Factory which will create this page objects?
Any ideas?
Page object model should only keep a map of the UI elements, nothing more. In many articles you'll see advises to put methods that interact with the page - but this is wrong.
/**
* Page Object encapsulates the Sign-in page.
*/
public class LoginPage{
private final WebDriver driver;
public LoginPage(WebDriver driver) {
this.driver = driver;
// Check that we're on the right page.
if (!"Login".equals(driver.getTitle())) {
// Alternatively, we could navigate to the login page, perhaps logging out first
throw new IllegalStateException("This is not the login page");
}
}
// The login page contains several HTML elements that will be represented as WebElements.
// The locators for these elements should only be defined once.
By usernameLocator = By.id("username");
By passwordLocator = By.id("passwd");
By loginButtonLocator = By.id("login");
/**
* Login as valid user
*
* #param userName
* #param password
* #return HomePage object
*/
public LoginPage loginValidUser(String userName, String password) {
driver.findElement(usernameLocator).sendKeys(userName);
driver.findElement(usernameLocator).sendKeys(password);
driver.findElement(loginButtonLocator).click();
return new HomePage(selenium);
}
}
This violates SRP, in the example above you have two reasons to change - if locator is changed and if the flow is changed. IMHO most times is a matter of trade off between smart tests and smart page objects. But you can implement your Action class which will take care of the interactions with the page.
It is really cool idea for PageObject. :)
You can use the liblary: https://github.com/DotNetSeleniumTools/DotNetSeleniumExtras
Extend the class https://github.com/DotNetSeleniumTools/DotNetSeleniumExtras/blob/master/src/PageObjects/DefaultPageObjectMemberDecorator.cs
You Class choul be looks like:
class CustomPageObjectMemberDecorator : DefaultPageObjectMemberDecorator, IPageObjectMemberDecorator
{
private BaseDriver driver;
public CustomPageObjectMemberDecorator(BaseDriver driver) => this.driver = driver;
public new object Decorate(MemberInfo member, IElementLocator locator)
{
FieldInfo field = member as FieldInfo;
PropertyInfo property = member as PropertyInfo;
Type targetType = null;
if (field != null)
{
targetType = field.FieldType;
}
bool hasPropertySet = false;
if (property != null)
{
hasPropertySet = property.CanWrite;
targetType = property.PropertyType;
}
var genericType = targetType.GetGenericArguments().FirstOrDefault();
if (field == null & (property == null || !hasPropertySet))
{
return null;
}
// IList<IWebElement>
if (targetType == typeof(IList<IWebElement>))
{
return base.Decorate(member, locator);
}
// IWebElement
else if (targetType == typeof(IWebElement))
{
return base.Decorate(member, locator);
}
// BaseElement and childs
else if (typeof(BaseElement).IsAssignableFrom(targetType))
{
var bys = CreateLocatorList(member);
var cache = ShouldCacheLookup(member);
IWebElement webElement = (IWebElement)WebElementProxy.CreateProxy(locator, bys, cache);
return GetElement(targetType, webElement, driver, field.Name);
}
// IList<BaseElement> and childs
else if (targetType.GetInterfaces().FirstOrDefault(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ICollection<>)) != null)
{
Type elementOfListTargetType = targetType.GetGenericArguments()[0];
if (typeof(BaseElement).IsAssignableFrom(elementOfListTargetType))
{
var cache = ShouldCacheLookup(member);
IList<By> bys = CreateLocatorList(member);
if (bys.Count > 0)
{
return CustomElementListProxy.CreateProxy(driver, field.Name, elementOfListTargetType, locator, bys, cache);
}
else
{
return null;
}
}
else
{
return null;
}
}
else
{
return null;
}
}
private Element GetElement(Type type, IWebElement webElement, BaseDriver baseDriver, string Name)
{
return (Element)type
.GetConstructor(new[] { typeof(IWebElement), typeof(BaseDriver), typeof(string) })
.Invoke(new object[] { webElement, baseDriver, Name });
}
}
Also you need init your page:
public BasePage(BaseDriver Driver) : base(Driver)
{
IPageObjectMemberDecorator pageObjectMemberDecorator = new CustomPageObjectMemberDecorator(_driver);
PageFactory.InitElements(this, _driver.RetryingElementLocator, pageObjectMemberDecorator);
}
Your PageObject will be looks like:
public class LoginPage : BasePage, ILoad
{
[FindsBy(How = How.XPath, Using = "//*[#class = 'linkButtonFixedHeader office-signIn']")] private Button SignIn;
[FindsBy(How = How.XPath, Using = "//input[#type = 'email']")] private TextInputField Email;
[FindsBy(How = How.XPath, Using = "//input[#type = 'submit']")] private Button Submit;
[FindsBy(How = How.XPath, Using = "//input[#type = 'password']")] private TextInputField Password;
public LoginPage() : base(new BaseDriver(), "https://outlook.live.com/")
{
}
public bool IsLoaded() =>
SignIn.IsVisible() &&
Email.IsVisible() &&
Submit.IsVisible();
public MainMailPage PositiveLogin(User user)
{
SignIn.Click();
Email.SendString(user.Login);
Submit.Click();
Password.SendString(user.Pass);
Submit.Click();
return Page<MainMailPage>();
}
}
Also you can extend FindByAttribute and PageObject will be simplified:
public class LoginPage : BasePage, ILoad
{
[XPath("//*[#class = 'linkButtonFixedHeader office-signIn']")] private Button SignIn;
[XPath("//input[#type = 'email']")] private TextInputField Email;
[XPath("//input[#type = 'submit']")] private Button Submit;
[XPath("//input[#type = 'password']")] private TextInputField Password;
public LoginPage() : base(new BaseDriver(), "https://outlook.live.com/")
{
}
public bool IsLoaded() =>
SignIn.IsVisible() &&
Email.IsVisible() &&
Submit.IsVisible();
public MainMailPage PositiveLogin(User user)
{
SignIn.Click();
Email.SendString(user.Login);
Submit.Click();
Password.SendString(user.Pass);
Submit.Click();
return Page<MainMailPage>();
}
}

Entity framework error as"New transaction is not allowed because there are other threads running in the session

We are using entity framework codefirst approach
I am new to entity framework and I am facing error while trying to do "New transaction is not allowed because there are other threads running in the session.
public class DatabaseBackup : IDataBackup
{
private readonly IMonarchDbContext m_db;
public DatabaseBackup(IMonarchDbContext podb)
{
if (podb == null)
throw new ArgumentNullException("podb");
m_db = podb;
}
public DBBackupHistory GetLatestBackupHistory(DBBackupFrequency backupFrequency = DBBackupFrequency.Periodic)
{
DBBackupHistory result = null;
// get the backup history of the given backuptype and populate the objects
var configId = m_db.DBBackupConfigurations.Where(c => c.ScheduleType == (int)backupFrequency && c.BackupStatus == 1).Distinct().Select(c => c.ConfigurationId).DefaultIfEmpty(-1).First();
if (configId > 0)
{
result = m_db.DBBackupHistorys.Where(b => b.Status == 1 && b.ConfigurationId == configId).OrderByDescending(lb => lb.BackupDatetime).FirstOrDefault();
}
return result;
}
public IEnumerable<DBBackupConfiguration> GetAllConfiguration()
{
var result = m_db.DBBackupConfigurations.Where(c => c.BackupStatus == 1).OrderByDescending(c => c.ConfigurationId);
return result;
}
public void Backup(DBBackupConfiguration config, int fileIndex)
{
Console.WriteLine("Running DB Backup type {0} to device {1}", (DBBackupType)config.BackupType, fileIndex);
m_db.StoredProc.SPBackup(config, fileIndex);
}
I am calling the below methods in another class as follows
private readonly IDataBackup m_dataBackup;
public int PerformBackup(int defaultPollIntervalInMinutes = 15)
{
// polling interval in Minutes
int pollInterval = defaultPollIntervalInMinutes;
int fileIndex = getCurrentDumpFileIndex();
// check for the backup configuration
var configurations = m_dataBackup.GetAllConfiguration();
foreach (var config in configurations)
{
var lastBackup = m_dataBackup.GetLatestBackupHistory(DBBackupFrequency.Weekly);
if (lastBackup == null)
{
m_dataBackup.Backup(config, fileIndex + 1);
break;
}
Here is the Db Context class is as below
public class MonarchDbContext:DbContext,IMonarchDbContext
{
private IStoredProcedure m_storedProc;
private static object m_dbIntializerSet;
public MonarchDbContext(string nameOrConnectionString)
: base( nameOrConnectionString )
{
//-- Set the DB initializer only once.
System.Threading.LazyInitializer.EnsureInitialized( ref m_dbIntializerSet,()=>{
Database.SetInitializer<MonarchDbContext>(null);
//-- Give debug builds a chance to overwrite the above.
_SetInitializerForDebugBuilds();
return new object();
});
Configuration.LazyLoadingEnabled = false;
Configuration.ProxyCreationEnabled = false;
var csb = new SqlConnectionStringBuilder( this.Database.Connection.ConnectionString );
csb.MultipleActiveResultSets = true;
this.Database.Connection.ConnectionString = csb.ToString();
var objectContext = ( this as IObjectContextAdapter ).ObjectContext;
objectContext.CommandTimeout = 3600;
}
#region Public "Tables"
public IDbSet<DBBackupConfiguration> DBBackupConfigurations { get; set; }
public IDbSet<DBBackupHistory> DBBackupHistorys { get; set; }
public IStoredProcedure StoredProc
{
get
{
return System.Threading.LazyInitializer.EnsureInitialized(ref m_storedProc, () => new BackupStoredProc(this.Database));
}
}
#endregion
please let me know how can i solve the issue.
I found the issue
I need to add toList() at the end of the Linq code and it just worked for me.
public IEnumerable<DBBackupConfiguration> GetAllConfiguration()
{
var result = m_db.DBBackupConfigurations.Where(c => c.BackupStatus == 1).OrderByDescending(c => c.ConfigurationId).ToList();
return result;
}
Just add the List to Ienumerbale types

Categories