BreezeJs predicates behave differently on unmapped properties - breeze

I have a situation when I needed to bring custom data for child entities along with some properties from their parents..Here is my Code:
Models:
public class InvoiceHeader
{
public int Id { get; set; }
public short LocationId { get; set; }
public DateTime EntryDate { get; set; }
// some other properties
public Location Location { get; set; }
}
public class InvoiceItemsDto
{
public int Id { get; set; }
public int InvoiceHeaderId { get; set; }
public string ProductName { get; set; } // Not mapped
// some other properties
// Added to get the headers entry date and location:
public short LocationId { get; set; } // Not mapped
public DateTime EntryDate { get; set; } // Not mapped
}
Controller:
[HttpGet]
public IQueryable<InvoiceItemsDto> InvoiceItemsHeaders()
{
return _contextProvider.Context.InvoiceItems.Select(a => new InvoiceItemsDto()
{
Id = a.Id,
HeaderId = a.HeaderId,
// Some Other properties
ProductName = a.Product.ProductName,
EntryDate = a.InvoiceHeader.EntryDate,
LocationId = a.InvoiceHeader.LocationId
});
}
Javascript:
metadataStore.registerEntityTypeCtor(
'InvoiceItems',InvoiceItemInit , InvoiceItemInitializer);
// Here goes the unmapped properties
function InvoiceItemInit() {
this.ProductName = "";
this.LocationId = 0;
this.EntryDate = new Date();
}
// For Css styling only:
function InvoiceItemInitializer(Item) {
Item.isSelected = ko.observable(false);
Item.isDeleted = ko.observable(false);
Item.focused = ko.observable(false);
}
// Action:
var locationId_p = Predicate.create('LocationId', '==', locationId());
var dateFrom_p = Predicate.create('EntryDate', '>=', dateFrom().toJSON()); // throws an error.
var dateTo_p = Predicate.create('EntryDate', '<=', dateTo().toJSON()); //throws an error.
var ProductName_p = Predicate.create('ProductName', 'contains', productName()); // throws an error.
var whereClause = locationId_p.and(dateFrom_p).and(dateTo_p).and(ProductName_p);
var query = EntityQuery.from('InvoiceItemsHeaders').toType(entityNames.invoiceItem)
.where(whereClause)
.skip(skipPage * 50)
.take(50)
.orderBy('headerId')
.inlineCount(true);
;
The errors are JSON serialization stuff (e.g. ProductName is not surrounded with %27% which I believe it's a string indicator)
Changing the code for predicates to:
var dateFrom_p = Predicate.create('EntryDate', '>=', "datetime'" + dateFrom().toJSON() + "'");
var dateTo_p = Predicate.create('EntryDate', '<=', "datetime'" + dateTo().toJSON() + "'");
var ProductName_p = Predicate.create('ProductName', 'contains', "'" + productName() + "'");
works.. However, that does not apply to mapped properties as they are being JSON'd correctly..
Is there any other way to work around this unmapped dates and strings?
EDIT:
I have edited the original post for correcting (ONLY) the Pascal and camel cases
(my bad)
To clarify:
All the mentioned properties within the predicates are unmapped
i.e LocationId, ProductName and EntryDate that's why they have to be in PascalCase.
The problem I get is with predicates on those properties (Except for LocationId (int type))
locationId() , dateFrom(), dateTo() and productName() are the observables' values which the user types/chooses
Executing the query with the following predicates:
Predicate.create('EntryDate', '>=', dateFrom());
Predicate.create('EntryDate', '<=', dateTo());
results in the following error:
"')' or operator expected at position 65 in '(((LocationId eq 1) and (EntryDate ge Mon Feb 02 2009 00:00:00 GMT+0300 (Arab Standard Time))) and (EntryDate le Sun Jan 18 2015 04:04:46 GMT+0300 (Arab Standard Time))'."
URL Generated:
http://localhost:8743/breeze/breeze/InvoiceItemsHeaders?$filter=(((LocationId%20eq%201)%20and%20(EntryDate%20ge%20Mon%20Feb%2002%202009%2000%3A00%3A00%20GMT%2B0300%20(Arab%20Standard%20Time)))%20and%20(EntryDate%20le%20Sun%20Jan%2018%202015%2003%3A18%3A28%20GMT%2B0300%20(Arab%20Standard%20Time))&$orderby=HeaderId&$top=50&$inlinecount=allpages
P.S. I don't get this error on a mapped property of type datetime which all (mapped or unmapped) properties values come from the known jquery calender for the user to choose the desired dates.
So I had to use the toJson() to fix the datetime format in an appropriate Json format:
console.log(dateFrom());// Mon Feb 02 2009 00:00:00 GMT+0300 (Arab Standard Time)
console.log(dateFrom().toJSON());// 2009-02-01T21:00:00.000Z
I've cheated the correct url that comes from a predicate on a mapped property of type datetime then I had to come up with the following in order to make the query work:
Predicate.create('EntryDate', '>=', "datetime'" + dateFrom().toJSON() + "'");
Predicate.create('EntryDate', '<=', "datetime'" + dateTo().toJSON() + "'");
As for the other unmapped property ProductName which is of type string the scenario is a bit different, running with the following predicate:
Predicate.create('ProductName', 'contains', productName());
Results in:
Error message:
"Could not find a property named 'abc' on type 'BreezeProj.InvoiceItemsDTO'"
url:
http://localhost:8743/breeze/breeze/InvoiceItemsHeaders?$filter=(substringof(abc%2CProductName)%20eq%20true)
'abc' here is a value passed to ProductName()
Again, I had to come up with the following for the query to work:
Predicate.create('ProductName', 'contains', "'" + productName() + "'");
Any thoughts?
Thanks
My response to Ward's second edit:
InvoiceItems is the entityType, that's how it's named in the database, I used InvoiceItemsDto
as a view to get the properties needed. I could've simply expanded the InvoiceHeader for EntryDate and Location or Product for ProductName
but I would find myself expanding many other navigation properties many times at the client side, which is troublesome.
EntryDate - for instance- is a property that is mapped to InvoiceItemsDto but not to the acrual entity InvoiceItems, hence, entityNames.invoiceItem is InvoiceItems
Posting the whole code above does not mean I want you to get sucked into the vortex of my application.
Perhaps I lack the ability to clear out my intentions, but
my question is indeed small and focused "Why Breeze didn't realize the data types of the "unmapped" properties with predicates when it does realize them in query results? "
Anyways, here is a link to the metadata you've requested:
http://pastebin.com/KGZKFLUU
again, Sorry for inconvience.

There are too many moving parts for my head to wrap around here. You're talking about multiple, unspecified errors. You've got predicates that touch multiple properties and I can't tell which are problematic. You're claiming a serialization error for one of the mapped properties (productName) but not saying what that that error is ("JSON serialization stuff" does not count).
It would be ideal if you could break this down to a tiny, executable (e.g., plunker) sample that focuses on one problem. Get rid of everything that doesn't matter (e.g, InvoiceItemInitializer, the distracting clauses in the query).
A few things leap off the page:
The InvoiceItemInit defines a ProductName (PascalCase) but your query predicate shows it as productName (camelCase) and the other properties appear to be camel.
If a property on the server is unmapped, I wouldn't expect that Breeze would allow a query that specified that property (not certain about that but it seems problematic).
You didn't show us the pertinent EntityType metadata on the client for the entities of interests so we can't really tell what's going on.
Why the .toJson calls in the predicate comparison values? Where have you seen that done before? I've neither done it myself nor seen it in any example code.
Why are the comparison values coming from functions? Why productName() instead of productName or 'foo'?
When you do highly unusual things, it's hard for us to guess where you stepped off the path. An executable repro may be your best chance for getting an answer.
My response to your Jan 16 response
That's better, thanks.
But many things still don't make sense to me. The most important gap: I can't see your client-side metadata so I don't know what Breeze thinks are the EntityTypes involved.
Please, whatever you do, do not paste your metadata here in full. Put it in a pastebin or a gist and reference it in your question.
You register a ctor for 'InvoiceItems' but that's not an EntityType as far as I can tell. 'InvoiceItems' looks more like a resource name, a target for a query; it's not obviously a type name (type names are typically singular, btw).
I see you are casting the query: .toType(entityNames.invoiceItem). What is entityNames.invoiceItem?
I don't see why you expect 'InvoiceItems' to be treated as an EntityType. I kind of get that it relates to InvoiceItemsDto but I don't see how Breeze could know that.
I don't know why you created the properties as unmapped in the first place when they are clearly mapped to the properties of InvoiceItemsDto. There is something fundamentally disconnected about InvoiceItemsDto and InvoiceItems.
I confess I also don't understand why Breeze didn't realize the data types of the "unmapped" properties defined in your ctor. But with everything else amiss I'm not sure what is going on.
Perhaps you can step back and tell me why you have the DTO in the first place and why you're trying to both hide and show properties that are actually on your InvoiceHeader business class.
I'm half afraid to hear the answer because I feel myself being sucked into the vortex of your application. That's not what we do here on S.O. We answer small focused questions; we don't wade through people's applications.
That's why it is so important for you to provide a working sample that is limited to one problem at a time.
Forgive my abrupt language. I do not mean to be so brusque. It's late for me too and I'm frustrated that I can't help when there are so many moving and missing pieces.
You may want to consider a short professional services engagement with IdeaBlade to help you get back on track.

Related

Edit operation not saving to the DB

I posted the question earlier, but didn't receive any correct responses, hence posting again with some edits. I have a function that accepts two parameters, IDs and Dates. When I had put breakpoints, I was able to see the Ids and the Dates selected on the page as parameter values. However, after hitting the process button, nothing happens, meaning this data isn't getting saved to the DB.
Model Classes:
public class Hello{
public string ID{ get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime? Date{ get; set; }
}
Controller Class:
[HttpGet]
public ActionResult Selection(string ids, string dates)
{
model = new Hello();
ExtensionDB db = new ExtensionDB();
string[] IDS = ids.Split(',');
string[] DATES = dates.Split(',');
List<Hello> list = new List<Hello>();
for (int i = 0; i < IDS.Length; i++)
{
if (IDS[i] != null && IDS[i] != "")
{
Hello item = new Hello { ID = IDS[i], Date = DateTime.Parse(DATES[i]) };
list.Add(item);
}
}
if (ModelState.IsValid)
{
foreach (var row in db.Table1)
{
foreach (var row2 in db.Table2)
{
if (row.UID== row2.CID) // UID and CID are Foreign keys that join these two tables
{
foreach (var item in list)
{
if (row.UID == Convert.ToInt32(item.ID))
{
row2.ReportedDate = item.Date;
}
db.SaveChanges();
}
}
}
}
ViewBag.Message = "Success";
return View(model);
}
else
{
ViewBag.Message = "Failed";
return View(model);
}
}
I will add the view class if needed, however the problem is here.. You can also refer to it here: Saving changes to the DB MVC
Your code does not attempt to update anything. Start with confirming what the data you are passing to this POST call contains, and what you want to do with it. It looks like what you are trying to do is update the dates for a number of records. Looking at your previous post (no need to re-post another question with the same code) there are a few things..
First: Structure the data you want to pass to the POST call into a collection of simple objects containing an id and a date. For example:
{
id = rid,
date = date
}
and add those to the collection named something like "updateData" rather than two separate arrays of IDs and dates. Then in the server-side code, declare a simple view model class:
public class UpdateDateViewModel
{
public int Id { get; set; }
public DateTime Date { get; set; }
}
In the ajax call instead of:
data: { ids: ids, dates: dates },
you'll want something like:
data: { updates: updateData },
where updateData is your collection of id + date pairs.
and use that view model in your method:
public ActionResult Process(IList updates)
Provided that request data is sent as Json, ASP.Net should translate that data automatically for you, though you may need to configure ASP.Net to translate the camelCase vs PascalCase. Worst case, to test, you can use camelCase property names ("id" and "date")
Now when it comes to updating the data: Server side, please get in the habit of using meaningful variable names, not "c", "i", etc. It makes code a lot easier to understand.
public ActionResult Process(IList<UpdateDateViewModel> updates)
{
using (db = new DB())
{
//rp = new RequestProcess(); - Assuming RequestProcess is an Entity?
//var c = rp.getStuff(); - No idea what this getStuff() method does...
foreach(var update in updates)
{
var request = db.RequestProcesses.Find(update.Id);
if (request != null)
request.RequestDate = update.Date; // If we find a matching request, update it's date.
else
{ // Doesn't exist, create it and add it to the DbSet.(table)
var request = new RequestProcess { Id = update.Id, RequestDate = update.Date };
db.RequestProcesses.Add(request);
}
db.SaveChanges();
}
}
}
Now this is a very bare bones guess at what you may be trying to do. Ideally though, updates should be completely separate from adds in the sense that an update should only deal with existing records. If it comes across an ID that it cannot find it should throw an error, ignore, and/or return a status to the user that something wasn't right. Creating new entries should be a separate call and ensure that records are properly initialized with their required fields.
Your original code looked to be taking a list of IDs, but then creating a new entity and calling that "getStuff" method that didn't have the DbContext, or any of the values from the POST call, but then attempting to copy values from that entity into the string parameters that you passed (which would overwrite the Json string) None of that would have updated an entity which would never have updated your data.
Take it slow and follow the examples before attempting to adapt them to your ideas. It will be a lot more constructive and less frustrating then writing a bunch of code that doesn't really make much sense, then wondering why it doesn't work. Your original code has probably a dozen or more problems and inefficiencies. Simply pasting it up on Stack will get a lot of confusing comments based on these problems which don't really help with the first issue you want to solve. Strip it back to the minimum, start with getting the data you need to the server in a meaningful way, then from that, attempt to use that data to update your entities.

Add temporary property to node in Neo4j Cypher for return

I am using the Neo4j .Net client to interface with a Neo4j database.
I often come by this issue when trying to map related nodes to nested classes :
For example mapping
(:Foo)-[:FOOBAR]->(:Bar)
to the form
public class Foo {
public string FooPropOne { get; set; }
public string FooPropTwo { get; set; }
public List<Bar> Bars { get; set; }
}
public class Bar {
public string BarPropOne { get; set; }
public string BarPropTwo { get; set; }
}
To get the correct output for deserialzation I have to write the query like so:
Foo foo = WebApiConfig.GraphClient.Cypher
.Match("(f:Foo)-[:FOOBAR]->(b:Bar)")
.With("#{
FooPropOne: f.FooPropOne,
FooPropTwo: f.FooPropTwo,
Bars: collect(b)
} AS Result")
.Return<Foo>("Result")
.SingleOrDefault();
To a certain extent this is fine, and works perfectly, but becomes quite unwieldy the more properties the Foo class has.
I was wondering if there was something I can do in the "With" statement to add a temporary property (in the above case "Bars") when returning the nodes, that does not require me to write every single property on the parent node?
Something like
.With("f.Bars = collect(b)")
.Return<Foo>("f")
that wont actually affect the data stored in the DB?
Thanks in advance for any relpies!!
I don't know of a way to add a temporary property like you want, personally - I would take one of two approaches, and the decision really comes down to what you like to do.
Option 1
The choosing properties version
var foo = client.Cypher
.Match("(f:Foo)-[:FOOBAR]->(b:Bar)")
.With("f, collect(b) as bars")
.Return((f, bars) => new Foo
{
FooPropOne = f.As<Foo>().FooPropOne,
FooPropTwo = f.As<Foo>().FooPropTwo,
Bars = Return.As<List<Bar>>("bars")
}).Results.SingleOrDefault();
This version allows you to be Type Safe with your objects you're bringing out, which on a class with lots of properties does limit the chance of a typo creeping in. This will give you back a Foo instance, so no need to do any other processing.
Option 2
The little bit of post-processing required version
var notQuiteFoo = client.Cypher
.Match("(f:Foo)-[:FOOBAR]->(b:Bar)")
.With("f, collect(b) as bars")
.Return((f, bars) => new
{
Foo = f.As<Foo>(),
Bars = bars.As<List<Bar>>()
}).Results;
This saves on the typing out of the properties, so in a big class, a time saver, and also means if you add extra properties you don't need to remember to update the query. However you do need to do some post-query processing, something like:
IEnumerable<Foo> foos = notQuiteFoo.Results.Select(r => {r.Foo.Bars = r.Bars; return r.Foo;});
Personally - I like Option 2 as it has less potential for Typos, though I appreciate neither is exactly what you're after.

How do I specify multiple keys in DataServiceKey attribute for WCF Data Service data context object

I am using Reflection provider for my WCF Data Service and my Data Context object has two key members, say EmpId and DeptId.
If I specify [DataServiceKey("EmpId", "DeptId")], the service doesn't work. When I try to access the collection with the URL http://localhost:55389/DataService.svc/EmployeeData, I get the following error:
The XML page cannot be displayed
Cannot view XML input using XSL style
sheet. Please correct the error and
then click the Refresh button, or try
again later. The following tags were
not closed: feed. Error processing
resource
'http://localhost:55389/DataService.svc/EmployeeData'.
With single member in the DataServiceKey, it works fine. I tried with Custom Data Provider and I could achieve this functionality. But if I can do it with the Reflection provider, that would be great.
I don't think the problem is the multiple keys. To confirm please use for example Fiddler or something similar to grab the whole response from the server and share the error in it (as I'm sure there will be one in there).
Guessing from the description I think the problem is that one of your key property values is null. That is not supported and would cause so called in-stream error which would leave the response XML incomplete (which seems to be your case).
OData can handle multiple keys but all keys must have a valid value. Review this for OData's rule. If you want to retrieve an entry with EmpId=1 and DeptId=someString, you should reconstruct your URI into something like:
http://localhost:55389/DataService.svc/EmployeeData(EmpId=1,DeptId='someString')
Be careful in OData queries because they are case sensitive.
That is weird, I just tried this:
public class Context
{
public IQueryable<Person> People {
get {
return (new List<Person> {
new Person { EmpId = 1, DeptId = 2, Name = "Dude" }
}).AsQueryable();
}
}
}
[DataServiceKey("EmpId", "DeptId")]
public class Person
{
public int EmpId { get; set; }
public int DeptId { get; set; }
public string Name { get; set; }
}
public class WcfDataService1 : DataService<Context>
{
// This method is called only once to initialize service-wide policies.
public static void InitializeService(DataServiceConfiguration config)
{
config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
config.DataServiceBehavior.MaxProtocolVersion =
DataServiceProtocolVersion.V2;
}
}
And it works just fine, do you notice any major differences?
-Alex

Json and Circular Reference Exception

I have an object which has a circular reference to another object. Given the relationship between these objects this is the right design.
To Illustrate
Machine => Customer => Machine
As is expected I run into an issue when I try to use Json to serialize a machine or customer object. What I am unsure of is how to resolve this issue as I don't want to break the relationship between the Machine and Customer objects. What are the options for resolving this issue?
Edit
Presently I am using Json method provided by the Controller base class. So the serialization I am doing is as basic as:
Json(machineForm);
Update:
Do not try to use NonSerializedAttribute, as the JavaScriptSerializer apparently ignores it.
Instead, use the ScriptIgnoreAttribute in System.Web.Script.Serialization.
public class Machine
{
public string Customer { get; set; }
// Other members
// ...
}
public class Customer
{
[ScriptIgnore]
public Machine Machine { get; set; } // Parent reference?
// Other members
// ...
}
This way, when you toss a Machine into the Json method, it will traverse the relationship from Machine to Customer but will not try to go back from Customer to Machine.
The relationship is still there for your code to do as it pleases with, but the JavaScriptSerializer (used by the Json method) will ignore it.
I'm answering this despite its age because it is the 3rd result (currently) from Google for "json.encode circular reference" and although I don't agree with the answers (completely) above, in that using the ScriptIgnoreAttribute assumes that you won't anywhere in your code want to traverse the relationship in the other direction for some JSON. I don't believe in locking down your model because of one use case.
It did inspire me to use this simple solution.
Since you're working in a View in MVC, you have the Model and you want to simply assign the Model to the ViewData.Model within your controller, go ahead and use a LINQ query within your View to flatten the data nicely removing the offending circular reference for the particular JSON you want like this:
var jsonMachines = from m in machineForm
select new { m.X, m.Y, // other Machine properties you desire
Customer = new { m.Customer.Id, m.Customer.Name, // other Customer properties you desire
}};
return Json(jsonMachines);
Or if the Machine -> Customer relationship is 1..* -> * then try:
var jsonMachines = from m in machineForm
select new { m.X, m.Y, // other machine properties you desire
Customers = new List<Customer>(
(from c in m.Customers
select new Customer()
{
Id = c.Id,
Name = c.Name,
// Other Customer properties you desire
}).Cast<Customer>())
};
return Json(jsonMachines);
Based on txl's answer you have to
disable lazy loading and proxy creation and you can use the normal methods to get your data.
Example:
//Retrieve Items with Json:
public JsonResult Search(string id = "")
{
db.Configuration.LazyLoadingEnabled = false;
db.Configuration.ProxyCreationEnabled = false;
var res = db.Table.Where(a => a.Name.Contains(id)).Take(8);
return Json(res, JsonRequestBehavior.AllowGet);
}
Use to have the same problem. I have created a simple extension method, that "flattens" L2E objects into an IDictionary. An IDictionary is serialized correctly by the JavaScriptSerializer. The resulting Json is the same as directly serializing the object.
Since I limit the level of serialization, circular references are avoided. It also will not include 1->n linked tables (Entitysets).
private static IDictionary<string, object> JsonFlatten(object data, int maxLevel, int currLevel) {
var result = new Dictionary<string, object>();
var myType = data.GetType();
var myAssembly = myType.Assembly;
var props = myType.GetProperties();
foreach (var prop in props) {
// Remove EntityKey etc.
if (prop.Name.StartsWith("Entity")) {
continue;
}
if (prop.Name.EndsWith("Reference")) {
continue;
}
// Do not include lookups to linked tables
Type typeOfProp = prop.PropertyType;
if (typeOfProp.Name.StartsWith("EntityCollection")) {
continue;
}
// If the type is from my assembly == custom type
// include it, but flattened
if (typeOfProp.Assembly == myAssembly) {
if (currLevel < maxLevel) {
result.Add(prop.Name, JsonFlatten(prop.GetValue(data, null), maxLevel, currLevel + 1));
}
} else {
result.Add(prop.Name, prop.GetValue(data, null));
}
}
return result;
}
public static IDictionary<string, object> JsonFlatten(this Controller controller, object data, int maxLevel = 2) {
return JsonFlatten(data, maxLevel, 1);
}
My Action method looks like this:
public JsonResult AsJson(int id) {
var data = Find(id);
var result = this.JsonFlatten(data);
return Json(result, JsonRequestBehavior.AllowGet);
}
In the Entity Framework version 4, there is an option available: ObjectContextOptions.LazyLoadingEnabled
Setting it to false should avoid the 'circular reference' issue. However, you will have to explicitly load the navigation properties that you want to include.
see: http://msdn.microsoft.com/en-us/library/bb896272.aspx
Since, to my knowledge, you cannot serialize object references, but only copies you could try employing a bit of a dirty hack that goes something like this:
Customer should serialize its Machine reference as the machine's id
When you deserialize the json code you can then run a simple function on top of it that transforms those id's into proper references.
You need to decide which is the "root" object. Say the machine is the root, then the customer is a sub-object of machine. When you serialise machine, it will serialise the customer as a sub-object in the JSON, and when the customer is serialised, it will NOT serialise it's back-reference to the machine. When your code deserialises the machine, it will deserialise the machine's customer sub-object and reinstate the back-reference from the customer to the machine.
Most serialisation libraries provide some kind of hook to modify how deserialisation is performed for each class. You'd need to use that hook to modify deserialisation for the machine class to reinstate the backreference in the machine's customer. Exactly what that hook is depends on the JSON library you are using.
I've had the same problem this week as well, and could not use anonymous types because I needed to implement an interface asking for a List<MyType>. After making a diagram showing all relationships with navigability, I found out that MyType had a bidirectional relationship with MyObject which caused this circular reference, since they both saved each other.
After deciding that MyObject did not really need to know MyType, and thereby making it a unidirectional relationship this problem was solved.
What I have done is a bit radical, but I don't need the property, which makes the nasty circular-reference-causing error, so I have set it to null before serializing.
SessionTickets result = GetTicketsSession();
foreach(var r in result.Tickets)
{
r.TicketTypes = null; //those two were creating the problem
r.SelectedTicketType = null;
}
return Json(result);
If you really need your properties, you can create a viewmodel which does not hold circular references, but maybe keeps some Id of the important element, that you could use later for restoring the original value.

Using Stored Procedures with Linq To Sql which have Additional Parameters

I have a very big problem and can't seem to find anybody else on the internet that has my problem. I sure hope StackOverflow can help me...
I am writing an ASP.NET MVC application and I'm using the Repository concept with Linq To Sql as my data store. Everything is working great in regards to selecting rows from views. And trapping very basic business rule constraints. However, I'm faced with a problem in my stored procedure mappings for deletes, inserts, and updates. Let me explain:
Our DBA has put a lot of work into putting the business logic into all of our stored procedures so that I don't have to worry about it on my end. Sure, I do basic validation, but he manages data integrity and conflicting date constraints, etc... The problem that I'm faced with is that all of the stored procedures (and I mean all) have 5 additional parameters (6 for inserts) that provide information back to me. The idea is that when something breaks, I can prompt the user with the appropriate information from our database.
For example:
sp_AddCategory(
#userID INT,
#categoryName NVARCHAR(100),
#isActive BIT,
#errNumber INT OUTPUT,
#errMessage NVARCHAR(1000) OUTPUT,
#errDetailLogID INT OUTPUT,
#sqlErrNumber INT OUTPUT,
#sqlErrMessage NVARCHAR(1000) OUTPUT,
#newRowID INT OUTPUT)
From the above stored procedure, the first 3 parameters are the only parameters that are used to "Create" the Category record. The remaining parameters are simply used to tell me what happened inside the method. If a business rule is broken inside the stored procedure, he does NOT use the SQL 'RAISEERROR' keyword when business rules are broken. Instead, he provides information about the error back to me using the OUTPUT parameters. He does this for every single stored procedure in our database even the Updates and Deletes. All of the 'Get' calls are done using custom views. They have all been tested and the idea was to make my job easier since I don't have to add the business logic to trap all of the various scenarios to ensure data quality.
As I said, I'm using Linq To Sql, and I'm now faced with a problem. The problem is that my "Category" model object simply has 4 properties on it: CategoryID, CategoryName, UserId, and IsActive. When I opened up the designer to started mapping my properties for the insert, I realized that there is really no (easy) way for me to account for the additional parameters unless I add them to my Model object.
Theoretically what I would LIKE to do is this:
// note: Repository Methods
public void AddCategory(Category category)
{
_dbContext.Categories.InsertOnSubmit(category);
}
public void Save()
{
_dbContext.SubmitChanges();
}
And then from my CategoryController class I would simply do the following:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(FormCollection collection)
{
var category = new Category();
try
{
UpdateModel(category); // simple validation here...
_repository.AddCategory(category);
_repository.Save(); // should get error here!!
return RedirectToAction("Index");
}
catch
{
// manage friendly messages here somehow... (??)
// ...
return View(category);
}
}
What is the best way to manage this using Linq to Sql? I (personally) don't feel that it makes sense to have all of these additional properties added to each model object... For example, the 'Get' should NEVER have errors and I don't want my repository methods to return one type of object for Get calls, but accept another type of object for CUD calls.
Update: My Solution! (Dec. 1, 2009)
Here is what I did to fix my problem. I got rid of my 'Save()' method on all of my repositories. Instead, I added an 'Update()' method to each repository and actually commit the data to the database on each CUD (ie. Create / Update / Delete) call.
I knew that each stored procedure had the same parameters, so I created a class to hold them:
public class MySprocArgs
{
private readonly string _methodName;
public int? Number;
public string Message;
public int? ErrorLogId;
public int? SqlErrorNumber;
public string SqlErrorMessage;
public int? NewRowId;
public MySprocArgs(string methodName)
{
if (string.IsNullOrEmpty(methodName))
throw new ArgumentNullException("methodName");
_methodName = methodName;
}
public string MethodName
{
get { return _methodName; }
}
}
I also created a MySprocException that accepts the MySprocArgs in it's constructor:
public class MySprocException : ApplicationException
{
private readonly MySprocArgs _args;
public MySprocException(MySprocArgs args) : base(args.Message)
{
_args = args;
}
public int? ErrorNumber
{
get { return _args.Number; }
}
public string ErrorMessage
{
get { return _args.Message; }
}
public int? ErrorLogId
{
get { return _args.ErrorLogId; }
}
public int? SqlErrorNumber
{
get { return _args.SqlErrorNumber; }
}
public string SqlErrorMessage
{
get { return _args.SqlErrorMessage; }
}
}
Now here is where it all comes together... Using the example that I started with in my initial inquiry, here is what the 'AddCategory()' method might look like:
public void AddCategory(Category category)
{
var args = new MySprocArgs("AddCategory");
var result = _dbContext.AddWidgetSproc(
category.CreatedByUserId,
category.Name,
category.IsActive,
ref args.Number, // <-- Notice use of 'args'
ref args.Message,
ref args.ErrorLogId,
ref args.SqlErrorNumber,
ref args.SqlErrorMessage,
ref args.NewRowId);
if (result == -1)
throw new MySprocException(args);
}
Now from my controller, I simply do the following:
[HandleError(ExceptionType = typeof(MySprocException), View = "SprocError")]
public class MyController : Controller
{
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Category category)
{
if (!ModelState.IsValid)
{
// manage friendly messages
return View(category);
}
_repository.AddCategory(category);
return RedirectToAction("Index");
}
}
The trick to managing the new MySprocException is to simply trap it using the HandleError attribute and redirect the user to a page that understands the MySprocException.
I hope this helps somebody. :)
I don't believe you can add the output parameters to any of your LINQ classes because the parameters do not persist in any table in your database.
But you can handle output parameters in LINQ in the following way.
Add the stored procedure(s) you whish to call to your .dbml using the designer.
Call your stored procedure in your code
using (YourDataContext context = new YourDataContext())
{
Nullable<int> errNumber = null;
String errMessage = null;
Nullable<int> errDetailLogID = null;
Nullable<int> sqlErrNumber = null;
String sqlErrMessage = null;
Nullable<int> newRowID = null;
Nullable<int> userID = 23;
Nullable<bool> isActive=true;
context.YourAddStoredProcedure(userID, "New Category", isActive, ref errNumber, ref errMessage, ref errDetailLogID, ref sqlErrNumber, ref sqlErrMessage, ref newRowID);
}
I haven' tried it yet, but you can look at this article, where he talks about stored procedures that return output parameters.
http://weblogs.asp.net/scottgu/archive/2007/08/16/linq-to-sql-part-6-retrieving-data-using-stored-procedures.aspx
Basically drag the stored procedure into your LINQ to SQL designer then it should do the work for you.
The dbContext.SubmitChanges(); will work only for ENTITY FRAMEWORK.I suggest Save,Update and delete will work by using a Single Stored procedure or using 3 different procedure.

Resources