|
| 1 | +# Switching Between Organization Units |
| 2 | + |
| 3 | +In most companies, a user belongs to more than one organization. Also, in some applications, we need to filter the data shown depending on the logged-in user's organization. For such scenarios, allowing users to select one of the organizations they belong to is a good practice. |
| 4 | + |
| 5 | +For creating a custom data filter, you can check [https://aspnetboilerplate.com/Pages/Documents/Articles%5CHow-To%5Cadd-custom-data-filter-ef-core](https://aspnetboilerplate.com/Pages/Documents/Articles%5CHow-To%5Cadd-custom-data-filter-ef-core). |
| 6 | + |
| 7 | +In order to allow users to change their active organization unit, we can design a UI like the one below; |
| 8 | + |
| 9 | + |
| 10 | + |
| 11 | +By using this dropdown, a user can switch between organization units. |
| 12 | + |
| 13 | +## Implementing Organization Unit Switching |
| 14 | + |
| 15 | +In this section, we'll dive into the implementation details of enabling users to switch between different organizational units within an ASP.NET Zero project. |
| 16 | + |
| 17 | +### Creating Claims Principal for Organization Unit |
| 18 | + |
| 19 | +In this section, we create this functionality by overriding the **CreateAsync** function within the `UserClaimsPrincipalFactory` class. This modification ensures that we can access the organizational unit information for logged-in users. |
| 20 | + |
| 21 | +```csharp |
| 22 | +public override async Task<ClaimsPrincipal> CreateAsync(User user) |
| 23 | +{ |
| 24 | + var userWithOrgUnits = await GetUserWithOrganizationUnitsAsync(user.Id); |
| 25 | + |
| 26 | + var claim = await base.CreateAsync(userWithOrgUnits); |
| 27 | + |
| 28 | + string organizationUnitsJson = JsonSerializer.Serialize(userWithOrgUnits.OrganizationUnits.First()); |
| 29 | + |
| 30 | + claim.Identities.First().AddClaim(new Claim("Organization_Unit", organizationUnitsJson)); |
| 31 | + |
| 32 | + return claim; |
| 33 | +} |
| 34 | +``` |
| 35 | + |
| 36 | +We are overwriting the **CreateAsync** method of `UserClaimsPrincipalFactory` to store user's selected organization unit in claims principal. |
| 37 | + |
| 38 | +Within this context, we add the first element of the Organization Unit as a claim during user principal creation. |
| 39 | + |
| 40 | +```csharp |
| 41 | +private async Task<User> GetUserWithOrganizationUnitsAsync(long userId) |
| 42 | +{ |
| 43 | + return await UserManager.Users |
| 44 | + .Include(u => u.OrganizationUnits) |
| 45 | + .Where(x => x.Id == userId) |
| 46 | + .FirstOrDefaultAsync(); |
| 47 | +} |
| 48 | +``` |
| 49 | + |
| 50 | +Private `GetUserWithOrganizationUnitsAsync` method retrieves a user with user's associated organization units from the database. |
| 51 | + |
| 52 | +Now, we can use `Organization_Unit` claim as the currently selected organization unit for logged-in user and filter any data using its value. |
| 53 | + |
| 54 | +### Modifying the Organization Unit in Session |
| 55 | + |
| 56 | +Now, we must allow logged-in user to switch selected organization unit. To do that, we first need to add a new field to `AbpSession` by following this [document](https://aspnetboilerplate.com/Pages/Documents/Articles%5CHow-To%5Cadd-custom-session-field-aspnet-core). |
| 57 | + |
| 58 | +```csharp |
| 59 | +public class MyAppSession : ClaimsAbpSession, ITransientDependency |
| 60 | +{ |
| 61 | + private const string OrganizationUnitClaimTypeName = "Organization_Unit"; |
| 62 | + private readonly UserManager _userManager; |
| 63 | + public MyAppSession( |
| 64 | + IPrincipalAccessor principalAccessor, |
| 65 | + IMultiTenancyConfig multiTenancy, |
| 66 | + ITenantResolver tenantResolver, |
| 67 | + IAmbientScopeProvider<SessionOverride> sessionOverrideScopeProvider, |
| 68 | + UserManager userManager) : |
| 69 | + base(principalAccessor, multiTenancy, tenantResolver, sessionOverrideScopeProvider) |
| 70 | + { |
| 71 | + _userManager = userManager; |
| 72 | + } |
| 73 | + |
| 74 | + public string OrganizationUnit => PrincipalAccessor.Principal?.Claims.FirstOrDefault(c => c.Type == OrganizationUnitClaimTypeName)?.Value; |
| 75 | + |
| 76 | + public async Task SwitchOrganizationUnitAsync(UserOrganizationUnit newUserOrganizationUnit) |
| 77 | + { |
| 78 | + var oldOrganizationUnit = JsonSerializer.Deserialize<UserOrganizationUnit>(OrganizationUnit); |
| 79 | + |
| 80 | + SetOrganizationUnit(newUserOrganizationUnit); |
| 81 | + |
| 82 | + await _userManager.RemoveFromOrganizationUnitAsync(oldOrganizationUnit.UserId, oldOrganizationUnit.OrganizationUnitId); |
| 83 | + } |
| 84 | + |
| 85 | + private void SetOrganizationUnit(string newOrganizationUnit) |
| 86 | + { |
| 87 | + var identity = PrincipalAccessor.Principal?.Identity as ClaimsIdentity; |
| 88 | + if (identity != null) |
| 89 | + { |
| 90 | + var oldOrganizationUnitClaim = identity.FindFirst(OrganizationUnitClaimTypeName); |
| 91 | + if (oldOrganizationUnitClaim != null) |
| 92 | + { |
| 93 | + identity.RemoveClaim(oldOrganizationUnitClaim); |
| 94 | + } |
| 95 | + |
| 96 | + identity.AddClaim(new Claim(OrganizationUnitClaimTypeName, newOrganizationUnit)); |
| 97 | + } |
| 98 | + } |
| 99 | +} |
| 100 | +``` |
| 101 | + |
| 102 | +With these additions, the `MyAppSession` class now includes methods to both retrieve and set the organization unit information in the session. This enables management of organization unit data throughout the user's session. |
| 103 | + |
| 104 | +### Usage Example: Switching Organization Units |
| 105 | + |
| 106 | +In this section, we will demonstrate how to use a method for switching a user's organization unit within a new service class. The following example shows the `OrganizationUnitManager` class, which uses `MyAppSession` to allow a user to change currently selected organization unit. |
| 107 | + |
| 108 | +```csharp |
| 109 | +public class OrganizationUnitManager |
| 110 | +{ |
| 111 | + private readonly MyAppSession _myAppSession; |
| 112 | + private readonly UserManager _userManager; |
| 113 | + |
| 114 | + public OrganizationUnitManager(MyAppSession myAppSession, UserManager userManager) |
| 115 | + { |
| 116 | + _myAppSession = myAppSession; |
| 117 | + _userManager = userManager; |
| 118 | + } |
| 119 | + |
| 120 | + public async Task SwitchOrganizationUnitAsync(UserOrganizationUnit newUserOrganizationUnit) |
| 121 | + { |
| 122 | + var oldOrganizationUnit = JsonSerializer.Deserialize<UserOrganizationUnit>(_myAppSession.OrganizationUnit); |
| 123 | + |
| 124 | + _myAppSession.SetOrganizationUnit(newUserOrganizationUnit); |
| 125 | + |
| 126 | + if (oldOrganizationUnit != null) |
| 127 | + { |
| 128 | + await _userManager.RemoveFromOrganizationUnitAsync(oldOrganizationUnit.UserId, oldOrganizationUnit.OrganizationUnitId); |
| 129 | + } |
| 130 | + |
| 131 | + await _userManager.AddToOrganizationUnitAsync(newUserOrganizationUnit.UserId, newUserOrganizationUnit.OrganizationUnitId); |
| 132 | + } |
| 133 | +} |
| 134 | +``` |
| 135 | + |
| 136 | +#### Usage Steps |
| 137 | + |
| 138 | +So, we completed the backend side to switch user's current organization unit in claims principal. So, let's see how we can use it from the client side. |
| 139 | + |
| 140 | +1. **Fill the user's organization units**: As shown in the image above in the introcution section, we need to fill the user's organization units to a dropdown. To do this, you can use `_userManager.GetOrganizationUnitsAsync(user)` and map its result to a DTO class and return it to client and fill the dropdown using returned list. |
| 141 | +2. **Retrieve the Current Organization Unit:** The oldOrganizationUnit is deserialized from the current session's OrganizationUnit claim. So, you can use this value to set selected value of the organization unit dropdown. |
| 142 | +3. **Set the New Organization Unit:** On the change event of the dropdown, call an API endpoint (This is not explained above but you can use ProfileAppService and create a new method in this application service class) and this endpoint should call; |
| 143 | + |
| 144 | +```csharp |
| 145 | +public async Task ChangeOrganizationUnit(UserOrganizationUnit newUserOrganizationUnit){ |
| 146 | + await _organizationUnitManager.SwitchOrganizationUnitAsync(newUserOrganizationUnit); |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +So, that's all and our app allows users to change their active organization unit on the UI. |
0 commit comments