OData v4 custom route using DTO problems - c#

I have setup an OData v4 controller from an EF 6 model. All works fine when using the generated code.
On some objects that will be referenced in drop downs I want to limit the data sent so DTOs seem like the best approach. So I now have a controller the works fine with the normal object but as soon as I use a DTO on a custom route I run into trouble.
I've used the code below and it generated the DTO collection properly but when it returns the data I end up on the client with a 406 Not Acceptable status.
Any suggestions about what is going wrong?
As a side note, is my strategy of wanting to provide a full and a cut down object the best way to do it (so far as standards go)?
// GET: odata/AMO_GeneralStateTypes(5)/AMO_GeneralStates_Basic
[EnableQuery]
[Route("odata/AMO_GeneralStateTypes({key})/AMO_GeneralStates_Basic")]
public IQueryable<AMO_GeneralStatesDTO> GetAMO_GeneralStates_Basic([FromODataUri] int key)
{
return db.AMO_GeneralStateTypes
.Where(m => m.StateTypeId == key)
.SelectMany(m => m.AMO_GeneralStates.Select(n => new AMO_GeneralStatesDTO()
{
StateId = n.StateId,
StateGuid = n.StateGuid,
ParentStateId = n.ParentStateId,
State = n.State,
TypeId = n.TypeId,
SortOrder = n.SortOrder,
Enabled = n.Enabled
}));
}

For 406 Not Acceptable, please make sure the return type is defined in the finally Edm Model. That is, AMO_GeneralStatesDTO is type that Edm model can serialize.
BTW, For Web API OData,
[Route("odata/AMO_GeneralStateTypes({key})/AMO_GeneralStates_Basic")]
should be
[ODataRoute("odata/AMO_GeneralStateTypes({key})/AMO_GeneralStates_Basic")]

I have met some problems like this in odata webapi for dto
here is my solution: you need to register your entitytype for odata or it will come a 406 error
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntityType<YourDTOType>();
Hope it's helpful

Related

Routing controllers with the same name in multiple OData contexts in the same app

I am using .Net Core 2.2 as my underlying framework here, so this information might not be inkeeping with how things are currently done (if so please, I welcome the feedback on how to put this right).
I have an Api App that initializes multiple OData contexts that happen to have some crossover in entity set names and thus controller names.
In this case I have a "Core" OData context + model and a "Members" context + model.
the net result is that both of these OData models contains a Users entity set with controllers looking like this ...
[ODataRoutePrefix("Members/User")]
public class UserController : MembersEntityODataController<Members.User> { }
[ODataRoutePrefix("Core/User")]
public class UserController : CoreEntityODataController<Core.User> { }
... they essentially do the same job but refer to entities stored in the same table but in different databases.
I can't seem to for the life of me figure out how to initialize / declare these controllers such that the routing actually works and instead all requests for both ~/Core/User and ~/Members/User result in a members user controller being passed on to handle the request.
My understanding is that this is the exact scenario that the ODataRoutePrefix attribute was built to solve (amongst others) and it doesn't seem to help here.
to make the process of adding new OData models to my API easier I wrapped up the model construction in my own model building but the net result is a core call to use odata during startup which looks like this ...
app.UseMvc(routeBuilder =>
{
routeBuilder.EnableDependencyInjection();
routeBuilder.MapRoute("default", "{controller=Home}/{action=Get}");
});
var builders = new ODataModelBuilder[] {
new MembersModelBuilder(),
new CoreModelBuilder(),
new B2BModelBuilder(),
new SearchModelBuilder()
};
foreach (var builder in builders)
{
var model = builder.Build();
app.UseOData(model.Context + "Api", builder.GetType().Name.Replace("ModelBuilder", ""), model.EDMModel);
}
Do I need to do something special here to make routing work as intended?
It does seem that this "type of scenario" has been considered before as I'm seeing things like this ...
https://github.com/OData/WebApi/issues/1494
... which talks about API versions ... my case isn't quite this, but it's close enough that I figure the same parts of the framework logic should apply (somewhat).
#xuzhg talks about the solution being to apply the ODataRoute Attribute on the Actions ...
[ODataRoute("Sensors", RouteName = "ODataRouteV1")]
public IActionResult Get()
[ODataRoute("Sensors", RouteName = "ODataRouteV2")]
public IActionResult Get()
... I need to do presumably be able to do the same thing at the controller level but alas this attribute cannot be used on classes only methods.
Obviously i'm sourcing my understanding from this ...
https://learn.microsoft.com/en-us/odata/webapi/attribute-routing
... which talks about using the ODataRoutePrefix attribute to apply context to the routing engine for when a controller should be selected.
Have I hit an edge case here ?

Querying data with Web API and entity framework through odata

I'm trying to make odata rest-based service calls through a URI.
I have an ADO.NET Entity data model that is mapped to one of the tables on the SQL back end. Which created the necessary edmx file under my "Models" folder
I then created a controller labeled spcontrol using the "Web API 2 Odata Controller with actions, using Entity Frameowrk" option. I then set the model class and Data context class and the visual studio scaffolding did the rest. The first few lines of the controller look like this...
public class spcontrolController : ODataController
{
private Entities db = new Entities();
// GET odata/spcontrol
[Queryable]
public IQueryable<Database_table_Name> Getspcontrol()
{
return db.Database_table_Name;
}
// GET odata/spcontrol(5)
[Queryable]
public SingleResult<Database_table_Name> GetDatabase_table_Name([FromODataUri] int key)
{
return SingleResult.Create(db.Database_table_Name.Where(Database_table_Name=> Database_table_Name.ID == key));
}
and then it goes on for the PUT, POST, PATCH, DELETE methods. In my WebApiConfig.cs file, I set the following lines
using System.Web.Http.OData.Builder;
using resttest.Models;
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Database_table_Name>("spcontrol");
config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());
Finally, I'm trying to query the table with odata. So my url looks something along the lines of
http://testsite:8012/odata/spcontrol?$top=5
However, this call does not return any data, but only returns the following text
{
"odata.metadata":"http://localhost:33421/odata/$metadata#spcontrol","value":[
]
}
How can I get the actual values within the SQL table? What am I doing wrong?
Looks like it is working fine - you just have no data to return so array in response payload is empty. Add some data and hit the endpoint again.
Run a profiler on your database to see what commands are being sent from your service to check correct tables are being used etc

How to hide identity column in EF Code First model for OData

This is really an EF Code First question, but sometimes the context of OData makes a difference. The question is simple. Our underlying SQL Server tables/views have Identity columns as the (surrogate) primary key. They also have a 'code' field which represent unique identifiers which users are familiar with. Simple example: CarModel: Id = 3, Code = 'Ford';
I have successfully create the EF Mode and OData to navigate using the 'Code' field as the navigation key, using Data Annotations, and the underlying tables/views have indexes on those columns, so this is acceptable. But I would really like to have the Id column as the key, and as the navigation property in the model, but not have it show up in the response. Perhaps this is where the OData part makes a difference, because I would prefer not to do any complex interception and re-shaping of the response. Setting the columns to 'private' or even 'internal' throws an error during model generation: 'Table X has no key defined'
Is it possible to define an identity column in the EF Model, but not have it part of the OData entity/response?
Edit:
So, my comment below is still valid. This is both not a good idea for identity/key columns, nor would 'hiding' them allow the model to compile correctly. However, thanks to some links from #mreyeros and a social.msdn post from Vitek Karas, here are some notes.
The System.Data.Services namespace has an IgnorePropertiesAttribute. It allows properties in your model to be 'hidden'. However, as Vitek says, it currently only works with the ReflectionProvider, not with EF. (And you need to make sure you are referencing the correct library if you are using a NuGet managed release.
That said, the fluent configuration API will work in EF/OData:
modelBuilder.Entity<Foo>().Ignore(f => f.Password);
However, not only does it hide a property in the response, it also hides it from the model and the database (which might be okay for read-only query models). So if you mark a navigation key as hidden, the model will not compile. And if you mark any other property as hidden, but reference it in a QueryInterceptor, an exception will be thrown.
What you need to do is make an odata controller that returns a projected subset of the original entity.
//in WebApi Config Method
config.MapHttpAttributeRoutes();
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<FullEntity>("FullData");
builder.EntitySet<SubsetEntity>("SubsetData");
config.Routes.MapODataServiceRoute("odata", "odata", builder.GetEdmModel());
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional, action = "GET" }
);
SetupJsonFormatters();
config.Filters.Add(new UncaughtErrorHandlingFilterAttribute());
... then have two Odata Controllers one for FulLData, one for SubsetData (with different security),
namespace myapp.Web.OData.Controllers
{
public class SubsetDataController : ODataController
{
private readonly IWarehouseRepository<FullEntity> _fullRepository;
private readonly IUserRepository _userRepository;
public SubsetDataController(
IWarehouseRepository<fullEntity> fullRepository,
IUserRepository userRepository
)
{
_fullRepository = fullRepository;
_userRepository = userRepository;
}
public IQueryable<SubsetEntity> Get()
{
Object webHostHttpRequestContext = Request.Properties["MS_RequestContext"];
System.Security.Claims.ClaimsPrincipal principal =
(System.Security.Claims.ClaimsPrincipal)
webHostHttpRequestContext.GetType()
.GetProperty("Principal")
.GetValue(webHostHttpRequestContext, null);
if (!principal.Identity.IsAuthenticated)
throw new Exception("user is not authenticated cannot perform OData query");
//do security in here
//irrelevant but this just allows use of data by Word and Excel.
if (Request.Headers.Accept.Count == 0)
Request.Headers.Add("Accept", "application/atom+xml");
return _fullRepository.Query().Select( b=>
new SubsetDataListEntity
{
Id = b.Id,
bitofData = b.bitofData
}
} //end of query
} //end of class
I am not 100% sure if this is the case but, if you apply the "ScaffoldColumn(false)" attribute to the property that you want to hide it may work. Here is the MSDN entry for that attribute

asp web api patch implementation

Assume i have this model
public partial class Todo
{
public int id { get; set; }
public string content { get; set; }
public bool done { get; set; }
}
And i send this as json data to my controller as a patch request.
This is mearly the action of toggeling a checkbox.
I think it makes sence that i only want to sent that to my server, and not the entire model.
{ "id":1, "done" : true }
What does my WebApi controller need to look like in order to correctly process this, simple, json patch request ? Should i be using web api for this, or should i use a more rpc styled approach with mvc ?
It seems like a very basic thing to do, but i can't seem to get it right !
I think i might need to use a different parameter in my controller method, but i'm not sure.
Thank you for your time.
You can find PATCH feature in the OData pre-release Nuget package: Microsoft.AspNet.WebApi.OData.
Information how you can use it to create an action for handling PATCH can be found in the Partial Updates (PATCH requests) section of the blog post about OData support in ASP.NET Web API.
Changing the method to PATCH doesn't change Web API behaviour in any way. There is no built in mechanism for doing partial updates. One of the reasons there was no PATCH method for so long is that there is no ubiquitous media type for applying patches to resources.
Secondly, you are asking Web API to do object serialization for you so there just is no such concept of applying a partially updated object. There would be so many conventions to agree on, what does a null value mean, what about an empty value, how do I say "don't update this DateTime". What about related objects, child items? How do you cause a child item to be deleted? Unless the CLR team implements some concept of a type that only contains a subset of members from another type, partial updates and object serialization are not going to go well together.
Aliostad mentions UpdateModel and that is possible when updating from a HTML form because the media type application/x-www-form-urlencoded explicitly allows for an arbitrary set of name value pairs. There is no "object serialization" going on. It is just a match of names from the form being matched to names on the Model object.
For myself, I created a new media type I use to do partial updates that works like a form but is more advanced in that it can handle hierarchial data and it maintains an order to the updates.
ASP.NET Web API seems to be missing UpdateModel, TryUpdateModel, etc.
In ASP.NET MVC, you could use them to achieve the desired effect. I have created a work item in ASP.NET Web Stack which you can vote for and if it gets enough votes, it will be implemented.
I used Microsoft.AspNet.WebApi.OData for my project and I had some problems working with JSON (working with numbers in my case). Also, the OData package has some dependencies which, from my point of view, are too big for a single feature (~7MB with all dependecies).
So I developed a simple library which do what you are asking for: SimplePatch.
How to use
Install the package using:
Install-Package SimplePatch
Then in your controller:
[HttpPatch]
public IHttpActionResult PatchOne(Delta<Todo> todo)
{
if (todo.TryGetPropertyValue(nameof(Todo.id), out int id)) {
// Entity to update (from your datasource)
var todoToPatch = Todos.FirstOrDefault(x => x.id == id);
if (todoToPatch == null) return BadRequest("Todo not found");
todo.Patch(todoToPatch);
// Now todoToPatch is updated with new values
} else {
return BadRequest();
}
return Ok();
}
The library support massive patch too:
[HttpPatch]
public IHttpActionResult PatchMultiple(DeltaCollection<Todo> todos)
{
foreach (var todo in todos)
{
if (todo.TryGetPropertyValue(nameof(Todo.id), out int id))
{
// Entity to update (from your datasource)
var entityToPatch = Todos.FirstOrDefault(x => x.id == Convert.ToInt32(id));
if (entityToPatch == null) return BadRequest("Todo not found (Id = " + id + ")");
person.Patch(entityToPatch);
}
else
{
return BadRequest("Id property not found for a todo");
}
}
return Ok();
}
If you use Entity Framework, you have to add only two lines of code after the call to the Patch method:
entity.Patch(entityToPatch);
dbContext.Entry(entityToPatch).State = EntityState.Modified;
dbContext.SaveChanges();
Furthermore, you can exclude some properties to be updated when the Patch method is called.
Global.asax or Startup.cs
DeltaConfig.Init((cfg) =>
{
cfg.ExcludeProperties<Todo>(x => x.id);
});
This is usefull when you are working with an entity and you don't want to create a model.

partial update with asp.net web api

I created a simple todo list application in asp.net mvc 3 with web api and dbContext. ( with backbone and requirejs for the client )
Everything works fine, but i am sort of bothered with the fact that i have to sent the entire model to the server if i check or uncheck a todo item as done.
I would like to only sent the "done" field when submitting data.
I should mention that i'm also using the JsonNetFormatter to use JSON.NET as the default Serializer ( explained here: http://blogs.msdn.com/b/henrikn/archive/2012/02/18/using-json-net-with-asp-net-web-api.aspx ).
Currently this is my api controller method to update the model
public HttpResponseMessage Put(Todo todo)
{
_db.Entry(todo).State = EntityState.Modified;
_db.SaveChanges();
return new HttpResponseMessage(HttpStatusCode.NoContent);
}
It takes this as json data
{"content":"Pick up milk","done":false,"id":10}
Off course this works, but it is updating the entire model, it should only update 1 field.
I can achieve to only send the changed fields to the server from the browser, but i am not sure what the web api method should look like.
I was thinking about doing something with FormCollection but this doesn't seem to work with the web api as it appears to be trying to serialize the submitted formvalues directly to FormCollection type, I get this error.
Cannot deserialize JSON object (i.e. {"name":"value"}) into type 'System.Web.Mvc.FormCollection'.
How can i send a partial update for 1 or more fields from a model to my web api ?
I only want to send the updated fields to the server, and from there only update those fields to the database. I certainly do not want to query the database before updating.
One approach would be to use a tool called Automapper and configure it so that null values don't overwrite existing ones when mapping Todo objects. For example:
Mapper.CreateMap<Todo,Todo>()
.ForMember(d => d.Id, o => o.Ignore())
.ForAllMembers(mo => mo.Condition(cond => !cond.IsSourceValueNull));
Then you would just have to map the received object value to the existing one, like this:
Mapper.Map(todo, item);
Another suggestion would be to use PATCH instead of PUT which is more appropriate to partial updates of resources according to REST.
You need to query the original object from the database, set its properties & call the _db.SaveChange()
public HttpResponseMessage Put(Todo todo){
var item = _db.Todo.First(i => i.id = todo.id);
item.Content = todo.Content;
item.Done = todo.Done;
_db.SaveChanges();
return new HttpResponseMessage<Todo>(HttpStatusCode.Accepted);
}
Ref.: http://msdn.microsoft.com/en-us/library/dd456854.aspx

Categories