Our company ships a suite of various applications that manipulate data in a database. Each application has its specific business logic, but all applications share a common subset of business rules. The common stuff is incapsulated in a bunch of legacy COM DLLs, written in C++, which use "classic ADO" (they usually call stored procedures, sometimes they use dynamic SQL). Most of these DLLs have XML-based methods (not to mention the proprietary-format-based methods!) to create, edit, delete and retrieve objects, and also extra action such as methods which copy and transform many entities quickly.
The middleware DLLs are now very old, our application developers want a new object-oriented (not xml-oriented) middleware that can be easily used by C# applications. Many people in the company say that we should forget old paradigms and move to new cool stuff such Entity Framework. They are intrigued by the simplicity of POCOs and they would like to use LINQ to retrieve data (The Xml-based query methods of the DLLs are not so easy to use and will never be as flexible as LINQ).
So I'm trying to create a mock-up for a simplified scenario (the real scenario is much more complex, and here I'll post just a simplified subset of the simplified scenario!). I'm using Visual Studio 2010, Entity Framework 5 Code First, SQL Server 2008 R2. Please have mercy if I make stupid mistakes, I'm a newby to Entity Framework. Since I have many different doubts, I'll post them in separate threads. This is the first one. Legacy XML methods have a signature like this:
bool Edit(string xmlstring, out string errorMessage)
With a format like this:
<ORDER>
<ID>234</ID>
<NAME>SuperFastCar</NAME>
<QUANTITY>3</QUANTITY>
<LABEL>abc</LABEL>
</ORDER>
The Edit method implemented the following business logic: when a Quantity is changed, an "automatic scaling" must be applied to all Orders which have the same Label. E.g. there are three orders: OrderA has quantity = 3, label = X. OrderB has quantity = 4, label = X. OrderC has quantity = 5, label = Y. I call the Edit method supplying a new quantity = 6 for OrderA, i.e. I'm doubling OrderA's quantity. Then, according to the business logic, OrderB's quantity must be automatically doubled, and must become 8, because OrderB and OrderA have the same label. OrderC must not be changed because it has a different label.
How can I replicate this with POCO classes and Entity Framework? It's a problem because the old Edit method can change only one order at a time, while Entity Framework can change a lot of Orders when SaveChanges is called. Furthermore, a single call to SaveChanges can also create new Orders. Temporary assumptions, just for this test: 1) if many Order Quantities are changed at the same time, and the scaling factor is not the same for all of them, NO scaling occurs; 2) newly added Orders are not automatically scaled even if they have the same label of a scaled order.
I tried to implement it by overriding SaveChanges.
POCO class:
using System;
namespace MockOrders
{
public class Order
{
public Int64 Id { get; set; }
public string Name { get; set; }
public string Label { get; set; }
public decimal Quantity { get; set; }
}
}
Migration file (to create indexes):
namespace MockOrders.Migrations
{
using System;
using System.Data.Entity.Migrations;
public partial class UniqueIndexes : DbMigration
{
public override void Up()
{
CreateIndex("dbo.Orders", "Name", true /* unique */, "myIndex1_Order_Name_Unique");
CreateIndex("dbo.Orders", "Label", false /* NOT unique */, "myIndex2_Order_Label");
}
public override void Down()
{
DropIndex("dbo.Orders", "myIndex2_Order_Label");
DropIndex("dbo.Orders", "myIndex1_Order_Name_Unique");
}
}
}
DbContext:
using System;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration;
using System.Linq;
namespace MockOrders
{
public class MyContext : DbContext
{
public MyContext() : base(GenerateConnection())
{
}
private static string GenerateConnection()
{
var sqlBuilder = new System.Data.SqlClient.SqlConnectionStringBuilder();
sqlBuilder.DataSource = @"localhost\aaaaaa";
sqlBuilder.InitialCatalog = "aaaaaa";
sqlBuilder.UserID = "aaaaa";
sqlBuilder.Password = "aaaaaaaaa!";
return sqlBuilder.ToString();
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new OrderConfig());
}
public override int SaveChanges()
{
ChangeTracker.DetectChanges();
var groupByLabel = from changedEntity in ChangeTracker.Entries<Order>()
where changedEntity.State == System.Data.EntityState.Modified
&& changedEntity.Property(o => o.Quantity).IsModified
&& changedEntity.Property(o => o.Quantity).OriginalValue != 0
&& !String.IsNullOrEmpty(changedEntity.Property(o => o.Label).CurrentValue)
group changedEntity by changedEntity.Property(o => o.Label).CurrentValue into x
select new { Label = x.Key, List = x};
foreach (var labeledGroup in groupByLabel)
{
var withScalingFactor = from changedEntity in labeledGroup.List
select new
{
ChangedEntity = changedEntity,
ScalingFactor = changedEntity.Property(o => o.Quantity).CurrentValue / changedEntity.Property(o => o.Quantity).OriginalValue
};
var groupByScalingFactor = from t in withScalingFactor
group t by t.ScalingFactor into g select g;
// if there are too many scaling factors for this label, skip automatic scaling
if (groupByScalingFactor.Count() == 1)
{
decimal scalingFactor = groupByScalingFactor.First().Key;
if (scalingFactor != 1)
{
var query = from oo in this.AllTheOrders where oo.Label == labeledGroup.Label select oo;
foreach (Order ord in query)
{
if (this.Entry(ord).State != System.Data.EntityState.Modified
&& this.Entry(ord).State != System.Data.EntityState.Added)
{
ord.Quantity = ord.Quantity * scalingFactor;
}
}
}
}
}
return base.SaveChanges();
}
public DbSet<Order> AllTheOrders { get; set; }
}
class OrderConfig : EntityTypeConfiguration<Order>
{
public OrderConfig()
{
Property(o => o.Name).HasMaxLength(200).IsRequired();
Property(o => o.Label).HasMaxLength(400);
}
}
}
It seems to work (barring bugs of course), but this was an example with just 1 class: a real production application may have hundreds of classes! I'm afraid that in a real scenario, with a lot of constraints and business logic, the override of SaveChanges could quickly become long, cluttered and error-prone. Some colleagues are also concerned about performance. In our legacy DLLs, a lot of business logic (such as "automatic" actions) lives in stored procedures, some colleagues are worried that the SaveChanges-based approach may introduce too many round-trips and hinder performance. In the override of SaveChanges we could also invoke stored procedures, but what about transactional integrity? What if I make changes to the database before I call "base.SaveChanges()", and "base.SaveChanges()" fails?
Is there a different approach? Am I missing something?
Thank you very much!
Demetrio
p.s. By the way, is there a difference between overriding SaveChanges and registering to "SavingChanges" event? I read this document but it does not explain whether there's a difference: http://msdn.microsoft.com/en-us/library/cc716714(v=vs.100).aspx
This post: Entity Framework SaveChanges - Customize Behavior?
says that "when overriding SaveChanges you can put custom logic before and AFTER calling base.SaveChanges". But are there other caveats/advantages/drawbacks?
I'd say this logic belongs either in your MockOrders.Order class, in a class from a higher layer which uses your Order class (e.g. BusinessLogic.Order) or in a Label class. Sounds like your label acts as a joining attribute so, without knowing the particulars, I'd say pull it out and make it an entity of its own, this will give you navigation properties so you can more naturally access all Orders with the same label.
If modifying the DB to normalise out Labels is not a goer, build a view and bring that into your entity model for this purpose.
I've had to do something similar, but I've created an IPrepForSave
interface, and implemented that interface for any entities that need to do some business logic before they're saved.
The interface (pardon the VB.NET):
Public Interface IPrepForSave
Sub PrepForSave()
End Interface
The dbContext.SaveChanges override:
Public Overloads Overrides Function SaveChanges() As Integer
ChangeTracker.DetectChanges()
'** Any entities that implement IPrepForSave should have their PrepForSave method called before saving.
Dim changedEntitiesToPrep = From br In ChangeTracker.Entries(Of IPrepForSave)()
Where br.State = EntityState.Added OrElse br.State = EntityState.Modified
Select br.Entity
For Each br In changedEntitiesToPrep
br.PrepForSave()
Next
Return MyBase.SaveChanges()
End Function
And then I can keep the business logic in the Entity itself, in the implemented PrepForSave()
method:
Partial Public Class MyEntity
Implements IPrepForSave
Public Sub PrepForSave() Implements IPrepForSave.PrepForSave
'Do Stuff Here...
End Sub
End Class
Note that I place some restrictions on what can be done in the PrepForSave()
method:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With