Compare Exchange sync multiple reservations (UNIQUE-constraints) - c#

When I was reading up on Compare Exhange for RavenDB I found the following user case in the documentation for reserving a email. Basically a way to enforcing a UNIQUE-constraint. This works great if you want to only enforce this constraint for one property but ones you introduce multiple properties (email and user name) it no longer works as expected.
See in Docs: Link
class Program
{
public class User
{
public string Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
static void Main(string[] args)
{
var store = new DocumentStore()
{
Urls = new[] {
"http://127.0.0.1:8080/"
},
Database = "example",
}.Initialize();
string name = "admin";
string email = "admin#example.com";
var user = new User
{
Name = name,
Email = email
};
using (IDocumentSession session = store.OpenSession())
{
session.Store(user);
// Try to reserve a new user email
// Note: This operation takes place outside of the session transaction,
// It is a cluster-wide reservation
CompareExchangeResult<string> namePutResult
= store.Operations.Send(
new PutCompareExchangeValueOperation<string>("names/" + name, user.Id, 0));
if (namePutResult.Successful == false)
{
throw new Exception("Name is already in use");
}
else
{
// Try to reserve a new user email
// Note: This operation takes place outside of the session transaction,
// It is a cluster-wide reservation
CompareExchangeResult<string> emailPutResult
= store.Operations.Send(
new PutCompareExchangeValueOperation<string>("emails/" + email, user.Id, 0));
// Unlock name again (Because if we dont the name wil be locked)
if (emailPutResult.Successful == false)
{
// First, get existing value
CompareExchangeValue<string> readResult =
store.Operations.Send(
new GetCompareExchangeValueOperation<string>("names/" + name));
// Delete the key - use the index received from the 'Get' operation
CompareExchangeResult<string> deleteResult
= store.Operations.Send(
new DeleteCompareExchangeValueOperation<string>("names/" + name, readResult.Index));
// The delete result is successful only if the index has not changed between the read and delete operations
if (deleteResult.Successful == false)
{
throw new Exception("The name is forever lost");
}
else
{
throw new Exception("Email is already in use");
}
}
}
// At this point we managed to reserve/save both the user name and email
// The document can be saved in SaveChanges
session.SaveChanges();
}
}
}
In the example above you can see why this no longer works as expected. Because now if the email Compare Exchange failed or is already taken there is a change the name Compare Exchange cannot be reversed/removed because removing a Compare Exchange can theoretically fail. Now because of this there is a change the user name will get permanently locked and can't be used again. This same problem also happens when you try to update the user name because you will have to unlock/remove the Compare Exchange for the old user name once the new one is reserved.
What is the best approach for something like this and what are the changes of this happening?

if you are in namePutResult.Successful context then you know for sure that namePutResult.Index is the unique index that was used to create the CompareExchange, so in case that email is taken, you can straight use the namePutResult.Index to remove the CompareExchange, in case of failure you can handle the exception (resend the DeleteCompareExchangeValueOperation`).
using (IDocumentSession session = store.OpenSession())
{
session.Store(user);
// Try to reserve a new user email
// Note: This operation takes place outside of the session transaction,
// It is a cluster-wide reservation
CompareExchangeResult<string> namePutResult
= store.Operations.Send(
new PutCompareExchangeValueOperation<string>("names/" + name, user.Id, 0));
if (namePutResult.Successful == false)
{
throw new Exception("Name is already in use");
}
else
{
// Try to reserve a new user email
// Note: This operation takes place outside of the session transaction,
// It is a cluster-wide reservation
CompareExchangeResult<string> emailPutResult
= store.Operations.Send(
new PutCompareExchangeValueOperation<string>("emails/" + email, user.Id, 0));
// Unlock name again (Because if we dont the name wil be locked)
if (emailPutResult.Successful == false)
{
// Delete the key - use the index of PUT operation
// TODO: handle failure of this command
CompareExchangeResult<string> deleteResult
= store.Operations.Send(
new DeleteCompareExchangeValueOperation<string>("names/" + name, namePutResult.Index));
if (deleteResult.Successful == false)
{
throw new Exception("The name is forever lost");
}
else
{
throw new Exception("Email is already in use");
}
}
}
// At this point we managed to reserve/save both the user name and email
// The document can be saved in SaveChanges
session.SaveChanges();
}

Have you tried to use the cluster-wide transaction session to store both of the compare-exchange values in a single transaction?
https://ravendb.net/docs/article-page/5.1/Csharp/client-api/session/cluster-transaction#create-compare-exchange

Related

How do I add a role to new members discord

I've tried different things but they never work. I tried to add the code to my new user joined task
public async Task AnnounceJoinedUser(SocketGuildUser user) //welcomes New Players
{
var channel = Client.GetChannel(447147292617736203) as SocketTextChannel; //gets channel to send message in
await channel.SendMessageAsync("Welcome " + user.Mention + " to the server! Have a great time"); //Welcomes the new user
}
But I dont know how to add a role to new user
Get the role from the guild that the user joined in first, then add the role to the user using user.AddRoleAsync().
private async Task UserJoined(SocketGuildUser socketGuildUser) {
ulong roleID = 123; //Some hard-coded roleID
var role = socketGuildUser.Guild.GetRole(roleID);
await socketGuildUser.AddRoleAsync(role);
//Without ID...
string roleName = "The role name to add to user"; //Or some other property
//Get the list of roles in the guild.
var guildRoles = socketGuildUser.Guild.Roles;
//Loop through the list of roles in the guild.
foreach(var guildRole in guildRoles) {
//If the current iteration of role matches the rolename
if(guildRole.Name.Equals(roleName, StringComparison.OrdinalIgnoreCase)) {
//Assign the role to the user.
await socketGuildUser.AddRoleAsync(guildRole);
//Exit Loop.
break;
}
}
}

Error Account with Id = "xxxxxx" does not exist

I have a custo workflow that creates an account and opportunities.
Sometimes I have this error: Account with Id = "xxxxxx" does not exist.
I don't know what's wrong in my code knowing that I find the account in the CRM.
Here are the steps of my plugin code:
Find the account by num (if it doesn't exist, I create them)
Get the account = Account
Create an opportunity with Opportunity["parentaccountid"] = Account;
Error message !
Code:
//Get opportunity
Guid id = retrieveOpportunity<string>("opportunity", "new_numero", numero, service);
Entity eOpportunity;
if (id != Guid.Empty)
{
eOpportunity = new Entity("opportunity", id);
}
else
{
eOpportunity = new Entity("opportunity");
}
//Get account
EntityReference eAccount = retrieveAccount<string>(accountCode, "account", "new_code", service);
if (eAccount == null)
{
eAccount = new Entity("account", "new_code", accountCode);
eAccount["name"] = "name";
UpsertRequest usMessage = new UpsertRequest()
{
Target = eAccount
};
//create account
UpsertResponse usResponse = (UpsertResponse)this._service.Execute(usMessage);
eOpportunity["parentaccountid"] = usResponse.Target;
}
else
{
eOpportunity["parentaccountid"] = eAccount;
}
UpsertRequest req = new UpsertRequest()
{
Target = eOpportunity
};
//upsert opportunity
UpsertResponse resp = (UpsertResponse)service.Execute(req);
if (resp.RecordCreated)
tracer.Trace("New opportunity");
else
tracer.Trace("Opportunity updated");
Sometimes there are several workflows that are started at the same time and that do the same thing (creating other opportunities)
You haven't shown us the entire plugin, so this is just a guess, but you're probably sharing your IOrganizationService at the class level, which is causing race conditions in your code, and one thread creates a new account in a different context, then its service gets overwritten by another thread, which is in a different database transaction that doesn't have the newly created account and it's erroring.
Don't share your IOrganziationService across threads!
Whenever you are trying to consume the created record in the same transaction, convert the plugin into Asynchronous mode - this will work.

EntityState must be set to null, Created (for Create message) or Changed (for Update message)

In my C# console application I am trying to update an account in CRM 2016. IsFaulted keeps returning true.
The error message it returns when I drill down is the following:
EntityState must be set to null, Created (for Create message) or Changed (for Update message).
Also in case it might cause the fault I have pasted my LINQ query at the bottom.
The answers I get from Google states either that I am mixing ServiceContext and ProxyService (which am not, I am not using it in this context). The others says that I am using context.UpdateObject(object) incorrectly, which I am not using either.
Update: Someone just informed me that the above error is caused because I am trying to return all the metadata and not just the updated data. Still I have no idea how to fix the error, but this information should be helpful.
private static void HandleUpdate(IOrganizationService crmService, List<Entity> updateEntities)
{
Console.WriteLine("Updating Entities: " + updateEntities.Count);
if (updateEntities.Count > 0)
{
try
{
var multipleRequest = new ExecuteMultipleRequest()
{
// Assign settings that define execution behavior: continue on error, return responses.
Settings = new ExecuteMultipleSettings()
{
ContinueOnError = true,
ReturnResponses = true
},
// Create an empty organization request collection.
Requests = new OrganizationRequestCollection()
};
foreach (var account in updateEntities)
{
multipleRequest.Requests.Add(
new UpdateRequest()
{
Target = account
});
}
ExecuteMultipleResponse response = (ExecuteMultipleResponse)crmService.Execute(multipleRequest);
if (response.IsFaulted)
{
int failedToUpdateAccount = 0;
foreach (ExecuteMultipleResponseItem singleResp in response.Responses)
{
if (singleResp.Fault != null)
{
string faultMessage = singleResp.Fault.Message;
var account = ((UpdateRequest)multipleRequest.Requests[singleResp.RequestIndex]).Target;
Log.Error($"Error update acc.id: {account.Id}.Error: {singleResp.Fault.Message}.");
failedToUpdateAccount++;
}
}
Log.Debug($"Failed to update {failedToUpdateAccount} accounts.");
}
else
{
Log.Debug("Execute multiple executed without errors");
}
}
catch (Exception ex)
{
Log.Error($"Error while executing Multiplerequest", ex);
}
}
}
// LINQ below
private static List<Account> GetAllActiveCRMAccounts(CRM2011DataContext CRMcontext)
{
Console.WriteLine("Start Getting CRMExistingAccounts ....");
List<Account> CRMExisterendeAccounts = new List<Account>();
try
{
CRMExisterendeAccounts = (from a in CRMcontext.AccountSet
where a.StateCode == AccountState.Active
where a.anotherVariable == 1
select new Account()
{
my_var1 = a.myVar1,
my_var2 = a.myVar2,
AccountId = a.AccountId,
anotherVar = a.alsoThisVar,
}).ToList();
}
catch (FaultException ex)
{
Log.Debug($"GetCRMExistingAccounts Exception { ex.Message}");
Console.WriteLine("GetCRMExistingAccounts Exception " + ex.Message);
throw new Exception(ex.Message);
}
return CRMExisterendeAccounts;
}
And yes, my variables has different names in my system.
The query returns the object just fine with all the correct data.
You can work around this in one of two ways:
1) Create your CRM2011DataContext with the MergeOption set to MergeOption.NoTracking. Entities loaded from a context that is not tracking will have a null EntityState property.
2) You can create a copy of your Entity and save the copy.

Stop AspNet Identity storing email address after validation error

I have a serivce that fetches a list of users from a legacy system and synchronises my AspNet Identity database. I’ve a problem when updating a user’s email address with UserManager.SetEmail(string userId, string email) and the validation fails. The user object in the UserStore retains the value of the invalid email address. I stop processing that user and skip to the next user in my list. Later when my service finds a new user to create, I use UserManager.Create(ApplicationUser user) and the database is updated with all outstanding changes including the invalid email address of the existing user.
Is there a way to stop the invalid email address being persisted? Is this a bug or am I just not using it correctly? Should I just manually take a backup of every object before any update and revert all values if the IdentityResult has an error?
//get LegacyUsers
foreach (AppUser appUser in LegacyUsers){
var user = UserManager.FindByName(appUser.userName);
if (user != null){
If (!user.Email.Equals(appUser.Email)){
var result = UserManager.setEmail(user.Id, appUser.Email)
if (!result.Succeeded){
//user object still has new value of email despite error, but not yet persisted to DB.
Log.Error(…);
continue;
}
}
}
else
{
ApplicationUser newUser = new ApplicationUser{
UserName = appUser.userName,
//etc
}
var result = UserManager.Create(newUser); //DB updates first user with new email aswell as inserting this new user
if (!result.Succeeded){
Log.Error(…);
continue;
}
}
}
I'm using version 2.2.1.40403 of Microsoft.AspNet.Identity.Core and Microsoft.AspNet.Identity.EntityFramework
This is happening because EF keeps track of models and updates all of the modified objects when SaveChanges() method is called by UserManager.Create() method. You could easily detach the user which has invalid email from the DbContext like this:
// first get DbContext from the Owin.
var context = HttpContext.GetOwinContext().Get<ApplicationDbContext>();
foreach (AppUser appUser in LegacyUsers){
var user = UserManager.FindByName(appUser.userName);
if (user != null){
If (!user.Email.Equals(appUser.Email)){
var result = UserManager.setEmail(user.Id, appUser.Email)
if (!result.Succeeded){
Log.Error(…);
// detach the user then proceed to the next one
context.Entry(user).State = EntityState.Detached;
continue;
}
}
}
else{
// rest of the code
}
}

Save changes back to database using entity framework

I have simple query that loads data from two tables into GUI. I'm saving loaded data to widely available object Clients currentlySelectedClient.
using (var context = new EntityBazaCRM())
{
currentlySelectedClient = context.Kliencis.Include("Podmioty").FirstOrDefault(d => d.KlienciID == klientId);
if (currentlySelectedClient != null)
{
textImie.Text = currentlySelectedClient.Podmioty.PodmiotOsobaImie;
textNazwisko.Text = currentlySelectedClient.Podmioty.PodmiotOsobaNazwisko;
}
else
{
textNazwa.Text = currentlySelectedClient.Podmioty.PodmiotFirmaNazwa;
}
}
So now if I would like to:
1) Save changes made by user how do I do it? Will I have to prepare something on database side? How do I handle modifying multiple tables (some data goes here, some there)? My current code seems to write .KlienciHaslo just fine, but it doesn't affect Podmioty at all. I tried different combinations but no luck.
2) Add new client to database (and save information to related tables as well)?
currentClient.Podmioty.PodmiotOsobaImie = textImie.Text; // not saved
currentClient.Podmioty.PodmiotOsobaNazwisko = textNazwisko.Text; // not saved
currentClient.KlienciHaslo = "TEST111"; // saved
using (var context = new EntityBazaCRM())
{
var objectInDB = context.Kliencis.SingleOrDefault(t => t.KlienciID == currentClient.KlienciID);
if (objectInDB != null)
{
// context.ObjectStateManager.ChangeObjectState(currentClient.Podmioty, EntityState.Modified);
//context.Podmioties.Attach(currentClient.Podmioty);
context.Kliencis.ApplyCurrentValues(currentClient); // update current client
//context.ApplyCurrentValues("Podmioty", currentClient.Podmioty); // update current client
}
else
{
context.Kliencis.AddObject(currentClient); // save new Client
}
context.SaveChanges();
}
How can I achieve both?
Edit for an answer (doesn't save anything ):
currentClient.Podmioty.PodmiotOsobaImie = textImie.Text; // no save
currentClient.Podmioty.PodmiotOsobaNazwisko = textNazwisko.Text; // no save
currentClient.KlienciHaslo = "TEST1134"; // no save
using (var context = new EntityBazaCRM())
{
if (context.Kliencis.Any(t => t.KlienciID == currentClient.KlienciID))
{
context.Kliencis.Attach(currentClient); // update current client
}
else
{
context.Kliencis.AddObject(currentClient); // save new Client
}
context.SaveChanges();
}
Apparently ApplyCurrentValues only works with scalar properties.
If you attach the currentClient then associated objects should also attach, which means they'll be updated when you SaveChanges()
But you'll get an Object with the key exists exception because you are already loading the object from the database into the objectInDB variable. The context can only contain one copy of a Entity, and it knows that currentClient is the same as objectInDB so throws an exception.
Try this pattern instead
if (context.Kliencis.Any(t => t.KlienciID == currentClient.KlienciID))
{
context.Kliencis.Attach(currentClient); // update current client
}
else
{
context.Kliencis.AddObject(currentClient); // save new Client
}
or if you're using an identity as the ID, then
// if the ID is != 0 then it's an existing database record
if (currentClient.KlienciID != 0)
{
context.Kliencis.Attach(currentClient); // update current client
}
else // the ID is 0; it's a new record
{
context.Kliencis.AddObject(currentClient); // save new Client
}
After some work and help from Kirk about the ObjectStateManager error that I was getting I managed to fix this. This code allows me to save both changes to both tables.
currentClient.Podmioty.PodmiotOsobaImie = textImie.Text;
currentClient.Podmioty.PodmiotOsobaNazwisko = textNazwisko.Text;
currentClient.KlienciHaslo = "TEST1134";
using (var context = new EntityBazaCRM()) {
if (context.Kliencis.Any(t => t.KlienciID == currentClient.KlienciID)) {
context.Podmioties.Attach(currentClient.Podmioty);
context.Kliencis.Attach(currentClient);
context.ObjectStateManager.ChangeObjectState(currentClient.Podmioty, EntityState.Modified);
context.ObjectStateManager.ChangeObjectState(currentClient, EntityState.Modified);
} else {
context.Kliencis.AddObject(currentClient); // save new Client
}
context.SaveChanges();
}

Categories