如何使用函数`MapExtraPropertiesTo`?

How to use the function `MapExtraPropertiesTo`?

ABP框架版本:4.4.3 PostgreSQL: 13.4

异常

代码:

        public async Task<IdentityUserDto> CreateAsync(IdentityUserCreateDto input)
        {
            await IdentityOptions.SetAsync();

            _logger.Debug("创建用户-input\r\n" + JsonConvert.SerializeObject(input, Formatting.Indented));

            var user = new Volo.Abp.Identity.IdentityUser(
                GuidGenerator.Create(),
                input.UserName,
                input.Email,
                CurrentTenant.Id
            );

            input.MapExtraPropertiesTo(user);

            (await UserManager.CreateAsync(user, input.Password)).CheckErrors();
            await UpdateUserByInput(user, input);
            (await UserManager.UpdateAsync(user)).CheckErrors();

            _logger.Debug("创建用户-user\r\n" + JsonConvert.SerializeObject(user, Formatting.Indented));

            await CurrentUnitOfWork.SaveChangesAsync();

            return ObjectMapper.Map<Volo.Abp.Identity.IdentityUser, IdentityUserDto>(user);
        }

我用MapExtraPropertiesToIdentityUserCreateDto变成了IdentityUser。但是 IdentityUser 的 属性 ExtraProperties 丢失了。

日志:

2021-11-22 08:57:13.008 +08:00 [DBG] 创建用户-input
{
  "Password": "xxxxxxxx",
  "UserName": "05150123",
  "Name": "xxx",
  "Surname": "xxx",
  "Email": "05150123@56kad.com",
  "PhoneNumber": null,
  "LockoutEnabled": true,
  "RoleNames": null,
  "ExtraProperties": {
    "Sex": 0,
    "UserType": 2,
    "SchoolUserType": 0,
    "SchoolCode": "05150123",
    "Country": "中国",
    "ProvinceId": "320000000000",
    "CityId": "320900000000",
    "DistrictId": null
  }
}

2021-11-22 08:57:13.303 +08:00 [DBG] 创建用户-user
{
  "TenantId": null,
  "UserName": "05150123",
  "NormalizedUserName": "05150123",
  "Name": "xxx",
  "Surname": "xxx",
  "Email": "05150123@56kad.com",
  "NormalizedEmail": "05150123@56KAD.COM",
  "EmailConfirmed": false,
  "PasswordHash": "AQAAAAEAACcQAAAAEBdhpA4Y3azFxR0f2LCfE0W3vL5PvOBCzzxBTxsP63KGUnCSpdiHnAcL5diniThA==",
  "SecurityStamp": "PU324ZRHDW7OO2SZHZUZ3Y6HF2H26U4P",
  "IsExternal": false,
  "PhoneNumber": null,
  "PhoneNumberConfirmed": false,
  "TwoFactorEnabled": false,
  "LockoutEnd": null,
  "LockoutEnabled": true,
  "AccessFailedCount": 0,
  "Roles": [],
  "Claims": [],
  "Logins": [],
  "Tokens": [],
  "OrganizationUnits": [],
  "IsDeleted": false,
  "DeleterId": null,
  "DeletionTime": null,
  "LastModificationTime": "2021-11-22T08:57:13.2831132+08:00",
  "LastModifierId": "3a004944-b21f-0bbc-2376-44007c2a9295",
  "CreationTime": "2021-11-22T08:57:13.1768976+08:00",
  "CreatorId": "3a004944-b21f-0bbc-2376-44007c2a9295",
  "ExtraProperties": {}, //lose
  "ConcurrencyStamp": "08c5abb6ab55424899f40d30b275975b",
  "Id": "3a005752-7630-f90f-3d3d-38da149eb416"
}
IdentityExtensionsEntityFrameworkCoreModule.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Modularity;

namespace IdentityExtensions.EntityFrameworkCore
{
    [DependsOn(
        typeof(IdentityExtensionsDomainModule),
        typeof(AbpEntityFrameworkCoreModule)
    )]
    public class IdentityExtensionsEntityFrameworkCoreModule : AbpModule
    {
        public override void PreConfigureServices(ServiceConfigurationContext context)
        {
            IdentityExtensionsEfCoreEntityExtensionMappings.Configure();

            base.PreConfigureServices(context);
        }

        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            context.Services.AddAbpDbContext<AppUserDbContext>(options =>
            {
                /* Remove "includeAllEntities: true" to create
                 * default repositories only for aggregate roots */
                options.AddDefaultRepositories();
            });

            context.Services.AddAbpDbContext<AggregateUniqueUserDbContext>(options =>
            {
                /* Remove "includeAllEntities: true" to create
                 * default repositories only for aggregate roots */
                options.AddDefaultRepositories();
            });

            context.Services.Configure<AbpDbContextOptions>(options =>
            {
                options.UseNpgsql(b => b.UseNetTopologySuite());
            });
        }
    }
}
IdentityExtensionsEfCoreEntityExtensionMappings.cs
using Microsoft.EntityFrameworkCore;
using System;
using Volo.Abp.Identity;
using Volo.Abp.ObjectExtending;
using Volo.Abp.Threading;

namespace IdentityExtensions.EntityFrameworkCore
{
    public static class IdentityExtensionsEfCoreEntityExtensionMappings
    {
        private static readonly OneTimeRunner OneTimeRunner = new();

        public static void Configure()
        {
            OneTimeRunner.Run(() =>
            {
                ObjectExtensionManager.Instance
                    .MapEfCoreProperty<IdentityUser, Guid?>(
                        nameof(AppUser.UniqueId),
                        (typeBuilder, propertyBuilder) => { }
                    )
                    .MapEfCoreProperty<IdentityUser, string>(
                        nameof(AppUser.OuterId),
                        (typeBuilder, propertyBuilder) =>
                        {
                            propertyBuilder.HasMaxLength(AppUserConsts.MaxOuterIdLength);
                        }
                    )
                    .MapEfCoreProperty<IdentityUser, int>(
                        nameof(AppUser.Sex),
                        (typeBuilder, propertyBuilder) =>
                        {
                            propertyBuilder.HasDefaultValue(0);
                        }
                    )
                    .MapEfCoreProperty<IdentityUser, int>(
                        nameof(AppUser.UserType),
                        (typeBuilder, propertyBuilder) =>
                        {
                            propertyBuilder.HasDefaultValue(0);
                        }
                    )
                    .MapEfCoreProperty<IdentityUser, int>(
                        nameof(AppUser.SchoolUserType),
                        (typeBuilder, propertyBuilder) =>
                        {
                            propertyBuilder.HasDefaultValue(0);
                        }
                    )
                    .MapEfCoreProperty<IdentityUser, string>(
                        nameof(AppUser.IDCard),
                        (typeBuilder, propertyBuilder) =>
                        {
                            propertyBuilder.HasMaxLength(AppUserConsts.MaxIDCardLength);
                        }
                    )
                    .MapEfCoreProperty<IdentityUser, string>(
                        nameof(AppUser.Avatar),
                        (typeBuilder, propertyBuilder) =>
                        {
                            propertyBuilder.HasMaxLength(AppUserConsts.MaxAvatarLength);
                        }
                    )
                    .MapEfCoreProperty<IdentityUser, string>(
                        nameof(AppUser.Title),
                        (typeBuilder, propertyBuilder) =>
                        {
                            propertyBuilder.HasMaxLength(AppUserConsts.MaxTitleLength);
                        }
                    )
                    .MapEfCoreProperty<IdentityUser, string>(
                        nameof(AppUser.Tags),
                        (typeBuilder, propertyBuilder) =>
                        {
                            propertyBuilder.HasConversion<string>();
                        }
                    )
                    .MapEfCoreProperty<IdentityUser, string>(
                        nameof(AppUser.Country),
                        (typeBuilder, propertyBuilder) =>
                        {
                            propertyBuilder.HasMaxLength(AppUserConsts.MaxCountryLength);
                        }
                    )
                    .MapEfCoreProperty<IdentityUser, string>(
                        nameof(AppUser.ProvinceId),
                        (typeBuilder, propertyBuilder) =>
                        {
                            propertyBuilder.HasMaxLength(AppUserConsts.MaxAreaCodeLength);
                        }
                    )
                    .MapEfCoreProperty<IdentityUser, string>(
                        nameof(AppUser.CityId),
                        (typeBuilder, propertyBuilder) =>
                        {
                            propertyBuilder.HasMaxLength(AppUserConsts.MaxAreaCodeLength);
                        }
                    )
                    .MapEfCoreProperty<IdentityUser, string>(
                        nameof(AppUser.DistrictId),
                        (typeBuilder, propertyBuilder) =>
                        {
                            propertyBuilder.HasMaxLength(AppUserConsts.MaxAreaCodeLength);
                        }
                    );
                    
                    ...
            });
        }
    }
}
AppUserDbContext.cs
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Data;
using Volo.Abp.EntityFrameworkCore;

namespace IdentityExtensions.EntityFrameworkCore
{
    [ConnectionStringName("Default")]
    public class AppUserDbContext : AbpDbContext<AppUserDbContext>, IAppUserDbContext
    {
        public DbSet<AppUser> Users { get; set; }

        /* Add DbSet properties for your Aggregate Roots / Entities here.
         * Also map them inside UserManagementDbContextModelCreatingExtensions.ConfigureUserManagement
         */

        public AppUserDbContext(DbContextOptions<AppUserDbContext> options)
            : base(options)
        {

        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            /* Configure the shared tables (with included modules) here */

            /* Configure your own tables/entities inside the ConfigureUserManagement method */

            builder.ConfigureAppUser();
        }
    }
}
AppUserDbContextModelCreatingExtensions.cs
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using Volo.Abp;
using Volo.Abp.EntityFrameworkCore.Modeling;
using Volo.Abp.Identity;
using Volo.Abp.Identity.EntityFrameworkCore;
using Volo.Abp.Users;
using Volo.Abp.Users.EntityFrameworkCore;

namespace IdentityExtensions.EntityFrameworkCore
{
    public static class AppUserDbContextModelCreatingExtensions
    {
        public static void ConfigureAppUser(
            [NotNull] this ModelBuilder builder,
            [CanBeNull] Action<IdentityModelBuilderConfigurationOptions> optionsAction = null)
        {
            Check.NotNull(builder, nameof(builder));

            builder.Entity<AppUser>(b =>
            {
                b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "Users"); //Sharing the same table "AbpUsers" with the IdentityUser

                b.ConfigureAbpUser();
                b.ConfigureFullAuditedAggregateRoot();
                b.ConfigureConcurrencyStamp();
                b.HasOne<IdentityUser>().WithOne().HasForeignKey<AppUser>(i => i.Id);

                b.ConfigureCustomUserProperties();
            });

            builder.Entity<IdentityUser>(b =>
            {
                b.ConfigureCustomUserProperties();
                b.ApplyObjectExtensionMappings();
            });

            builder.Entity<IdentityUserLogin>().HasKey(x => new { x.UserId, x.LoginProvider });
            builder.Entity<IdentityUserRole>().HasKey(x => new { x.UserId, x.RoleId });
            builder.Entity<IdentityUserToken>().HasKey(x => new { x.UserId, x.LoginProvider });
            builder.Entity<IdentityUserOrganizationUnit>().HasKey(x => new { x.UserId, x.OrganizationUnitId });
        }

        public static void ConfigureCustomUserProperties<TUser>(this EntityTypeBuilder<TUser> b) where TUser : class, IUser
        {
            b.Property<Guid?>(nameof(AppUser.UniqueId))
                .HasColumnName(nameof(AppUser.UniqueId))
                .HasComment("聚合唯一id");

            b.Property<string>(nameof(AppUser.OuterId))
                .HasColumnName(nameof(AppUser.OuterId))
                .HasComment("外部id")
                .HasMaxLength(AppUserConsts.MaxOuterIdLength);

            b.Property<AppUserSex>(nameof(AppUser.Sex))
                .HasColumnName(nameof(AppUser.Sex))
                .HasComment("性别")
                .HasConversion(
                    v => Convert.ToInt32(v),
                    v => (AppUserSex)Enum.Parse(typeof(AppUserSex), v.ToString()))
                .HasDefaultValue((AppUserSex)0);

            b.Property<AppUserType>(nameof(AppUser.UserType))
                .HasColumnName(nameof(AppUser.UserType))
                .HasComment("用户类型")
                .HasConversion(
                    v => Convert.ToInt32(v),
                    v => (AppUserType)Enum.Parse(typeof(AppUserType), v.ToString()))
                .HasDefaultValue((AppUserType)0);

            b.Property<AppUserSchoolUserType>(nameof(AppUser.SchoolUserType))
                .HasColumnName(nameof(AppUser.SchoolUserType))
                .HasComment("校园用户类型")
                .HasConversion(
                    v => Convert.ToInt32(v),
                    v => (AppUserSchoolUserType)Enum.Parse(typeof(AppUserSchoolUserType), v.ToString()))
                .HasDefaultValue((AppUserSchoolUserType)0);

            b.Property<string>(nameof(AppUser.IDCard))
                .HasColumnName(nameof(AppUser.IDCard))
                .HasComment("身份证号")
                .HasMaxLength(AppUserConsts.MaxIDCardLength);

            b.Property<string>(nameof(AppUser.Avatar))
                .HasColumnName(nameof(AppUser.Avatar))
                .HasComment("头像")
                .HasMaxLength(AppUserConsts.MaxAvatarLength);

            b.Property<string>(nameof(AppUser.Title))
                .HasColumnName(nameof(AppUser.Title))
                .HasComment("职称")
                .HasMaxLength(AppUserConsts.MaxTitleLength);

            b.Property<Dictionary<string, string>>(nameof(AppUser.Tags))
                .HasColumnName(nameof(AppUser.Tags))
                .HasComment("标签")
                .HasConversion(
                    v => JsonConvert.SerializeObject(v),
                    v => JsonConvert.DeserializeObject<Dictionary<string, string>>(v));

            b.Property<string>(nameof(AppUser.Country))
                .HasColumnName(nameof(AppUser.Country))
                .HasComment("国家")
                .HasMaxLength(AppUserConsts.MaxCountryLength);

            b.Property<string>(nameof(AppUser.ProvinceId))
                .HasColumnName(nameof(AppUser.ProvinceId))
                .HasComment("省份id")
                .HasMaxLength(AppUserConsts.MaxAreaCodeLength);

            b.Property<string>(nameof(AppUser.CityId))
                .HasColumnName(nameof(AppUser.CityId))
                .HasComment("城市id")
                .HasMaxLength(AppUserConsts.MaxAreaCodeLength);

            b.Property<string>(nameof(AppUser.DistrictId))
                .HasColumnName(nameof(AppUser.DistrictId))
                .HasComment("区县id")
                .HasMaxLength(AppUserConsts.MaxAreaCodeLength);

            ...
        }
    }
}
AppUser.cs
    public class AppUser : FullAuditedAggregateRoot<Guid>, IUser
    {
        #region Base properties

        /* These properties are shared with the IdentityUser entity of the Identity module.
         * Do not change these properties through this class. Instead, use Identity module
         * services (like IdentityUserManager) to change them.
         * So, this properties are designed as read only!
         */

        public virtual Guid? TenantId { get; private set; }

        public virtual string UserName { get; private set; }

        public virtual string Name { get; private set; }

        public virtual string Surname { get; private set; }

        public virtual string Email { get; private set; }

        public virtual bool EmailConfirmed { get; private set; }

        public virtual string PhoneNumber { get; private set; }

        public virtual bool PhoneNumberConfirmed { get; private set; }

        #endregion

        [NotMapped]
        public override ExtraPropertyDictionary ExtraProperties { get; protected set; }

        private AppUser()
        {

        }

        ...

https://community.abp.io/articles/how-to-add-custom-property-to-the-user-entity-6ggxiddr

这解决了我的问题。

private static void ConfigureExtraProperties()
{
    ObjectExtensionManager.Instance.Modules().ConfigureIdentity(identity =>
    {
        identity.ConfigureUser(user =>
        {
            user.AddOrUpdateProperty<string>(
                UserConsts.TitlePropertyName,
                options =>
                {
                    options.Attributes.Add(new RequiredAttribute());
                    options.Attributes.Add(
                        new StringLengthAttribute(UserConsts.MaxTitleLength)
                    );
                }
            );
            user.AddOrUpdateProperty<int>(
                UserConsts.ReputationPropertyName,
                options =>
                {
                    options.DefaultValue = UserConsts.MinReputationValue;
                    options.Attributes.Add(
                        new RangeAttribute(UserConsts.MinReputationValue, UserConsts.MaxReputationValue)
                    );
                }
            );
        });
    });
}