I have a DbSet<Items> collection.
The primary key is a Guid. I don't want to order by this primary key. I want to order by an editable decimal property named "Order".
The code I have is very simple, and it works great until the user puts a "$top" parameter into the request:
public class ItemsController : ApiController
{
protected DbContext ctx = // ...
// GET api/documents
[EnableQuery()]
public IQueryable<Item> Get()
{
return ctx.Items.OrderBy(o => o.Order).AsQueryable();
}
When the user puts "$top" into the query string, the order gets all messed up (it presumably forces the ordering to be done by the primary key, for consistent paging results -- however, in my situation, this is having the opposite effect, it's preventing me from having consistent paging results).
I've tried moving .AsQueryable() to be earlier in the query (before the .OrderBy(...) clause), I've tried it without the .AsQueryable(), I've tried it with two AsQueryables, etc.
There are going to be a lot of items in this table, so it needs to be done via an IQueryable (enumerating all of the items on the web server via IEnumerable is not an option here).
The only thing that has worked so far is passing in "$orderby=Order" from the client, but I don't want to force that (seems like it will get forgotten easily).
1.) Is there anything I can do to make ordering by my Order property the default behavior here?
2.) Or failing that, is there anyway to trick WebApi / OData into thinking that a custom "$orderby=Order" clause was specified?
To override default sort order, you need to set property EnsureStableOrdering of EnableQueryAttribute to false, like describe here:
A true value indicates the original query should be modified when
necessary to guarantee a stable sort order. A false value indicates
the sort order can be considered stable without modifying the query.
Query providers that ensure a stable sort order should set this value
to false. The default value is true.
So in your code, changes the action attribute like this:
// GET api/documents
[EnableQuery(EnsureStableOrdering = false)]
public IQueryable<Item> Get()
{
return ctx.Items.OrderBy(o => o.Order).AsQueryable();
}
You can manually invoke the odata in your controller. This should create the proper sorted IQueryable and then apply the $top and any other odata like $filter and $skip. Now you don't have to return an IQueryable which was causing the problem because the actual query was being executed later in the pipeline.
public class ItemsController : ApiController
{
protected DbContext ctx = // ...
public IEnumerable<Item> Get(ODataQueryOptions<Item> odata)
{
var collection = ctx.Items.OrderBy(o => o.Order);
if (odata == null)
{
//return a default max size of 100
return collection.Take(100).ToList();
}
var results = odata.ApplyTo(collection.AsQueryable()) as List<Item>;
//still provide a max incase the $top wasn't specified.
//you could check the odata to see if $top is there or not.
return results.Take(100);
}
}
More information can be found in the WebApi documentation.
Related
I've overridden a a record value like the following :
public function getWeightAttribute($weight)
{
if($weight)
{
return $weight;
}
return 70;
}
Now I have a collection of that model and I want to know if the original value was null or not.
I want to do it without connecting to DB again, Actually I want to make sure the user has filled the weight and some other fields while registering.
Some sections are working based on the above override and I don't want to mess them up neither use extra connections to db.
Thanks In Advance,
In order to get the original value, you can use:-
$fetchData->getAttributes()['weight'];
I have a custom attribute on my User model that's calculates the length of some other tables and returns an integer value:
public function GetCurrentQueueLengthAttribute()
{
// return int
}
I then have an API endpoint that returns a "Team" with all its users (simple Spark pivot)
public function show($teamId)
{
$query = Team::query();
$query->with('users')->where('id', $teamId);
$team = $query->first();
return $team->users->sortBy('currentQueueLength');
return $team;
}
The issue is that the returned data doesn't change order. There are no errors, just the same order of the users every time.
Is there something I'm missing?
The sortBy function is not to be mistaken by the orderBy function, the first one sorts a collection, the second one alters the sql of the query builder.
To be able to use the sortBy function one first needs to retrieve the collection. These functions can still be chained by using:
return $team->users()->sortBy('currentQueueLength');
optionally one could also use orderByRaw if you are willing to write a custom sql query for the sorting.
In my edmx model are 2 related tables: Challenge and ChallengeNote (has FK back to ChallengeID)
I can do this in breeze all day long
var qry = dataservice.getQuery("Challenges");
However, this fails every time:
var qry = dataservice.getQuery("Challenges").expand("ChallengeNotes");
The searchFailed is called and is the only error information in the console.
return dataservice.execute(qry.inlineCount(true))
.then(seachSucceeded)
.fail(searchFailed);
Does Breeze support relational data like this?
Does one need to write some custom code to support?
What am I missing?
Here's related answered question, but I was already following (unless I missed something) the answer's solution (and why I have the 2 context.Configuration settings in my ContextProvider).
breezejs-error-when-loading-an-entity-with-related-data
Here's another similar question that's been unanswered breeze-expand-query-fails-with-object-object-has-no-method-getproperty
Here's my provider code (want to use the BeforeSaveEntity override further on in the project):
public class ModelProvider : EFContextProvider<ModelEntities>
{
public ModelProvider()
: base()
{
this.Context.Configuration.LazyLoadingEnabled = false;
this.Context.Configuration.ProxyCreationEnabled = false;
}
}
Here's my controller code:
[BreezeController]
public class DataController : ApiController
{
readonly ModelProvider _contextProvider = new ModelProvider();
[HttpGet]
public string Metadata()
{
return _contextProvider.Metadata();
}
[Queryable(AllowedQueryOptions = AllowedQueryOptions.All)]
[HttpGet]
public IQueryable<Challenge> Challenges()
{
return _contextProvider.Context.Challenges.Include(x => x.ChallengeNotes);
}
[HttpPost]
public SaveResult SaveChanges(JObject saveBundle)
{
return _contextProvider.SaveChanges(saveBundle);
}
[HttpGet]
public IQueryable<ChallengeNote> ChallengeNotes()
{
return _contextProvider.Context.ChallengeNotes;
}
}
When I browse to the URL, it's including the related entity:
http://localhost:53644/breeze/data/Challenges?$filter=Active%20eq%20true&$top=10&$expand=ChallengeNotes&$inlinecount=allpages
Here is the data coming from the Controller
At this point all things, imo, are pointing to Breeze configuration on either the Server or Client.
TIA
Breeze absolutely does support this, but you do need to make sure that your Entity Framework model is set up correctly. Take a look at the DocCode sample in the Breeze zip for a number of examples of using both expand (client side) or EF include (server side) clauses.
One guess about your problem is that you are using the Breeze camelCasing naming convention and therefore your "expand" clause should be
var qry = dataservice.getQuery("Challenges").expand("challengeNotes");
i.e. "challengeNotes" (note the casing) is the name of the client side property that corresponds to a server side property of "ChallengeNotes". To clarify, "expand" clauses take the names of client side "properties" as parameters and property names are what are transformed as a result of the Breeze.NamingConvention.
In contrast, a query resource name i.e. "Challenges" in your example is the name of the server side resource ( as a result of marking your "Challenges" method with the [HttpGet] annotation. This name is NOT affected by the NamingConvention.
Side notes: Your example has both an expand and an Include clause. Either of these is sufficient all by itself. You do not need both. In general you can either include an "expand" clause in your client side query OR have an Entity Framework "Include" clause on the server. The advantage of the first is that you can control the expand on the client, the advantage of the second is that you can insure that every query for a specified resource always fetches some related entities.
Hope this helps!
I have a situation where starting from the 2nd call and subsequent ones (Ajax GET calls), AutoMapper is reusing the previous value (the value from the 1st call that comes from a click in an action link). It's like a "caching" problem...
public virtual ActionResult List(int assessmentId, int? chapterId, bool? isMenuClick)
{
Mapper.CreateMap<Element, AssessmentQuestionViewModel>().
ForMember(dest => dest.AssessmentId, opt => opt.MapFrom(e => assessmentId));
...
}
It doesn't matter if I use UseValue, ResolveUsing or MapFrom in the above opt => lambda. The behavior is the same, that is, it reuses the value from previous calls.
AssessmentId property does not exist in the source type ( Element ). This way I try to assign AssessmentId a value that "may" change dynamically during subsequent calls to the method where I have this code. assessmentId is a parameter in my ASP.NET MVC action method as shown above in the method signature.
Then I call this code in the List action method:
var questions =
Mapper.Map<IEnumerable<Element>, IEnumerable<AssessmentQuestionViewModel>>
(Database.Elements.Where(e => !elementIds.Contains(e.ElementId) &&
e.Standard.ChapterId == chapterId));
The first time, questions is OK, that is, all AssessmentQuestionViewModel objects have the AssessmentId property set correctly as per the CreateMap defined.
Starting from the 2nd call, it reuses the assessmentId from the 1st call and it messes up with my business logic because I expect it to map AssessmentId to the updated assessmentId that's being passed as a parameter to the List method.
Just to be sure: I've set a breakpoint in the code and I can see that the value of the assessmentId parameter is correct. It's just the returned mapped objects questions that have the wrong value in the AssessmentId property - a value that differs from the current assessmentId value. The values should be equal as I understand it since I'm asking AutoMapper to do the mapping using that current value.
I have AutoMapper 2.2.1-ci9000 (Prerelease), but I tested this with the previous version and I saw this same behavior. I updated to the Prerelease thinking that this "misbehavior" would go away.
I think this is a bug. Please correct me if I'm wrong or if I'm trying to use it in a way not supported. :)
I think the problem here your trying to create multiple mappings of the same type - which AutoMapper doesn't support. Everytime your List action is called, you create a new mapping (which has a different ForMember(...) clause). AutoMapper won't throw an exception it just ignores the duplicate mapping so what you are seeing here isn't a bug, it's expected behaviour.
ForMember is infact called on every map, however, you have a scoping issue here as your variable is hard-coded into the expression. As a work-around you could do something like:
public class MyController
{
public MyController()
{
// define mapping once, but make assessment expression dynamic
Mapper.CreateMap<Element, AssessmentQuestionViewModel>().
ForMember(dest => dest.AssessmentId, opt => opt.MapFrom(e => GetCurrentAssessmentId()));
}
private int GetCurrentAssessmentId()
{
return (int)TempData["AssessmentId"];
}
public ActionResult List(int assessmentId, ...)
{
// store current assessment temporarily
TempData.Add("AssessmentId", assessmentId);
// execute mapping
var questions = Mapper.Map<IEnumerable<Element>, IEnumerable<AssessmentQuestionViewModel>>
(Database.Elements.Where(e => !elementIds.Contains(e.ElementId) &&
e.Standard.ChapterId == chapterId));
}
}
I will say though, your jumping through a lot of hoops for this to work, it would be much simpler to manually set the property without the help of AutoMapper e.g.
var questions = Mapper.Map<IEnumerable<Element>, IEnumerable<AssessmentQuestionViewModel>>(...);
foreach (var q in questions)
{
q.AssessmentId = assessmentId;
}
I'm using logical delete in my system and would like to have every call made to the database filtered automatically.
Let say that I'm loading data from the database in the following way :
product.Regions
How could I filter every request made since Regions is an EntitySet<Region> and not a custom method thus not allowing me to add isDeleted = 0
So far I found AssociateWith but I'd hate to have to write a line of code for each Table -> Association of the current project...
I'm looking into either building generic lambda Expressions or.. something else?
You could create an extension method that implements your filter and use that as your convention.
public static class RegionQuery
{
public static IQueryable<Region> GetAll(this IQueryable<Region> query, bool excludeDeleted=true)
{
if (excludeDeleted)
return query.Regions.Where(r => !r.isDeleted);
return query.Regions;
}
}
So whenever you want to query for regions you can make the following call to get only the live regions still providing an opportunity to get at the deleted ones as well.
context.Regions.GetAll();
It my be a little wonky for access the Products property, but still doable. Only issue is you would have to conform to the convention. Or extend the containing class.
someProduct.Regions.GetAll();
I hope that helps. That is what I ended up settling on because I haven't been able to find a solution to this either outside of creating more indirection. Let me know if you or anyone else comes up with a solution to this one. I'm very interested.
It looks to me like you're using a relationship between your Product and Region classes. If so, then somewhere, (the .dbml file for auto-generated LINQ-to-SQL), there exists a mapping that defines the relationship:
[Table(Name = "Product")]
public partial class Product
{
...
private EntitySet<Region> _Regions;
[Association(Storage = "_Regions")]
public EntitySet<Region> Regions
{
get { return this._Regions; }
set { this._Regions.Assign(value); }
}
...
}
You could put some logic in the accessor here, for example:
public IEnumerable<Region> Regions
{
get { return this._Regions.Where(r => !r.isDeleted); }
set { this._Regions.Assign(value); }
}
This way every access through product.Regions will return your filtered Enumerable.