Working on a WebApi project that's backed by mssql with EntityFramework, and also Oracle (12c) using oracle's ManagedDataAccess.Client.OracleConnection. We use autofac to inject an instance of our context per request, but all oracle access is just done ad hoc.
We have certain operations that depend on both databases at the same time, so we opted to use the TransactionScope object to manage the transaction.
For the most part it works well, the light weight transactions that are promoted to distributed work great. But there is one issue I've encountered after completing a distributed transaction.
Given:
public void Test()
{
var preItem = new HelpItem
{
Field1 = "pre batch";
};
_context.Items.Add(preItem);
_context.SaveChanges(); // This save always works.
var batchResult = FooService.BatchOperation(true);
var postItem = new HelpItem
{
Field1 = "post batch";
};
_context.Items.Add(postItem);
_context.SaveChanges(); // This will succeed/fail depending on whether FooService caused a distributed transaction.
}
With the BatchOperation method as:
public Result BatchOperation(bool triggerDtc)
{
using (var transaction = new new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
if (triggerDtc){
// Make requests to both databases.
} else {
// Make request to one database.
}
// Always complete for the sake of the demonstration.
transaction.Complete();
}
}
If a distributed transaction is encountered and then completed & fully disposed EF doesn't seem to be able to recover and go back to working as it was before the transaction came into play.
The error:
Distributed transaction completed. Either enlist this session in a new
transaction or the NULL transaction.
What would be the correct way to handle this?
For this particular case you can simply create another transaction around the second part:
var batchResult = FooService.BatchOperation(true);
using (var transaction = new new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
var postItem = new HelpItem
{
Field1 = "post batch";
};
_context.Items.Add(postItem);
_context.SaveChanges(); // This save depends on whether FooService caused a distributed transaction.
transaction.Complete();
}
But this issue came up because the FooService.BatchOperation method was altered with just a lookup to the other database, unknowingly breaking every method out there that continues to use the context after calling it. With normal transaction a single EF context can freely be used in and out of them without issue, is there any way to achieve the same with a distributed transaction?
EDIT:
This really just has me confused now. Just the act of making a request in another (non distributed) transactionscope is enough to restore EF functionality.
public IHttpActionResult Test()
{
var preItem = new HelpItem
{
Field1 = "pre batch";
};
_context.Items.Add(preItem);
_context.SaveChanges(); // This save works.
var batchResult = FooService.BatchOperation(true);
using (var transaction = new new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
var lookupAnything = _context.Items.ToList();
transaction.Complete(); // This is optional, because we really don't care and it's disposed either way.
}
var postItem = new HelpItem
{
Field1 = "post batch";
};
_context.Items.Add(postItem);
_context.SaveChanges(); // Now this always works.
}
Obviously I can't just go around putting this everywhere, so still not sure what the actual solution is.
Related
I have two entities bound as one-to-one via foreignkey: CreateTenantDto and SaasTenantCreateDto.
I need to use TWO repositories (_abpTenantRepository is an instance of 3rd party repository from ABP Framework) to insert those entities into DB. I am trying to use ABP UnitOfWork implementation for this. After SaasTenantCreateDto entity is inserted, I am trying to insert CreateTenantDto entry which depends on it. If I use OnCompleted event to insert a CreateTenantDto record - the method does not enter OnCompleted before returning newTenantDto and the latter is returned as a null (the records are inserted finally, but I want to return the inserted entity if it's inserted successfully). If I don't use OnCompleted at all - the method hangs (looks like DB lock). If I use two nested UnitOfWork objects - the method hangs as well. If I use the scope for working with two repositories -
using (var scope = ServiceProvider.CreateScope())
{
var unitOfWorkManager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
using (var tenantUow = unitOfWorkManager.Begin(new AbpUnitOfWorkOptions { IsTransactional = true }))
{ ... }
}
it hangs also... It is definitely the lock and it has to do with accessing the id from the newly created newAbpTenant: I can see that in SQL Developer Sessions
enq: TX - row lock contention
and guilty session is another my HttpApi host session. Probably, the reason is as Oracle doc says: "INSERT, UPDATE, and DELETE statements on the child table do not acquire any locks on the parent table, although INSERT and UPDATE statements wait for a row-lock on the index of the parent table to clear." - SaveChangesAsync causes new record row lock?
How to resolve this issue?
//OnModelCreatingBinding
builder.Entity<Tenant>()
.HasOne(x => x.AbpTenant)
.WithOne()
.HasPrincipalKey<Volo.Saas.Tenant>(x => x.Id)
.HasForeignKey<Tenant>(x => x.AbpId);
...
b.Property(x => x.AbpId).HasColumnName("C_ABP_TENANT").IsRequired();
//Mapping ignoration to avoid problems with 'bound' entities, since using separate repositories for Insert / Update
CreateMap<CreateTenantDto, Tenant>().ForMember(x => x.AbpTenant, opt => opt.Ignore());
CreateMap<UpdateTenantDto, Tenant>().ForMember(x => x.AbpTenant, opt => opt.Ignore());
public class CreateTenantDto
{
[Required]
public int Id { get; set; }
...
public Guid? AbpId { get; set; }
public SaasTenantCreateDto AbpTenant { get; set; }
}
public async Task<TenantDto> CreateAsync(CreateTenantDto input)
{
try
{
TenantDto newTenantDto = null;
using (var uow = _unitOfWorkManager.Begin(new AbpUnitOfWorkOptions { IsTransactional = true, IsolationLevel = System.Data.IsolationLevel.Serializable }))
{
var abpTenant = await _abpTenantManager.CreateAsync(input.AbpTenant.Name, input.AbpTenant.EditionId);
input.AbpTenant.MapExtraPropertiesTo(abpTenant);
var newAbpTenant = await _abpTenantRepository.InsertAsync(abpTenant);
await uow.SaveChangesAsync();
var tenant = ObjectMapper.Map<CreateTenantDto, Tenant>(input);
tenant.AbpId = newAbpTenant.Id;
var newTenant = await _tenantRepository.InsertAsync(tenant);
newTenantDto = ObjectMapper.Map<Tenant, TenantDto>(newTenant);
await uow.CompleteAsync();
}
return newTenantDto;
}
//Implementation by ABP Framework
public virtual async Task CompleteAsync(CancellationToken cancellationToken = default)
{
if (_isRolledback)
{
return;
}
PreventMultipleComplete();
try
{
_isCompleting = true;
await SaveChangesAsync(cancellationToken);
await CommitTransactionsAsync();
IsCompleted = true;
await OnCompletedAsync();
}
catch (Exception ex)
{
_exception = ex;
throw;
}
}
I have finally resolved the problem using the following approach (but it is not using TWO repositories which seems to be impossible to implement, since we need to manipulate DbContext directly):
Application service layer:
//requiresNew: true - to be able to use TransactionScope
//isTransactional: false, otherwise it won't be possible to use TransactionScope, since we would have active ambient transaction
using var uow = _unitOfWorkManager.Begin(requiresNew: true);
var abpTenant = await _abpTenantManager.CreateAsync(input.AbpTenant.Name, input.AbpTenant.EditionId);
input.AbpTenant.MapExtraPropertiesTo(abpTenant);
var tenant = ObjectMapper.Map<CreateTenantDto, Tenant>(input);
var newTenant = await _tenantRepository.InsertAsync(tenant, abpTenant);
await uow.CompleteAsync();
return ObjectMapper.Map<Tenant, TenantDto>(newTenant);
Handmade InsertAsync method on Repository (EntityFrameworkCore) layer:
using (new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled))
{
var newAbpTenant = DbContext.AbpTenants.Add(abpTenant).Entity;
tenant.AbpId = newAbpTenant.Id;
var newTenant = DbContext.Tenants.Add(tenant).Entity;
if (autoSave)
{
await DbContext.SaveChangesAsync(GetCancellationToken(cancellationToken));
}
return newTenant;
}
It's better if this issue is explained with an example. I have a database table Person with an int column named [Num]. It has only a record with the initial value of Num == 0.
In my PersonAppService.cs, there are the following 2 methods
public void TestIncrementA()
{
using (var uow = _unitOfWorkManager.Begin(new UnitOfWorkOptions { IsolationLevel = IsolationLevel.RepeatableRead })
{
var person = _personRepository.Get(1);
person.Num += 1;
Thread.Sleep(3000);
uow.Complete();
}
}
public void TestIncrementB()
{
using (var uow = _unitOfWorkManager.Begin(new UnitOfWorkOptions { IsolationLevel = IsolationLevel.RepeatableRead })
{
var person = _personRepository.Get(1);
person.Num += 1;
uow.Complete();
}
}
The 2 methods are essentially the same which increment the value of the column Num by one except that the first method delays the thread.
Now in the console of a web browser, I run the following commands in quick succession.
abp.services.app.person.testIncrementA();
abp.services.app.person.testIncrementB();
I would expect the value of Num in my database to be 2 now since it's been incremented twice. However it's only 1.
It's clear the RepeatableRead UoW is not locking the row properly. I have also tried using the attribute [UnitOfWork(IsolationLevel.RepeatableRead)] to no avail.
But, if I were to set the following in the PreInitialize of a module, it works.
Configuration.UnitOfWork.IsolationLevel = IsolationLevel.RepeatableRead;
This will unfortunately force RepeatableRead app-wide. Is there something that I'm overlooking?
To set a different isolation level from the ambient unit of work, begin another with RequiresNew:
using (var uow = _unitOfWorkManager.Begin(new UnitOfWorkOptions
{
Scope = TransactionScopeOption.RequiresNew, // Add this
IsolationLevel = IsolationLevel.RepeatableRead
})
{
...
}
Explanation
From https://aspnetboilerplate.com/Pages/Documents/Unit-Of-Work:
If a unit of work method calls another unit of work method, both use the same connection & transaction. The first entered method manages the connection & transaction and then the others reuse it.
The default IsolationLevel for a unit of work is ReadUncommitted if it is not configured. ...
Conventional Unit Of Work Methods
Some methods are unit of work methods by default:
...
All Application Service methods.
...
I'm facing a singular problem...
I need to update an entity, but i don't know when it is really updated
My method is
#Override
#Transactional(isolation = Isolation.SERIALIZABLE)
public void lightOn(int idInterruttore) {
Interruttore interruttore = dao.findById(idInterruttore);
String inputPin = interruttore.getInputPin();
String pinName = interruttore.getRelePin();
GpioController gpio = interruttore.getGpio();
GpioPinDigitalOutput rele = gpio.provisionDigitalOutputPin(RaspiPin.getPinByName(pinName));
try {
DateTime date = new DateTime();
Date now = date.toDate();
int i = 1;
while (getInput(inputPin, gpio) != 1) {
if(i > 1){
logger.debug(String.format("Try n %s", i));
}
pushButton(rele);
Thread.sleep(1000);
i++;
}
dao.updateInterruttore(idInterruttore, now, true);
} catch (GpioPinExistsException | InterruptedException gpe) {
logger.error("GPIO giĆ esistente", gpe);
} finally {
gpio.unprovisionPin(rele);
}
logger.debug(String.format("After the update status should be true and it's %s",
interruttore.isStato()));
}
updateInterruttore is (i used this form to be sure to call the commit after the update... I have the lock Option because multiple call can be done to this method but only the first must update
#Override
public void updateInterruttore(int idInterruttore, Date dateTime, boolean stato) {
Session session = getSession();
Transaction tx = session.beginTransaction();
String update = "update Interruttore i set i.dateTime = :dateTime, i.stato = :stato where idInterruttore = :idInterruttore";
session.createQuery(update).setTimestamp("dateTime", dateTime).setBoolean("stato", stato)
.setInteger("idInterruttore", idInterruttore).setLockOptions(LockOptions.UPGRADE).executeUpdate();
tx.commit();
}
}
Well... when I update the log says me:
After the update status should be true and it's false
This happens only the first time I call the method, the second time interruttore.isStato is correctly true.
Why this happens?
This happens because you're updating the database directly with the update statement. Hibernate does not update automatically an already loaded entity in this case. If you reload the entity after the call to dao.updateInterruttore you should get the updated data.
Two notes:
1) You are using a query to apply the update. In that case, Hibernate will no update the entity that is in the session. Unless you update the entity itself and call session.save(interruttore), then the entity will not be updated. (But the update shows up in the DB.) Furthermore, I don't understand why you just don't update the entity and save it via session.save().
2) You are annotating the service method with #Transactional. (Assuming that's Spring annotation) If you use JTA, your tx.commit() will have no effect. But once the method completes, your transaction is committed. (or rolled back if the method throws an exception) If you are not using JTA, then get rid of #Transactional and manage transaction in your DAO method, as you are doing. But that's considered bad practice.
I am trying to test a simple NHibernate-based auditing mechanism that stores one row per changed property into a changelog table. What it actually does, is perform the actual insert statement as expected and perform the audit logging twice.
So, this is what I do:
string connectionString = #"Data Source=.\SQLEXPRESS;Initial Catalog=audittest;Integrated Security=SSPI;";
FluentConfiguration config = Fluently.Configure().Database(MsSqlConfiguration.MsSql2008
.ConnectionString(c => c.Is(connectionString)).ShowSql())
.Mappings(x => x.FluentMappings.Add<Class1ClassMap>())
.Mappings(x => x.FluentMappings.Add<ChangeLogMap>())
.ExposeConfiguration(cfg =>
{
NHibernateAuditListener listener = new NHibernateAuditListener();
cfg.AppendListeners(ListenerType.PostInsert, new[] { listener });
});
ISessionFactory sf = config.BuildSessionFactory();
ISession session = sf.OpenSession();
using (ITransaction tr = session.BeginTransaction())
{
session.Save(new Class1()
{
FirstName="Peter",
LastName="Pan",
Id=100
});
tr.Commit();
}
EDIT:
Altered the logging code to something simple to see the failure:
public void OnPostInsert(PostInsertEvent #event)
{
if (#event.Entity is IAuditable)
{
Console.WriteLine("----write audit----");
for (int index = 0; index < #event.State.Length; index++)
Console.WriteLine("----store changes of property {0}----",
#event.Persister.PropertyNames[index]);
}
}
This generates the following output:
NHibernate: INSERT INTO "Class1" (FirstName, LastName, Id) VALUES (#p0, #p1, #p2); #p0 = 'Peter' [Type: String (0)], #p1 = 'Pan' [Type: String (0)], #p2 = 1 [Type: Int64 (0)]
----write audit----
----store changes of property FirstName----
----store changes of property LastName----
----write audit----
----store changes of property FirstName----
----store changes of property LastName----
As you see, it's not the EventHandler code that's erroneous, but the framework calling it that behaves unexpectedly (calling the OnPostInsert method twice). Any ideas why this is happening?
SAMPLE PROJECT DOWNLOAD
Okay everybody, the problem exists in the detail of the handling within the programm. You are building up a FluentConfiguration-instance which on the fly creates the the basic NHibernate configuration.
This is done on the call of these 2 lines (variable config is of type FluentConfiguration):
new SchemaExport(config.BuildConfiguration()).Create(true, true);
and
ISessionFactory sf = config.BuildSessionFactory();
The FluentConfiguration caches the first created instance and reuses it for creating the new instance for the ISessionFactory-instance. On both calls the ExposeConfiguration of FluentConfiguration instance is called. So there are 2 instances of the NHibernateAuditListener within the session that is persisting the data.
Try it like this:
string connectionString = #"Data Source=.\SQLEXPRESS;Initial Catalog=audittest;Integrated Security=SSPI;";
var config = Fluently.Configure().Database(MsSqlConfiguration.MsSql2008
.ConnectionString(c => c.Is(connectionString)).ShowSql())
.Mappings(x => x.FluentMappings.Add<Class1ClassMap>())
.Mappings(x => x.FluentMappings.Add<ChangeLogMap>())
.ExposeConfiguration(cfg =>
{
NHibernateAuditListener listener = new NHibernateAuditListener();
cfg.AppendListeners(ListenerType.PostInsert, new[] { listener });
})
.BuildConfiguration();
new SchemaExport(config).Create(true, true);
Console.WriteLine("----------------------------------------------");
ISessionFactory sf = config.BuildSessionFactory();
ISession session = sf.OpenSession();
using (ITransaction tr = session.BeginTransaction())
{
session.Save(new Class1()
{
FirstName="Peter",
LastName="Pan",
Id=100
});
tr.Commit();
}
Within config you have now the real NHibernate Configuration instance, with only one Listener registered.
Got it?!
Currently I'm developing an OAuth2 authorization server using DotNetOpenAuth CTP version. My authorization server is in asp.net MVC3, and it's based on the sample provided by the library. Everything works fine until the app reaches the point where the user authorizes the consumer client.
There's an action inside my OAuth controller which takes care of the authorization process, and is very similar to the equivalent action in the sample:
[Authorize, HttpPost, ValidateAntiForgeryToken]
public ActionResult AuthorizeResponse(bool isApproved)
{
var pendingRequest = this.authorizationServer.ReadAuthorizationRequest();
if (pendingRequest == null)
{
throw new HttpException((int)HttpStatusCode.BadRequest, "Missing authorization request.");
}
IDirectedProtocolMessage response;
if (isApproved)
{
var client = MvcApplication.DataContext.Clients.First(c => c.ClientIdentifier == pendingRequest.ClientIdentifier);
client.ClientAuthorizations.Add(
new ClientAuthorization
{
Scope = OAuthUtilities.JoinScopes(pendingRequest.Scope),
User = MvcApplication.LoggedInUser,
CreatedOn = DateTime.UtcNow,
});
MvcApplication.DataContext.SaveChanges();
response = this.authorizationServer.PrepareApproveAuthorizationRequest(pendingRequest, User.Identity.Name);
}
else
{
response = this.authorizationServer.PrepareRejectAuthorizationRequest(pendingRequest);
}
return this.authorizationServer.Channel.PrepareResponse(response).AsActionResult();
}
Everytime the program reaches this line:
this.authorizationServer.Channel.PrepareResponse(response).AsActionResult();
The system throws an exception which I have researched with no success. The exception is the following:
Only parameterless constructors and initializers are supported in LINQ to Entities.
The stack trace: http://pastebin.com/TibCax2t
The only thing I've done differently from the sample is that I used entity framework's code first approach, an I think the sample was done using a designer which autogenerated the entities.
Thank you in advance.
If you started from the example, the problem Andrew is talking about stays in DatabaseKeyNonceStore.cs. The exception is raised by one on these two methods:
public CryptoKey GetKey(string bucket, string handle) {
// It is critical that this lookup be case-sensitive, which can only be configured at the database.
var matches = from key in MvcApplication.DataContext.SymmetricCryptoKeys
where key.Bucket == bucket && key.Handle == handle
select new CryptoKey(key.Secret, key.ExpiresUtc.AsUtc());
return matches.FirstOrDefault();
}
public IEnumerable<KeyValuePair<string, CryptoKey>> GetKeys(string bucket) {
return from key in MvcApplication.DataContext.SymmetricCryptoKeys
where key.Bucket == bucket
orderby key.ExpiresUtc descending
select new KeyValuePair<string, CryptoKey>(key.Handle, new CryptoKey(key.Secret, key.ExpiresUtc.AsUtc()));
}
I've resolved moving initializations outside of the query:
public CryptoKey GetKey(string bucket, string handle) {
// It is critical that this lookup be case-sensitive, which can only be configured at the database.
var matches = from key in db.SymmetricCryptoKeys
where key.Bucket == bucket && key.Handle == handle
select key;
var match = matches.FirstOrDefault();
CryptoKey ck = new CryptoKey(match.Secret, match.ExpiresUtc.AsUtc());
return ck;
}
public IEnumerable<KeyValuePair<string, CryptoKey>> GetKeys(string bucket) {
var matches = from key in db.SymmetricCryptoKeys
where key.Bucket == bucket
orderby key.ExpiresUtc descending
select key;
List<KeyValuePair<string, CryptoKey>> en = new List<KeyValuePair<string, CryptoKey>>();
foreach (var key in matches)
en.Add(new KeyValuePair<string, CryptoKey>(key.Handle, new CryptoKey(key.Secret, key.ExpiresUtc.AsUtc())));
return en.AsEnumerable<KeyValuePair<string,CryptoKey>>();
}
I'm not sure that this is the best way, but it works!
It looks like your ICryptoKeyStore implementation may be attempting to store CryptoKey directly, but it's not a class that is compatible with the Entity framework (due to not have a public default constructor). Instead, define your own entity class for storing the data in CryptoKey and your ICryptoKeyStore is responsible to transition between the two data types for persistence and retrieval.