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.
Related
After upgrading from .Net Core 2.1 to .Net 5 (and subsequently upgrading my Stripe .net library to 39.45) I noticed some code of mine is now broken. I am specifically getting an error on the subscription object because it no longer has a "Plan" object on it. In my code below, I have my own internal table called 'Subscription' which actually contains a few more fields for convenience. Regardless, it looks like they moved the 'Plan' obj class from the Subscription class.
var service = new SubscriptionService();
var subscription = service.Get(successInvoice.SubscriptionId);
if (subscription != null)
{
var internalCustomer = _dbContext.Customers.First();
var internalSubscription = _dbContext.Subscriptions.FirstOrDefault(s => s.ExternalSubscriptionId == subscription.Id);
if (internalCustomer != null)
{
//If the subscription does not exist, it means this is the first time they are being charged
//and a new subscription must be added as well
if (internalSubscription == null)
{
internalSubscription = new Business.Entities.Billing.Subscription
{
CustomerId = internalCustomer.Id,
Customer = internalCustomer,
ExternalProductId = subscription.Plan.ProductId,
Amount = subscription.Plan.AmountDecimal.Value / 100,
Interval = subscription.Plan.Interval,
IntervalCount = subscription.Plan.IntervalCount,
ExternalSubscriptionId = subscription.Id
};
//Add this new subscription
_dbContext.Subscriptions.Add(internalSubscription);
_dbContext.SaveChanges();
}
So these lines of code are throwing the error:
ExternalProductId = subscription.Plan.ProductId,
Amount = subscription.Plan.AmountDecimal.Value / 100,
Interval = subscription.Plan.Interval,
IntervalCount = subscription.Plan.IntervalCount,
Does anyone know where they moved Plan to, or specifically how I can retrieve a Plan if I have a subscription? The purpose of this code is that I want to make my own
Plan was removed from Subscription as part of 39.1.2.
You now will need to do it like this:
var service = new PlanService();
service.Get("price_1IjsKp2eZvKYlo2C4nz7QGls");
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
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
}
}
I am creating a custom workflow that is - Triggered on the create of a record (Custom Activity).
I need to be able to access the data from the Custom Activity above within my Custom workflow but I am have a hard time finding a reference on how to get the information from the newly created record.
Any advice?
Thanks in advanced.
Edit:
public sealed class FeeInvoiceGenerator : CodeActivity
{
[Input("MyFee")]
[ReferenceTarget("fee")]
[RequiredArgument]
public InArgument<EntityReference> SomeFee { get; set; }
protected override void Execute(CodeActivityContext executionContext)
{
ITracingService tracingService = executionContext.GetExtension<ITracingService>();
try
{
tracingService.Trace("Creating Invoice for Fee");
WorkFlowHelper workFlowHelper = new WorkFlowHelper();
workFlowHelper.debugMessagesOn = true;
//creates connection info
InvoiceFeeHelper invoiceFeeHelper = new InvoiceFeeHelper();
invoiceFeeHelper.ConnectionInfo(workFlowHelper);
invoiceFeeHelper.CreatingConnection(workFlowHelper, executionContext);
//initialize other classes
FeeMaster feeMaster = new FeeMaster();
InvoiceMaster invoiceMaster = new InvoiceMaster();
InvoiceFeeMaster invoiceFeeMaster = new InvoiceFeeMaster();
EntityReference getFee = this.SomeFee.Get(executionContext);
String feeId = getFee.Id.ToString();
invoiceFeeMaster.CreateInvoiceFromFee(workFlowHelper, invoiceFeeHelper, invoiceMaster, feeMaster, feeId, executionContext);
}
catch (Exception ex)
{
throw new NotImplementedException("error occured" + ex.Message);
}
}
}
However, I am running into an issue where I cannot access the [Set Properties] within the workflow itself to assign the input. (at least this is what I have seen from other examples online and it is blank for me)
I have also tried to use:
IWorkflowContext workFlowContext = workFlowHelper.context.GetExtension<IWorkflowContext>();
Guid _feeRecordID = workFlowContext.PrimaryEntityId;
in order to get the records Id to no avail. The other portions of my custom workflow work, If I pass in a guid from a 'Fee'(the record I need to grab), everything works great.
Am I doing something wrong?
Second Edit:
I needed to restart IIS on the CRM server before it would recognize that there were inputs to be used.
Couple of ways you could do this:
Use an input parameter to pass all the data you need.
Use an input parameter to pass the ID of the record, and then use the IOrganizationService to retrieve the rest of the data.
Please see Custom Workflow Activities (Workflow Assemblies) for Microsoft Dynamics CRM for examples and further detail.
I am not sure what your WorkflowHelper does but you should not really have to use it.
// Create the context
var context = executionContext.GetExtension<IWorkflowContext>();
//primary entityid
var _feeRecordId = context.PrimaryEntityId
Below is my Intializer.cs and I was told in order to keep my Guids i had to use Navigation properties so that i had the right relations in my database(Reusing a GUID in EF Code First DatabaseIntializer). That seems to solves the issues i had earlier but now that i want to take my information and use a Seed to actually add it to the database, i am not sure how to satisfy this error. I get the error for addUsers(Applications apps)"eflogin.Models.Applications is a 'type' being used like a variable." I got the feeling i am doing this way wrong.
public class DatabaseIntializer : DropCreateDatabaseIfModelChanges<DataContext>
{
protected override void Seed(DataContext context)
{
addApplications().ForEach(a => context.Applications.Add(a));
addUsers(Applications apps).ForEach(u => context.User.Add(u));
// if i take out Applications apps
// i get No overload for method"addUsers" takes 0 arguments
}
private static List<Applications> addApplications()
{
var apps = new List<Applications>
{
new Applications
{
ApplicationId = Guid.NewGuid(),
ApplicationName = "Test Login"
}
};
return apps;
}
private static List<Users> addUsers(Applications apps)
{
var use = new List<Users>
{
new Users
{
UserId = Guid.NewGuid(),
UserApplication = apps,
UserName = "Ralph",
IsAnonymouse = false,
LastActivityDate = System.DateTime.Now
}
};
return use;
}
The problem is your are passing in the type and instance in the call to the addUsers method.
addUsers(Applications apps)
If you remove Applications and just leave apps like so.
addUsers(apps)
You will get another error because you are passing in a collection of objects and the method expects a single instance.
Here is a suggested edit to your Seed method that should get you past both errors.
var apps = addApplications();
apps.ForEach(a => context.Applications.Add(a));
foreach (var app in apps)
{
var users = addUsers(app)
users.ForEach(u => context.User.Add(u));
}
Note: I think keeping the entity names plural helps in causing some confusion.