I want to create a user with a role in the same transaction but i have an issue with the implementation. In order to use the userStore in the transaction and have it not save the changes automatically and ignore my transaction i had to turn off AutoSaveChanges. This makes it so it will wait until i call save changes. This works fine but because the userstore now does not return the userId when i call manager.Create due to this being off I dont have an Id to pass into userManager.AddToRole. Is there any way to add the user i am trying to create to a role within the same transaction?
If you start your transaction manually, then commit it, everything that was written to DB inside your transaction will be held inside your transaction. And you can rollback that if you want to.
Do something like that:
var dbContext = // get instance of your ApplicationDbContext
var userManager = // get instance of your ApplicationUserManager
using (var transaction = dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted))
{
try
{
var user = // crate your ApplicationUser
var userCreateResult = await userManger.CreateAsync(user, password);
if(!userCreateResult.Succeeded)
{
// list of errors in userCreateResult.Errors
transaction.Rollback();
return userCreateResult.Errors;
}
// new Guid for user now saved to user.Id property
var userId = user.Id;
var addToRoleresult = await userManager.AddToRoleAsync(user.Id, "My Role Name");
if(!addToRoleresult.Succeeded)
{
// deal with errors
transaction.Rollback();
return addToRoleresult.Errors;
}
// if we got here, everything worked fine, commit transaction
transaction.Commit();
}
catch (Exception exception)
{
transaction.Rollback();
// log your exception
throw;
}
}
Hope this helps.
Related
First I want to say I'm not SL developer. I just need to modify one legacy Silverlight 5 application.
It is using RIA services and XAP is hosted in Asp.Net page.
User on login page enters credentials and is able to select database from dropdown. Whole web is using multiple connections and user is able to select database to connect.
This selected database (or any identificator for data connection) is sent do XAP's InitParams, so I can access it from SL.
private void Application_Startup(object sender, StartupEventArgs e)
{
foreach (var item in e.InitParams)
{
Resources.Add(item.Key, item.Value);
}
var selectedConnectionString = GetInitParam("ConnectionString");
// TODO: Different way to store connection string
SetCookie("ConnectionString", selectedConnectionString);
RootVisual = new LoadingPage();
}
Currently I'm trying to use cookie to store selected database. I found it somewhere as one possible solution. But it needs to change.
Ok, then we have DomainService.
public class CommissionDomainService : LinqToEntitiesDomainService<CommissionEntitiesContext>
{
...
}
I know that I need to use CreateObjectContext to change ConnectionString in service. So I have:
protected override CommissionEntitiesContext CreateObjectContext()
{
// TODO: Different way to store connection string
string connectionStringName;
if (System.Web.HttpContext.Current.Request.Cookies["ConnectionString"] != null)
{
connectionStringName = System.Web.HttpContext.Current.Request.Cookies["ConnectionString"].Value;
}
else
{
throw new Exception("Missing connectionStringName");
}
var connectionStringSettings = ConfigurationManager.ConnectionStrings[connectionStringName];
var entityCs = new EntityConnectionStringBuilder
{
Metadata = "res://*/CommissionEntities.csdl|res://*/CommissionEntities.ssdl|res://*/CommissionEntities.msl",
Provider = connectionStringSettings.ProviderName,
ProviderConnectionString = connectionStringSettings.ConnectionString
};
return new CommissionEntitiesContext(entityCs.ConnectionString);
}
Again, I used Cookie to pass value from application to service.
But it is not the best idea. Because of cookie and because of persistance etc.
My question is, how to pass my ConnectionString value from main application to DomainService? Or Can I access some application context from service? Or maybe can I get connection string somewhere in EntitiesContext?
Ok, I did it this way.
I made selected database part of user identity. Because I'm using Owin, I just used one of Claims.
So when user logs in, I just put one claim with selected database
// build a list of claims
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.UserData, selectedDatabase)
};
// create the identity
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationType);
// sign in
Context.GetOwinContext().Authentication.SignIn(new AuthenticationProperties { IsPersistent = false }, identity);
Then in DomainService I used Initialize and CreateObjectContext methods
private string _connectionStringName;
public override void Initialize(DomainServiceContext context)
{
// Načteme z kontextu usera zvolenou databázi
var claim = ((ClaimsIdentity)context.User.Identity).FindFirst(ClaimTypes.UserData);
_connectionStringName = claim.Value;
base.Initialize(context);
...
}
protected override CommissionEntitiesContext CreateObjectContext()
{
if (string.IsNullOrEmpty(_connectionStringName))
{
throw new Exception("Missing connectionStringName");
}
var connectionStringSettings = ConfigurationManager.ConnectionStrings[_connectionStringName];
var entityCs = new EntityConnectionStringBuilder
{
Metadata = "res://*/CommissionEntities.csdl|res://*/CommissionEntities.ssdl|res://*/CommissionEntities.msl",
Provider = connectionStringSettings.ProviderName,
ProviderConnectionString = connectionStringSettings.ConnectionString
};
return new CommissionEntitiesContext(entityCs.ConnectionString);
}
I have a method that does some work in a transaction:
public async Task<int> AddAsync(Item item)
{
int result;
using (var transaction = await _context.Database.BeginTransactionAsync())
{
_context.Add(item);
// Save the item so it has an ItemId
result = await _context.SaveChangesAsync();
// perform some actions using that new item's ItemId
_otherRepository.Execute(item.ItemId);
transaction.Commit();
}
return result;
}
I'd like to add unit tests to check that if _context.SaveChangesAsync or _otherRepository.Execute fail then the transaction is rolled back, is that possible?
I can't see a way to do that using InMemory or SQLite?
#Ilya Chumakov's excellent answer allowed me to unit test for the transaction. Our discussion in the comments then exposed some interesting points that I thought were worth moving into an answer so they'd be more permanent and easier to see:
The primary point is that the events logged by Entity Framework change dependent on the database provider, which surprised me. If using the InMemory provider you get just one event:
Id:1; ExecutedCommand
Whereas if you use Sqlite for the in-memory database you get four events:
Id:1; ExecutedCommand
Id:5; BeginningTransaction
Id:1; ExecutedCommand
Id:6; CommittingTransaction
I hadn't expected the events logged to change depending on the DB provider.
To anyone wanting to look into this more, I captured the event details by changing Ilya's logging code as follows:
public class FakeLogger : ILogger
{
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
Func<TState, Exception, string> formatter)
{
var record = new LogRecord
{
EventId = eventId.Id,
RelationalEventId = (RelationalEventId) eventId.Id,
Description = formatter(state, exception)
};
Events.Add(record);
}
public List<LogRecord> Events { get; set; } = new List<LogRecord>();
public bool IsEnabled(LogLevel logLevel) => true;
public IDisposable BeginScope<TState>(TState state) => null;
}
public class LogRecord
{
public EventId EventId { get; set; }
public RelationalEventId RelationalEventId { get; set; }
public string Description { get; set; }
}
And then I adjusted my code that returns an in-memory database so that I could switch in-memory DB provider as follows:
public class InMemoryDatabase
{
public FakeLogger EfLogger { get; private set; }
public MyDbContext GetContextWithData(bool useSqlite = false)
{
EfLogger = new FakeLogger();
var factoryMock = Substitute.For<ILoggerFactory>();
factoryMock.CreateLogger(Arg.Any<string>()).Returns(EfLogger);
DbContextOptions<MyDbContext> options;
if (useSqlite)
{
// In-memory database only exists while the connection is open
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
options = new DbContextOptionsBuilder<MyDbContext>()
.UseSqlite(connection)
.UseLoggerFactory(factoryMock)
.Options;
}
else
{
options = new DbContextOptionsBuilder<MyDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
// don't raise the error warning us that the in memory db doesn't support transactions
.ConfigureWarnings(x => x.Ignore(InMemoryEventId.TransactionIgnoredWarning))
.UseLoggerFactory(factoryMock)
.Options;
}
var ctx = new MyDbContext(options);
if (useSqlite)
{
ctx.Database.EnsureCreated();
}
// code to populate the context with test data
ctx.SaveChanges();
return ctx;
}
}
Finally, in my unit test I made sure to clear the event log just before the assert part of my test to ensure I don't get a false positive due to events that were logged during the arrange part of my test:
public async Task Commits_transaction()
{
using (var context = _inMemoryDatabase.GetContextWithData(useSqlite: true))
{
// Arrange
// code to set up date for test
// make sure none of our setup added the event we are testing for
_inMemoryDatabase.EfLogger.Events.Clear();
// Act
// Call the method that has the transaction;
// Assert
var result = _inMemoryDatabase.EfLogger.Events
.Any(x => x.EventId.Id == (int) RelationalEventId.CommittingTransaction);
You could check EF Core logs for a RelationalEventId.RollingbackTransaction event type. I provided full details here:
How to trace an Entity Framework Core event for integration testing?
How it could look:
Assert.True(eventList.Contains((int)RelationalEventId.CommittingTransaction));
I think you are asking about how to rollback when a commit fails, EF core will auto rollback on if any of the statement failed
Read more here
, if you are asking for other reason or you want to do something when rollback happens, just to add try catch blocks,
using (var transaction = await
_context.Database.BeginTransactionAsync()){
try {
_context.Add(item);
// Save the item so it has an ItemId
result = await _context.SaveChangesAsync();
// perform some actions using that new item's ItemId
_otherRepository.Execute(item.ItemId);
transaction.Commit();
}
catch (Exception)
{
// failed, Do something
} }
I have this code in my application. If the insert fails, I would like to add information about the failure to the Audit table. Perhaps the inner exception message from the exception in the note area. Is there a way that I could do this and then still have the procedure exit with that same exception details back to the caller?
[Route("Post")]
[ValidateModel]
public async Task<IHttpActionResult> Post([FromBody]Phrase phrase)
{
phrase.StatusId = (int)EStatus.Saved;
UpdateHepburn(phrase);
db.Phrases.Add(phrase);
var audit = new Audit()
{
Entity = (int)EEntity.Phrase,
Action = (int)EAudit.Insert,
Note = phrase.English,
UserId = userId,
Date = DateTime.UtcNow,
Id = phrase.PhraseId
};
db.Audits.Add(audit);
await db.SaveChangesAsync();
return Ok(phrase);
}
You can catch the original exception and rethrow it afterwards:
try
{
await db.SaveChangesAsync();
}
catch (Exception)
{
try
{
// TODO: add to the audit here, also in a try/catch as this might fail as well
}
catch
{
}
// rethrow the original exception
throw;
}
In one of my apps, I'm rewriting Asp Core Identity UserManager's CreateAsync, to in addition to creating new user in UserStore - create a new row in a separate statistics table (on a different dbcontext). Problem is, I would like both of those operations to be fired inside a transaction, so that if one fails - the other does not commit. Here's the code :
public override async Task<IdentityResult> CreateAsync(TUser user, string password)
{
// We need to use transactions here.
var result = await base.CreateAsync(user, password);
if (result.Succeeded)
{
var appUser = user as IdentityUser;
if (appUser != null)
{
// create user stats.
var userStats = new UserStats()
{
ActionsCount = 0,
EventsCount = 0,
FollowersCount = 0,
ProjectsCount = 0,
UserId = appUser.Id
};
_ctx.UserStats.Add(userStats);
await _ctx.SaveChangesAsync();
}
}
return result;
}
Thing is, I have no idea how to set up such a transaction, as it seems TransactionScope is not a part of .Net Core yet (?) and getting currently scoped DbContext for base.CreateAsync() with HttpContext.GetOwinContext() does not work as HttpContext seems to be missing this method (referencing Microsoft.Owin.Host.SystemWeb or Microsoft.AspNet.WebApi.Owin as hinted in some other stack overflow answers won't do - both are not compatible with .netcore). Any help ?
Firstly you need to setup the dbcontext you are using with identity with a scoped lifetime:
services.AddDbContext<MyDbContext>(ServiceLifetime.Scoped); // this is the important bit
services.AddIdentity<User, Role>(options =>
{
})
.AddEntityFrameworkStores<MyDbContext, int>()
.AddDefaultTokenProviders();
Then when you need to create a transaction you do the following:
using (var transaction = await _myDbContext.Database.BeginTransactionAsync())
{
var result = await _userManager.CreateAsync(newUser, password);
try
{
if (result.Succeeded)
{
DBItem newItem = new DBItem();
_myDbContext.Add(newItem);
await _myDbContext.SaveChangesAsync();
transaction.Commit();
}
}
catch (Exception e)
{
transaction.Rollback();
}
}
In My ASP.NET MVC5 Identity 2 Application trying to use transactions but it is not working.please see the below code the transactions not working.If var saveteacher = _teacherService.Create(aTeacher); not insert successfully then AspNetUsers not rollback from database.
Code:
using (var dataContext = new SchoolMSDbContext())
{
using (var trans = dataContext.Database.BeginTransaction(IsolationLevel.ReadCommitted))
{
try
{
var adminresult =await UserManager.CreateAsync(user, teacherViewModel.Password);
if (adminresult.Succeeded)
{
aTeacher.Id = user.Id;
var saveteacher = _teacherService.Create(aTeacher);
}
else
{
trans.Rollback();
ModelState.AddModelError("", adminresult.Errors.First());
return View();
}
trans.Commit();
}
catch (Exception ex)
{
trans.Rollback();
Console.WriteLine(ex.InnerException);
}
}
}
I think the problem could be with async stuff.
Try creating the transaction like this:
TransactionScope transaction = new TransactionScope(System.Transactions.TransactionScopeAsyncFlowOption.Enabled);
(you'll have to add System.Transactions) to references.
To commit transaction go transaction.Complete() to rollback do transaction.Dispose().
The problem is that you are creating a new instance of SchoolMSDbContext when you should be acquiring the existing one from the HttpContext.
Example:
using (var dataContext = HttpContext.GetOwinContext().Get<ApplicationDbContext>())
{
//...
}
Make sure _teacherService and UserManager uses same DB Context. No need to create a TransactionScope.