Overview
I'm building a simple Help Desk application that will allow us to manager our support tickets and assign them to users who's belong to the Support Technician role. The Ticket entity is pretty simple.
Ticket : FullAuditedAggregateRoot
Properties
- Number (int) -> auto generated (read/only) identiy field
- Issue (string)
- Tags (string)
- Solution (string)
Number Property Modifications
- After creating the Ticket entity with ABP Suite, I added
.UseIdentityColumn(1000, 1);
to the Ticket's auto generated context model located here:HelpDeskDbContextModelCreatingExtensions.cs
builder.Entity<Ticket>(b =>
{
b.ToTable(HelpDeskConsts.DbTablePrefix + "Tickets", HelpDeskConsts.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.TenantId).HasColumnName(nameof(Ticket.TenantId));
b.Property(x => x.Number).HasColumnName(nameof(Ticket.Number)).UseIdentityColumn(1000, 1);
b.Property(x => x.Tags).HasColumnName(nameof(Ticket.Tags));
b.Property(x => x.Issue).HasColumnName(nameof(Ticket.Issue));
b.Property(x => x.Solution).HasColumnName(nameof(Ticket.Solution));
});
- Next, I removed the Number field from the create UI, and set the Number field to read only on the edit UI. This all works fine! I was able to create a new Ticket, and sure enough the Number field was auto generated to 1000 as expected.
- After creating the Ticket, I then click Actions -> Edit, set the Solution to "Turn on RTU."
- Next I clicked Save
Error
I suspect this error is caused when the UpdateAsync(Id, Ticket) tries to update the Number identiy field.
Question # 1
How do I correct this issue? Note this also happens when I try to delete the ticket.
I'm assuming I need to modify the TicketAppService Update/Delete
[Authorize(HelpDeskPermissions.Tickets.Edit)]
public virtual async Task<TicketDto> UpdateAsync(Guid id, TicketUpdateDto input)
{
var ticket = await _ticketRepository.GetAsync(id);
ObjectMapper.Map(input, ticket);
var updatedTicket = await _ticketRepository.UpdateAsync(ticket);
return ObjectMapper.Map<Ticket, TicketDto>(updatedTicket);
}
Support Technication (Owner) Navigation Link
Where is the AppUserDto located?
24 Answer(s)
-
0
do you have an Id property in your entity ? if so you also have number property which is identity. I think you are on the wrong way. I would not make number as identity column. Create a SequentialNumberRepository and keep the last number in a seperate table I'll not implement this manager but the signature can be
public interface ISequentialNumberManager : IDomainService { Task<int> GetNextAsync(string sequenceName); }
public class SequentialNumber : AggregateRoot<Guid>, IMultiTenant { public virtual Guid? TenantId { get; protected set; } public virtual string SequenceName { get; protected set; } public virtual int CurrentValue { get; protected set; } protected SequentialNumber() { } public SequentialNumber([NotNull]string sequenceName, Guid? tenantId = null) { SequenceName = Check.NotNull(sequenceName, nameof(sequenceName)); TenantId = tenantId; CurrentValue = 1; ConcurrencyStamp = Guid.NewGuid().ToString(); } public void Increment() { ++CurrentValue; } }
-
0
@Alper,
The Ticket entity does have an Id (Guid). It inherits from FullAuditedAggregateRoot. I guess I could use an int/long for the Id and display the Id on the UI as the Ticket #. However, the ticket numbers would not be sequancial across tenants so that wouldn't work.
To be honest I'm not following your proposed solution. Are you suggesting that I create a new entity/repo (SequentialNumber) just to store a Ticket Number? We do not need SequenceName, just a int/long. This seems like a lot of extra effort to provide this Ticket Number requirement. Especially since the RDBMS already provides this functionalitly.
Questions How are you guys storing the Question #? This question is #150. Where is that value storted? Is it the Question Id, a property of Question, or is it a navigation link that is stored in seperate table?
I need to create a Navigation Link to an application/tenant user. Where is the AppUserDto located?
-
0
we are using the same strategy. we have a table/collection that stores the
SequentialNumber
. This way, it's being DBMS agnostic. The DTO's generated via ABP Suite is located in*.Application.Contracts
project. ForexampleBookDto
is heresrc\Acme.BookStore.Application.Contracts\Books\BookDto.cs
-
0
@Alper, I cannot find the AppUserDto in the applications contrarcts project. The AppUsers model is in the domian project where you'd expect it to be.
-
0
If you created it via Suite, it creates the entity DTO files in
Acme.BookStore.Application.Contracts\EntityNamePlural\
But AppUser seems like database table name, not an entity name! Did you name the user entity AppUser? If you name it AppUser then Suite generatesAppUserDto
and it's being used inIAppUserAppService.cs
Open your Visual Studio and search forAppUserDto
inIAppUserAppService
Task<AppUserDto> CreateAsync(AppUserCreateDto input);
-
0
-
1
Hi Sean,
Ok! I see it now...
So let's do it step by step. First of all, ABP Suite asks you a
DTO
of the dependent entity. AsAppUser
entity comes from the template and has notDTO
, we'll create the correspondingDTO
class. I'll explain it based on the BookStore project (change allAcme.BookStore
placeholders to your actual project name)Create a new folder named Users in the root directory of Acme.BookStore.Application.Contracts project. Add the below
DTO
file:public class AppUserDto : IdentityUserDto { }
Now, we need to create a mapping for this new
DTO
. Go to BookStoreApplicationAutoMapperProfile.cs and add the below line:CreateMap<AppUser, AppUserDto>().Ignore(x => x.ExtraProperties);
Now we can pick this DTO from navigatin property modal
Click the Save and Generate button
finally you might need to add this localization to the en.json
"AppUser": "User"
-
0
-
1
AppUser.ExtraProperties
is a new property. And it's not mapped in your application. So you can basically ignore this property mapping. You did a mapping as below:CreateMap<AppUser, AppUserDto>();
change it
CreateMap<AppUser, AppUserDto>().Ignore(x => x.ExtraProperties);
-
0
@alper,
Can you please elaborate on your previeous SequentialNumberRepository post, and provide a working example? I'm sure this example would be useful for others, and I'll also include and expalin this techneqe in the Acme.HelpDesk tutorial.
How would you wire this up to a specific entity property
Ticket.Number
(i.e.javascript
,code-behind
)How would you handle the case when the entity insert fails? Would you simply skip that seq #, or should the
SequentialNumberRepository
provide aDecrement()
method for this case? I'd vote for simply skipping that seq #, because providing aDecrement()
option opens a whole new set of potential issues/problems.Finally, is it okay to make the target property an index field to ensure it's unique? (i.e.
b.HasIndex(x => new {x.TenantId , x.Number}).IsUnique(true);
)
-
0
The questions are really out of ABP context. These are basic coding concepts which you can find many resource on the internet.
-
0
@alper, it appears that @hikalkan has plans to provide documentation, and probably examples for domain services. I'm not sure why these questions would be out of context. Maybe a full working example is out of context, but I'm not sure where I'd find basic coding example for creating ABP domain services on the internet. I'll admit you guys are very smart and what seems basic to you may not be basic to us ABP newbies. Furthermore, we are willing to pay for professional services if that is required. Would you please have someone help us with this or send us a quote for professional services?
-
1
hi @sean, what I mean by "basic coding concepts", creating a repository and storing an incremental number in that repository is really not related with the framework.
We are trying to make full working samples to show you basics of the framework https://docs.abp.io/en/commercial/latest/samples/index. as new questions come, we will add more examples. On the other hand, we are not into claiming extra money to answer your questions. We are here to help you
I made a Gist to show you how to create sequential numbers. https://gist.github.com/ebicoglu/d7d4a05a20a5393b64f1ffcd17a5f52c
When you see it, there's no framework related code.. Creating a domain service is as simple as below :)
public class SequentialNumberManager : DomainService, ISequentialNumberManager
-
0
@alper thanks! This helps a lot! I was thinking a domain service was more complicated.
-
0
@Alper,
I created the following files based on the Gist you created. I'm assuming all of them with exception to
EfCoreSequentialNumberRepository
belong in the Domain project.I guess the next step is to add the following DBContext items create and delplay a new migration.
SupportDbContext.cs
public DbSet<SequentialNumber> SequentialNumbers { get; set; }
SupportDbContextModelCreatingExtensions.cs
builder.Entity<SequentialNumber>(b => { b.ToTable(SupportConsts.DbTablePrefix + "SequentialNumbers", SupportConsts.DbSchema); b.ConfigureByConvention(); b.Property(x => x.TenantId).HasColumnName(nameof(SequentialNumber.TenantId)); b.Property(x => x.SequenceName).HasColumnName(nameof(SequentialNumber.SequenceName)).IsRequired(); b.Property(x => x.CurrentValue).HasColumnName(nameof(SequentialNumber.CurrentValue)).IsRequired(); b.HasIndex(x => new { x.TenantId, x.SequenceName }).IsUnique().HasFilter(null); });
Since Domain services (implement IDomainService interface) are registered as transient automatically the next step is to inject an ISequentialNumberManager into the CreateModal.
private readonly ISequentialNumberManager _sequentialNumberManager; public CreateModalModel(ITicketAppService ticketAppService, ISequentialNumberManager sequentialNumberManager) { _tagAppService = ticketAppService; _sequentialNumberManager = sequentialNumberManager; }
Finally, we set the Ticket.Number in
OnPostAsync()
prior to posting.public async Task<IActionResult> OnPostAsync() { Ticket.Number = await _sequentialNumberManager.GetNextAsync("Ticket"); await _tagAppService.CreateAsync(Ticket); return NoContent(); }
Thanks for your amazing support on this one @Alper!
-
0
@alpher,
I followed your previous instructions, however when I try to add a migration I get the following error. I think I need to somehow ignore the ExtraProperties for the navigation LookupDtos.
-
0
If
AppUser
hasExtraProperties
property andAppUserDto
doesn't have this property, then AutoMapper throws such a validation error. So, this is an expected case. You can ignore it or create ExtraProperties in your DTO, based on your business requirement. -
0
@hikalkan, I am trying to ignore the AppUser ExtraProperties, but I'm still receiving this error. I think, it's because the Ticket entity has a navigation link to a AppUser.
However, I cannot ignore the ExtraProperties in the Dto
CreateMap<Ticket, TicketDto>().Ignore(x => x.Assignee.ExtraProperties);
because x (TicketDto) does not contain Assignee. -
0
for the migration error; this occurs, if you reference
AppUser
in another entity. EF Core tries to mapIdentityUser
andAppUser
to the same table (AbpUsers
). this is not possible in EF Core...See the following issue comment, there's an explanation for this case https://github.com/abpframework/abp/issues/3807#issuecomment-627694184
-
0
I suggest to not define navigation properties to the AppUser (in DDD, this is not a good practice). However, if you really want it, the explanation mentioned by @alper can be implemented.
-
0
Thanks guys!
@hikalkan, I am fairly new to DDD so please forgive me for my lack of knowledge. If a navigation properties to the AppUser is not good practice, then what is a best practice for this use case in DDD? Is there a better way to utilize the existing system user? I don't think creating and maintaining multiple user entities is very practical.
- When we create a support ticket we need to assign it to one of our Support Technicians.
-
0
what is a best practice for this use case in DDD
If you want to truely implement DDD, you can see my presentation: https://www.youtube.com/watch?v=Yx3Y3-GC9EE It also covers your question.
I don't think creating and maintaining multiple user entities is very practical.
Never do that. Just add "Guid UserId" (a reference) instead of "AppUser User" (navigation property). You then need to make join LINQ if you want to access a user related to a ticket.
However, it is still possible to add a navigation property, but you should understand the migration system that we've created. Unfortunately it is not easy to create such a modolar systems with EF Core, so it has some little difficulties to use. See this document as a reference: https://docs.abp.io/en/abp/latest/Entity-Framework-Core-Migrations
-
0
@hikalkan thanks!
I have watched your presentation a few time before. (You do a great job of explaining things in that presentation!) To be honest I didn't relazie the navigation properties in ABP Suite were actaully creating an EF naviagation property. I thought it was only creating and storing the Id. I used the Naviation Properties in ABP Suite simply for the modal picker. :)
So we really shouldn't ever use the ABP Suite Navigation Properties if we what to follow the DDD best practice. Right?
All we really wanted from the begining was to stort Guid Ids.
Is this approch OK? Use ABP Suite to create the properties for the Id's, and then manaully add HasIndex(), and then the FK into the generated migration?
Where can I find an example of adding a modal Picker for an Id field similar to the one that ABP Suite creates when you choose Modal in the NP UI? I've looked in the EasyCRM project but I don't see an example of a Modal picker.
-
0
if you don't want Suite create navigation property, it's easy to remove; edit this template
Server.Entity.Partials.NavigationPropertyDefinition.txt
and remove the marked row
I created a post about this topic https://community.abp.io/articles/how-to-add-the-user-entity-as-a-navigation-property-furp75ex