dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.52k stars 3.13k forks source link

Database.BeginTransaction() throwing on InMemory Repository #2866

Closed hkoestin closed 1 year ago

hkoestin commented 8 years ago

I have a working code for EF7 beta6 with the SQL CE 4.0 data-provider, which uses some transactions inside.

Now I want to unit-test these things. However, when I switch the context options to be in-memory, I get the following exception: "No service for type 'Microsoft.Data.Entity.Storage.IRelationalConnection' has been registered." Stack: at Microsoft.Framework.DependencyInjection.ServiceProviderExtensions.GetRequiredService(IServiceProvider provider, Type serviceType) at Microsoft.Framework.DependencyInjection.ServiceProviderExtensions.GetRequiredService[T](IServiceProvider provider) at Microsoft.Data.Entity.Infrastructure.AccessorExtensions.GetService[TService](IAccessor`1 accessor) at Microsoft.Data.Entity.RelationalDatabaseFacadeExtensions.GetRelationalConnection(DatabaseFacade databaseFacade) at Microsoft.Data.Entity.RelationalDatabaseFacadeExtensions.BeginTransaction(DatabaseFacade databaseFacade)

Basically, the code to reproduce:

var options = new DbContextOptionsBuilder<BloggingContext>();
options.UseInMemoryDatabase(persist: true);
//  options.UseSqlCe(@"Data Source=Blogging.sdf"); --> works fine

using (var x = new BloggingContext(entityOptions))
{
    var tx = x.Database.BeginTransaction(); // Fails, when using InMemoryDatabase
    x.Blogs.Add(new Blog() {Url = "URL-" + (i*10)});
   ...
}

What am I doing wrong? Am I missing some context-options?

I would have thought, that these things work out of the box.

Cheers, Harald

ErikEJ commented 8 years ago

@hkoestin This is early beta software so do not expect anything to Work "out of the box" :smile:
Have you tried removing the use of Transactions? And why do you need Transactions in the first place, SaveChanges is transactional already ?

hkoestin commented 8 years ago

Well, if I remove the Transactions, it is working - even "out of the box" :smile:

However, that is not really the plan.

I need the transactions in place, because we have repositories (= DbContext) per aggregate. As long as we only touch one aggregate in one command, it is totally fine to just have "no" transaction there, because, as you said, SaveChanges anyhow pulls up a transaction. Unfortunately, the world is not ideal all the time, and in some cases we need to touch two aggregates, meaning, we need to touch two Repositories (= DbContexts). To make things transactional then, I would like to "share" the DB connection (and thus the transaction) for the two DbContexts. That is why I would like to use the transactions.

rowanmiller commented 8 years ago

This is by-design because the transaction APIs are specific to relational data stores. That said, the exception is not at all helpful so we opened #2879 to provide an exception that tells you why the API does not work.

hkoestin commented 8 years ago

@rowanmiller thanks for you reply and your thoughts. I still don't see a reason, why I can't use the BeginTransaction method on an InMemory store. It could just ignore it - or even implement some of the ACID properties (namely ACI). As a user of the whole thing, I don't mind the actual implementation behind (may it be SQL or Azure Doc Store or InMemory). I want to start a Transaction and then perform actions.

Reason for me is exchangeability for unit testing. I do not want to build in additional logic for this. I just want to configure my context options and pass them in.

I would just love to have it like:

// for production ...
var contextOptions = new DbContextOptionsBuilder().UseSqlServer("....").Options;

// OR for testing ...
var contextOptions = new DbContextOptionsBuilder().UseInMemory().Options;

// inject them and use them, no matter which kind of store it may be
var context = new DbContext(contextOptions);

I see no need for other variations here.

Do you see my point? It would be a ease of testing all over!

davidroth commented 8 years ago

@hkoestin

One of EF7s goals is to remove any magic and general abstraction over different datastores. Its better to make all operation which cannot generally be abstracted explicit. I think this is a good principle because it is impossible to reasonable abstract/hide all the different behaviors and features of the various datastores ef7 will support.

In your concrete example, Database.BeginTransaction is an extension method from the EntityFramework.Relational package and cannot work with other none-relational datastores since its explicit for relational stores and uses the System.Data.IsolationLevel enumeration for further configuration of the transaction behavior. (For example some of the isolation levels may not be supported in Azure Doc store or InMemoryStore, etc.).

I think you would have to create your own version of BeginTransaction() if you want to hide the fact that you are either using an InMemory store or a relation store. For example you could create your own extension method which checks if the the context is running in relational mode and then conditionally begin a transaction. Be aware that your unit tests may then not cover the bugs which are specific to your relational store ;)

rowanmiller commented 8 years ago

@hkoestin we did talk about having a flag that you could set to make the relational specific APIs a no-op when called on a non-relational store. So you would try and begin a transaction on in-memory... it would throw saying "that's not supported but change this flag if you want it to just no-op". Would that work for you?

hkoestin commented 8 years ago

@rowanmiller yes, this would already help a lot. Still, I would favor a real transaction (even in memory) over a no-op, but it is better than nothing.

Background: I want to be able to run basic unit (and integration) tests really fast by using the in-memory features of EF7. Setting up a whole context there would just cost to much. And I am talking about roughly 5000 tests. It is a big difference, if setting up the db-context takes 20ms or is almost instant.

For tests, which need a real transaction, e.g. they are testing rollback features or similar, I still can configure an ordinary SQLCE provider. That is ok.

rowanmiller commented 8 years ago

@hkoestin yep your scenario makes total sense. BTW we have talked about promoting the concept of transactions to core (rather than relational). Not all data stores have transactions... but the vast majority do.

hkoestin commented 8 years ago

@rowanmiller Love to hear that the concept of transactions may be known to more (and not only relational) databases :smile:

S4lt5 commented 8 years ago

@hkoestin I am in the same boat for sure. I absolutely love what I can do with an inmemory database, test server, and some tinkering with code. I have nearly all of what would have been scripted or manual tests as xunit tests and it has been fantastic. However, not being able to test methods with transaction scope is a huge letdown right now. I don't care if the transaction doesn't work, but right now I can't even run that code.

rowanmiller commented 8 years ago

@Yablargo we promoted the transaction APIs to core in our working code base. So when RC2 release you'll be able to use them with InMemory (https://github.com/aspnet/EntityFramework/issues/2891)

S4lt5 commented 8 years ago

@rowanmiller Thanks, after trying and failing for a day to wire this in rc1, I just wrote a helper in my base dbContext that calls the transaction only when not in memory. Looking forward with the new version eagerly.

kimwandev commented 7 years ago

Is this already available in EF Core 1.1.0?

rowanmiller commented 7 years ago

Yes, it was actually included in the 1.0.0 release

kimwandev commented 7 years ago

So if I use InMemory option I should be able to test methods that uses BeginTransactions? I'm using 1.0.0 core and I am experiencing a runtime error in using InMemory DB when testing methods with BeginTransaction. Thanks for your response!

rowanmiller commented 7 years ago

@kimwandev can you post the exception that you are getting?

kimwandev commented 7 years ago
screen shot 2017-02-03 at 2 44 53 am
rowanmiller commented 7 years ago

I think the exception message tells you everything you need to know 😄...

Warning as error exception for warning 'InMemoryEventId.TransactionIgnoredWarning': Transactions are not supported by the in-memory store. See http://go.microsoft.com/fwlink/?LinkId=800142. To suppress this Exception use the DbContextOptionsBuilder.ConfigureWarnings API. ConfigureWarnings can be used when overriding the DbContext.OnConfiguring method or using AddDbContext on the application service provider.

So here is what your configuration code would look like...

options
    .UseInMemoryDatabase()
    .ConfigureWarnings(w => w.Ignore(InMemoryEventId.TransactionIgnoredWarning))
kimwandev commented 7 years ago

@rowanmiller Thank you so much for pointing that out. I have tested it and it worked like a charm. I am going to my eye doctor after my shift and get a pair of glasses so that I can read the exceptions carefully next time 😄

Again, Thanks for your help!

Regards, Kimwan Ogot

generik0 commented 6 years ago

@rowanmiller I am a little confused about this issue. The inmemorydb is that now mainly for testing? ms testing in-memory

But if we cannot make transactions, how can we use it for tessting e.g. our repositories that accept a transactions.

Thanks for your time

ajcvickers commented 5 years ago

@generik0 It depends what you are trying to test. If the code is largely database agnostic and just needs to get data from somewhere, then in-memory is a good choice. If the code depends on relational concepts, like transactions, but is largely agnostic to the specific relational database engine, then SQLite in-memory might be better. If the code depends on specific functionality of the database engine, then the only option is to test with that specific engine.

generik0 commented 5 years ago

@ajcvickers Yes you are right, it depends on what is being tested!. The issue with SQLite it that the migrations in Ef7 are database secific. So it you want to use SQLite but are actually running with e.g. MySQL you need to maintain 2 sets of migrations. This is why the in memory is good. No migrations. The mssqllocaldb we normally use for integration tests as then we only have one migration.

The way we normally do architecture is -> cqrs handler -> repository-> dbcontext/dbaets. This fits very good with testing. The majority of the repository tests are fine as memory because the repository method calls have a common dbcontext or a uow passed to them. The uow we can mock so the transaction is not created, but make the mock return the dbcontext for the dbset calls. In the cqrs handler, yes we create the uow/transaction. But just need to mock all interfaces her, so no issue here either.

My problem was,. I was doing integration tests in the cqrs handler, where a transaction was being created. The in memory is not a candidate for integration tests on this level, as transactions are not supported. It would have been nice, much faster tests, but technically not possible.

0xced commented 4 years ago

And here's a great intro on how to use SQLite in-memory for testing: Testing EF Core in Memory using SQLite.