Open Closed

How To: Create a Help Desk Ticket Entity #150


0
sean.alford created

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

  1. 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));
});
  1. 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.

  1. After creating the Ticket, I then click Actions -> Edit, set the Solution to "Turn on RTU."

  1. 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
    alper created
    Support Team

    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
    sean.alford created

    @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
    alper created
    Support Team

    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. Forexample BookDto is here src\Acme.BookStore.Application.Contracts\Books\BookDto.cs

  • 0
    sean.alford created

    @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
    alper created
    Support Team

    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 generates AppUserDto and it's being used in IAppUserAppService.cs Open your Visual Studio and search for AppUserDto in IAppUserAppService

    Task<AppUserDto> CreateAsync(AppUserCreateDto input);
    
  • 0
    sean.alford created

    @Alper,

    I did not create the AppUser entity. It is definded within the ABP.io app-pro template.

    I searched for the AppUserDto within the solution, but nothing was found. As you can see it is not defined in the Application.Contracts where you'd expect it to be.

  • 1
    alper created
    Support Team

    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. As AppUser entity comes from the template and has not DTO, we'll create the corresponding DTO class. I'll explain it based on the BookStore project (change all Acme.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>();
    

    Now we can pick this DTO from navigatin property modal

    Click the Save and Generate button


    And here's how it looks like

    finally you might need to add this localization to the en.json

    "AppUser":  "User" 
    
  • 0
    sean.alford created

    @alper, I created the AppUserDto and mapping like you suggested in the previous answer. However, I get this error when I try to generated the migration.

  • 1
    alper created
    Support Team

    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
    sean.alford created

    @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 a Decrement() method for this case? I'd vote for simply skipping that seq #, because providing a Decrement() 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
    alper created
    Support Team

    The questions are really out of ABP context. These are basic coding concepts which you can find many resource on the internet.

  • 0
    sean.alford created

    @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
    alper created
    Support Team

    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
    sean.alford created

    @alper thanks! This helps a lot! I was thinking a domain service was more complicated.

  • 0
    sean.alford created

    @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
    sean.alford created

    @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.

    AutoMapping

    **Migration Error **

  • 0
    hikalkan created
    Support Team

    If AppUser has ExtraProperties property and AppUserDto 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
    sean.alford created

    @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
    alper created
    Support Team

    for the migration error; this occurs, if you reference AppUser in another entity. EF Core tries to map IdentityUser and AppUser 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
    hikalkan created
    Support Team

    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
    sean.alford created

    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.

    1. When we create a support ticket we need to assign it to one of our Support Technicians.
  • 0
    hikalkan created
    Support Team

    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
    sean.alford created

    @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?

    Manually Added Indexs

    Manually Added FK

    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
    alper created
    Support Team

    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