/// <summary> /// Filters the model dataset by the primary key obtained from the request context /// </summary> /// <param name="context">The current API context</param> /// <param name="dataset">The current dataset to be filtered</param> /// <returns>The filtered dataset</returns> public virtual async Task <IQueryable <TModel> > FilterDataAsync( IApiContext <TModel, object> context, IQueryable <TModel> dataset) { // step one is just get the keys object?[] PrimaryKeyValues = this.Parameters.Select( (param, i) => { // retrieve value string ParameterValue = param.GetValue(context.Request) ?? throw new ConditionFailedException("No value provided for primary key"); Type KeyType = this.PrimaryKey[i].PropertyInfo.PropertyType; // and parse return(ParameterResolver.ParseParameter(ParameterValue, KeyType)); }).ToArray(); // so that ef can still use sql queries, let's generate an expression tree for it to use ParameterExpression ModelParameter = Expression.Parameter(typeof(TModel)); // generate expressions. boils down to: ModelParameter.Property == PrimaryKeyValues[i] Expression[] ComparisonExpressions = this.PrimaryKey.Select( (property, i) => (Expression)Expression.Equal( Expression.Property(ModelParameter, property.PropertyInfo), Expression.Constant(PrimaryKeyValues[i]))).ToArray(); // && them all together Expression AggregateExpression = ComparisonExpressions.Aggregate(Expression.AndAlso); // and create the lambda Expression <Func <TModel, bool> > FilterExpression = Expression.Lambda <Func <TModel, bool> >(AggregateExpression, ModelParameter); return(dataset.Where(FilterExpression)); }
/// <summary> /// Filters this route's dataset by a parameter value /// </summary> /// <param name="property">An expression getting the property to filter for</param> /// <param name="retriever">A function that will get the value of the parameter for a given request</param> /// <typeparam name="T">The type of the parameter to filter with</typeparam> /// <returns>This <see cref="RestModelOptionsBuilder{TModel, TUser}" /> object, for chaining</returns> public RestModelOptionsBuilder <TModel, TUser> FilterByParameterEqual <T>(Expression <Func <TModel, T> > property, ParameterRetriever retriever) { PropertyInfo Info = RestModelOptionsBuilder <TModel, TUser> .ExtractProperty(property); ParameterExpression ModelParameter = Expression.Parameter(typeof(TModel)); MemberExpression PropertyExpression = Expression.Property(ModelParameter, Info); Type PropertyType = typeof(T); this.Filter( (c, d) => { string?ParamValue = retriever.GetValue(c.Request); if (ParamValue == null) { throw new ConditionFailedException("Failed to parse request parameter"); } T Parsed = (T)ParameterResolver.ParseParameter(ParamValue, PropertyType); // ModelParameter.Property == ParamValue Expression ComparisonExpression = Expression.Equal(PropertyExpression, Expression.Constant(Parsed)); // and create the lambda Expression <Func <TModel, bool> > FilterExpression = Expression.Lambda <Func <TModel, bool> >(ComparisonExpression, ModelParameter); return(d.Where(FilterExpression)); }); return(this); }
/// <summary> /// Sets a value on parsed models before the operation occurs. /// </summary> /// <param name="property">The property to set the value on</param> /// <param name="retriever">A parameter retriever to use to get the value for a request</param> /// <returns>This <see cref="RestModelOptionsBuilder{TModel, TUser}" /> object, for chaining</returns> public RestModelOptionsBuilder <TModel, TUser> SetValue(PropertyInfo property, ParameterRetriever retriever) { Type PropertyType = property.PropertyType; return(this.SetValue( property, c => ParameterResolver.ParseParameter(retriever.GetValue(c.Request), PropertyType))); }
/// <summary> /// Sets the value of all properties on this <see cref="Response{TModel}"/> that have the given attribute applied to them. If no such properties exist, no action will occur. /// </summary> /// <typeparam name="TAttribute">The attribute to match on the properties to set</typeparam> /// <param name="value">The string value to assign to that property</param> /// <remarks> /// This method will attempt to convert the string value to a supported type if the type of the matching property is not string. /// </remarks> public void SetString <TAttribute>(string value) where TAttribute : Attribute { PropertyInfo[] Matching = this.Properties.Where(p => p.GetCustomAttribute <TAttribute>(false) != null).ToArray(); foreach (PropertyInfo ToSet in Matching) { ToSet.GetSetMethod()?.Invoke( this, new[] { ParameterResolver.ParseParameter(value, ToSet.PropertyType) }); this.SetProperties.Add(ToSet); } }
/// <summary> /// Sets the value of all properties on this <see cref="Response{TModel}"/> that have the <see cref="ResponseValueAttribute"/> with the given name applied to them. If no such properties exist, no action will occur. /// </summary> /// <param name="name">The name given to the <see cref="ResponseValueAttribute"/> on the properties to set</param> /// <param name="value">The string value to assign to that property</param> /// <remarks> /// This method will attempt to convert the string value to a supported type if the type of the matching property is not string. /// </remarks> public void SetString(string name, string value) { if (name == null) { throw new ArgumentNullException(nameof(name)); } PropertyInfo[] Matching = this.Properties.Where(p => p.GetCustomAttribute <ResponseValueAttribute>()?.Name == name).ToArray(); foreach (PropertyInfo ToSet in Matching) { ToSet.GetSetMethod()?.Invoke( this, new[] { ParameterResolver.ParseParameter(value, ToSet.PropertyType) }); this.SetProperties.Add(ToSet); } }
/// <summary> /// Updates the models specified in the request body by their primary key /// </summary> /// <param name="context">The current API context</param> /// <param name="dataset">The filtered dataset to operate on</param> /// <returns>The affected models</returns> public virtual async Task <IEnumerable <TModel> > OperateAsync( IApiContext <TModel, object> context, IQueryable <TModel> dataset) { TContext DatabaseContext = context.Services.GetRequiredService <TContext>(); // todo: make better List <TModel> UpdatedList = new List <TModel>(); if (context.Parsed == null) { throw new OperationFailedException("Must have a parsed body to update"); } foreach (ParseResult <TModel> Result in context.Parsed) { // by default assume the properties were parsed with the rest of the model // todo: this is the ugliest thing i have ever seen object?[] Values = this.ParameterDelegates == null ? this.Properties.Select(p => p.GetGetMethod()?.Invoke(Result.ParsedModel, null)) .ToArray() : this.Properties.Zip(this.ParameterDelegates).Select( v => ParameterResolver.ParseParameter( v.Second.GetValue(context.Request), v.First.PropertyType)).ToArray(); if (Values.Any(v => v == null)) { throw new OperationFailedException( "Missing expected value for update operation", new ArgumentNullException( this.Properties[Array.FindIndex(Values, v => v == null)].Name, "Parameter was null"), 400); } // so that ef can still use sql queries, let's generate an expression tree for it to use ParameterExpression ModelParameter = Expression.Parameter(typeof(TModel)); // generate expressions. boils down to: ModelParameter.Property == Values[i] Expression[] ComparisonExpressions = this.Properties.Zip(Values).Select( v => (Expression)Expression.Equal( Expression.Property(ModelParameter, v.First), Expression.Constant(v.Second))).ToArray(); // && them all together Expression AggregateExpression = ComparisonExpressions.Aggregate(Expression.AndAlso); // and create the lambda Expression <Func <TModel, bool> > FilterExpression = Expression.Lambda <Func <TModel, bool> >(AggregateExpression, ModelParameter); IEnumerable <TModel> ExistingModels = dataset.Where(FilterExpression); foreach (TModel Existing in ExistingModels) { foreach (PropertyInfo Updated in Result.PresentProperties) { object?NewValue = Updated.GetGetMethod()?.Invoke(Result.ParsedModel, null); Updated.GetSetMethod()?.Invoke(Existing, new[] { NewValue }); } UpdatedList.Add(Existing); // TODO: obviously it's 1000x better to update all at once. do that await DatabaseContext.UpdateAsync(Existing); } } return(UpdatedList); }