fullstackhero / blazor-starter-kit

Clean Architecture Template for Blazor WebAssembly Built with MudBlazor Components.
MIT License
3.45k stars 726 forks source link

How to connect user to an entity. i.e. Products listing show which user (name, firstname etc) created the product? #326

Open squashleague opened 2 years ago

squashleague commented 2 years ago

Describe your documentation need There should be some documentation on how to connect entities to the BlazorHeroUser

Requirement

  1. How to connect an entity to a user
  2. Being able to list the user who created the product in the Product listing page

Expected behavior An example of how the product listing page could potentially show the user (username, full name etc) who created the product

It seems like a common task that perhaps should have some guidence? In my particular case I have a an entity with an owned entity so I'm not sure how to follow the ChatHistory example totally

Similar questions have been asked before

239

248

261

MomboMan commented 2 years ago

All this information is in the database table dbo.AuditTrails (assuming your class inherits AuditableEntity which Brands does.) The table contains the table changed, the DateTime, Old Values, New Values and who changed or updated it. You should be able to dig out what you need.

Craig

dotnetshadow commented 2 years ago

@MomboMan I think the main issue is that currently the CreatedBy stores the user id, the dbo.AuditTrails have the info, the problem is that if you want to link that to the users name, first name, last name how would you go about it?

I don't think you should be looking up dbo.AuditTrails in order to get the username in the product listing page

How would you populate the username here?

Product Listing Name Price User
Product1 $100 User1
Product2 $60 User2
Product3 $70 User1
fretje commented 2 years ago

When you have a user id, you can simply fetch the user details using the IUserService. There's a GetAsync method there which returns a UserResponse which has all the data you need.

MomboMan commented 2 years ago

I don't think you should be looking up dbo.AuditTrails in order to get the username in the product listing page

If your trying to find out who Created the brand then you're going to be digging in the AuditTrails table anyway, yes?

This is my code in AuditTrails.razor.cs. I'm not just showing what changed, I'm wanting to show who changed what*:

        private async Task GetDataAsync()
        {
            var users = await UserManager.GetAllAsync();
            if (users.Succeeded) {
                var response = await AuditManager.GetCurrentUserTrailsAsync();
                if (response.Succeeded) {
                    var query = from evnt in response.Data
                                join user in users.Data on evnt.UserId equals user.Id
                                select ( new RelatedAuditTrail {
                                    AffectedColumns = evnt.AffectedColumns,
                                    DateTime = evnt.DateTime,
                                    Id = evnt.Id,
                                    NewValues = evnt.NewValues,
                                    OldValues = evnt.OldValues,
                                    PrimaryKey = evnt.PrimaryKey,
                                    TableName = evnt.TableName,
                                    Type = evnt.Type,
                                    UserName = user.UserName,
                                    LocalTime = DateTime.SpecifyKind(evnt.DateTime, DateTimeKind.Utc).ToLocalTime()
                                });
                    Trails = query.ToList();
                }
                else {
                    foreach (var message in response.Messages) {
                        _snackBar.Add(message, Severity.Error);
                    }
                }

            }
        }

Then the code from AuditTrails.razor:

        <RowTemplate>
            <MudTd DataLabel="Id">@context.Id</MudTd>
            <MudTd DataLabel="Name">
                <MudHighlighter Text="@context.TableName" HighlightedText="@_searchString" />
            </MudTd>
            <MudTd DataLabel="Date">
                <MudItem>
                    <MudChip Icon="@Icons.Material.Filled.Watch" IconColor="Color.Secondary" Label="true" Color="Color.Surface">@_localizer["Local"] : @context.LocalTime.ToString("G", CultureInfo.CurrentCulture)</MudChip>
                </MudItem>
                <MudItem>
                     <MudChip Icon="@Icons.Material.Filled.Watch" IconColor="Color.Secondary" Label="true" Color="Color.Surface">@_localizer["User"] : @context.UserName</MudChip>**
                </MudItem>
            </MudTd>
            <MudTd DataLabel="Tax">@context.Type</MudTd>
            <MudTd Style="text-align:right">
                <MudButton Variant="Variant.Filled" DisableElevation="true" EndIcon="@Icons.Filled.KeyboardArrowDown" IconColor="Color.Secondary" OnClick="@(() => ShowBtnPress(context.Id))">@((context.ShowDetails == true)? _localizer["Hide"] : _localizer["Show"]) @_localizer["Trail Details"]</MudButton>
            </MudTd>
        </RowTemplate>

Lastly this is the inherited class, RelatedAuditTrail, mentioned above in AuditTrails.razor.cs:

        public class RelatedAuditTrail : AuditResponse
        {
            public bool ShowDetails { get; set; } = false;
            public DateTime LocalTime { get; set; }
            public string UserName { get; set; }
        }

*I still have to change the AuditService to get all trails and not just the current user.

Craig

dotnetshadow commented 2 years ago

When you have a user id, you can simply fetch the user details using the IUserService. There's a GetAsync method there which returns a UserResponse which has all the data you need.

@fretje cheers for the repsonse, the problem I see is that if you are are showing a listing page you might have 50 records so for each userid you might be doing 50 requests GetAsync just to get the username

dotnetshadow commented 2 years ago

If your trying to find out who Created the brand then you're going to be digging in the AuditTrails table anyway, yes?

@MomboMan I do understand that the information is available in the AuditTrails and I can get the user information.

I think to simplify things I'll explain it this way. What I want to achieve is for the Products Page.

Can you show me how you would modify the Pages/Catalog/Products.razor file so that you can show 1 extra column username, see the following example. How can the username be populated?

<MudTd DataLabel="Name">
                <MudHighlighter Text="@context.Name" HighlightedText="@_searchString" />
            </MudTd>
            <MudTd DataLabel="Brand">
                <MudHighlighter Text="@context.Brand" HighlightedText="@_searchString" />
            </MudTd>
            <MudTd DataLabel="Description">
                <MudHighlighter Text="@context.Description" HighlightedText="@_searchString" />
            </MudTd>
            <MudTd DataLabel="Barcode">
                <MudHighlighter Text="@context.Barcode" HighlightedText="@_searchString" />
            </MudTd>
            <MudTd DataLabel="Username">
                @context.Username <----------------------------------------------------
            </MudTd>

Possible Solution One way I was thinking was to introduce my own Users table that links to the Identity user table via the Id. Then when a user registers you populate both tables. Throughout the project I just care about my Users table for additional info. Not sure if it's the right way to go about things?

Appreciate your feedback

fretje commented 2 years ago

@fretje cheers for the repsonse, the problem I see is that if you are are showing a listing page you might have 50 records so for each userid you might be doing 50 requests GetAsync just to get the username

This is a case for caching I would think. Or otherwise fetch the whole list of users (GetAllAsync) and then map the userid's to the names... in that case it's only 2 requests: one to get the listing page and one to get all the users. You then kind of "join" those 2 together in code in stead of in sql.

dotnetshadow commented 2 years ago

@fretje cheers for the repsonse, the problem I see is that if you are are showing a listing page you might have 50 records so for each userid you might be doing 50 requests GetAsync just to get the username

This is a case for caching I would think. Or otherwise fetch the whole list of users (GetAllAsync) and then map the userid's to the names... in that case it's only 2 requests: one to get the listing page and one to get all the users. You then kind of "join" those 2 together in code in stead of in sql.

Yeah that's how I currently ended up doing it for now, but just thought there would be a better way to do it

MomboMan commented 2 years ago

@dotnetshadow

Reread the GetDataAsync() method above. It performs a join on the two tables and inserts the UserName into the new class RelatedAuditTrail.

You want something similar. So you would add this to Products.razor.cs:

        // Top of the file
        public List<ExtendedProductsPage> Extended= new();

        // bottom of file
        public class ExtendedProductsPage : GetAllPagedProductsResponse
        {
            public string UserName { get; set; }
        }

And also in Products.razor.cs change

private MudTable<GetAllPagedProductsResponse> _table;

To

private MudTable<ExtendedProductsPage> _table;

then in Products.razor

        <RowTemplate>
            <MudTd DataLabel="Id">@context.Id</MudTd>
            <MudTd DataLabel="Name">
                <MudHighlighter Text="@context.Name" HighlightedText="@_searchString" />
            </MudTd>
            <MudTd DataLabel="Brand">
                <MudHighlighter Text="@context.Brand" HighlightedText="@_searchString" />
            </MudTd>
            <MudTd DataLabel="Description">
                <MudHighlighter Text="@context.Description" HighlightedText="@_searchString" />
            </MudTd>
            <MudTd DataLabel="Barcode">
                <MudHighlighter Text="@context.Barcode" HighlightedText="@_searchString" />
            </MudTd>
            <MudTd DataLabel="User Name">
                <MudHighlighter Text="@context.UserName" HighlightedText="@_searchString" />
            </MudTd>
...

And then change the GetProductsAsync() into something close to the GetAllAsync() above. Replacing the Audit stuff with Products stuff. I'd write it but I would need to change too much of my code. You will need to get rid of the pagination stuff and write a GetAllAsync() method under the Application branch. (p.s. I've never got the pagination stuff to work properly.)

Or at least that should be close.

Craig

dotnetshadow commented 2 years ago

@MomboMan Awesome thanks, much appreciated. I can see that you are pretty much grabbing the list of users upfront then joining to it. A similar approach to @fretje. That was my original implementation even before adding to this discussion. Maybe @iammukeshm might have an alternative approach

ransems commented 2 years ago

@fretje cheers for the repsonse, the problem I see is that if you are are showing a listing page you might have 50 records so for each userid you might be doing 50 requests GetAsync just to get the username

This is a case for caching I would think. Or otherwise fetch the whole list of users (GetAllAsync) and then map the userid's to the names... in that case it's only 2 requests: one to get the listing page and one to get all the users. You then kind of "join" those 2 together in code in stead of in sql.

Yeah that's how I currently ended up doing it for now, but just thought there would be a better way to do it

Testing an implementation myself, and need that to work, and loading all users will not work at scale (we have over 100k users so far). ONe thought I had was to make the API call a ViewModel, with the with Viewmodel a complex view-model(s) call (joined entities) return the viewModel, with the link (name, email, etc of what you want returned for the user link...