Update a User in both CMS & Commerce

Vote:
 

In our eCommerce project we have a 'Personal Details' section where a user can do a number of things:

  1. Update their Salutation (Mr, Mrs etc.)**
  2. Update First and Second Name
  3. Update Date of Birth
  4. Update Gender**
  5. Change Password
  6. Change Username**

**These fields are added to the [dbo].[mcmd_MetaEnum] and [dbo].[cls_Contact] tables in Commerce via an InitilizationModule (see previous posts here and here)

My question is, what steps do I need to take to ensure that I can successfully update a User's details and have it synchronize across both the CMS databases and the Commerce databases?

I am aware that I can get the current user via the below line

var user = CustomerContext.Current;

However, user doesn't expose any of my new MetaField values: GenderSalutationUsername? How do I access (and update) database fields that I have added myself?

My follow up question will then be: once I am able to update the user's personal details in Commerce how do I then ensure that the CMS database will share the same values?

Below you can see a screenshot of the [dbo].[AspNetUsers] table. The only value that may change and need to be updated in the CMS as well Commerce is the UserName value.  

Is this as straightforward as the below?

var userProvider = ServiceLocator.Current.GetInstance<UIUserProvider>();
var httpContext = ServiceLocator.Current.GetInstance<IHttpContextAccessor>();
var username = httpContext.HttpContext.User.Identity.Name;         
var userInfo = userProvider.FindUsersByNameAsync(username, 10, 10).FirstOrDefaultAsync();
userInfo.Result.Username = "change username";

// do I need to run a save method here or is it automatically saved?
#281827
Edited, Jun 14, 2022 15:19
Vote:
 

MetaFields are only part of the Commerce backing data storage. You'll have meta fields for the commerce customer with a number out of the box and the ability to define your own.

AspNetUsers is part of the asp.net identity table, there's no meta fields here this is down to the user data type which you use for identity.  You'd just update the values that are properties of the user data type you use for your user record.

See

https://www.yogihosting.com/aspnet-core-identity-create-read-update-delete-users/ 

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/customize-identity-model?view=aspnetcore-6.0 

So you'll need to update both if you want to keep them in sync when changes happen but I usually just only keep commerce as the source of truth for non standard values

#281850
Jun 14, 2022 16:41
Vote:
 

As far as username goes it should be something like

// get user object from the storage
var user = await userManager.FindByIdAsync(userId);

// change username and email
user.Username = "NewUsername";
user.Email = "New@email.com";

// Persiste the changes
await userManager.UpdateAsync(user);
#281851
Jun 14, 2022 16:44
Vote:
 

Thanks Scott, however I'm not sure you have answered one of my questions:

However, user doesn't expose any of my new MetaField values: Gender, Salutation & Username? How do I access (and update) database fields that I have added myself?

After defining my own new fields for the [dbo].[cls_Contact] table, how do I then interact with those fields programmatically? At present, running the following code doesn't expose my new fields. I would expect to see GenderSalutation Username appear:

#281893
Jun 15, 2022 8:42
Scott Reed - Jun 15, 2022 9:34
From your description I thought you were successfully updating the customer values but wanted to sync the fields in the asp.net identity table in the CMS as you said in the CMS database and posted the table. Quan has answered but that's commerce values. My response was about the identity table which is the user table used in CMS
Vote:
 

For the custom fields, you can access them by 

contact["FieldName"] = "FieldValue";

#281894
Jun 15, 2022 8:46
Vote:
 

Thanks Quan, will that update the database straight away or do you then need to run a save method after?

#281895
Jun 15, 2022 8:49
Vote:
 

You should need to call AcceptChanges(). it's only the assignment :) 

#281898
Jun 15, 2022 12:46
Vote:
 

Hi again Quan, I am having issues updating a user from the [dbo].[cls_Contact] table.

I followed one of your blogs on how to grab a specific user from the table via their email address (annoyingly, I can't find it now). I now need to update some of the values of the user and then save the changes.

In your earlier comment you mention calling 'AcceptChanges()' but I can't see where that method is? You can see my attempt below with 'SaveChanges()' but I get the following error. Any thoughts?

#282145
Jun 20, 2022 18:51
Vote:
 

nathano, are you using aspnet identity? Why not grab the commerce user from from ApplicationUserManager.FindByEmail instead?

inject ApplicationUserManager<SiteUser> into your ctor then call FindByEmail(). Try saving then.

#282186
Jun 21, 2022 10:38
Vote:
 

Surjit, I need to update the [dbo].[cls_Contact] table specifically and update the fields. This is not using ASP.NET Identity because that does not give me access to the table.

In my simplified approach below I have take the UserId from a default user that comes with Commerce. You can see the highlighted user below from  [dbo].[cls_Contact] 

If I debug my code I can:

  1. Get the user by invoking the GetContactById() method
  2. Access all of the fields from the [dbo].[cls_Contact] table
  3. Update these fields (I can see when debugging that the new field values are being assigned)

However, when I invoke SaveChanges() I keep receiving an 'Object reference not set to an instance of the object' error. Why does this keep happening? What is not being instantiated? 

var placeholderEmail = "admin@example.com";
var placeholderId = new Guid("4DC8C10F-1902-42DD-874C-CA6684E327EC");
var placeholderAddress = CustomerAddress.CreateInstance();
           
var context = CustomerContext.Current.GetContactById(placeholderId);
context.LastName = "sampleName";

// the below fields have been added via Initialization Modules
context["AcademicTitle"] = "Dr.";
context["Salutation"] = "Mr";

// I have even added placeholder values below to avoid the error
context.PreferredBillingAddress = placeholderAddress;
context.PreferredShippingAddress = placeholderAddress;

context.SaveChanges();
#282191
Edited, Jun 21, 2022 13:25
Vote:
 

I honestly cant see anything out of the ordinary with the code you provided.

  1. Is there a stack trace along with the object reference error?
  2. Can you check your variable context is actually being assigned a CustomerContact object
#282192
Jun 21, 2022 15:34
Vote:
 

The problem is how you are trying to create the address.  Please see here for example on how to add new address to customer.  Also note you should save the contact first before adding the address.

https://github.com/episerver/Foundation/blob/main/src/Foundation/Infrastructure/Commerce/Install/Steps/AddCustomers.cs#L396

#282193
Edited, Jun 21, 2022 16:01
Vote:
 

Mark, I have been investigating the CustomerService class in Foundation and, more specifically, CreateFoundationContact() and the SetPreferredAddresses() methods to try and resolve this and I can see that they follow your guide on setting the addresses after the contact has been saved.

I have attempted to recreate this locally but I am still recieving the same error. The error arrives when I try to invoke SaveChanges()

You can see my code below:

      private void CreateCommerceUser()
      {
         var contact = CommerceContact.New();

         contact.FirstName = "Fname";
         contact.LastName = "Lname";
         contact.Email = "test@test.com";
         contact.UserId = "test@test.com";
         contact.RegistrationSource = "StartupUser";
        // error arrives when the below is invoked: 'Object not set to an instance of an object'
         contact.SaveChanges();
         SetPreferredAddresses(contact.Contact);
      }
     // at present this code is never run
      private void SetPreferredAddresses(CustomerContact contact)
      {
         var changed = false;

         var publicAddress = contact.ContactAddresses.FirstOrDefault(a => a.AddressType == CustomerAddressTypeEnum.Public);
         var preferredBillingAddress = contact.ContactAddresses.FirstOrDefault(a => a.AddressType == CustomerAddressTypeEnum.Billing);
         var preferredShippingAddress = contact.ContactAddresses.FirstOrDefault(a => a.AddressType == CustomerAddressTypeEnum.Shipping);

         if (publicAddress != null)
         {
            contact.PreferredShippingAddress = contact.PreferredBillingAddress = publicAddress;
            changed = true;
         }

         if (preferredBillingAddress != null)
         {
            contact.PreferredBillingAddress = preferredBillingAddress;
            changed = true;
         }

         if (preferredShippingAddress != null)
         {
            contact.PreferredShippingAddress = preferredShippingAddress;
            changed = true;
         }

         if (changed)
         {
            contact.SaveChanges();
         }
      }

And then, my CommerceContact class is below and has been influenced by the FoundationContact class.

public class CommerceContact
   {
      public CommerceContact() => Contact = new CustomerContact();
      public CommerceContact(CustomerContact contact) => Contact = contact ?? new CustomerContact();
      public CustomerContact Contact { get;}

      public Guid ContactId 
      {
         get => Contact?.PrimaryKeyId ?? Guid.Empty;
         set => Contact.PrimaryKeyId = new PrimaryKeyId(value);
      }

      public string FirstName
      {
         get => Contact.FirstName;
         set => Contact.FirstName = value;
      }

      public string LastName
      {
         get => Contact.LastName;
         set => Contact.LastName = value;
      }

      public string FullName
      {
         get => Contact.FullName;
         set => Contact.FullName = value;
      }
      public DateTime? BirthDate
      {
         get => Contact.BirthDate;
         set => Contact.BirthDate = value;
      }

      public string Email
      {
         get => Contact.Email;
         set => Contact.Email = value;
      }
      public CommerceOrganization CommerceOrganization
      {
         get => Contact != null && Contact.ContactOrganization != null ? new CommerceOrganization(Contact.ContactOrganization) : null;
         set => Contact.OwnerId = value.OrganizationEntity.PrimaryKeyId;
      }

      //public string UserLocationId
      //{
      //   get => Contact.GetStringValue("UserLocation";
      //   set => Contact[Constant.Fields.UserLocation] = value;
      //}

      public string UserId
      {
         get => Contact.UserId;
         set => Contact.UserId = $"String:{value}";
      }
      public string RegistrationSource
      {
         get => Contact.RegistrationSource;
         set => Contact.RegistrationSource = value;
      }

      public bool AcceptMarketingEmail
      {
         get => Contact.AcceptMarketingEmail;
         set => Contact.AcceptMarketingEmail = value;
      }

      public DateTime? ConsentUpdated
      {
         get => Contact.ConsentUpdated;
         set => Contact.ConsentUpdated = value;
      }

      public void SaveChanges() => Contact.SaveChanges();
      public static CommerceContact New() => new CommerceContact(CustomerContact.CreateInstance());

   }
#282251
Jun 22, 2022 13:58
Vote:
 

Nathano, I copy / pasted your code minus setting the address and it works just fine. Saves it to the database.

I suspect your issue isn't the code. I executed yours from a Home Controller, so I know all the commerce context has loaded up.

Where are you executing yours? mvc controller? schedule job? maybe a http endpoint?

#282261
Jun 22, 2022 16:24
Vote:
 

Surjit, I had the code in its own `UserManager` class, so not in a Controller. I just tried the below code in an action method within a controller and I still get the error. Am I missing some additional setup to make this work? How do I know whether my 'commerce context' has loaded up correctly? Really bizarre.

      public IActionResult UpdatePersonalDetails()
      {
         Guid contactId = Guid.NewGuid();

         CustomerContact contact = CustomerContact.CreateInstance();
         contact.FirstName = "Fname";
         contact.LastName = "Lname";
         contact.Email = "test@test.com";
         contact.UserId = "test@test.com";
         contact.RegistrationSource = "StartupUser";

         contact.SaveChanges();

         return Ok();
      }
#282266
Jun 22, 2022 18:54
Vote:
 

Hmm...well for starters you said you have an ecommerce project...can I assume it's LIVE? and also have other commerce features working? Such as pulling catalogue product info, adding to cart and checking out?

If the answer is yes then maybe we need to look at anything custom you've added to the CustomerContact object to see if anything is marked as required.

if the answer is no then I would look at the Startup.cs and compare it to Foundation. Make sure you have all the services called.

In your first post you make a reference to AspNetUsers which implies you have identity installed. In the startup.cs you may need to make sure the connectionstring setup against identity is the commerce db connection string, not the cms db.

#282271
Jun 22, 2022 22:47
Vote:
 

This has been resolved now.

Whenever I was working with the  [dbo].[cls_Contact] table I was receiving an 'Object not instantiated to an instance of an Object' error but no reference to what object. 

After receiving help from a colleague it became clear that the new MetaField values that I added to the [dbo].[cls_Contact] table did not have 'isNullable' set to to true. Therefore, I had fields in my table without values that were, technically, not allowed to be null.

To fix this I did the following:

  1. cleared my database
  2. updated my InitializationModule to ensure the new fields can be nullable
  3. re-ran my project, which re-ran my InitializationModule
  4. then used the below code to save a user
  5. success
      // the below code is technically to update a user but the concept is the same.

      public bool UpdateCommerceUser(string salutation, string academicTitle, string firstName, string lastName, string birth, string gender)
      {
         var placeholderId = new Guid("4DC8C10F-1902-42DD-874C-CA6684E327EC");
         try
         {
            var context = CustomerContext.Current.GetContactById(placeholderId);

            context["Salutation"] = salutation;
            context["AcademicTitle"] = academicTitle;
            context.FirstName = firstName;
            context.LastName = lastName;
            context.FullName = $"{firstName} {lastName}";
            context["Gender"] = gender;

            context.SaveChanges();

            return true;
         }
         catch (ObjectNotFoundException ex)
         {
            return false;
         }
      }
#282334
Edited, Jun 23, 2022 13:57
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.