For auditing reasons I am overriding the SaveChanges function. However, I want to capture to original and current values as the original object (i.e person) so that I can serialize both the before and after.
Public Overrides Function SaveChanges() As Integer
ChangeTracker.DetectChanges()
Dim ctx As ObjectContext = DirectCast(Me, IObjectContextAdapter).ObjectContext
Dim objectStateEntryList As List(Of ObjectStateEntry) = ctx.ObjectStateManager.
GetObjectStateEntries(EntityState.Added Or EntityState.Modified Or EntityState.Deleted).ToList()
For Each ent As ObjectStateEntry In objectStateEntryList
If Not ent.IsRelationship Then
Dim objectType As Type = ObjectContext.GetObjectType(ent.Entity.GetType)
Dim audit As New Audit With {
.ObjectId = ent.EntityKey.EntityKeyValues.First.Value,
.ObjectType = ObjectContext.GetObjectType(ent.Entity.GetType).Name,
.User = (From u In Users Where u.Username = My.User.Name).First
}
With audit
Select Case ent.State
Case EntityState.Added
.Action = "Created"
.Detail = "Record created"
Case EntityState.Deleted
.Action = "Deleted"
.Detail = "Record deleted"
Case EntityState.Modified
Dim newObj As String = SerializeToString(
Convert.ChangeType(ent.Entity, objectType)
)
.Action = "Modified"
.Detail = newObj.ToString
End Select
End With
End If
Next
Return MyBase.SaveChanges()
End Function
That's how far I got, but when I try and ChangeType it throws "Object must implement IConvertible".
The last time I worked on a project that absolutely needed to track every change, we used a history table and an ON UPDATE trigger. Changing data would fire the trigger, which would then copy the original row into the history table.
This is 100% EF compatible, but you need to set it up separately for each table.
This doesn't exactly answer your main question but may help you to improve your auditing.
Disclaimer: I'm the owner of the project Entity Framework Plus
I recommend you to look at our EF+ Audit Feature, all auditing information can be easily retrieved using this library.
// using Z.EntityFramework.Plus; // Don't forget to include this.
var ctx = new EntityContext();
// ... ctx changes ...
var audit = new Audit();
audit.CreatedBy = "ZZZ Projects"; // Optional
ctx.SaveChanges(audit);
// Access to all auditing information
var entries = audit.Entries;
foreach(var entry in entries)
{
foreach(var property in entry.Properties)
{
}
}
// CALL your serializer here
SerializeToString(entries, ...);
The code is Open Source.
Related
I have the following method which is called from Ajax:
[Authorize]
[ValidateInput(false)]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public JsonNetResult CreateOrUpdateTimeRecord(TimeRecord tr)
{
TimeRecord trLocal;
if (tr.Id == -1 || tr.Id == 0)
{
trLocal = new TimeRecord
{
Description = tr.Description,
StartTime = tr.StartTime,
EndTime = tr.EndTime,
User =new myTimeMvc.Models.NHibernate.Models.User {Id = tr.User.Id},// _userRepo.Get(tr.User.Id),
Hdt = new Hdt {Id = tr.Hdt.Id}//_hdtRepo.Get(tr.Hdt.Id)
};
_timeRepo.Insert(trLocal);
}
else
{
trLocal = _timeRepo.Get(tr.Id);
trLocal.Description = tr.Description;
trLocal.StartTime = tr.StartTime;
trLocal.EndTime = tr.EndTime;
_timeRepo.Update(trLocal);
}
...
}
As you can see my TimeRecord has a reference to User and Hdt. Now I started to work with NHibernate Profiler which complains when I resolve my properties by loading them from their coresponding repositories. Which is clear to me since I actually don't need to query the database for that since I have the ID's for this objects.
User = _userRepo.Get(tr.User.Id),
Hdt = _hdtRepo.Get(tr.Hdt.Id)
But I'm not 100% sure if I can use this instead:
User =new myTimeMvc.Models.NHibernate.Models.User {Id = tr.User.Id},,
Hdt = new Hdt {Id = tr.Hdt.Id}
I guess NHibernate lazy proxies work the same way since they only contain just the ID of the related object and load the rest when it is needed. Do I have to attach this "new" oject anyway to my session?
Can someone tell me what is the correct way to do this?
Cheers,
Stefan
There are a few ways how to achieve that. One of them could be using the Load() method. Check Ayendes post: NHibernate – The difference between Get, Load and querying by id, an extract:
Load will never return null. It will always return an entity or throw an exception. Because that is the contract that we have we it, it is permissible for Load to not hit the database when you call it, it is free to return a proxy instead.
Other words, we can do something like this
User = _userRepo.Load(tr.User.Id),
Hdt = _hdtRepo.Load(tr.Hdt.Id)
Where the Load would be encapsulating the session.Load()
I added an Anonymous type to my project:
'Put the courses from the XML file in an Anonymous Type
Dim courses = From myCourse In xDoc.Descendants("Course")
Select New With
{
.StateCode = myCourse.Element("StateCode").Value, _
.Description = myCourse.Element("Description").Value, _
.ShortName = myCourse.Element("ShortName").Value, _
.LongName = myCourse.Element("LongName").Value, _
.Type = myCourse.Element("Type").Value, _
.CipCode = CType(myCourse.Element("CIPCode"), String) _
}
For Each course In courses
If Not UpdateSDECourseCode(acadYear, course.StateCode, course.Description, course.Type, course.ShortName, course.LongName, course.CipCode) Then errors.Add(String.Format("Cannot import State Course Number {0} with Year {1} ", course.StateCode, acadYear))
Next
After doing so, a Unit Test failed:
Public Function GetAreaTypeList() As List(Of Type)
Dim types As New List(Of Type)
Dim asmPath As String = IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "My.Stuff.dll")
For Each t As Type In Reflection.Assembly.LoadFrom(asmPath).GetTypes()
If t.Namespace.StartsWith("My.Stuff.BatchUpdater") Then
If t.BaseType Is GetType(My.Stuff.BatchUpdater.Area) Then
types.Add(t)
End If
End If
Next
Return types
End Function
It fails because a new type has been added to the project (VB$AnonymousType_0`6) and it does not have a property called Namespace.
I fixed by making the following change to the IF Statement:
If Not t.Namespace Is Nothing AndAlso t.Namespace.StartsWith("My.Stuff.BatchUpdater") Then
Since I don't fully understand what's happening, I feel leery about my code change.
Why is the Namespace Nothing for the Anonymous type?
Would you fix your Unit Test in the same fashion? Or should it be something more specific (e.g. If Not t.Names = "VB$AnonymousType_0`6")
UPDATE
decyclone gave me the info I needed to create a better test:
For Each t As Type In Reflection.Assembly.LoadFrom(asmPath).GetTypes()
'Ignore CompilerGeneratedAttributes (e.g. Anonymous Types)
Dim isCompilerGeneratedAttribute = t.GetCustomAttributes(False).Contains(New System.Runtime.CompilerServices.CompilerGeneratedAttribute())
If Not isCompilerGeneratedAttribute AndAlso t.Namespace.StartsWith("My.Stuff.BatchUpdater") Then
'...Do some things here
End If
Next
Honestly, it could be improved more with a LINQ query, but this suits me.
Anonymous methods and types are decorated with CompilerGeneratedAttribute. You can check for their existance to identify an anonymous type.
var anonymous = new { value = 1 };
Type anonymousType = anonymous.GetType();
var attributes = anonymousType.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false);
if (attributes.Any())
{
// Anonymous
}
You can filter these out in your test.
It is also possible to mark a user defined type with CompilerGeneratedAttribute. So maybe you can combine it with checking if Namespace is null or not
var anonymous = new { value = 1 };
Type anonymousType = anonymous.GetType();
var attributes = anonymousType.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false);
if (attributes.Any() && anonymousType.Namespace == null)
{
// Anonymous
}
I'm getting this error from Moq via NUnit, and it doesn't make much in the way of sense to me.
"Expected invocation on the mock at least once, but was never performed: x => x.DeleteItem(.$VB$Local_item)"
"at Moq.Mock.ThrowVerifyException(MethodCall expected, IEnumerable1 setups, IEnumerable1 actualCalls, Expression expression, Times times, Int32 callCount)
at Moq.Mock.VerifyCalls(Interceptor targetInterceptor, MethodCall expected, Expression expression, Times times)
at Moq.Mock.Verify[T](Mock mock, Expression1 expression, Times times, String failMessage)
at Moq.Mock1.Verify(Expression`1 expression)
at PeekABookEditor.UnitTests.ItemBrowsing.Can_Delete_Item() in C:\Projects\MyProject\MyProject.UnitTests\Tests\ItemBrowsing.vb:line 167"
Very similar code works well in C#, so the error might be minor and syntactical on my part.
Here's my code:
<Test()> _
Public Sub Can_Delete_Item()
'Arrange: Given a repository containing some item...
Dim mockRepository = New Mock(Of IItemsRepository)()
Dim item As New Item With {.ItemID = "24", .Title = "i24"}
mockRepository.Setup(Function(x) x.Items).Returns(New Item() {item}.AsQueryable())
'Act ... when the user tries to delete that product
Dim controller = New ItemsController(mockRepository.Object)
Dim result = controller.Delete(24)
'Assert ... then it's deleted, and the user sees a confirmation
mockRepository.Verify(Sub(x) x.DeleteItem(item))
result.ShouldBeRedirectionTo(New With {Key .action = "List"})
Assert.AreEqual(DirectCast(controller.TempData("message"), String), "i24 was deleted")
End Sub
The guilty line appears to be "mockRepository.Verify(Sub(x) x.DeleteItem(item))"
Any thoughts on how to fix this?
Working C# code isn't the exact same, but here it is:
[Test]
public void Can_Delete_Product()
{
// Arrange: Given a repository containing some product...
var mockRepository = new Mock<IProductsRepository>();
var product = new Product { ProductID = 24, Name = "P24"};
mockRepository.Setup(x => x.Products).Returns(
new[] { product }.AsQueryable()
);
// Act: ... when the user tries to delete that product
var controller = new AdminController(mockRepository.Object);
var result = controller.Delete(24);
// Assert: ... then it's deleted, and the user sees a confirmation
mockRepository.Verify(x => x.DeleteProduct(product));
result.ShouldBeRedirectionTo(new { action = "Index" });
controller.TempData["message"].ShouldEqual("P24 was deleted");
}
In your VB test method, you create Item with a string ItemID = "24", but you call the controller.Delete method with an integer value of 24.
Check your controller code and see if the type discrepancy results in the item not being identified correctly, so either DeleteItem is not called at all, or is called with a different Item.
I am using LINQ to SQL queries to return data in my application. However I find it is now needful for me to return the column Names. Try as I might I have been completely unable to find out how to do this on the internet.
So if my LINQ entity table has the properties (Last_Name, First_name, Middle_Name) I need to return:
Last_name
First_Name
Middle_name
rather than the usual
Smith
John
Joe
You could certainly do it with some LINQ-To-Xml directly against the ".edmx" file or the embedded model resources in the compiled assembly.
The below query gets the field (not column) names. If you need the columns then just change the query to suit.
var edmxNS = XNamespace.Get(#"http://schemas.microsoft.com/ado/2007/06/edmx");
var schemaNS = XNamespace.Get(#"http://schemas.microsoft.com/ado/2006/04/edm");
var xd = XDocument.Load(#"{path}\Model.edmx");
var fields =
from e in xd
.Elements(edmxNS + "Edmx")
.Elements(edmxNS + "Runtime")
.Elements(edmxNS + "ConceptualModels")
.Elements(schemaNS + "Schema")
.Elements(schemaNS + "EntityType")
from p in e
.Elements(schemaNS + "Property")
select new
{
Entity = e.Attribute("Name").Value,
Member = p.Attribute("Name").Value,
Type = p.Attribute("Type").Value,
Nullable = bool.Parse(p.Attribute("Nullable").Value),
};
Lets assume you're talking about the Contact Table in the assembly named YourAssembly in a Context called MyDataContext
Using Reflection against a Table
You can use reflection to get the properties like you would any type
var properties = from property in
Type.GetType("YourAssembly.Contact").GetProperties()
select property.Name
;
foreach (var property in properties)
Console.WriteLine(property);
As shaunmartin notes this will return all properties not just Column Mapped ones. It should also be noted that this will return Public properties only. You'd need to include a BindingFlags value for the bindingAttr Parameter of GetProperties to get non-public properties
Using the Meta Model
You can use the Meta Model System.Data.Linq.Mapping to get the fields ( I added IsPersistant to only get the Column Mapped properties)
AttributeMappingSource mappping = new System.Data.Linq.Mapping.AttributeMappingSource();
var model = mappping.GetModel(typeof (MyDataContext));
var table = model.GetTable(typeof (Contact));
var qFields= from fields in table.RowType.DataMembers
where fields.IsPersistent == true
select fields;
foreach (var field in qFields)
Console.WriteLine(field.Name);
Using Reflection from a query result
If on the other hand you wanted it from a query result you can still use reflection.
MyDataContextdc = new MyDataContext();
Table<Contact> contacts = dc.GetTable<Contact>();
var q = from c in contacts
select new
{
c.FirstName,
c.LastName
};
var columns = q.First();
var properties = (from property in columns.GetType().GetProperties()
select property.Name).ToList();
I stumbled upon this answer to solve my own problem and used Conrad Frix 's answer. The question specified VB.NET though and that is what I program in. Here are Conrad's answers in VB.NET (they may not be a perfect translation, but they work):
Example 1
Dim PropertyNames1 = From Prprt In Type.GetType("LocalDB.tlbMeter").GetProperties()
Select Prprt.Name
Example 2
Dim LocalDB2 As New LocalDBDataContext
Dim bsmappping As New System.Data.Linq.Mapping.AttributeMappingSource()
Dim bsmodel = bsmappping.GetModel(LocalDB2.GetType())
Dim bstable = bsmodel.GetTable(LocalDB.tblMeters.GetType())
Dim PropertyNames2 As IQueryable(Of String) = From fields In bstable.RowType.DataMembers
Where fields.IsPersistent = True
Select fields.Member.Name 'IsPersistant to only get the Column Mapped properties
Example 3
Dim LocalDB3 As New LocalDBDataContext
Dim qMeters = From mtr In LocalDB3.tblMeters
Select mtr
Dim FirstResult As tblMeter = qMeters.First()
Dim PropertyNames3 As List(Of String) = From FN In FirstResult.GetType().GetProperties()
Select FN.Name.ToList()
To display the results:
For Each FieldName In PropertyNames1
Console.WriteLine(FieldName)
Next
For Each FieldName In PropertyNames2
Console.WriteLine(FieldName)
Next
For Each FieldName In PropertyNames3
Console.WriteLine(FieldName)
Next
Please also read Conrad's answer for notes on each method!
in my database there are 3 tables
CustomerType
CusID
EventType
EventTypeID
CustomerEventType
CusID
EventTypeID
alt text http://img706.imageshack.us/img706/8806/inserevent.jpg
Dim db = new CustomerEventDataContext
Dim newEvent = new EventType
newEvent.EventTypeID = txtEventID.text
db.EventType.InsertOnSubmit(newEvent)
db.SubmitChanges()
'To select the last ID of event'
Dim lastEventID = (from e in db.EventType Select e.EventTypeID Order By EventTypeID Descending).first()
Dim chkbx As CheckBoxList = CType(form1.FindControl("CheckBoxList1"), CheckBoxList)
Dim newCustomerEventType = New CustomerEventType
Dim i As Integer
For i = 0 To chkbx.Items.Count - 1 Step i + 1
If (chkbx.Items(i).Selected) Then
newCustomerEventType.INTEVENTTYPEID = lastEventID
newCustomerEventType.INTSTUDENTTYPEID = chkbxStudentType.Items(i).Value
db.CustomerEventType.InsertOnSubmit(newCustomerEventType)
db.SubmitChanges()
End If
Next
It works fine when I checked only 1 Single ID of CustomerEventType from CheckBoxList1. It inserts data into EventType with ID 1 and CustomerEventType ID 1. However, when I checked both of them, the error message said
Cannot add an entity that already exists.
Any suggestions please? Thx in advance.
Did you change the EventID before you pressed the button again. To me it looks as you did not. This would result that the code tries to insert the event with the ID 1 into the database, although, it is already there.
Maybe try increasing the event ID automatically or check whether the event is alreay present before trying to insert it.
Edit:
OK, here is what I think you want to do ... if I understood it correctly (it's in C# as I am more fluent in that language - however you should be able to easily convert the algorithm to VB - so just take it as pseudo code):
var db = new CustomerEventDataContext();
var newEvent = db.EventTypeSingleOrDefault(x => x.EventTypeId == txtEventID.Text);
if (newEvent != null) {
newEvent = new EventType();
newEvent.EventTypeId = txtEventID.Text;
db.EventType.InsertOnSubmit();
}
var chkbx = (CheckBoxList) form1.FindControl("CheckBoxList1");
for (int i = 0; i < chkbx.Items.Count; i++) {
var value = chkbxStudentType.Items(i).Value;
if (db.CustomerEventTypes.SingleOrDefault(x => x.EventTypeId == newEvent.EventTypeId) != null) {
// item already exists
} else {
var newCustomerEventType = new CustomerEventType();
newCustomerEventType.INTEVENTTYPEID = newEvent.EventTypeId;
newCustomerEventType.INTSTUDENTTYPEID = value;
db.CustomerEventType.InsertOnSubmit(newCustomerEventType);
}
}
db.SubmitChanges();
Two things I noticed:
You were adding the new EventType and then selecting the last event type based upon the id. This may not result in the item you just added.
You do not have to call SubmitChanges after each InsertOnSubmit. The DataBaseContext implementaton holds the inserted objects for you and you can reference them. Then you do a single submit to commit all changes. Note, however, in some complex circumstances a separate SubmitChanges is necessary, but this is rarely the case.