I am using Entity Framework code-first and it's behaving very strangely when I use transactions.
In the code below you will find a simple example. As you can see - even though transactions are created - they are never committed. Despite NOT having a commit - my first transaction is being saved to the database in about 50% of unit test runs.
Sometimes data only saved in DB in 10% of runs. I have no reasonable explanation and loosing my mind trying to solve it..
Code:
[TestMethod]
public void TestConcurrentTransactions2()
{
int invoiceId = 1560;
var context1 = new ApplicationDbContext();
var transaction1 = context1.Database.BeginTransaction();
var invoice1 = new OracleDatabaseService.Models.Invoice()
{
InvoiceId = (++invoiceId).ToString(),
ClientId = "2",
ExternalSystemId = "0"
};
context1.Invoices.Add(invoice1);
context1.SaveChanges();
var context2 = new ApplicationDbContext();
var transaction2 = context2.Database.BeginTransaction();
var invoice2 = new OracleDatabaseService.Models.Invoice()
{
InvoiceId = (++invoiceId).ToString(),
ClientId = "2",
ExternalSystemId = "0"
};
context2.Invoices.Add(invoice2);
context2.SaveChanges();
//transaction2.Commit();
}
I'm using EF 6.2.0 and an Oracle-managed data access provider.
This behavior seems to be very situational and the only way to reproduce it is by re-running the test case over and over again. I seem to be able to repro the issue more often if I go and change invoiceId while the previous test hasn't finished executing. In this case - the test will still finish executing successfully but a record in DB will appear. Though in most cases, this doesn't really matter and data will be pushed to DB randomly.
So my questions are:
Is this an OK behavior that transaction is being committed automatically?
How do I force EF to only commit transactions when I need to?
Is there an alternative mechanism to handle transactions in EF?
UPDATE 23.08.2018
So, here is a code which almost certainly will repro the issue at least once per run:
[TestMethod]
public void Test1()
{
int invoiceId = 3350;
Parallel.For(0, 30, i =>
{
var context1 = new ApplicationDbContext();
var transaction1 = context1.Database.BeginTransaction();
var invoice1 = new OracleDatabaseService.Models.Invoice()
{
InvoiceId = (invoiceId + i).ToString(),
ClientId = "2",
ExternalSystemId = "0"
};
context1.Invoices.Add(invoice1);
context1.SaveChanges();
//transaction1.Commit();
});
}
Here is a fix attempt that seems to be working, though I don't fit into a service code pattern very well. I need to be able to come back to a service later and either roll-back or commit a transaction.
[TestMethod]
public void Test2()
{
int invoiceId = 3350;
Parallel.For(0, 30, i =>
{
using (var context = new ApplicationDbContext())
{
var transaction = context.Database.BeginTransaction();
var invoice = new OracleDatabaseService.Models.Invoice()
{
InvoiceId = (invoiceId + i).ToString(),
ClientId = "3",
ExternalSystemId = "0"
};
context.Invoices.Add(invoice);
context.SaveChanges();
//transaction.Commit();
}
});
}
This is my attempt to implement a service that uses DBContext. As you will see in the code - a descructor will check whether there is a context or transaction present and dispose of them. Seems to be working OK. Though, what will happen if any of the parallel processes fail? Is the descructor called then? I need somebody to review the code, possibly.
public class DbService
{
ApplicationDbContext _context;
DbContextTransaction _transaction;
public DbService()
{
_context = new ApplicationDbContext();
_transaction = _context.Database.BeginTransaction();
}
public void InsertInvoice(int invoiceId)
{
var invoice1 = new OracleDatabaseService.Models.Invoice()
{
InvoiceId = (invoiceId).ToString(),
ClientId = "3",
ExternalSystemId = "0"
};
_context.Invoices.Add(invoice1);
_context.SaveChanges();
}
~DbService()
{
if (_transaction != null)
{
_transaction.Rollback();
_transaction.Dispose();
}
if (_context != null)
{
_context.Dispose();
}
}
}
and test:
[TestMethod]
public void Test3()
{
int invoiceId = 3350;
Parallel.For(0, 30, i =>
{
var srvc = new DbService();
srvc.InsertInvoice(invoiceId + i);
});
}
As was suggested by #WynDysel in a comment section - the problem is resolvable by putting a context in a using block.
The actual reasons for the issue are still unknown to me. It looks logical, that unless something is explicitly said to be committed - to be committed. Well, I guess I have to live with this solution for now.
Perhaps I should make some clarifications about the reasons why I was not using the using block to begin with. It's because the DbContext is used from within a service. Within a service there are multiple operations being done in scope of the same transaction.
To multiple entities of database. So when the code is ready for commit - a Commit() method is executed and all of the changes done are pushed to DB at once. Otherwise if something goes wrong along the way, then all of the changes are rolled back. So for this I needed a service and normally am not allowed to use a using block by design.
To make a long story short - I will be using following service for managing context and transaction.
public class DbService : IDisposable
{
private bool _isDisposed = false;
private ApplicationDbContext _context;
private DbContextTransaction _transaction;
public DbService()
{
_context = new ApplicationDbContext();
_transaction = _context.Database.BeginTransaction();
}
public void InsertInvoice(int invoiceId)
{
try
{
var invoice1 = new OracleDatabaseService.Models.Invoice()
{
InvoiceId = (invoiceId).ToString(),
ClientId = "3",
ExternalSystemId = "0"
};
_context.Invoices.Add(invoice1);
_context.SaveChanges();
}
catch (Exception)
{
Dispose(false);
throw;
}
}
public void Commit(bool isFinal)
{
if (!_isDisposed)
{
_transaction.Commit();
if (isFinal)
{
Dispose(false);
}
else
{
_transaction.Dispose();
_transaction = _context.Database.BeginTransaction();
}
}
}
public void Rollback(bool isFinal)
{
if (!_isDisposed)
{
if (isFinal)
{
Dispose(false);
}
else
{
_transaction.Rollback();
_transaction.Dispose();
_transaction = _context.Database.BeginTransaction();
}
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
// Free other state (managed objects).
}
if (_transaction != null)
{
if (_transaction.UnderlyingTransaction.Connection != null)
{
_transaction.Rollback();
}
_transaction.Dispose();
}
if (_context != null)
{
_context.Dispose();
}
_isDisposed = true;
}
}
~DbService()
{
Dispose(false);
}
}
It is still possible to use the service in a using block. If something along the way goes wrong then a destructor shall be called to roll back the transaction and dispose of a context. There are 2 helper methods for committing and rolling back chnages manually. It could either be a final commit when the service is no longer needed or a temporary commit of current transaction and initialization of a new transaction while keeping an integrity of a service.
InsertInvoice method's contexts are also wrapped in a try/catch block in case something unexpected goes wrong.
I can't afford to insert any pending transaction data on a production environment so am taking all possible precautions! Perhaps I will be asking a question on Github about this issue Entity Framework creators themselves.
Update #1
It is very unfortunate, but the code I provided above does NOT guarantee that records will not be inserted. You have to make some additional validations, when using the service.
For example, this testcase will cause the data to be inserted into database sometimes:
[TestMethod]
public void TestFail()
{
int invoiceId = 3700;
Parallel.For(0, 30, i =>
{
var srvc = new DbService();
srvc.InsertInvoice(invoiceId + i, i);
if (i > 15)
{
throw new Exception();
}
});
}
And following code will guarantee disposal of context correctly:
[TestMethod]
public void TestGood()
{
int invoiceId = 3700;
Parallel.For(0, 30, i =>
{
DbService srvc = null;
try
{
srvc = new DbService();
srvc.InsertInvoice(invoiceId + i, i);
if (i > 25)
throw new Exception();
}
catch(Exception ex)
{
if (srvc != null)
srvc.Dispose();
throw ex;
}
});
}
Related
I have a model class:
public class Work
{
public long Id { get; set; }
[Required]
public string Name { get; set; }
}
I want this Work.Name will be unique, so I define the DbContext:
public class MyDbContext : DbContext
{
public MyDbContext () : base() { }
public MyDbContext (DbContextOptions<MyDbContext > options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Work>(entity =>
entity.HasIndex(e => e.Name).IsUnique()
);
}
public DbSet<Work> Works { get; set; }
}
And I want to test this, so I have a test like this:
[Fact]
public void Post_InsertDuplicateWork_ShouldThrowException()
{
var work = new Work
{
Name = "Test Work"
};
using (var context = new MyDbContext (options))
{
context.Works.Add(work);
context.SaveChanges();
}
using (var context = new MyDbContext (options))
{
context.Works.Add(work);
context.SaveChanges();
}
using (var context = new MyDbContext (options))
{
Assert.Equal(1, context.Works.Count());
}
}
( The option object contains settings for InMemoryDatabase)
I don't really know what to check, but the test failed in the Assert, not in the second SaveChanges(). The database (the context) contains two objects with the same Name.
I went over all the relevant questions, but I did not see anyone answering what I was asking.
As others pointed out InMemory database provider ignore all possible constraints.
My suggestion would be then to use Sqlite provider with "in-memory' feature, which will throw an exception for duplicate unique keys.
public MyDbContext CreateSqliteContext()
{
var connectionString =
new SqliteConnectionStringBuilder { DataSource = ":memory:" }.ToString();
var connection = new SqliteConnection(connectionString);
var options = new DbContextOptionsBuilder<MyDbContext>().UseSqlite(connection);
return new MyDbContext(options);
}
private void Insert(Work work)
{
using (var context = CreateSqliteContext())
{
context.Works.Add(work);
context.SaveChanges();
}
}
[Fact]
public void Post_InsertDuplicateWork_ShouldThrowException()
{
var work1 = new Work { Name = "Test Work" };
var work2 = new Work { Name = "Test Work" };
Insert(work1);
Action saveDuplicate = () => Insert(work2);
saveDuplicate.Should().Throw<DbUpdateException>(); // Pass Ok
}
The test fails because the second SaveChanges() will throw an exception from the database that tells you that you cannot add another item because it already contains an object with that Name.
Unique constraints are not enforced silently. Instead, attempting to add a duplicate value will throw an exception when you try to do it. This is so that you can actually react to it, instead of only noticing it after the fact (when you see that the data you attempted to add is not there).
You can test that by using Assert.Throws:
[Fact]
public void Post_InsertDuplicateWork_ShouldThrowException()
{
var work = new Work
{
Name = "Test Work"
};
using (var context = new MyDbContext (options))
{
context.Works.Add(work);
context.SaveChanges();
}
using (var context = new MyDbContext (options))
{
context.Works.Add(work);
Assert.Throws<Exception>(() => context.SaveChanges());
}
}
You can also specify the exact exception there (I don’t remember on top of my head which exception it exactly is that is thrown there), and you can also assign it to a variable (Assert.Throws() returns the exception) and verify the exception message to make sure that this is the exact exception you expect.
I'm currently writing integration tests using nunit for a previously untested server that was written in C# using ApiController and Entity Framework. Most of the tests run just fine, but I've ran into two that always cause the database to time out. The error messages look something like this:
System.Data.Entity.Infrastructure.DbUpdateException : An error occurred while updating the entries. See the inner exception for details.
System.Data.Entity.Core.UpdateException : An error occurred while updating the entries. See the inner exception for details.
System.Data.SqlClient.SqlException : Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding.
System.ComponentModel.Win32Exception : The wait operation timed out
The first test that's timing out:
[TestCase, WithinTransaction]
public async Task Patch_EditJob_Success()
{
var testJob = Data.SealingJob;
var requestData = new Job()
{
ID = testJob.ID,
Name = "UPDATED"
};
var apiResponse = await _controller.EditJob(testJob.ID, requestData);
Assert.IsInstanceOf<StatusCodeResult>(apiResponse);
Assert.AreEqual("UPDATED", testJob.Name);
}
The other test that's timing out:
[TestCase, WithinTransaction]
public async Task Post_RejectJob_Success()
{
var rejectedJob = Data.SealingJob;
var apiResponse = await _controller.RejectJob(rejectedJob.ID);
Assert.IsInstanceOf<OkResult>(apiResponse);
Assert.IsNull(rejectedJob.Organizations);
Assert.AreEqual(rejectedJob.JobStatus, JobStatus.OnHold);
_fakeEmailSender.Verify(
emailSender => emailSender.SendEmail(rejectedJob.Creator.Email, It.Is<string>(emailBody => emailBody.Contains(rejectedJob.Name)), It.IsAny<string>()),
Times.Once());
}
These are the controller methods that these tests are using:
The timeout always happens on the first call to await db.SaveChangesAsync() within the controller. Other controller methods that are being tested also call SaveChangesAsync without any problem. I've also tried calling SaveChangesAsync from within the failing tests and it works fine there. Both of these methods they are calling work normally when called from within the controller, but time out when called from the tests.
[HttpPatch]
[Route("editjob/{id}")]
public async Task<IHttpActionResult> EditJob(int id, Job job)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != job.ID)
{
return BadRequest();
}
Job existingJob = await db.Jobs
.Include(databaseJob => databaseJob.Regions)
.FirstOrDefaultAsync(databaseJob => databaseJob.ID == id);
existingJob.Name = job.Name;
// For each Region find if it already exists in the database
// If it does, use that Region, if not one will be created
for (var i = 0; i < job.Regions.Count; i++)
{
var regionId = job.Regions[i].ID;
var foundRegion = db.Regions.FirstOrDefault(databaseRegion => databaseRegion.ID == regionId);
if (foundRegion != null)
{
existingJob.Regions[i] = foundRegion;
db.Entry(existingJob.Regions[i]).State = EntityState.Unchanged;
}
}
existingJob.JobType = job.JobType;
existingJob.DesignCode = job.DesignCode;
existingJob.DesignProgram = job.DesignProgram;
existingJob.JobStatus = job.JobStatus;
existingJob.JobPriority = job.JobPriority;
existingJob.LotNumber = job.LotNumber;
existingJob.Address = job.Address;
existingJob.City = job.City;
existingJob.Subdivision = job.Subdivision;
existingJob.Model = job.Model;
existingJob.BuildingDesignerName = job.BuildingDesignerName;
existingJob.BuildingDesignerAddress = job.BuildingDesignerAddress;
existingJob.BuildingDesignerCity = job.BuildingDesignerCity;
existingJob.BuildingDesignerState = job.BuildingDesignerState;
existingJob.BuildingDesignerLicenseNumber = job.BuildingDesignerLicenseNumber;
existingJob.WindCode = job.WindCode;
existingJob.WindSpeed = job.WindSpeed;
existingJob.WindExposureCategory = job.WindExposureCategory;
existingJob.MeanRoofHeight = job.MeanRoofHeight;
existingJob.RoofLoad = job.RoofLoad;
existingJob.FloorLoad = job.FloorLoad;
existingJob.CustomerName = job.CustomerName;
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!JobExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return StatusCode(HttpStatusCode.NoContent);
}
[HttpPost]
[Route("{id}/reject")]
public async Task<IHttpActionResult> RejectJob(int id)
{
var organizations = await db.Organizations
.Include(databaseOrganization => databaseOrganization.Jobs)
.ToListAsync();
// Remove job from being shared with organizations
foreach (var organization in organizations)
{
foreach (var organizationJob in organization.Jobs)
{
if (organizationJob.ID == id)
{
organization.Jobs.Remove(organizationJob);
}
}
}
var existingJob = await db.Jobs.FindAsync(id);
existingJob.JobStatus = JobStatus.OnHold;
await db.SaveChangesAsync();
await ResetJob(id);
var jobPdfs = await DatabaseUtility.GetPdfsForJobAsync(id, db);
var notes = "";
foreach (var jobPdf in jobPdfs)
{
if (jobPdf.Notes != null)
{
notes += jobPdf.Name + ": " + jobPdf.Notes + "\n";
}
}
// Rejection email
var job = await db.Jobs
.Include(databaseJob => databaseJob.Creator)
.SingleAsync(databaseJob => databaseJob.ID == id);
_emailSender.SendEmail(
job.Creator.Email,
job.Name + " Rejected",
notes);
return Ok();
}
Other code that might be relevant:
The model being used is just a normal code-first Entity Framework class:
public class Job
{
public Job()
{
this.Regions = new List<Region>();
this.ComponentDesigns = new List<ComponentDesign>();
this.MetaPdfs = new List<Pdf>();
this.OpenedBy = new List<User>();
}
public int ID { get; set; }
public string Name { get; set; }
public List<Region> Regions { get; set; }
// etc...
}
To keep the database clean between tests, I'm using this custom attribute to wrap each one in a transaction (from http://tech.trailmax.info/2014/03/how-we-do-database-integration-tests-with-entity-framework-migrations/):
public class WithinTransactionAttribute : Attribute, ITestAction
{
private TransactionScope _transaction;
public ActionTargets Targets => ActionTargets.Test;
public void BeforeTest(ITest test)
{
_transaction = new TransactionScope();
}
public void AfterTest(ITest test)
{
_transaction.Dispose();
}
}
The database connection and controller being tested is build in setup methods before each test:
[TestFixture]
public class JobsControllerTest : IntegrationTest
{
// ...
private JobsController _controller;
private Mock<EmailSender> _fakeEmailSender;
[SetUp]
public void SetupController()
{
this._fakeEmailSender = new Mock<EmailSender>();
this._controller = new JobsController(Database, _fakeEmailSender.Object);
}
// ...
}
public class IntegrationTest
{
protected SealingServerContext Database { get; set; }
protected TestData Data { get; set; }
[SetUp]
public void SetupDatabase()
{
this.Database = new SealingServerContext();
this.Data = new TestData(Database);
}
// ...
}
This bug was apparently caused by the use of await within a TransactionScope. Following the top answer to this question, I added the TransactionScopeAsyncFlowOption.Enabled parameter when constructing the TransactionScope and the timeout issue went away.
I am currently using the following approach for saving my data in multiple tables in database, which I am extracting from excel files.
public class Saver
{
public static int SaveCensusBatch(string key, ICollection<tbl_Life_Census> collection)
{
using (var db = new AuraEntities())
{
var entry = new tbl_Life_Master() { UUID = key, tbl_Life_Census = collection };
db.tbl_Life_Master.Add(entry);
db.SaveChanges();
return 1;
}
}
public static int SaveLifeData(string key2, ICollection<tbl_Life_General_Info> collection)
{
using (var db = new AuraEntities())
{
var entry = new tbl_Life_Master() { UUID = key2, tbl_Life_General_Info = collection };
db.tbl_Life_Master.Add(entry);
db.SaveChanges();
return 1;
}
}
public static T GetDBRecordByPK<T>(string key) where T : class
{
using (var db = new AuraEntities())
{
var t = db.Set<T>().Find(key);
return t;
}
}
}
Following is the code for calling this in main:
foreach (var r in results)
{
r.UUID = key.ToString();
}
Saver.SaveCensusBatch(key.ToString(), results);
Saver.SaveLifeData(key.ToString(), results3);
var master = Saver.GetDBRecordByPK<tbl_Life_Master>(key.ToString());
Please suggest me how can I do everything under one 'using block' and one function only instead of implementing several functions. This is because I have to insert data into 20-30 tables simultaneously.
You can do it by making all the Saver class' methods non-static (it also prevents keeping data in memory when you don't need it), and make it implement IDisposable interface. After that, you only need to follow the disposable pattern that Microsoft suggests (https://msdn.microsoft.com/en-us/library/b1yfkh5e%28v=vs.110%29.aspx), for example:
public class Saver : IDisposable
{
private readonly AuraEntities db;
private bool disposed;
public Saver()
{
db = new AuraEntities();
disposed = false;
}
public int SaveCensusBatch(string key, ICollection<tbl_Life_Census> collection)
{
var entry = new tbl_Life_Master() { UUID = key, tbl_Life_Census = collection };
db.tbl_Life_Master.Add(entry);
return 1;
}
public int SaveLifeData(string key2, ICollection<tbl_Life_General_Info> collection)
{
var entry = new tbl_Life_Master() { UUID = key2, tbl_Life_General_Info = collection };
db.tbl_Life_Master.Add(entry);
return 1;
}
public T GetDBRecordByPK<T>(string key) where T : class
{
var t = db.Set<T>().Find(key);
return t;
}
public void Save()
{
db.SaveChanges();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposed)
{
return;
}
if (disposing && db != null)
{
db.Dispose();
disposed = true;
}
}
}
And then calling the methods within an using statement like this:
using(var saver = new Saver())
{
saver.SaveCensusBatch(key.ToString(), results);
saver.SaveLifeData(key.ToString(), results3);
saver.Save();
}
Updated: It is better if you save from outside the saver class. Because he whole process will behave as a single transaction, and it won't be persisted if an exception is raised.
You could select your results into a List and pass the list to the function. In that save function you could do your loop for each UUID and have the code for the three previous functions within the loop. Then call the db.SaveChanges() at the end outside the loop. The db.saveChanges() works as a transaction and would rollback if there was an error at any point during the save.
I would like to use Entity Framework Code first approach with SQLCE4 database. Everything seems to be really nice but I have problem with debugging sql queries. I found that EFTracing from http://efwrappers.codeplex.com/ should be exactly what I need but I don't know how to use it without app.config file. I am not big fan of this configuration. I want to use only C# code to set everything up and running. I think it should be fine to use code like this:
using (System.Data.Common.DbConnection c =
new EFTracingProvider.EFTracingConnection(
new System.Data.SqlServerCe.SqlCeConnection(conn)))
{
using (var context = new MyContext(c))
{
var a = from data in context.Projects select data;
}
}
But it doesn't work. It throws exception:
Unable to determine the provider name for connection of type
EFTracingProvider.EFTracingConnection'.
Is there any simple way how to correctly create wrapped connection only in code?
Solution for my problem is following DbContext object.
public class MyContext : DbContext
{
public MyContext()
: base(CreateConnection("Data Source=file.sdf",
"System.Data.SqlServerCe.4.0"), true)
{ }
public DbSet<Project> Projects { get; set; }
public static bool TraceEnabled = true;
private static DbConnection CreateConnection(string connectionString,
string providerInvariantName)
{
DbConnection connection = null;
if (TraceEnabled)
{
EFTracingProviderConfiguration.RegisterProvider();
EFTracingProviderConfiguration.LogToConsole = true;
string wrapperConnectionString = String.Format(#"wrappedProvider={0};{1}",
providerInvariantName, connectionString);
connection = new EFTracingConnection()
{
ConnectionString = wrapperConnectionString
};
}
else
{
DbProviderFactory factory = DbProviderFactories.GetFactory(providerInvariantName);
connection = factory.CreateConnection();
connection.ConnectionString = connectionString;
}
return connection;
}
}
So now I can use just context and connection is created automatically for wrapped or unwrapped SqlCe depending on TraceEnabled property.
using (var context = new MyContext())
{
var a = context.Projects.FirstOrDefault();
}
The genuine way to trace SQL queries is to call the ToString method like that :
var t = from c in _entities.CompanyDetail
select c;
string test = t.ToString();
I don't know EFTracing, but you might want to try MVCMiniProfiler. Despite the name MVCMiniProfiler also provide SQL queries profiling and work without config file.
I've done this by creating a wrapper class around the ObjectContext and using that wrapper instead of the original context. Here's an example context wrapper:
public partial class LoggedContext : MyContext
{
public LoggedContext()
: this("name=MyEntities") // Adjust this to match your entities
{
}
public LoggedContext(string connectionString)
: base(EntityConnectionWrapperUtils.CreateEntityConnectionWithWrappers(connectionString)
{
}
private EFTracingConnection TracingConnection
{
get { return this.UnwrapConnection<EFTracingConnection>(); }
}
public event EventHandler<CommandExecutionEventArgs> CommandExecuting
{
add { this.TracingConnection.CommandExecuting += value; }
remove { this.TracingConnection.CommandExecuting -= value; }
}
public event EventHandler<CommandExecutionEventArgs> CommandFinished
{
add { this.TracingConnection.CommandFinished += value; }
remove { this.TracingConnection.CommandFinished -= value; }
}
public event EventHandler<CommandExecutionEventArgs> CommandFailed
{
add { this.TracingConnection.CommandFailed += value; }
remove { this.TracingConnection.CommandFailed -= value; }
}
}
I also have a static class that defines the tracing output method and has a static method to initialize tracing. Here:
public static class EFTracingExtensions
{
private static ILogger _logger;
public static void InitSqlTracing(ILogger logger)
{
_logger = logger;
EFTracingProviderConfiguration.RegisterProvider();
if (logger.IsLoggingEnabled()) // Don't add logging hooks if logging isn't enabled
{
EFTracingProviderConfiguration.LogAction = new Action<CommandExecutionEventArgs>(AppendSqlLog);
}
}
private static void AppendSqlLog(CommandExecutionEventArgs e)
{
if (e.Status != CommandExecutionStatus.Executing) // we only care about Finished and Failed
{
StringBuilder msg = new StringBuilder(e.ToTraceString().TrimEnd());
msg.Append(Environment.NewLine);
if (e.Result is SqlDataReader)
{
int rows = ((SqlDataReader)e.Result).HasRows ? ((SqlDataReader)e.Result).RecordsAffected : 0;
msg.AppendFormat("*** {0} rows affected", rows);
}
else if (e.Result is int)
{
msg.AppendFormat("*** result: {0}", e.Result);
}
else
{
msg.AppendFormat("*** finished, result: {0}", e.Result);
}
msg.Append(Environment.NewLine);
msg.AppendFormat(" [{0}] [{1}] in {2} seconds", e.Method, e.Status, e.Duration);
_logger.Log(msg.ToString(), LoggerCategories.SQL);
}
}
}
ILogger is the logging interface I'm using. You need to substitute your own interface/methods.
The InitSqlTracing method is invoked once when my program starts up, and then the LoggedContext class is used to log all the SQL generated by Entity Framework.
Putting it all together with your sample code:
EFTracingExtensions.InitSqlTracing(logger); // only call this once
using (var context = new LoggedContext())
{
var a = from data in context.Projects select data;
}
I have a problem with some simple code, I'm refactoring some existing code from LINQ to SQL to the Entity Framework. I'm testing my saves and deletes, and the delete is really bugging me:
[TestMethod]
public void TestSaveDelete()
{
ObjectFactory.Initialize(x =>
{
x.For<IArticleCommentRepository>().Use<ArticleCommentRepository>();
});
PLArticleComment plac = new PLArticleComment();
plac.Created = DateTime.Now;
plac.Email = "myemail";
plac.Name = "myName";
plac.Text = "myText";
plac.Title = "myTitle";
IArticleCommentRepository acrep = ObjectFactory.GetInstance<IArticleCommentRepository>();
try
{
PortalLandEntities ple = new PortalLandEntities();
int count = ple.PLArticleComment.Count();
acrep.Save(plac);
Assert.AreEqual(ple.PLArticleComment.Count(), count + 1);
//PLArticleComment newPlac = ple.PLArticleComment.First(m => m.Id == plac.Id);
//ple.Attach(newPlac);
acrep.Delete(plac);
Assert.AreEqual(ple.PLArticleComment.Count(), count + 1);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Every time i try to run this code, I get an exception in the delete statement, telling me that its not contained within the current ObjectStateManager.Please note that both my Save and delete looks like this:
public void Delete(PLCore.Model.PLArticleComment comment)
{
using (PortalLandEntities ple = Connection.GetEntityConnection())
{
ple.DeleteObject(comment);
ple.SaveChanges();
}
}
public void Save(PLCore.Model.PLArticleComment comment)
{
using (PortalLandEntities ple = Connection.GetEntityConnection())
{
ple.AddToPLArticleComment(comment);
ple.SaveChanges();
}
}
and the connection thingy:
public class Connection
{
public static PortalLandEntities GetEntityConnection()
{
return new PortalLandEntities();
}
}
Any ideas on what i could do to make it work?
You cannot load an entity from one ObjectContext (in your case, an ObjectContext is an instance of PortalLandEntities) and then delete it from another ObjectContext, unless you detach it from the first and attach it to the second. Your life will be much, much simpler if you use only one ObjectContext at a time. If you cannot do that, you must manually Detach and then Attach first, all the while keeping track of which entities are connected to which ObjectContext.
How to use DI with your Connection : make it non-static.
public class Connection
{
private PortalLandEntities _entities;
public PortalLandEntities GetEntityConnection()
{
return _entities;
}
public Connection(PortalLandEntities entities)
{
this._entities = entities;
}
}
Then use a DI container per request. Most people do this via a controller factory.