wenmo8 发布的文章

近日,华为云上线的《华为开发者空间快速入门》课程,汇聚了理论知识讲解、案例介绍和丰富的实践练习,通过循序渐进的学习路径
,开发者可以快速了解华为开发者空间
,体验更简洁、更高效、更友好的华为开发者空间应用开发旅程。

开发者通过课程学习,可以体验华为开发者空间具有的开发桌面(云主机)便捷访问、开发者工具合集开箱即用、开发者沙箱调测资源灵活调用、开发者成长数据云上保存等核心功能。
点击链接
,开始学习课程。

课程看点

  1. 课程由浅入深带领开发者了解华为开发者空间,包含鲲鹏、昇腾、鸿蒙等华为优质开发工具和资源、真实项目开发案例等,构建对华为开发者空间的系统性认知框架;

  2. 不同技术领域的案例演示,让开发者亲历开发应用,提升运用华为开发者空间解决实际编码场景问题的能力。

认知了解篇:华为开发者空间支持开发者工作旅程

课程从开发桌面、工具使用、调测环境、资产存储、会员权益、获取激励、技术支持方面详细介绍了华为开发者空间是如何支持开发者的工作旅程。

开发者通过云主机中预置好的CodeArts IDE等工具及运行时组件进行开发时,可以选择使用Java/Python/C/C++等语言。

接下来分享一个用户案例的实践
《Python语言程序课程设计》
环境搭建、维护,方便开发者更好理解:

某高校老师在实践类教学环境准备中面临缺少环境、缺少工具、重要数据易丢失等问题。

通过华为开发者空间云主机,将教学环境一次性预装好,形成一套标准化的开箱即用教学环境,学生和老师不仅节省了环境搭建的时间,而且老师的教学数据和学生的学习实践数据实时保存在华为开发者空间,学生能够保存和随时查看实践数据。

案例演示篇:华为开发者空间场景案例实践

案例演示课程包括理论讲解视频、案例演示视频、指导文档以及实践练习等,涉及人工智能、软件开发、Serverless不同技术领域的案例。这些案例均基于云主机操作,帮助开发者掌握云主机及华为工具的基本使用。在 “基于云主机的 Web 项目增添 AI 助手” 案例中,讲解和演示如何将 AI 技术融入 Web 项目,实现智能问答功能;“鲲鹏 DevKit 源码迁移” 案例则聚焦于让开发者亲身体验鲲鹏 DevKit 工具强大的代码迁移能力;“基于 FunctionGraph 的画图应用开发” 案例,开发者可以熟练操作函数创建、插件安装到应用部署以及与 API 网关集成等步骤,从而掌握 FunctionGraph 的基本使用方法。

实践练习篇:使用华为开发者空间构建个人应用

在华为开发者空间的课程体系中,我们配备了实践练习环节, 可以让开发者学以致用,完整的体验开发流程。本次实践主题为“基于云主机实现 AI 对春联”,让开发者在云主机内使用Python环境搭建,并完成模型训练以及推理预测,涉及使用CodeArts IDE、OBS、Ubuntu、Python等技术。在此过程中,开发者将熟练掌握运用华为开发者空间进行应用构建的技能,为日后的实际项目开发筑牢坚实基础。

拓展篇:Ubuntu系统和CodeArts IDE for Python使用入门

在本章节课程中,介绍了Ubuntu系统与 CodeArts IDE for Python 的基本使用方法,可以帮助开发者熟悉操作系统和使用工具。

课程介绍了Ubuntu系统里主要目录的用途,包括开关机、查看系统信息等基础命令和关键热键的操作。

同时,课程还介绍了CodeArts IDE for Python的使用方法,如断点调试、添加注释等基础操作。CodeArts IDE 是面向华为云开发者提供的智能化可扩展桌面集成开发环境(IDE),它将精简的源代码编辑器与强大的开发者工具结合在一起。CodeArts IDE代码编辑-构建-调试的无缝循环使开发者不必在环境配置上花费太多时间,可以更多地专注于实现创意。

华为开发者空间是开发者专属的云上成长空间,汇聚了以华为云为底座的鲲鹏、昇腾、鸿蒙根技术工具和资源。华为开发者空间为每位开发者提供了一台云主机,让开发者可以低门槛使用华为工具、共享华为生态资源。未来,华为开发者空间将持续提升平台体验,集成更多的根生态资源,让开发者持续使用、持续成长、持续创新。

点击关注,第一时间了解华为云新鲜技术~

1. Thread.UncaughtExceptionHandler

UncaughtExceptionHandler‌

是一个接口,用于处理线程因未捕获异常而突然终止的情况。

虽然,通常都会在线程执行的代码中加try...catch来捕获异常,那么如果某些异常没有被catch住(比如,线程突然死掉了)那么我们将不知道发生了什么。因此,给每个现在设置一个未捕获异常处理器很有必要。

@Slf4j
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        log.info("线程异常: {}", t.getName(), e);
    }
}


public static void main(String[] args) {
    //  设置全局默认的未捕获异常处理器
    Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());

    Thread thread = new Thread(() -> {
        int a = 1 / 0;
    });
    //  给某个线程设置自己的未捕获异常处理器
    thread.setUncaughtExceptionHandler(((t, e) -> {
        System.out.println("线程执行异常!线程名称: " + t.getName());
        logger.error("线程执行异常!名称: {}", t.getName(), e);
    }));

    thread.start();
}

通常我们采用线程池的方式使用线程,下面是在线程池中使用方式

public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(3, new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
            return t;
        }
    });

    executorService.execute(new Runnable() {
        @Override
        public void run() {
            int a = 1 / 0;
        }
    });
}

2. CountDownLatch(倒计时)

CountDownLatch 是 Java 中的一个同步工具类,它允许一个或多个线程等待其他线程完成操作。

public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(3, new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
            return t;
        }
    });

    int count = 10; //  10个任务
    CountDownLatch latch = new CountDownLatch(count);
    for (int i = 0; i < count; i++) {
//            executorService.execute(()->{
//                try {
//
//                } catch (Exception ex) {
//
//                } finally {
//                    latch.countDown();
//                }
//
//            });
        executorService.execute(new MyTask(latch));
    }

    try {
        latch.await();  //  等待所有异步任务执行完成
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }

    //  执行后续处理逻辑
}

static class MyTask implements Runnable {

    private CountDownLatch latch;

    public MyTask(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void run() {
        try {

        } catch (Exception e) {

        } finally {
            latch.countDown();
        }
    }
}

3. Semaphore(信号量)

Semaphore 是一个用于控制同时访问特定资源的线程数量的同步工具。它通过维护一个许可集来管理对资源的访问。线程在访问资源之前必须从信号量中获取许可,访问完成后释放许可。如果没有可用的许可,线程将被阻塞,直到有可用的许可为止。

/**
 * 控制并发执行的任务数量
 */
public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(3);

    Semaphore semaphore = new Semaphore(10);

    //  模拟100个附件同时上传
    for (int i = 0; i < 100; i++) {
        executorService.execute(()->{
            try {
                semaphore.acquire();
                upload();
            } catch (Exception e) {

            } finally {
                semaphore.release();
            }
        });
    }
    
    executorService.shutdown();
}

/**
 * 附件上传操作
 */
public static void upload() {
    //  假设,最多同时处理10个附件,太多的话可能会内存溢出,为了保护它,不让它挂掉,我们可以控制并发请求数量
    //  ......
}

上面的例子,我们在调用端限制并发请求数来达到保护被调用方的目的,其实也可以写在被调用端,效果是一样的,在调用方和被调用方其中一方做控制就行。

4. Redisson分布式锁和同步器

Redisson 是 Redis 的Java客户端,在分布式环境下,Redission实现了Semaphore和CountDownLatch。

https://redisson.org/docs/data-and-services/locks-and-synchronizers/

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.41.0</version>
</dependency>

Semaphore基本用法

RSemaphore semaphore = redisson.getSemaphore("mySemaphore");

// acquire single permit
semaphore.acquire();

// or acquire 10 permits
semaphore.acquire(10);

// or try to acquire permit
boolean res = semaphore.tryAcquire();

// or try to acquire permit or wait up to 15 seconds
boolean res = semaphore.tryAcquire(15, TimeUnit.SECONDS);

// or try to acquire 10 permit
boolean res = semaphore.tryAcquire(10);

// or try to acquire 10 permits or wait up to 15 seconds
boolean res = semaphore.tryAcquire(10, 15, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       semaphore.release();
   }
}

CountDownLatch基本用法

RCountDownLatch latch = redisson.getCountDownLatch("myCountDownLatch");

latch.trySetCount(1);
// await for count down
latch.await();

// in other thread or JVM
RCountDownLatch latch = redisson.getCountDownLatch("myCountDownLatch");
latch.countDown();

参考

https://blog.csdn.net/weixin_42373241/article/details/139441473

前言

Material Design 是由 Google 开发的一套设计指南,提供统一的设计语言,使用户界面更加直观、美观和一致。

Material Design In XAML Toolkit 是一个用于在WPF 程序开发中实现 Material Design 设计语言的开源界面控件库。

项目介绍

MaterialDesignInXamlToolkit 是一个适用于 Windows 桌面的开源 WPF UI 控件库,帮助大家在 C# 和 VB.NET 中轻松实现 Google 的 Material Design 风格用户界面。

该框架提供一组丰富且功能强大的控件、样式和效果,支持全面的 UI 组件,并允许自定义主题颜色、字体等。通过使用 MaterialDesignInXamlToolkit,可以快速开发现代化、美观且用户友好的应用程序。

核心组件

除了包含标准的控件主题外,该套件还包含了一些常用的控件:时钟、对话框、浮动按钮、卡片、齐全图标等。

项目特点

  • 适用于 Windows 桌面(WPF)的全面且易用的 Material Design 主题和控件库
  • 主要 WPF 框架控件的 Material Design 样式 支持主题的附加控件,包括多动作按钮、卡片、对话框和时钟
  • 轻松配置调色板(设计时和运行时均可配置)
  • 完整的 Material Design Icons 图标包
  • 平滑的过渡效果
  • 与 Dragablz 和 MahApps 兼容
  • 源项目中包含演示程序

项目使用

使用 Microsoft Visual Studio 2022创建一个 WPF 项目。

1、安装工具包

通过 Visual Studio 的 NuGet 包管理器安装工具包,或使用以下命令:

Install-Package MaterialDesignThemes

2、修改 App.xaml

添加以下代码到 App.xaml文件中:

<Applicationx:Class="Example.App"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <!--使用捆绑的主题,设置基础主题为浅色,主色为深紫色,次色为酸橙色-->
                <materialDesign:BundledThemeBaseTheme="Light"PrimaryColor="DeepPurple"SecondaryColor="Lime" />

                <!--引用 Material Design 2 的默认样式-->
                <ResourceDictionarySource="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign2.Defaults.xaml" /> 
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

如果希望使用 Material Design 3,请将上述代码片段中的
ResourceDictionary
行修改为引用 MaterialDesign3.Defaults.xaml:

<ResourceDictionarySource="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign3.Defaults.xaml" />

3、修改 MainWindow.xaml

在 MainWindow.xaml 中应用 Material Design 样式:

<ButtonStyle="{StaticResource MaterialDesignFlatButton}"Content="Click Me!" />

项目效果

1、首页卡片布局

页面采用卡片式布局,能直观展示数据。

2、支持主题风格切换

该控件库提供多种颜色主题,满足不同场景下的配色需求。

用户可以根据需要轻松切换浅色、深色或自定义主题,确保应用程序在任何环境下都能保持一致且美观的外观。

3、按钮

交互式按钮是界面开发中不可或缺的一部分。

该控件库提供了丰富的按钮样式,如圆形按钮、图标按钮、带进度条的按钮和带有文字提示的按钮等,能够轻松实现类似 Web 的交互效果。

4、输入验证框

表单验证通常在 ViewModel 中编写业务逻辑代码或在 XAML 中使用验证表达式。

该控件库内置了多种验证器,简化了代码实现,并提供了友好的界面提示,类似于移动端的输入和错误提示,提升了用户体验。

5、标签(Chips)

标签(Chips)用于标记和过滤数据。当列表显示的数据较多时,标签可以帮助用户快速筛选和定位所需信息,增强了交互性和便捷性。

6、卡片(Cards)

卡片式布局为数据展示带来了新的活力。

相比传统的列表和表格,卡片形式更加直观和清爽,适合以图片+文字的方式呈现内容,让用户一目了然地获取信息,提升整体体验。

7、图标包(Icon Pack)

该控件库包含成百上千种矢量图标,极大地丰富了开发选项。

图标可用于菜单、按钮等多种场景,方便快捷地美化界面。

8、分组框(Group Boxes)

分组框(Group Boxes)允许对相关内容进行分组,增强界面的组织性和可读性。

其自定义 Header 功能使得添加图片和其他元素变得非常容易,进一步提升设计灵活性。

9、进度条

各式各样的进度条设计,解决了传统进度条调整复杂的难题。

该控件库提供的几种样式几乎覆盖了一般需求,若需进一步定制也十分简单,极大地方便了开发者。

10、对话框

遮罩式对话框不仅酷炫,还提供全窗体遮罩和局部用户控件对话框两种选择。

11、抽屉(Drawer)

抽屉式控件支持从东、南、西、北四个方向打开,增加了界面的动态感和交互性。

无论是侧边栏还是底部工具栏,抽屉控件都能提供美观的解决方案。

12、简单提示

最后但同样重要的是,简单提示信息功能。

这种方式避免了频繁弹框打扰用户,通过即时自动关闭的提示信息,用户只需一眼即可获取必要的反馈,既简洁又高效。

项目地址

GitHub:
https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit

官网:
http://materialdesigninxaml.net/

总结

以上仅展示了MaterialDesignInXamlToolkit控件的部分功能。更多实用特性和详细信息,请大家访问项目地址。

希望通过本文能为WPF UI 开发提供有价值的参考。欢迎在评论区留言交流,分享您的宝贵经验和建议。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!

在其它手机端,若想保存图片到相册,需要申请对应的权限,而鸿蒙中对应的权限为受限开放权限,普通应用一般不让申请,这个时候我们可以使用安全保存控件来临时申请权限,用于保存图片到相册。

受限开放权限

应用权限分为三类,一类是对所有应用开放,所有应用均可申请使用;一类是受限开放权限,仅少量符合特殊场景的应用可在通过审批后,使用受限权限;最后一类是仅对MDM(Mobile Device Management)设备管理应用开放。

保存图片到相机涉及到的权限是ohos.permission.WRITE_IMAGEVIDEO,仅特殊场景与功能才可申请此权限,例如应用需要克隆、备份或同步图片/视频类文件,其它场景下使用安全控件来临时申请权限。

使用安全控件保存本地图片到相机

我们先使用安全控件让用户点击临时获取权限,获取到权限后,再使用photoAccessHelper来将我们本地的图片保存在相册,示例如下

import { photoAccessHelper } from '@kit.MediaLibraryKit'

@Entry
@ComponentV2
struct Index {

  build() {
    Column() {
      SaveButton({ icon: SaveIconStyle.LINES, text: SaveDescription.SAVE_TO_GALLERY, buttonType: ButtonType.Capsule })
        .onClick(() => {
          this.savePhotoToGallery().then(() => {
            this.getUIContext().getPromptAction().showToast({ message: '保存成功' })
          }).catch((err: Error) => {
            this.getUIContext().getPromptAction().showToast({ message: err.message })
          })
        })
    }
  }

  public async savePhotoToGallery(): Promise<void> {
    const ctx = getContext()
    const helper = photoAccessHelper.getPhotoAccessHelper(ctx)
    const src = ctx.resourceDir + '/icon.png'
    const request = photoAccessHelper.MediaAssetChangeRequest.createImageAssetRequest(ctx, src)
    return helper.applyChanges(request)
  }
}

以上示例请保证icon.png在本地真实的存在。当我们使用helper调用applyChanges时,因为是在安全控件点击后调用的,临时获取权限,可以正常执行,若不在安全控件内,则需保证已获取对应的权限。

## 使用安全控件保存服务端图片到相机

服务端图片我们一般使用下载服务将图片下载到本地,若本地不需要备份,则直接将下载好的图片buffer保存到相册即可。我们将本地图片转成buffer来模拟服务端下载后的图片,再使用photoAccessHelper创建一个相册图片资源,并将我们的图片buffer写入到这个图片资源中,就可以将图片保存到相册了,示例如下

import { photoAccessHelper } from '@kit.MediaLibraryKit'
import fs from '@ohos.file.fs'
@Entry
@ComponentV2
struct Index {

  build() {
    Column() {
      SaveButton({ icon: SaveIconStyle.LINES, text: SaveDescription.SAVE_TO_GALLERY, buttonType: ButtonType.Capsule })
        .onClick(() => {
          this.savePhotoToGallery().then(() => {
            this.getUIContext().getPromptAction().showToast({ message: '保存成功' })
          }).catch((err: Error) => {
            this.getUIContext().getPromptAction().showToast({ message: err.message })
          })
        })
    }
  }

  public async savePhotoToGallery(): Promise<void> {
    const ctx = getContext()
    const helper = photoAccessHelper.getPhotoAccessHelper(ctx)
    return Promise.all([helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'png').then((uri) => {
      return fs.open(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
    }), ctx.resourceManager.getMediaContent($r('app.media.app_icon').id, 0)]).then((array) => {
      fs.writeSync(array[0].fd, array[1].buffer)
      fs.closeSync(array[0].fd)
    })
  }
}

使用保存确认弹窗保存图片

前面的方式都是强依赖于安全控件,但在有些场景下,我们没办法使用安全控件,比如在H5页面中,再比如在Flutter页面中等等,这个时候我们可以借助保存确认弹窗来保存图片。当我们通过photoAccessHelper调用showAssetsCreationDialog时,系统会弹出一个确认弹窗,用户点击允许,则我们可以将图片保存到相册,若用户点击禁止,则不能保存图片到相册。使用这种方式,我们就不用强依赖于安全控件了,示例如下

import { photoAccessHelper } from '@kit.MediaLibraryKit'
import fs from '@ohos.file.fs'

@Entry
@ComponentV2
struct Index {
  @Local isShowHome: boolean = false

  build() {
    Column() {
      Button('保存图片到相册').onClick(()=>{
        this.savePhotoToGallery().then(() => {
          this.getUIContext().getPromptAction().showToast({ message: '保存成功' })
        }).catch((err: Error) => {
          this.getUIContext().getPromptAction().showToast({ message: err.message })
        })
      })
    }
  }

  public async savePhotoToGallery(): Promise<void> {
    const ctx = getContext()
    const helper = photoAccessHelper.getPhotoAccessHelper(ctx)
    const src = ctx.resourceDir + '/icon.png'
    const desFileUris = await helper.showAssetsCreationDialog([src], [{
      title: 'test',
      fileNameExtension: 'png',
      photoType: photoAccessHelper.PhotoType.IMAGE
    }])
    const desFile = fs.openSync(desFileUris[0], fs.OpenMode.WRITE_ONLY)
    const srcFile = fs.openSync(src, fs.OpenMode.READ_ONLY)
    fs.copyFileSync(srcFile.fd, desFile.fd)
    fs.closeSync(srcFile)
    fs.closeSync(desFile)
  }
}

以上示例请保存icon.png在本地真实存在。

扩展属性

  • 我们发现abp的默认都会有一个ExtraProperties属性,那么他的作用是什么呢?当我们使用abp的时候,需要对其原有的表进行扩展或者添加我们想要的字段,又不想改源码,以最小的方式实现,这个时候就体现它的价值了。
  • 我们添加的数据都会在ExtraProperties以JSON对象方式进行存储。

如何添加

  • 在Domain.Shared层中提供了一个xxxModuleExtensionConfigurator类

示例

  • 给AbpUser表扩展一个用户头像地址的属性
 public static class JuiceModuleExtensionConfigurator
 {
        private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner();

        public static void Configure()
        {
            OneTimeRunner.Run(() =>
            {
                ConfigureExistingProperties();
                ConfigureExtraProperties();
            });
        }
        private static void ConfigureExtraProperties()
        {
            ObjectExtensionManager.Instance.Modules()
                .ConfigureIdentity(identity =>
                {
                    identity.ConfigureUser(user =>
                    {
                        user.AddOrUpdateProperty<string>( //property type: string
                            "AvatarUrl",
                            property =>
                            {
                                property.Attributes.Add( new StringLengthAttribute(128)); // 最大长度为28
                                // ...property.Attributes.Add( new RequiredAttribute()); 还有很多特性直接可以使用
                            }
                        );
                    });
                });
        }
 }
  • 如何写入或者更新到数据库
private readonly IdentityUserManager _userManager;
public TestAppService(IdentityUserManager userManager)
{
    _userManager = userManager;
}
public async Task AddUserAsync()
{
    var user = new IdentityUser(GuidGenerator.Create(), "test-user", "test@qq.com");
    user.ExtraProperties["AvatarUrl"] = "test-avatar-url";
    await _userManager.CreateAsync(user);
}
  • 效果

疑问?

  • 这个数据是一个json格式保存在数据库的扩展字段的?假如我的场景是要加索引呢?这个时候这种方式就不行了?

  • 映射成数据库字段列


    • 在EntityFrameworkCore层的xxxEfCoreEntityExtensionMappings下集成一下代码
public static class JuiceEfCoreEntityExtensionMappings
{
    private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner();
    public static void Configure()
    {
        JuiceGlobalFeatureConfigurator.Configure();
        JuiceModuleExtensionConfigurator.Configure();
        OneTimeRunner.Run(() =>
        {
            ObjectExtensionManager.Instance
                .MapEfCoreProperty<IdentityUser, string>(
                    "AvatarUrl",
                    (entityBuilder, propertyBuilder) =>
                    {
                        propertyBuilder.HasMaxLength(128);
                    }
                );
        });
    }
}
  • 执行ef core迁移
dotnet ef migrations addAvatarUrl
dotnet database update
  • 效果

  • 如何通过代码新增和编辑方式还是通过扩展字段操作和之前保持一致。

注意

  • AutoMapper的时候记得需要mapper扩展属性(MapExtraProperties),如下:
public class AbpIdentityApplicationModuleAutoMapperProfile : Profile
{
    public AbpIdentityApplicationModuleAutoMapperProfile()
    {
        CreateMap<IdentityUser, IdentityUserDto>()
            .MapExtraProperties();

        CreateMap<IdentityRole, IdentityRoleDto>()
            .MapExtraProperties();
    }
}
  • 我们自写自己的模块的时候,应该也支持ExtraProperties,这样灵活性会更好。

ABP vNext Pro