Activities of "maliming"

Hi,

I tried to reproduce this locally on a freshly generated 9.2.2 Angular template (Identity Roles and Users pages). The ExtensibleTable component code in 9.1.x and 9.2.x is the same, so it's a useful comparison but not proof your exact setup is unaffected. On my side both pages render correctly after data load, no resize needed — so this doesn't look like a generic 9.1.2 framework defect; it's more likely related to project-specific layout, theme customization, browser state, or timing in your environment.

Could you share a bit more so we can narrow it down:

  1. A short GIF or screen recording showing the issue (enter the page → empty table → resize → data appears). The video helps a lot more than a text description — we want to see what the empty state actually looks like (header rendered? loading spinner stuck? rows present but blank cells?).
  2. Versions of @abp/ng.components, @abp/ng.theme.lepton-x, and @swimlane/ngx-datatable from your package.json.
  3. Browser, version, screen resolution, and zoom level.
  4. Any custom CSS or theme overrides on top of LeptonX.
  5. While the table is in the "empty" state, open browser DevTools → check console for errors/warnings, and inspect the ngx-datatable host element's computed width / height (often this kind of issue comes from the container being 0×0 at first render due to a hidden/flex parent).
  6. Does the issue still happen in an incognito window (rules out extensions / cached service worker), and on a fresh 9.1.2 template with no customizations (rules out something specific to your project).

In the meantime, inside your existing root AppComponent you can add this to force a recalculate after every navigation:

import { NgZone, OnInit, inject } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { filter } from 'rxjs/operators';

export class AppComponent implements OnInit {
  private router = inject(Router);
  private zone = inject(NgZone);

  ngOnInit() {
    this.router.events
      .pipe(filter(e => e instanceof NavigationEnd))
      .subscribe(() => {
        this.zone.runOutsideAngular(() => {
          setTimeout(() => window.dispatchEvent(new Event('resize')), 0);
          setTimeout(() => window.dispatchEvent(new Event('resize')), 100);
        });
      });
  }
}

This is the same mechanism your manual window resize is triggering today, dispatched programmatically. It's a temporary workaround while we narrow down the root cause from the details above.

Thanks

Hi,

Both have already been addressed, so you don't need to wait for v10.5:

  • Scriban was bumped to 7.0.0 in v10.4.0, which clears the security advisories. Just upgrading your ABP packages picks it up.
  • AutoMapper is a different story: AutoMapper 14.x is the last free version and won't get a patch for GHSA-rvv3-g6hj-g44x. Volo.Abp.AutoMapper itself still references 14.x and will keep showing the advisory. The supported fix is to switch to Volo.Abp.LuckyPenny.AutoMapper (available since v10.3.0), which targets the patched commercial AutoMapper (16.x). It's a drop-in replacement — only two changes in your own modules:
// *.csproj
-<PackageReference Include="Volo.Abp.AutoMapper" />
+<PackageReference Include="Volo.Abp.LuckyPenny.AutoMapper" />
// YourModule.cs
-[DependsOn(typeof(AbpAutoMapperModule))]
+[DependsOn(typeof(AbpLuckyPennyAutoMapperModule))]

All types stay in the same namespaces (AbpAutoMapperOptions, IMapperAccessor, etc.), so your existing Profiles and CreateMap calls keep working as-is. LuckyPenny has a free Community License for organizations with annual gross revenue under $5,000,000 USD and that never received more than $10,000,000 USD in outside capital (sign-up page); otherwise paid plans. Full setup (including the license key configuration): https://abp.io/docs/latest/framework/infrastructure/luckypenny-automapper

If you'd rather not take on a commercial dependency, the free alternative is Mapperly (what ABP modules use internally in v10). Migration guide: https://abp.io/docs/latest/release-info/migration-guides/AutoMapper-To-Mapperly

About the VS 2026 PMC behavior: that's upstream NuGet Audit kicking in when a vulnerable package is restored — it's not something $ErrorActionPreference can override. Once you upgrade to v10.4.x and switch Volo.Abp.AutoMapper to either Volo.Abp.LuckyPenny.AutoMapper or Volo.Abp.Mapperly, the audit stops flagging those packages and Add-Migration / Update-Database work normally again.

Thanks

Hi,

You're right, this is a real bug. The Guid.TryParse(parts[0], ...) branch in QdrantVectorStore.BuildPointId collapses every chunk of the same data source onto the data source's GUID as the PointId, so each new chunk overwrites the previous one and only the last chunk survives in Qdrant. Verified end-to-end against a local Qdrant container: with the original code 3 stored chunks produce 1 point in the collection; with the underscore branch removed they produce 3 distinct points.

Removing that branch (so every non-GUID id falls through to the SHA256 hash) is the correct fix. Your workaround is essentially what we'll ship.

The fix is scheduled for the next release. Existing Qdrant data already affected needs to be reindexed (DeleteByWorkspaceIdAsync on the workspace, then re-run indexing) since only one chunk per document was actually stored.

Thanks

Hi,

You were right — reproduced and root-caused. The bug is in framework-shared code (AbpExtensibleDataGrid.razor), not in Volo.Saas. Affects every JSON-only extension column (Saas Tenants, Identity Users, etc.) and is present in both 10.3.0 and 10.4.0.

Fix: https://github.com/abpframework/abp/pull/25480 — will ship in the next 10.3.x patch.

Until then MapEfCoreProperty remains the simplest workaround.

Thanks

: )

Hi,

Quick clarifications on both your points:

  1. You're right about the constructor — TenantAppService in 10.3.0 takes the 9 parameters you listed. More importantly, an override shouldn't be needed at all. The framework copies ExtraProperties from Tenant to SaasTenantDto by default in 10.3.0, the same way it does on IdentityUser. There's no intentional change in Volo.Saas that would make the Saas grid behave differently from Identity — both use Mapperly with the same attributes. So your read on this being a regression in your environment (rather than a designed-in difference) is the right framing.

  2. We followed your steps end-to-end on a fresh 10.3.0 Blazor Web App locally (Pro NuGet packages pinned to 10.3.0, SQL Server, JSON-only TaxNumber registered via ConfigureSaas.ConfigureTenant.AddOrUpdateProperty<>, seeded with tenant.SetProperty("TaxNumber", "12345678") + InsertAsync). Everything works as expected: the value lands in the SaasTenants.ExtraProperties JSON column, the entity loads it back, the mapping carries it through, the grid cells render, and GET /api/saas/tenants returns:

{
  "items": [{
    "name": "repro-10690",
    "extraProperties": { "TaxNumber": "12345678" }
  }]
}

So the JSON-only flow is intact on a clean 10.3.0 setup. Something in your project is breaking it before the mapping step.

The MapEfCoreProperty clue is the most useful signal you gave. That switch doesn't change the CLR/mapping API — both paths still read tenant.ExtraProperties[...]. It only changes physical storage (JSON column vs. a real DB column). So the failing step is most likely the EF load itself: the JSON value in the SaasTenants.ExtraProperties column isn't being deserialized into the in-memory ExtraProperties dictionary on the entity, and the mapping then correctly copies an empty dictionary downstream.

To narrow this down, could you do one or more of these:

  1. Add a quick log right after fetching the entity and share the output:
var entities = await TenantRepository.GetListAsync(...);
var first = entities.FirstOrDefault();
Logger.LogInformation(
    "Repro: ExtraProperties.Count = {Count}, TaxNumber from entity = {Value}",
    first?.ExtraProperties.Count,
    first?.GetProperty<string>("TaxNumber"));

If TaxNumber is null at the entity level while the DB has {"TaxNumber":"12345678"}, the bug is in EF deserialization — which is exactly the layer MapEfCoreProperty happens to bypass.

  1. Open Chrome DevTools → Network on the Tenants page and share the raw JSON response of GET /api/saas/tenants. If extraProperties.TaxNumber is missing in the server response, the issue is server-side (consistent with the theory above). If it's present, the issue is on the Blazor grid binding side instead.

  2. When you upgraded from 10.1.1 → 10.3.0, did you add and apply a new EF migration? If yes, could you share the generated migration diff for the SaasTenants table — especially anything touching the ExtraProperties column?

  3. If easier, a minimal reproduction project (private GitHub repo with https://github.com/maliming invited, or a zip to liming.ma@volosoft.com) lets us reproduce in your exact configuration.

We'd rather find the real root cause than recommend MapEfCoreProperty as the permanent answer — JSON-only should work the same way it does on IdentityUser.

Thanks

Hi,

You're right, the current Volo.AIManagement.VectorStores.Qdrant module only parses host/port out of the connection string and calls new QdrantClient(host, port), so there is no way to pass an API key or enable HTTPS through the built-in settings.

The workaround is to replace the two built-in services — QdrantVectorStoreFactory and QdrantVectorStore — with your own. The factory reads Qdrant:ApiKey and Qdrant:Https from appsettings.json (keeping the secret out of the DB), builds a QdrantClient with them, and wraps it in your custom IVectorStore. The workspace's VectorStoreSettings field stays in the existing host:port format.

1. MyQdrantVectorStoreFactory.cs

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Qdrant.Client;
using Volo.Abp.DependencyInjection;
using Volo.AIManagement.VectorStores;

namespace MyCompanyName.MyProjectName;

[ExposeServices(typeof(IVectorStoreFactory))]
public class MyQdrantVectorStoreFactory : IVectorStoreFactory, ITransientDependency
{
    public string Provider => "Qdrant";

    private readonly ILoggerFactory _loggerFactory;
    private readonly IConfiguration _configuration;

    public MyQdrantVectorStoreFactory(ILoggerFactory loggerFactory, IConfiguration configuration)
    {
        _loggerFactory = loggerFactory;
        _configuration = configuration;
    }

    public Task<IVectorStore> CreateAsync(Guid workspaceId, string? connectionSettings = null)
    {
        var (host, port) = ParseHostPort(connectionSettings!);
        var apiKey = _configuration["Qdrant:ApiKey"];
        var https = _configuration.GetValue<bool>("Qdrant:Https");

        var client = new QdrantClient(host, port, https, apiKey, loggerFactory: _loggerFactory);
        return Task.FromResult<IVectorStore>(
            new MyQdrantVectorStore(client, workspaceId, _loggerFactory.CreateLogger<MyQdrantVectorStore>()));
    }

    private static (string Host, int Port) ParseHostPort(string raw)
    {
        if (raw.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
            raw.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
        {
            var uri = new Uri(raw);
            return (uri.Host, uri.Port > 0 ? uri.Port : 6334);
        }
        var idx = raw.IndexOf(':');
        return idx > 0 && int.TryParse(raw[(idx + 1)..], out var p) ? (raw[..idx], p) : (raw, 6334);
    }
}

2. MyQdrantVectorStore.cs — same shape as the built-in QdrantVectorStore, but the constructor takes a pre-built QdrantClient:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Qdrant.Client;
using Qdrant.Client.Grpc;
using static Qdrant.Client.Grpc.Conditions;
using Volo.AIManagement.VectorStores;

namespace MyCompanyName.MyProjectName;

public class MyQdrantVectorStore : IVectorStore, IDisposable
{
    private readonly QdrantClient _client;
    private readonly ILogger<MyQdrantVectorStore> _logger;
    private readonly Guid _workspaceId;
    private int? _workspaceDimension;
    private readonly SemaphoreSlim _initLock = new(1, 1);

    public MyQdrantVectorStore(QdrantClient client, Guid workspaceId, ILogger<MyQdrantVectorStore> logger)
    {
        _client = client;
        _workspaceId = workspaceId;
        _logger = logger;
    }

    private string CollectionName => $"ai_vector_embeddings_{_workspaceId:N}";

    public async Task InitializeAsync(CancellationToken cancellationToken = default)
    {
        await _client.ListCollectionsAsync(cancellationToken);
    }

    public async Task<string> StoreAsync(Guid workspaceId, string id, float[] embedding,
        Dictionary<string, object>? metadata = null, CancellationToken cancellationToken = default)
    {
        await EnsureCollectionAsync(embedding.Length, cancellationToken);

        var payload = new Dictionary<string, Value>();
        if (metadata != null)
        {
            foreach (var kvp in metadata) payload[kvp.Key] = ToValue(kvp.Value);
        }
        payload["workspace_id"] = ToValue(_workspaceId.ToString());
        payload["creation_time"] = ToValue(DateTime.UtcNow.ToString("O"));
        payload["original_id"] = ToValue(id);

        var point = new PointStruct
        {
            Id = ToPointId(id),
            Vectors = new Vectors { Vector = new Vector { Data = { embedding } } }
        };
        foreach (var kvp in payload) point.Payload[kvp.Key] = kvp.Value;

        await _client.UpsertAsync(CollectionName, new[] { point }, cancellationToken: cancellationToken);
        return id;
    }

    public async Task<List<VectorSearchResult>> SearchAsync(Guid workspaceId, float[] queryEmbedding, int topK,
        CancellationToken cancellationToken = default)
    {
        if (!await CollectionExistsAsync(cancellationToken)) return new();

        var results = await _client.SearchAsync(CollectionName, queryEmbedding, limit: (ulong)topK,
            searchParams: new SearchParams { HnswEf = 512 }, cancellationToken: cancellationToken);

        return results.Select(p =>
        {
            var meta = FromPayload(p.Payload);
            var id = meta != null && meta.TryGetValue("original_id", out var oid) ? oid?.ToString() ?? p.Id.ToString() : p.Id.ToString();
            return new VectorSearchResult { Id = id, Score = p.Score, Metadata = meta };
        }).ToList();
    }

    public async Task<bool> DeleteAsync(Guid workspaceId, string id, CancellationToken cancellationToken = default)
    {
        if (!await CollectionExistsAsync(cancellationToken)) return false;
        await _client.DeleteAsync(CollectionName, new[] { ToPointId(id) }, wait: true, cancellationToken: cancellationToken);
        return true;
    }

    public async Task<int> DeleteByWorkspaceIdAsync(Guid workspaceId, CancellationToken cancellationToken = default)
    {
        if (!await CollectionExistsAsync(cancellationToken)) return 0;
        await _client.DeleteCollectionAsync(CollectionName, cancellationToken: cancellationToken);
        _workspaceDimension = null;
        return 1;
    }

    public async Task<int> DeleteAllByDataSourceAsync(Guid workspaceId, Guid dataSourceId, CancellationToken cancellationToken = default)
    {
        if (!await CollectionExistsAsync(cancellationToken)) return 0;
        await _client.DeleteAsync(CollectionName, filter: MatchKeyword("data_source_id", dataSourceId.ToString()),
            wait: true, cancellationToken: cancellationToken);
        return 1;
    }

    public void Dispose()
    {
        _client.Dispose();
        _initLock.Dispose();
    }

    private async Task EnsureCollectionAsync(int dimension, CancellationToken cancellationToken)
    {
        await _initLock.WaitAsync(cancellationToken);
        try
        {
            if (_workspaceDimension == dimension) return;

            if (!await CollectionExistsAsync(cancellationToken))
            {
                await _client.CreateCollectionAsync(CollectionName,
                    vectorsConfig: new VectorParams
                    {
                        Size = (ulong)dimension,
                        Distance = Distance.Cosine,
                        HnswConfig = new HnswConfigDiff { M = 16, EfConstruct = 200 }
                    },
                    cancellationToken: cancellationToken);
            }
            _workspaceDimension = dimension;
        }
        finally { _initLock.Release(); }
    }

    private async Task<bool> CollectionExistsAsync(CancellationToken cancellationToken) =>
        (await _client.ListCollectionsAsync(cancellationToken)).Any(c => c == CollectionName);

    private static PointId ToPointId(string id)
    {
        if (Guid.TryParse(id, out var g)) return g;
        if (id.Contains('_') && Guid.TryParse(id.Split('_', 2)[0], out var gp)) return gp;
        return BitConverter.ToUInt64(System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(id)), 0);
    }

    private static Value ToValue(object? v) => v switch
    {
        string s => new Value { StringValue = s },
        int i => new Value { IntegerValue = i },
        long l => new Value { IntegerValue = l },
        float f => new Value { DoubleValue = f },
        double d => new Value { DoubleValue = d },
        bool b => new Value { BoolValue = b },
        null => new Value { NullValue = NullValue.NullValue },
        _ => new Value { StringValue = v.ToString() ?? "" }
    };

    private static Dictionary<string, object>? FromPayload(Google.Protobuf.Collections.MapField<string, Value> payload)
    {
        if (payload == null || payload.Count == 0) return null;
        var meta = new Dictionary<string, object>();
        foreach (var kvp in payload)
        {
            if (kvp.Key is "workspace_id" or "creation_time") continue;
            meta[kvp.Key] = kvp.Value.KindCase switch
            {
                Value.KindOneofCase.StringValue => kvp.Value.StringValue,
                Value.KindOneofCase.IntegerValue => kvp.Value.IntegerValue,
                Value.KindOneofCase.DoubleValue => kvp.Value.DoubleValue,
                Value.KindOneofCase.BoolValue => kvp.Value.BoolValue,
                _ => kvp.Value.ToString()!
            };
        }
        return meta;
    }
}

3. Remove the built-in factory registration in any of your existing modules that already depend on AIManagementQdrantVectorStoreModule. It has to be done in PostConfigureServices (not PreConfigureServices or ConfigureServices), because AbpApplicationBase runs Services.AddAssembly(...) between PreConfigureServices and ConfigureServices — if you call RemoveAll too early, the built-in factory hasn't been auto-registered yet and the call silently does nothing.

public override void PostConfigureServices(ServiceConfigurationContext context)
{
    context.Services.RemoveAll(d => d.ImplementationType == typeof(QdrantVectorStoreFactory));
}

RemoveAll is needed (instead of [Dependency(ReplaceServices = true)]) because IVectorStoreFactory is consumed as IEnumerable<IVectorStoreFactory> and resolved by _factories.FirstOrDefault(f => f.Provider == "Qdrant"). If both factories stay registered, the framework one is registered first (its module loads before yours) and wins — your factory is never called.

4. appsettings.json (the workspace's VectorStoreSettings field stays as qdrant.example.com:6334):

{
  "Qdrant": {
    "ApiKey": "YOUR_PRODUCTION_API_KEY",
    "Https": true
  }
}

We'll add first-class ApiKey / UseHttps support to the Qdrant module in the next release so this workaround is no longer needed.

Thanks

Thanks DominaTang

Hi,

The recommended way is to upgrade ABP incrementally, one minor version at a time (10.0 → 10.1 → 10.2 → 10.3 → 10.4) and apply each migration guide. Jumping directly from 9.3.2 to 10.4.0 mixes 5 versions of breaking changes and makes root-causing very hard. The migration guides are here:

  • https://abp.io/docs/10.4/release-info/migration-guides/abp-10-0
  • https://abp.io/docs/10.4/release-info/migration-guides/abp-10-1
  • https://abp.io/docs/10.4/release-info/migration-guides/abp-10-2
  • https://abp.io/docs/10.4/release-info/migration-guides/abp-10-3
  • https://abp.io/docs/10.4/release-info/migration-guides/abp-10-4

Below are answers to each issue you listed.

1. "No object mapping was found for the specified source and destination types"

This happens because in v10 your solution loads both AbpAutoMapperModule (your existing code) and AbpMapperlyModule (loaded indirectly by ABP modules that already migrated to Mapperly). Both register IAutoObjectMappingProvider and the last-loaded module wins, so ObjectMapper.Map<TSource, TDestination>(...) ends up looking for a mapper in the wrong provider.

Fix: pick one provider and register it explicitly in your root module's ConfigureServices:

// If you keep AutoMapper:
public override void ConfigureServices(ServiceConfigurationContext context)
{
    context.Services.AddAutoMapperObjectMapper();
}
// If you switch to Mapperly:
public override void ConfigureServices(ServiceConfigurationContext context)
{
    context.Services.AddMapperlyObjectMapper();
}

Doc: https://abp.io/docs/10.4/release-info/migration-guides/AutoMapper-To-Mapperly#set-default-mapping-provider

2. AutoMapper situation in v10 and which path to take

Volo.Abp.AutoMapper is still shipped and works in v10. The reason this topic comes up so much is that:

  • AutoMapper 14.x (which Volo.Abp.AutoMapper is built on) has a known DoS vulnerability (GHSA-rvv3-g6hj-g44x) and the 14.x line will not get a patch.
  • AutoMapper 15.x is commercial-only.

ABP gives you three valid options. Pick one and don't mix them across the solution:

Option A — Keep Volo.Abp.AutoMapper (no commercial license) ABP Framework has applied a code-level mitigation (MaxDepth = 64) to the integration to address the GHSA-rvv3-g6hj-g44x DoS vector. This is the lowest-effort option if your project does not need the patched AutoMapper.

Option B — Move to Volo.Abp.LuckyPenny.AutoMapper (commercial AutoMapper 15.x) This is a true drop-in replacement and only requires two changes, no namespace updates:

  1. In every *.csproj:

    <PackageReference Include="Volo.Abp.LuckyPenny.AutoMapper" />
    

    (replacing Volo.Abp.AutoMapper)

  2. In every module that previously depended on AbpAutoMapperModule:

    [DependsOn(typeof(AbpLuckyPennyAutoMapperModule))]
    public class MyModule : AbpModule { /* unchanged */ }
    

All types remain in the Volo.Abp.AutoMapper namespace, so using directives stay the same. Volo.Abp.LuckyPenny.AutoMapper and Volo.Abp.AutoMapper must not be referenced together — choose one or the other.

LuckyPenny has a free Community License for organizations with annual gross revenue under $5,000,000 USD and that never received more than $10,000,000 USD in outside capital (sign-up page). Otherwise see paid plans.

License key configuration (do this only on server-side projects, not Blazor WebAssembly / MAUI):

[DependsOn(typeof(AbpLuckyPennyAutoMapperModule))]
public class MyModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        var licenseKey = context.Configuration["AutoMapper:LicenseKey"];

        Configure<AbpAutoMapperOptions>(options =>
        {
            options.Configurators.Add(ctx =>
            {
                ctx.MapperConfiguration.LicenseKey = licenseKey;
            });
        });
    }
}

Doc: https://abp.io/docs/10.4/framework/infrastructure/luckypenny-automapper

Option C — Migrate to Volo.Abp.Mapperly Mapperly is free and is what ABP modules use internally in v10. Five global search-and-replace steps, then convert each Profile class into a MapperBase<TSource, TDestination> partial class:

[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]
public partial class MyEntityToMyEntityDtoMapper : MapperBase<MyEntity, MyEntityDto>
{
    public override partial MyEntityDto Map(MyEntity source);
    public override partial void Map(MyEntity source, MyEntityDto destination);
}

Use TwoWayMapperBase<TSource, TDestination> for ReverseMap(), [MapperIgnoreTarget] for Ignore(...), [MapProperty] for ForMember(...), and override AfterMap for custom logic. Doc: https://abp.io/docs/10.4/release-info/migration-guides/AutoMapper-To-Mapperly

Whichever option you pick, do this after the version upgrade is stable, not during.

3. WithNavigationProperties DTOs / LookupDto / "invalid navigation display properties"

Two things to clarify first:

  • Suite v10 supports both AutoMapper and Mapperly templates. It detects which one to use by scanning the target project for existing files: if *AutoMapperProfile.cs exists it stays on AutoMapper; if *Mappers.cs exists it switches to Mapperly; if both exist, AutoMapper wins. The AutoMapper templates themselves were not significantly changed between v9 and v10, so if you stay on AutoMapper, regenerated code looks essentially the same as in v9.
  • The Mapperly templates emit a different shape per entity (e.g. {Entity}WithNavigationPropertiesTo{Entity}WithNavigationPropertiesDtoMapper, a {NavEntity}ToLookupDto{Type}Mapper per navigation, child-collection helpers, a CombinedAfterMap). These are the files that may collide with hand-edited mappings after a Mapperly switch.

How to fix the "invalid navigation display properties" compile errors:

  • In Suite, open each entity → for every Navigation Property and Navigation Collection, verify the Display Property field points to a property that still exists on the navigated entity. Suite emits destination.DisplayName = source.{DisplayProperty}; inside AfterMap, so a stale or wrong property name compiles into invalid code. This is the single most common cause of these specific errors after regeneration.
  • Don't keep both *AutoMapperProfile.cs and *Mappers.cs in the same project during the transition. Pick one provider per project and remove the other family of files, otherwise Suite picks AutoMapper but you may also have orphan Mapperly classes pulled in by namespace.
  • For manual / hand-written mappers, use file names that don't match Suite's search patterns (*AutoMapperProfile.cs, *ApplicationMapperlyMappers.cs, *ApplicationMappers.cs, *BlazorMappers.cs, *WebMappers.cs, *Mappers.cs). For example, MyEntityManualMappings.cs is safe — MyEntityMappers.cs is not.

4. ABP Suite compatibility with migrated v9 → v10 solutions

Suite v10 works against v10 solutions, but it is not a "v9 → v10 auto-migration" tool. For an upgraded solution:

  • Do not bulk-regenerate existing entities. The output will overwrite your manual edits and may mix v9-era code with v10 templates (especially around Blazor pages and DataGrid usage, which changed because of Blazorise 2.0 — see #6 below).
  • Regenerate only entities that genuinely need updates, one at a time, and diff the result before saving.
  • For new entities, Suite v10 works as expected.

5. Identity Pro DbContext

Two cumulative changes since 10.0 that you need to apply, in addition to other small EF migrations triggered by various ABP modules.

  • 10.1: IdentityUserPasswordHistory and IdentityUserPasskey are added under the Identity Pro module. Run Add-Migration and Update-Database after upgrading to 10.1.

  • 10.2: IIdentityProDbContext adds the UserInvitations DbSet. If your DbContext implements IIdentityProDbContext (the standard startup-template setup), add this property to your DbContext class so it satisfies the new interface member and is included in the model:

    public DbSet<UserInvitation> UserInvitations { get; set; }
    

    Then Add-Migration + Update-Database again.

Docs:

  • https://abp.io/docs/10.4/release-info/migration-guides/abp-10-1
  • https://abp.io/docs/10.4/release-info/migration-guides/abp-10-2

6. Blazorise breaking changes (these all land in ABP 10.2)

Width, CurrentPage, bind-* are all part of the Blazorise 2.0 upgrade that ABP 10.2 brings. The complete list (apply all of them) is here: https://abp.io/docs/10.4/release-info/migration-guides/blazorise-2-0-migration

Highlights:

  • Input renames: TextEdit → TextInput, DateEdit → DateInput, NumericEdit → NumericInput, etc.
  • Binding API unified to Value / ValueChanged:
    • @bind-Text, @bind-Checked, @bind-Date, @bind-SelectedValue → @bind-Value
  • DataGrid:
    • CurrentPage → Page on the native Blazorise DataGrid. Important: ABP's AbpExtensibleDataGrid still exposes CurrentPage — do not rename it there.

    • Column Width changed from string to fluent sizing:

      Width="Width.Px(30)"   // was Width="30px"
      Width="Width.Is50"     // was Width="50%"
      Width="Width.Is100"    // was Width="100%"
      
      // Dynamic string values (e.g. when Width comes from configuration or an entity):
      Width="@BlazoriseFluentSizingParse.Parse(column.Width)"
      

      For non-standard percentages (e.g. 33%) use BlazoriseFluentSizingParse.Parse("33%") — Blazorise's Width static helper does not have a generic Percent(...) method.

    • Inside DisplayTemplate, context.Property → context.Item.Property. The same rule applies to Clicked / Visible / ConfirmationMessage in DataGridEntityActionsColumn and DataGridCommandColumn.

  • Modal: Size and Centered move from <ModalContent> to <Modal>.
  • Dropdown: RightAligned="true" → EndAligned="true".
  • Autocomplete: MinLength → MinSearchLength.
  • Empty placeholder needs an explicit TValue: <SelectItem></SelectItem> → <SelectItem TValue="string"></SelectItem>.

7. Recommended upgrade order

  1. .NET 9 → .NET 10 first (update <TargetFramework>, global.json, install SDK 10).
  2. ABP 9.3.2 → 10.0, keep your existing mapping setup for now. Apply the 10.0 migration guide and create a new EF Core migration (OpenIddict 7 + InboxProcessor field changes + module entity changes). Get the solution compiling and running before touching anything else.
  3. 10.0 → 10.1: apply 10.1 migration guide, new EF migration for IdentityUserPasswordHistory / IdentityUserPasskey. Also update custom Swagger filters if any (Swashbuckle 10 + Microsoft.OpenApi 2 are not source-compatible).
  4. 10.1 → 10.2: this is the big UI step — apply the Blazorise 2.0 migration to your Blazor pages, add DbSet<UserInvitation> to your DbContext, create a new EF migration.
  5. 10.2 → 10.3: small changes, update PermissionIntegrationController clients if any (now HttpPost).
  6. 10.3 → 10.4: small changes; if you use email/SMS 2FA, re-test (codes are single-use now).
  7. Decide AutoMapper vs LuckyPenny AutoMapper vs Mapperly only after the version upgrade is fully stable.
  8. Regenerate Suite-managed code last, and only for entities that genuinely need it.

If you use MySQL, please check the 10.0 guide carefully — the EF Core MySQL providers have limited .NET 10 support at the moment.

Thanks

Hi,

Thanks for the SQL results — they actually rule out the most common cause I had in mind. Both test2 and all three grant rows have TenantId = NULL, so this is not a host/tenant isolation issue. The RequiredPermissionName is also NULL, so the check goes straight to the resource permission path. And the direct user grant U / 3b23e6ff-... matches test2's Id exactly, so on paper UserResourcePermissionValueProvider.CheckAsync should return Granted immediately.

The fact that it still fails means the mismatch is happening at runtime, not in the data. To pin it down I need two more things from you.

1) A HAR file of the failing request. Please sign test2 out, sign in fresh, reproduce the error once, then export a HAR from the Patient Portal browser and share it. Instructions are in the second answer of https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problems — DevTools → Network → right-click → "Save all as HAR with content". The HAR contains the full request (URL, headers including Authorization: Bearer ..., response body, status code), which lets me check three things at once: (a) which API host the call actually went to, (b) what the sub claim in the token is — it needs to equal 3b23e6ff-aa66-8029-6ca7-3a214c3d575f, otherwise the U grant won't match, and (c) the traceId in the response body, so we can match it against the API log. Signing out and back in before reproducing rules out a stale token / cached session.

2) API-side debug logs of one failed request. Please enable Debug-level logging on the API that's serving the failing request, reproduce the error once, and share the log around the failed call. The standard way to enable it is described here: https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problems — just bump your appsettings.json Serilog Default level to Debug, keep Microsoft.EntityFrameworkCore at Warning. The permission checker logs at Debug level which providers it tried and what each returned, so the log will show us exactly which value provider produced Undefined and why.

You can email the HAR and log files to me directly at liming.ma@volosoft.com — they tend to be large and may contain sensitive request data, so email is easier than attaching here.

One side question while you're at it: in your multi-portal setup, is the Patient Portal calling the same API host (same DB) as the admin portal where you opened the workspace permission modal? If they're separate services with separate databases (a common microservice setup), the grants you queried might live in a different DB than the one performing the check.

Thanks

Showing 441 to 450 of 12924 entries
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on November 02, 2026, 06:56
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.