Which is the best approach for updating multiple records at a time using ASP.NET Web API and OData against an Entity Framework database?
Let's suppose we have a table in our database with a "State" field, and we want to do something similar to this SQL statement: "UPDATE Table set State = 1". Should I do a loop with GET and PUT for each record? I do not like it at all, I guess it must be a better way to accomplish this.
Thank you
It looks like you can't do this natively in OData, but there is one approach that will definitely work; just use a native Web API action to perform the update.
E.g.
[HttpPut]
[Route("resources/state")]
public IHttpActionResult UpdateState(int newState)
{
db.Database.SqlCommand("UPDATE Table SET State = #p0", newState);
}
You'd call this from a client using a PUT /resources/state?newState=1.
It might be clearer to make newState an enum:
public enum State
{
New = 0,
Processed = 1,
Error = 2,
etc.
}
[HttpPut]
[Route("resources/state")]
public IHttpActionResult UpdateState(State newState)
{
db.Database.SqlCommand("UPDATE Table SET State = #p0", (int)newState);
}
Then your call becomes PUT /resources/state?newState=Processed which is a little clearer.
OData supports such kinds of operations. You can use OData Action to update the state.
Blog: odata-actions-and-functions
Sample: ODataActionSample
Related
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'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);
}
I am working on an MVC3 project whose model is designed using Code First approach. I am using EF4 for ORM and have a need where I need two thing -
1. Intercept the context.SaveChages method
2.Do my own custom update query for one specific entity type only.
I got the first part working by overriding the SaveChanges() method like -
public override int SaveChanges()
{
var modifiedItems = this.ChangeTracker.Entries().Where(e => e.State == System.Data.EntityState.Modified && e.Entity is myEntityName);
foreach (var item in modifiedItems)
{
//ToDo: Write UPDATE Sql Query here
}
return base.SaveChanges();
}
How can I write my update query?
Thanks!
This is not possible. You can only map custom stored procedures to be used instead of CUD operations (only with EDMX) generated by EF but still you will have single stored procedure call per each operation and entity instance.
I've got a situation where I need to prefetch some entities through a many-to-many relationship. So it's like the classic BlogPost <- BlogPostTag -> Tag situation.
Yes, I'm aware of LoadOptions but I can't use it because it's a web application and I'm using the one datacontext per request pattern.
It also seems you can't use projection to prefetch many-to-many relationships. Yes? No?
I want to return IQueryable<Tag> based on a set of Blogs. The best I can do is get it to return IQueryable<IEnumerable<Tag>> by doing the following:
public IQueryable<Tag> GetJobsCategories(IQueryable<BlogPost> blogPosts)
{
var jobCats = from bp in blogPosts
select bp.BlogPostTags.Select(x => x.Tag);
return jobCats;
}
Can I flatten that? Am I missing something obvious? Is there another approach I can take?
And no, I can't change ORMs ;-)
This will work, you can just drill down in the linq query
public IQueryable<Tag> GetJobsCategories(IQueryable<BlogPost> blogPosts)
{
return from bp in blogPosts
from tag in bp.BlogPostTags
select tag;
}
If you declare the method as such:
public static IQueryable<Tag> GetJobsCategories(this IQueryable<BlogPost> blogPosts)
You can use it as extension method on queryables. For instance
myContext.BlogPosts.GetJobsCategories()
I am trying to build an ADO.NET Data Service with lots of entities and a few service operations. On one side I created a ASP.NET Web Application, in which an ADO.NET Entity Data Model and an ADO.NET Data Service are located. On the other side I created a second ASP.NET Web Application that has a Service Reference to the Data Service.
Entities are coming through very well, I can use LINQ to retrieve the data I want:
TestEntities entities = new TestEntities(
new Uri("http://localhost/service/service.svc"));
var query = from customer in entities.Customers
where customer.ID == 1234
select customer;
query.ToList();
This works. However, retrieving information through Service Operations completely eludes me.
Data Service-side code:
public static void InitializeService(IDataServiceConfiguration config) {
config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);
}
[WebInvoke]
public IQueryable<Customer> GetSomeCustomers() {
TestEntities entities = new TestEntities();
return from customer in entities.Customers
where customer.ID > 0 && customer.ID < 20
select customer;
}
When I added the Service reference to my client project, Visual Studio didn't pick up on any Service Operations. I know I can access them through constructed URIs and the BeginExecute method of either the DataServiceContext object or the TestEntities object (in this case), or something like that, but that is not how I want it.
What I want is to use LINQ to go through the returned data of the Service Operation.
Is this possible? It should be, right?
Simple stuff once you know.
Just a few things to know:
Currently DataServiceClientGenerator (which uses the EntityClassGenerator) doesnt create methods for the service operations.
Using CreateQuery method on the context is not supported for service operations, currently they work because there is no validation on the client side for that (you will notice that if you use CreateQuery the "()" is added to the end of the Query Method like this "http://localhost/service.svc/method()?parameter=2", you can use CreateQuery but it is not recommended.
Not all Service operations return values, but for this example i will only show an example for the ones that do.
public partial class NorthwindEntities
{
public IQueryable<Order> OrdersByRegion(int regionId)
{
return this.Execute<Orders>(new Uri(string.Format("{0}OrdersByCountry?regionId={1}", this.BaseUri, regionId), UriKind.RelativeOrAbsolute));
}
}
If you require more information please feel free to ask any questions.
PS.: On your example you dont need to create a new data context on your service operation (server side) the DataService has already a reference instantiated when the service is called.
You can actually override the create of the data context on the service side like this:
protected override NorthwindEntities CreateDataSource()
{
return new NorthwindEntities();
}