Hi,
The fix is out — Volo.Abp.Elsa.SourceCode is now on our NuGet feed and ABP Studio recognizes the module. Please give it another try from Suite or via the CLI:
abp get-source Volo.Abp.Elsa
It should download the full Elsa source code now. Let me know if you hit any issues.
Thanks
Hi thanhvl1,
The link is a relative path because Applications["MVC"].RootUrl isn't set when the invite email is generated. Two cases:
Non-tiered solution (*.Web / *.Blazor.Server / *.Blazor.WebApp / *.HttpApi.HostWithIds) — the template already wires App:SelfUrl to Applications["MVC"].RootUrl. Just make sure App:SelfUrl in appsettings.json is a full URL with scheme + host, e.g. https://your-app.example.com.
Tiered solution (separate *.HttpApi.Host + *.AuthServer) — the InviteUsers API runs in *.HttpApi.Host, but its ConfigureUrls doesn't register Applications["MVC"].RootUrl. Add this one line in *.HttpApi.Host/*HttpApiHostModule.cs:
Configure<AppUrlOptions>(options =>
{
options.Applications["MVC"].RootUrl = configuration["AuthServer:Authority"];
// ...existing Angular registrations
});
The Account/InviteUser page is served by the AuthServer in a tiered solution, which is why we point to AuthServer:Authority. After this, the email link becomes absolute like https://your-authserver.example.com/Account/InviteUser?token=... and clickable.
We'll fix this in the project template in the next ABP version.
Thanks!
Hi,
Quick update: the framework fix we'll ship is simpler than what I described earlier. Instead of moving the picture download out of the unit of work, we just bound the outbound HTTP call with a short timeout. The default HttpClient.Timeout is 100 seconds, which is why the lock could be held that long — capping it to 5 seconds is enough to make the AbpUsers row lock release on its own, so AuthServer no longer needs to be restarted.
The patch adds a configurable option:
public class AbpAccountOptions
{
// ...
public TimeSpan ExternalProfilePictureDownloadTimeout { get; set; } = TimeSpan.FromSeconds(5);
}
And RegisterExternalUserAsync applies it to the HttpClient before GetStreamAsync. Profile pictures from external providers are normally tens of KB and finish in well under a second, so 5s leaves plenty of headroom for slow networks while preventing the lock from being held long enough to matter.
If you want to apply the same fix locally before the next release, two options:
Keep your current workaround (removing the picture ClaimActions mapping) — still valid and avoids the call entirely. Trade-off: no avatar auto-import.
If you want to keep importing the avatar, override RegisterModel.RegisterExternalUserAsync in your own MyRegisterModel : RegisterModel and copy the original method, adding one line right after HttpClientFactory.CreateClient():
var httpClient = HttpClientFactory.CreateClient();
httpClient.Timeout = TimeSpan.FromSeconds(5); // add this line
That alone makes the lock window predictable. With this in place, even when the external picture URL is slow or unreachable, the transaction commits within 5 seconds and the table is unlocked — no restart required.
Thanks
Hi,
Glad to hear abp logout / abp login fixed the blank Modules page.
About the Elsa Workflows source code download — this is on our side, not a license issue. We're working on the fix now and I'll reply here again once it's ready, so you can download it.
Thanks
Hi,
We reproduced this. The cause is in Volo.Abp.Account.Pro.Public.Web/Pages/Account/Register.cshtml.cs:
OnPostAsync is wrapped in [UnitOfWork], and inside RegisterExternalUserAsync it awaits httpClient.GetStreamAsync(picture) to download the external provider's profile picture. The default HttpClient has no timeout, so when the picture URL is slow or unreachable (e.g. https://graph.microsoft.com/v1.0/me/photo/$value returning 404 or being blocked by your network), the transaction stays open with the AbpUsers X lock held, and every other read on that table is blocked. In the MicroService template the AuthServer also connects directly to the identity database, so the lock affects both services. Restarting AuthServer kills the connection, the transaction rolls back, and the lock is released — that's why a restart "fixes" it.
Note: just lowering HttpClient.Timeout is not a full fix. The lock is still held until the HTTP call completes or times out, so the window only shrinks from ~100s to N seconds; concurrent reads are still blocked for N seconds on every external registration.
Workaround
Stop mapping the picture claim in your AuthServer module's ConfigureExternalProviders so the download path is skipped. Remove the picture-related ClaimActions for each external provider you use:
.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, options =>
{
// remove this line:
// options.ClaimActions.MapCustomJson("picture", _ => "https://graph.microsoft.com/v1.0/me/photo/$value");
options.SaveTokens = true;
})
.AddGoogle(GoogleDefaults.AuthenticationScheme, options =>
{
// remove this line:
// options.ClaimActions.MapJsonKey(AbpClaimTypes.Picture, "picture");
})
.AddTwitter(TwitterDefaults.AuthenticationScheme, options =>
{
// remove this line:
// options.ClaimActions.MapJsonKey(AbpClaimTypes.Picture, "profile_image_url_https");
options.RetrieveUserDetails = true;
});
With the picture claim no longer set, RegisterExternalUserAsync skips the HTTP download entirely (the if (!picture.IsNullOrWhiteSpace()) branch is not entered), so the UoW transaction commits immediately after the INSERT AbpUsers and the lock is released. AuthServer no longer needs to be restarted.
Trade-off: new external users won't have their avatar auto-imported from the provider; they can upload one later from the profile page.
We'll address this properly in an upcoming release by moving the picture download outside the UoW transaction so the avatar import keeps working without holding the lock.
Thanks
Hi,
First, a quick note on the broader question: you don't have to use ABP's image services at all. ABP is just ASP.NET Core under the hood, so any .NET imaging library you prefer — ImageSharp, Magick.NET, SkiaSharp, OpenCvSharp, or an external API — works fine. Register it in DI like any other service and call it from your application/domain service. The built-in Volo.Abp.Imaging.* packages are there as a convenience (with a pluggable provider model), not a requirement. Pick whatever fits your processing needs best.
That said, if you do go with ABP's services, the attribute-based example above works well for the simple "upload → compress/resize → save" case. For your scenario (compress + background removal + place on canvas), you'll likely want to chain steps inside an application service, so the cleaner approach is to inject the services directly.
IImageCompressor / IImageResizer in a serviceBoth are registered as transient by default — you can inject them anywhere and orchestrate the pipeline yourself:
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using Volo.Abp.Application.Services;
using Volo.Abp.BlobStoring;
using Volo.Abp.Content;
using Volo.Abp.Imaging;
public class UserImageAppService : ApplicationService
{
private readonly IImageCompressor _compressor;
private readonly IImageResizer _resizer;
private readonly IBlobContainer<UserImageContainer> _blob;
public UserImageAppService(
IImageCompressor compressor,
IImageResizer resizer,
IBlobContainer<UserImageContainer> blob)
{
_compressor = compressor;
_resizer = resizer;
_blob = blob;
}
public async Task UploadAsync(IRemoteStreamContent file)
{
// 1) resize
var resized = await _resizer.ResizeAsync(
file.GetStream(),
new ImageResizeArgs(width: 800, height: 800, mode: ImageResizeMode.Max),
file.ContentType);
// 2) compress
var compressed = await _compressor.CompressAsync(resized.Result, file.ContentType);
// 3) (your background-removal step here — see below)
// 4) (your canvas composition step here — see below)
await _blob.SaveAsync(file.FileName, compressed.Result, overrideExisting: true);
}
}
A few things worth knowing about the built-in image services:
image/jpeg, png, gif, bmp, tiff, webp. SVG/vector inputs are returned untouched (ImageProcessState.Unsupported), so handle that explicitly if your users may upload SVGs.[CompressImage] / [ResizeImage] attributes accept a params string[] of parameter names if your action has non-image arguments, e.g. [CompressImage("file")] will only touch the parameter named file.ImageSharpCompressOptions (DefaultQuality, JpegEncoder, PngEncoder, WebpEncoder) in your module's ConfigureServices.There's no built-in support, but two practical paths — both are just plain .NET integrations, ABP doesn't get in the way:
HttpClient with IHttpClientFactory and call something like remove.bg, Cloudflare Workers AI, Azure AI Vision, or Replicate. Put the API key in appsettings.json and bind via IOptions<T>.Microsoft.ML.OnnxRuntime. More setup, but no per-call cost and no data leaves your server.Either way, the call returns a PNG with transparent background — feed it into the canvas step below.
Since you're already pulling in ImageSharp via Volo.Abp.Imaging.ImageSharp, you can reuse it directly for composition — no extra dependency:
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
public async Task<byte[]> ComposeOnCanvasAsync(Stream foregroundPng, int canvasWidth, int canvasHeight)
{
using var canvas = new Image<Rgba32>(canvasWidth, canvasHeight, Color.White);
using var fg = await Image.LoadAsync(foregroundPng);
fg.Mutate(x => x.Resize(new ResizeOptions
{
Size = new Size(canvasWidth, canvasHeight),
Mode = ResizeMode.Max
}));
var x = (canvasWidth - fg.Width) / 2;
var y = (canvasHeight - fg.Height) / 2;
canvas.Mutate(c => c.DrawImage(fg, new Point(x, y), 1f));
using var ms = new MemoryStream();
await canvas.SaveAsPngAsync(ms);
return ms.ToArray();
}
If the processing is heavy (large images, AI calls), wrap the whole pipeline in an ABP background job so the upload endpoint returns quickly and the user gets the final result asynchronously.
Docs: https://abp.io/docs/latest/framework/infrastructure/image-manipulation Sample: https://github.com/abpframework/abp-samples/tree/master/ImageManipulation
Thanks.
Hi,
Great to hear the filter is working — happy it landed cleanly.
About the menu/permission behaviour: looking at the code, Payment Plans and Payment Requests are defined as independent top-level permissions (not parent-child), and each menu item requires only its own permission, so unchecking Payment Plans alone shouldn't hide Payment Requests. Could you share the exact steps you took in the permission modal (which checkboxes you toggled, and whether you used the "Select all in this tab" checkbox at the top)? That'll help me reproduce it on my side and figure out what's actually going on.
Thanks again for the detailed feedback.
Hi,
Glad the column part already works. For filtering by an extension property like PaymentReference there's one extra catch worth knowing: the generated client proxy for paymentRequestAdmin.getList hardcodes the query-string field list to the props of PaymentRequestGetListInput (filter / creationDateMax / creationDateMin / paymentType / status / sorting / skipCount / maxResultCount). So if you just add input.paymentReference = ... in getFilter(), that field gets silently dropped on the way out.
The pattern that works (verified end-to-end on a fresh ABP 10.4 MVC + SQL Server demo, with the same Object Extension you have):
ajax: abp.libs.datatables.createAjax((input, ajaxParams) => {
var pr = $("#inputPaymentReference").val();
if (pr) {
ajaxParams = $.extend(true, {}, ajaxParams, { data: { PaymentReference: pr } });
}
return service.getList(getFilter(input), ajaxParams);
}),
ajaxParams.data ends up appended to the URL on a GET, so the server sees ?...&PaymentReference=xxx. No need to touch the client proxy.
Index.cshtml:<abp-column size-lg="_6" size-md="_12">
<label for="inputPaymentReference" class="form-label">Payment Reference</label>
<input type="text" id="inputPaymentReference" class="form-control" />
</abp-column>
The existing $("#PaymentRequestFilterForm input,select").change(...) handler in index.js will pick it up and reload the table.
MapEfCoreProperty<PaymentRequest, string>("PaymentReference", ...)) creates a real column, you can filter at the SQL level with EF.Property<string>:[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IPaymentRequestAdminAppService))]
public class MyPaymentRequestAdminAppService : PaymentRequestAdminAppService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IRepository<PaymentRequest, Guid> _repo;
public MyPaymentRequestAdminAppService(
IPaymentRequestRepository paymentRequestRepository,
IRepository<PaymentRequest, Guid> repo,
IHttpContextAccessor httpContextAccessor)
: base(paymentRequestRepository)
{
_repo = repo;
_httpContextAccessor = httpContextAccessor;
}
public override async Task<PagedResultDto<PaymentRequestWithDetailsDto>> GetListAsync(PaymentRequestGetListInput input)
{
var pr = _httpContextAccessor.HttpContext?.Request.Query["PaymentReference"].ToString();
if (string.IsNullOrWhiteSpace(pr))
{
return await base.GetListAsync(input);
}
var query = (await _repo.WithDetailsAsync())
.WhereIf(!input.Filter.IsNullOrWhiteSpace(),
p => p.Currency.Contains(input.Filter) || p.Gateway.Contains(input.Filter) || p.ExternalSubscriptionId == input.Filter)
.WhereIf(input.CreationDateMax != null, p => p.CreationTime <= input.CreationDateMax)
.WhereIf(input.CreationDateMin != null, p => p.CreationTime >= input.CreationDateMin)
.WhereIf(input.PaymentType != null, p => p.Products.Any(x => x.PaymentType == input.PaymentType))
.WhereIf(input.Status != null, p => p.State == input.Status)
.Where(p => EF.Property<string>(p, "PaymentReference").Contains(pr));
var totalCount = await query.CountAsync();
var entities = await query
.OrderBy(input.Sorting.IsNullOrEmpty() ? nameof(PaymentRequest.CreationTime) + " desc" : input.Sorting)
.Skip(input.SkipCount).Take(input.MaxResultCount)
.ToListAsync();
return new PagedResultDto<PaymentRequestWithDetailsDto>(
totalCount,
ObjectMapper.Map<List<PaymentRequest>, List<PaymentRequestWithDetailsDto>>(entities));
}
}
Re: the source download — yes, full module source is not part of the Team licence, and the assemblies are obfuscated so extracting embedded resources from the dll won't work either. For this case the only files you actually need to override are Pages/Payment/Requests/index.js (and Index.cshtml). Drop me an email at liming.ma@volosoft.com and I'll send those two files to you so you have a clean starting point.
Thanks.
Great. If you run into any other issues, please feel free to let us know.