Hi,
This popup comes from the final dotnet ef migrations add step. Suite generates the entity files first, then runs that command. dotnet ef builds the startup project and its referenced projects before adding the migration, and that build is failing — so the files are there but no migration is created. It's the same failure stage as in your question #10822, but the popup doesn't include the underlying build errors.
Could you run the following from the solution root and share the logs.txt file? It will contain the build errors the popup omits.
dotnet build > logs.txt
Please hold off on generating more entities until we've checked that output, since Suite writes the files before it reaches the migration step.
If the file is too large for this thread, you can send it to liming.ma@volosoft.com.
Thanks
Hi,
This is a bug in Suite's navigation property edit dialog. Opening an existing navigation property resets the "Property name", entity set name and DTO name fields back to the defaults derived from the target entity name, so PartnerId becomes CustomerId in the form. The metadata itself is only overwritten when you confirm the dialog with OK and then save the entity — after that, the next generation renames everything to Customer. Clicking Cancel leaves the metadata unchanged. It's not related to the framework or Studio upgrade.
To repair your solution, set these five fields back in every affected navigation-property entry under .suite/entities — including the entries in the child entity JSON files — and regenerate:
{
"Name": "PartnerId",
"ReferencePropertyName": "Partner",
"EntitySetName": "Partners",
"EntityNameWithDuplicationNumber": "Partner",
"EntitySetNameWithDuplicationNumber": "Partners"
}
If you created or applied an EF Core migration while the metadata contained the reset names, the JSON repair does not revert that migration — check those migrations separately.
The name-reset bug is still present in 10.6.0; no released Suite version contains the fix yet. We've fixed it and it will be included in the upcoming patch releases. Until then, re-check the custom names in that dialog before clicking OK (Cancel is fine).
One more note for your Angular app: on 10.3/10.4 the generated Angular lookup methods and URLs don't match the backend routes for renamed navigation properties (customer-lookup vs partner-lookup). Upgrading to 10.6.0 fixes this Angular part (not the name-reset bug above).
Thanks
Hi,
ABP Suite doesn't currently provide precision/scale options for decimal properties. I'll share this with the Suite team as a feature request.
About your questions: Suite updates the existing DbContext file in place instead of regenerating it from a template, so your loop is preserved by the current entity generator — but newly generated model configuration can be added after it, and there is no protected custom-code marker inside the generated DbContext. So I don't recommend relying on a loop at the end of OnModelCreating.
Use EF Core's ConfigureConventions hook on the same DbContext instead. A partial class or a custom IModelCustomizer isn't needed:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
base.ConfigureConventions(configurationBuilder);
configurationBuilder.Properties<decimal>().HavePrecision(18, 6);
}
Suite's entity generation doesn't add or remove code inside this method, so the override is preserved when entities are regenerated.
This sets decimal(18,6) as the default for all decimal and decimal? properties in this DbContext. Explicit per-property HasPrecision(...) or HasColumnType(...) configuration still takes precedence.
Thanks
Hi,
These build errors are a separate topic from this cross-module question, so it's better to open a new question for them — we'll dig into it properly there.
A quick pointer in the meantime: L is the localizer, and in a Blazor module it comes from the module's component base (something like OperationRequestComponentBase, which inherits AbpComponentBase and sets the LocalizationResource). Suite's generated pages @inherits that base to get L. A freshly created module has it and compiles fine, so those CS0103: 'L' errors point to that base being missing or not matching in your OperationRequest.Blazor project.
When you open the new question, please include:
OperationRequest.Blazor has a ...ComponentBase class that inherits AbpComponentBase — its class name and namespace.@inherits ... line from one of the failing generated .razor files._Imports.razor of that project.With those we can pinpoint why L isn't resolving.
Thanks
Hi,
Thanks, that makes it clear. The key point: Suite's standard cross-module selector only offers aggregate roots. As long as Department stays a child of Company, the request won't get a Suite-generated reference to it through that selector — that part has to be wired by hand. So it comes down to keeping the master-detail (and wiring this one reference yourself) or making Department a first-class entity.
Keeping the master-detail (recommended for your case):
You can keep Department as a child and still let a request pick one. A clean shape:
Company the normal Suite way (you get the company picker and a real FK), and add a plain DepartmentId (Guid) property for the chosen department.Departments CRUD permission just to create a request.Application.Contracts (add the module/package reference), not by touching its repository or DbContext.*.Extended.cs / customizable-code files so re-generating the entity doesn't overwrite it.Things to keep in mind on this path:
DepartmentId property (only a navigation property does). If you want DB-level enforcement you'd add the FK yourself via a migration.Company → Department mapping uses cascade, but that's for physical deletes — audited entities soft-delete by default, so a deleted department is just hidden by the data filter, and an existing request's DepartmentId would then resolve to nothing. Decide how to handle it (e.g. block deleting a department that's in use, or store the department name on the request for historical display).Technically that DepartmentId points at an entity living inside the Company aggregate, so it's a pragmatic reference rather than a standard aggregate-root reference — Master Data stays responsible for validating and resolving it.
Alternative — make Department an aggregate root:
Then the request references it the normal Suite way (auto-generated picker and relationship). You'd keep the "manage departments under a company" experience with a custom grid filtered by CompanyId on the Company page — that's custom UI, not Suite's master-detail feature. More setup up front, but cleaner if Department ends up referenced from more places than just requests.
Thanks
Hi,
You're right, and no — updating LastSignInTime should not fail an otherwise valid login. This is a bug in the password-grant token flow: concurrent successful logins update LastSignInTime on the same user, and the resulting concurrency conflict can fail otherwise valid token requests. We'll fix this in the framework.
Until the fix is released, you can override the TokenController in your AuthServer project so a concurrency conflict while updating the timestamp does not fail the token request. The method body is copied from the base implementation; only the last part (the LastSignInTime update) is changed to run in a separate unit of work with a freshly loaded user. Replace the namespace with your AuthServer project's namespace:
using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Identity;
using Volo.Abp.OpenIddict;
using Volo.Abp.OpenIddict.Controllers;
using Volo.Abp.Security.Claims;
using Volo.Abp.Uow;
namespace MyCompanyName.MyProjectName;
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(TokenController))]
public class MyTokenController : TokenController
{
protected override async Task<IActionResult> SetSuccessResultAsync(OpenIddictRequest request, IdentityUser user)
{
// Clear the dynamic claims cache.
await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId);
// Create a new ClaimsPrincipal containing the claims that
// will be used to create an id_token, a token or a code.
var principal = await SignInManager.CreateUserPrincipalAsync(user);
var rememberMe = request.GetParameter("RememberMe").ToString();
if (!rememberMe.IsNullOrWhiteSpace() && bool.TryParse(rememberMe, out var rememberMeValue) && rememberMeValue)
{
var claim = new Claim(AbpClaimTypes.RememberMe, true.ToString()).SetDestinations(OpenIddictConstants.Destinations.AccessToken);
principal.Identities.FirstOrDefault()?.AddClaim(claim);
}
principal.SetScopes(request.GetScopes());
principal.SetResources(await GetResourcesAsync(request.GetScopes()));
await OpenIddictClaimsPrincipalManager.HandleAsync(request, principal);
await IdentitySecurityLogManager.SaveAsync(
new IdentitySecurityLogContext
{
Identity = OpenIddictSecurityLogIdentityConsts.OpenIddict,
Action = OpenIddictSecurityLogActionConsts.LoginSucceeded,
UserName = request.Username,
ClientId = request.ClientId
}
);
try
{
// Update the last sign-in time in a separate unit of work with a freshly
// loaded user, so a concurrency conflict can't fail the token request.
using (var uow = UnitOfWorkManager.Begin(new AbpUnitOfWorkOptions(), requiresNew: true))
{
var freshUser = await UserManager.GetByIdAsync(user.Id);
freshUser.SetLastSignInTime(DateTimeOffset.UtcNow);
var result = await UserManager.UpdateAsync(freshUser);
if (result.Succeeded)
{
await uow.CompleteAsync();
}
}
}
catch (AbpDbConcurrencyException)
{
// Ignore a concurrency conflict in the timestamp update.
}
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
}
With this override, concurrent valid password-grant requests are no longer failed by the LastSignInTime concurrency conflict. The request that loses the timestamp race skips that update and can still receive its token.
Once a framework version containing the fix is released, you can remove the override.
Thanks
Hi,
You couldn't find it because the AbpLicenseCode is not shown on the My Organizations page. It is generated for your organization on the fly: when you create a Pro solution with the CLI / ABP Studio while logged in with a developer account, it is written into the generated appsettings.secrets.json file.
The license code is only produced when the logged-in account is set as a developer of your organization and the license is active. If the developer assignment was done after your last login, you need to sign in again:
abp login <username>
Then create a new Pro solution with abp new and check the appsettings.secrets.json again. You can copy the value into your other projects of the same organization.
If it is still missing or empty after re-login, send us your abp cli version (abp --version) and the exact abp new command you used, so we can check whether it's an account issue or a template/CLI issue.
Thanks
Hi,
The value you used is the wrong one. The API Key on the My Organizations page is used for commercial package access (NuGet feed), it is not the AbpLicenseCode. That's why the license check rejects it with ABP-LIC-0017 and the app stops at InitializeApplicationAsync.
The AbpLicenseCode is a long Base64 string generated for your organization. It is not shown on the My Organizations page; the CLI / ABP Studio obtains it from the account service when you create a Pro solution while logged in, and writes it into the generated appsettings.secrets.json file.
So you can copy it from the appsettings.secrets.json of any Pro solution created for your organization:
{
"AbpLicenseCode": "<copy the value from your solution's appsettings.secrets.json>"
}
If you don't have such a solution yet, run abp login <username> and create one with abp new, then take the value from the generated appsettings.secrets.json and put it into the Volo.Chat.Blazor.Server.Host settings.
Thanks
Hi,
Both are possible, but neither is built-in, so you need a small amount of custom code on the server side. Since your UI is a custom React app, you already own the client part; here is the server part.
The chat module's Message entity supports extra properties, so you can link a file to a message without modifying the module:
Upload the file (or the pasted screenshot blob) from React first and get a file id back. Chat users usually don't have the file management permissions, so wrap your own upload endpoint around IFileManager.CreateAsync (pass tenantId: CurrentTenant.Id and overrideExisting: false) and apply your own authorization, size/MIME and storage quota checks there. Use unique file names (e.g. a GUID prefix) — pasted screenshots tend to reuse names like image.png, and uploading the same name into the same directory with overrideExisting replaces the content of previously sent attachments.
Send the message through your own application service. It keeps the chat module's permission checks, verifies the sender owns the attached file, and saves the attachment id in the same transaction:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.Authorization;
using Volo.Abp.Data;
using Volo.Abp.Features;
using Volo.Abp.PermissionManagement;
using Volo.Abp.Uow;
using Volo.Abp.Users;
using Volo.Abp.Validation;
using Volo.Chat;
using Volo.Chat.Authorization;
using Volo.Chat.Conversations;
using Volo.Chat.Messages;
using Volo.Chat.Users;
using Volo.FileManagement.Files;
namespace MyCompanyName.MyProjectName.Chat;
public class SendMessageWithAttachmentInput
{
public Guid TargetUserId { get; set; }
[Required]
[DynamicStringLength(typeof(ChatMessageConsts), nameof(ChatMessageConsts.MaxTextLength), nameof(ChatMessageConsts.MinTextLength))]
public string Message { get; set; }
public Guid? AttachmentFileId { get; set; }
}
[RequiresFeature(ChatFeatures.Enable)]
[Authorize(ChatPermissions.Messaging)]
public class ChatAttachmentAppService : ApplicationService
{
private readonly MessagingManager _messagingManager;
private readonly IChatUserLookupService _chatUserLookupService;
private readonly IMessageRepository _messageRepository;
private readonly IRealTimeChatMessageSender _realTimeChatMessageSender;
private readonly IPermissionFinder _permissionFinder;
private readonly IFileDescriptorRepository _fileDescriptorRepository;
public ChatAttachmentAppService(
MessagingManager messagingManager,
IChatUserLookupService chatUserLookupService,
IMessageRepository messageRepository,
IRealTimeChatMessageSender realTimeChatMessageSender,
IPermissionFinder permissionFinder,
IFileDescriptorRepository fileDescriptorRepository)
{
_messagingManager = messagingManager;
_chatUserLookupService = chatUserLookupService;
_messageRepository = messageRepository;
_realTimeChatMessageSender = realTimeChatMessageSender;
_permissionFinder = permissionFinder;
_fileDescriptorRepository = fileDescriptorRepository;
}
public virtual async Task<Guid> SendMessageAsync(SendMessageWithAttachmentInput input)
{
var targetUser = await _chatUserLookupService.FindByIdAsync(input.TargetUserId);
if (targetUser == null)
{
throw new BusinessException("Volo.Chat:010002");
}
if (!await _permissionFinder.IsGrantedAsync(targetUser.Id, ChatPermissions.Messaging))
{
throw new BusinessException("Volo.Chat:010004");
}
if (!await AuthorizationService.IsGrantedAsync(ChatPermissions.Searching) &&
!await _messagingManager.HasConversationAsync(targetUser.Id))
{
throw new AbpAuthorizationException(code: AbpAuthorizationErrorCodes.GivenRequirementHasNotGrantedForGivenResource);
}
if (input.AttachmentFileId.HasValue)
{
// Only allow attaching files uploaded by the sender.
var file = await _fileDescriptorRepository.GetAsync(input.AttachmentFileId.Value);
if (file.CreatorId != CurrentUser.GetId())
{
throw new AbpAuthorizationException(code: AbpAuthorizationErrorCodes.GivenRequirementHasNotGrantedForGivenResource);
}
}
Message message;
using (var uow = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: true))
{
message = await _messagingManager.CreateNewMessage(
CurrentUser.GetId(),
targetUser.Id,
input.Message
);
if (input.AttachmentFileId.HasValue)
{
message.SetProperty("AttachmentFileId", input.AttachmentFileId.Value);
await _messageRepository.UpdateAsync(message);
}
await uow.CompleteAsync();
}
var senderUser = await _chatUserLookupService.FindByIdAsync(CurrentUser.GetId());
await _realTimeChatMessageSender.SendAsync(
targetUser.Id,
new ChatMessageRdto
{
Id = message.Id,
SenderName = senderUser.Name,
SenderSurname = senderUser.Surname,
SenderUserId = senderUser.Id,
SenderUsername = senderUser.UserName,
Text = input.Message
}
);
return message.Id;
}
public virtual async Task<List<ChatMessageWithAttachmentDto>> GetConversationAsync(GetConversationInput input)
{
var messages = await _messagingManager.ReadMessagesAsync(input.TargetUserId, input.SkipCount, input.MaxResultCount);
return messages.Select(x => new ChatMessageWithAttachmentDto
{
Id = x.Message.Id,
Message = x.Message.Text,
MessageDate = x.Message.CreationTime,
Side = x.UserMessage.Side,
AttachmentFileId = x.Message.GetProperty<Guid?>("AttachmentFileId")
}).ToList();
}
}
public class ChatMessageWithAttachmentDto
{
public Guid Id { get; set; }
public string Message { get; set; }
public DateTime MessageDate { get; set; }
public ChatMessageSide Side { get; set; }
public Guid? AttachmentFileId { get; set; }
}
For real-time updates, keep using the module's built-in ReceiveMessage SignalR event as a "new message" notification on the React side, then call your GetConversationAsync endpoint to load the messages including the attachment ids. This way you don't need a second real-time DTO. Note that ReceiveMessage only goes to the receiver; the sender updates its own view after SendMessageAsync returns.
For downloading, the chat receiver usually doesn't have the file management permissions, so expose your own download endpoint: check that the current user belongs to the conversation and the file is attached to a message in it, then return the file content.
A few notes:
ChatMessageAttachment { MessageId, FileDescriptorId }) instead of the single extra property.The file management module is a plain directory/file tree; there is no built-in "attach file to entity" concept. The standard approach is a link entity in your own code:
using System;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
namespace MyCompanyName.MyProjectName.Companies;
public class CompanyDocument : AuditedAggregateRoot<Guid>, IMultiTenant
{
public virtual Guid? TenantId { get; protected set; }
public virtual Guid CompanyId { get; protected set; }
public virtual Guid FileDescriptorId { get; protected set; }
public virtual string DocumentType { get; set; }
protected CompanyDocument()
{
}
public CompanyDocument(Guid id, Guid companyId, Guid fileDescriptorId, Guid? tenantId)
: base(id)
{
CompanyId = companyId;
FileDescriptorId = fileDescriptorId;
TenantId = tenantId;
}
}
Store only the FileDescriptorId (don't add EF navigations to the file management entities), check the current user's access to both the company and the file in your endpoints (when creating a link, but also when listing/downloading/deleting — the file management APIs only check their own permissions, they don't know about the company), and manage the lifecycle in your own code, e.g. delete the file when the last CompanyDocument referencing it is deleted. If you don't need the file management UI at all, you can also skip the module and store the files directly with the BLOB storing system (IBlobContainer), keeping the blob name on your entity: https://abp.io/docs/latest/framework/infrastructure/blob-storing
An alternative is one directory per company (created via IDirectoryDescriptorAppService.CreateAsync), keeping the directory id on the Company. This lets you reuse the file management APIs directly, but note that deleting a directory recursively deletes all files in it.
Thanks
Hi,
Thanks — that settles it. Since a transaction has to save the department (raised per company + department), the department is referenced on its own from another module, so it should be an aggregate root rather than a child of Company. Child entities are meant to be reached only through their aggregate root, so referencing one directly from another module is what you want to avoid.
A few practical points for the change:
Department from Child to Master in the UI. You'll recreate it as a Master entity with an aggregate-root base class (AggregateRoot<Guid>, AuditedAggregateRoot<Guid>, or FullAuditedAggregateRoot<Guid>), remove the inline department management from the Company page, and check the generated migration so you don't drop existing Department data.Department, add Company as a navigation property (this generates the CompanyId and the relationship) and give it a string property like Name — the cross-module picker needs a string for the display field. It'll then show up for the transaction just like Company does; the module dependency and .suite/module-entity-extension.json you set up for Company already cover it.Department when its Company is deleted.CompanyId and DepartmentId, validate on the server side (in the app service / domain, not only in the Blazor UI) that the department belongs to the company — the generated code only null-checks the ids. If the transaction doesn't need the company on its own, storing just DepartmentId avoids the duplicate state.Thanks