I'm querying my service using a url like:
http://a.com:3080/odata/DiscussionVM(6)?$expand=Section,User
on controller method:
[EnableQuery(MaxExpansionDepth = 7)]
public SingleResult<DiscussionVM> GetDiscussionVM([FromODataUri] int key)
{
return SingleResult.Create(db.DiscussionVMs.Where(discussionVM => discussionVM.DiscussionId == key));
}
This works and returns the correct JSON.
However I then run the slightly more advanced query on a different model:
http://a.com:3080/odata/OrganisationVM(30)?&$expand=Categories($expand=Discussions($expand=Section,User))
and controller action:
// GET: odata/OrganisationVM(5)
[EnableQuery(MaxExpansionDepth = 5, AllowedQueryOptions = AllowedQueryOptions.All)]
public SingleResult<OrganisationVM> Get([FromODataUri] int key)
{
return SingleResult.Create(db.OrganisationVMs.Where(organisationVM => organisationVM.OrganisationId == key));
}
this returns the below DiscussionVM JSON:
{
#odata.type: "#Models.DiscussionVM",
DiscussionId: 6,
Section_SectionID: 1005,
User_Id: "4cecc52e-ac3a-4696-ac6c-175af2a6378a",
DateCreated: "2014-12-06T00:00:00Z",
OrgCat_OrganisationCategoryId: 1,
Text: "Dummy section",
Html: null,
IsUserCreated: true,
Organisation_OrganisationId: null,
Positives: null,
Negatives: null,
CommentCount: 1
}
But contains no User or Section object. No error is thrown. The correct objects are queried (profiled) in the database and data including user and section are returned.
I discovred that oData needs the expanded entities to be referenced in its Edm Model.
if not it will stop expanding after the first level, that's why further expands will not work.
Just add your expandable EntitySet to the ODataConventionModelBuilder in your IEdmModel (in MapODataServiceRoute's model config) :
var builder = new ODataConventionModelBuilder();
// ...
builder.EntitySet<Categories>("categories");
// ...
Hope this helps.
From what Brad and I have gathered in this SO answer, it could be a matter of mixing complex types with entity types. Expand plays very well if all your types are entities, but if you mix both, you end up with weird behavior like you and I are having.
If you do mix them, the expand cascade has to start with entity types and end with complex types. The expand chain seems to end where a complex type has an entity type property.
This could come from v3 where a complex type referring to an entity type was flat not supported. It is in V4 but it is not totally clean with WebAPI as we can see.
The lack of documentation and support on the matter makes it difficult to claim this is the absolute truth, but at least it explained my situation and made my stuff work. Hope it helps you too.
I have never seen your $expand syntax before. Where did you get it from? I think you must expand your query the following way:
http://a.com:3080/odata/OrganisationVM(30)?$expand=Categories/Discussions/Section,Categories/Discussions/User
Assuming Odata V4, here are some examples of the standard.
Related
I would like to clarify which approach I should use for my resolver functions in Apollo + GraphQL
Let's assume the following schema:
type Post {
id: Int
text: String
upVotes: Int
}
type Author{
name: String
posts: [Post]
}
schema {
query: Author
}
The ApoloGraphql tutorial suggests a resolver map like this:
{Query:{
author(_, args) {
return author.findAll()
}
}
},
Author {
posts: (author) => author.getPosts(),
}
As far as I know, every logic regarding posts e.g. get author with posts where the count of post upVotes > args.upVotes, must be handled in the author method. That gives us the following resolver map:
{Query:{
author(_, args) {
return author.findAll({
include:[model: Post]
where: {//post upVotes > args.upVotes}
})
}
},
Author {
posts: (author) => author.getPosts(),
}
Calling author, will first select the author with posts in one joined query, where posts are greater than args.upVotes. Then it will select the posts for that author again, in an additional query because of Author ... getPosts()
Technically, I can reach the same result by removing Author, since posts are already included in the small author method.
I have the following questions:
Do I need this statement? In which cases?
Author {
posts: (author) => author.getPosts(),
}
If no, then how can I find out if the posts field was requested so that
I can make the posts include conditionally, depending not only on the
arguments, but also on the requested fields?
If yes, which posts will contain the final result? Posts from the
include statement, or the getPosts()?
The resolver map you included in your question isn't valid. I'm going to assume you meant something like this for the Author type:
Author {
posts: (author) => author.getPosts(),
}
As long as your author query always resolves to an array of objects that include a posts property, then you're right in thinking it doesn't make sense to include a customer resolver for the posts field on the Author type. In this case, your query's resolver is already populating all the necessary fields, and we don't have to do anything else.
GraphQL utilizes a default resolver that looks for properties on the parent (or root) object passed down to the resolver and uses those if they match the name of the field being resolved. So if GraphQL is resolving the posts field, and there is no resolver for posts, by default it looks at the Author object it's dealing with, and if there is a property on it by the name of posts, it resolves the field to its value.
When we provide a custom resolver, that resolver overrides the default behavior. So if your resolver was, for example:
posts: () => []
then GraphQL would always return an empty set of posts, even if the objects returned by author.findAll() included posts.
So when would you need to include the resolver for posts?
If your author resolver didn't "include" the posts, but the client requested that field. Like you said, the problem is that we're potentially making an unnecessary additional call in some cases, depending on whether your author resolver "includes" the posts or not. You can get around that by doing something like this:
posts: (author) => {
if (author.posts) return author.posts
return author.getPosts()
}
// or more succinctly
posts: author => author.posts ? author.posts : author.getPosts()
This way, we only call getPosts if we actually need to get the posts. Alternatively, you can omit the posts resolver and handle this inside your author resolver. We can look at the forth argument passed to the resolver for information about the request, including which fields were requested. For example, your resolver could look something like this:
author: (root, args, context, info) => {
const include = []
const requestedPosts = info.fieldNodes[0].selectionSet.selections.includes(s => s.name.value === 'posts'
if (requestedPosts) include.push(Post)
return Author.findAll({include})
}
Now your resolver will only include the posts for each author if the client specifically requested it. The AST tree object provided to the resolver is messy to parse, but there are libraries out there (like this one) to help with that.
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.
I have a situation in AutoMapper where I need to create a mapping with an interface destination. This is not a problem, and when using the normal Mapper.Map, it returns a proxy class as expected. However, when I try to do something similar with .Project().To(), which I need to use because an ORM is involved, I have issues. Here is the exact code that is failing which I replicated in a unit test:
AutoMapper.Mapper.CreateMap<RoleDto, IRole>(); //both just have Id/Name
IQueryable<RoleDto> roleDtos = new List<RoleDto>
{
new RoleDto { Id = 1, Name = "Role1" },
new RoleDto { Id = 2, Name = "Role2" }
}.AsQueryable();
//this works:
List<IRole> roles = roleDtos.Select(
roleDto => AutoMapper.Mapper.Map<IRole>(roleDto)
).ToList();
//this does not work:
List<IRole> roles2 = roleDtos.Project().To<IRole>().ToList();
I'm getting ArgumentException:
ArgumentException: Type 'UnitTestProject5.IRole' does not have a default constructor
In my real implementation the .Select is being performed on an Entity Framework collection, which is why I need to stick with .Project().To().
I have no issues with .Project().To() if the destination is not an interface. Additionally, I have no issues with the interface destination if I use .Map().
How can I get the interface destination and .Project.To() to work at the same time? Why is .Project.To() not giving me proxy classes like .Map() is? Any ideas?
Thanks!
Mapper.Map() takes the linq-to-objects route to materialize objects. As you said, AutoMapper is capable of creating types on the fly if the mapped target is an interface.
Project().To() is a way to translate the whole query, including the mapping, into SQL. Which is great, because only the properties that are required for the target object are included in the SQL query. However, the things AutoMapper does for creating types on the fly (undoubtedly some Refection voodoo) can never be part of an expression tree that can be converted into SQL. That's why Project.To simply tries to new up an object, even if it's an interface.
You'll have to use a concrete type as a mapping target. Of course, this type can implement an interface, so you can keep the independence you want.
I've managed to create number of readonly Web Api OData services following the tutorials here: http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api. I'm therefore employing the ODataConventionModel builder to create the model from a set of entities (incidentally coming from a Telerik ORM). This all seems to work fine and I can happily issue queries, view the metadata and so forth on the service.
I've now tried to turn my attention to the other CRUD operations - firstly Create and have stumbled into a problem! Namely, the Post method fires correctly (CreateEntity) but the entity parameter is null - by doing a check against the ModelState.IsValid, it shows that the problem is a null ID (key) value. This is unsurprising because the database uses a Database Generated Identity for the ID column and therefore the ID would be created when the entity is saved into the database context.
I've therefore tried all sorts of ways of marking the ID column as database generated, but haven't managed to find anything. Strangely, I can't seem to find even one post of someone asking for this - surely I can't be the only one?!
I noted that when looking at the EF modelbuilder (for example here: http://forums.asp.net/t/1848984.aspx/1) there appears to be a means of affecting the model builder with a .HasDatabaseGeneratedOption property, but no similar option exists in the System.Web.Http.OData equivalent.
So the questions therefore are:
Is there a means of altering the model builder (or something else) so that the controller will accept the object and deserialize the entity even with a null key value?
If so, how can I do this?
If not, any suggestions as to other options?
I realise that I could potentially just populate the object with an (in this case) integer value from the client request, but this seems a) semantically wrong and b) won't necessarilly always be possible as a result of the client toolkit that might be used.
All help gratefully received!
Many thanks,
J.
You need to create a viewmodel for insert which does not contain the ID parameter. Use Automapper to map the properties of the incoming insert-model to your data entities.
The problem that you're having is that ID is a required attribute in your data model because it is your PK, except during insert, where it shouldn't be specified.
In my case, my database-generated key is a Guid.
As a work-around, in my TypeScript client code, I submit (via http POST) the object with an empty Guid like this: Note: ErrorId is the key column.
let elmahEntry: ELMAH_Error = {
Application: 'PTUnconvCost',
Host: this.serviceConfig.url,
Message: message,
User: that.userService.currentUserEmail,
AllXml: `<info><![CDATA[\r\n\r\n${JSON.stringify(info || {})}\r\n\r\n]]></info>`,
Sequence: 1,
Source: source,
StatusCode: 0,
TimeUtc: new Date(Date.now()),
Type: '',
ErrorId: '00000000-0000-0000-0000-000000000000'
};
Then, in my WebApi OData controller, I check to see if the key is the empty guid, and if so, I replace it with a new Guid, like this:
// POST: odata/ELMAH_Error
public IHttpActionResult Post(ELMAH_Error eLMAH_Error)
{
if (eLMAH_Error.ErrorId == Guid.Empty)
{
eLMAH_Error.ErrorId = Guid.NewGuid();
}
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.ELMAH_Error.Add(eLMAH_Error);
try
{
db.SaveChanges();
}
catch (DbUpdateException)
{
if (ELMAH_ErrorExists(eLMAH_Error.ErrorId))
{
return Conflict();
}
else
{
throw;
}
}
return Created(eLMAH_Error);
}
Using MVC 3 with Razor view engine.
I have this View:
#model dynamic
#{
var products = (List<ListItemBaseModel>)Model.Products;
var threshold = (int)(Model.Threshold ?? 1);
var id = Guid.NewGuid().ToString();
}
It is called from another view using this code:
#Html.Partial("PartialViewName", new { Products = Model, Threshold = 5 })
In both Views, when I debug them and watch Model, it seems to contain the correct object.
When I execute the code I get an error on the var products = line saying:
'object' does not contain a definition for 'Products'
Why do I see this error?
When I watch the Model object in debugging mode it looks all right (having 2 properties: Products and Threshold)
I just tried this (dynamic view model in CSHTML) and got the same error as your when using an anonymous class, but it worked fine if I created a named class. I searched but haven't seen this documented anywhere.
// error
return View(new { Foo = 1, Bar = "test" });
// worked
return View(new TestClass { Foo = 1, Bar = "test" });
David Ebbo clarified that you can't pass an anonymous type into a dynamically-typed view because the anonymous types are compiled as internal. Since the CSHTML view is compiled into a separate assembly, it can't access the anonymous type's properties. Due to this forum post, David Ebbo clarified on (Dec 22 2011) that MVC 3 now has direct support for dynamic.
On .NET 4.0 Anonymous types can easily be converted to ExpandoObjects and thus all the problems are fixed with the overhead of the conversion itself.
Check out here
This has nothing to do with anonymous types having internal properties
It is perfectly possible to pass anonymous types from a view to a partial view
I encountered the same problem today and it was nothing (directly) to do with the problem of passing anonymous types and their inherent internal properties.
As such, in relation to the OPs question, the answer by #Lucas is irrelevant - even though the workaround will work.
In the OPs question, an anonymous type is being passed from a view in assembly X to a partial in assembly X, therefore the problem that David Ebbo outlined of the properties being internal for anonymous types is of no consequence; the types compiled for the view, the partial and the anonymous type are all contained in the same assembly.
So what is causing the sudden failure to pass an anonymous type from a view to a partial?
At least in my situation, I discovered that it was due to having another view in the SAME FOLDER that specifies a model type that cannot be resolved. Views get compiled at runtime, and so it would make sense as a failure at runtime to compile the views would also mean a failure to compile the dynamic types and the partial would simply receive an object. It's not immediately obvious what is going on, but in the OPs specific example (and mine) this is more than likely the cause of the problem.
It is interesting to note that if the model type is correct but another part of the view doesn't compile then anonymous types are not affected in the same way. This must be down to how Razor breaks up the dynamic compilation of the component parts of the view.
Once you correct the offending view, either rebuild the whole solution or clean and rebuild the project before checking to see if it's fixed.
To ensure you are not caught out by this again you can enable compile time compilation of your Razor views by adding this to your csproj file:
<PropertyGroup>
<MvcBuildViews>true</MvcBuildViews>
</PropertyGroup>
Add the following class anywhere in your solution (use System namespace, so its ready to use without having to add any references) -
namespace System
{
public static class ExpandoHelper
{
public static ExpandoObject ToExpando(this object anonymousObject)
{
IDictionary<string, object> anonymousDictionary = HtmlHelper.AnonymousObjectToHtmlAttributes(anonymousObject);
IDictionary<string, object> expando = new ExpandoObject();
foreach (var item in anonymousDictionary)
expando.Add(item);
return (ExpandoObject)expando;
}
}
}
When you send the model to the view, convert it to Expando :
return View(new {x=4, y=6}.ToExpando());
Instead of using the dynamic Model type within the partial view.
You can call the anonymous object attributes using #ViewData.Eval("foo") instead of #Model.foo.
Then you can remove #Model dynamic from the view.
I came across this issue recently when passing some attributes between views for the Facebook Social Comments Integration. Example code:
Html.RenderPartial(#"Layouts/Partials/_Comments", new {currentUrl = Model.CurrentPage.GetAbsoluteUrl(), commentCount = 5 });
Then in my view I just had this div:
<div class="fb-comments" data-href="#ViewData.Eval("currentUrl")" data-numposts="#ViewData.Eval("commentCount")" data-width="100%"></div>
i am not sure that you are getting this error because you are not implementing the work-around. i got the same error in a partial view. the solution was just to clean the build and rebuild it. if the syntax is correct, the code should work, but the razor engine may not be updating the code changes properly.
I worked around this issue by using a Dictionary.
#Html.Partial("_Partial", new Dictionary<string, string> { { "Key1", "Val1" }, { "Key2", "Val2" }, { "Key3", "Val3" } });
To use dynamic type you need to reference Microsoft.CSharp assembly