Closed pauloneill closed 1 year ago
I agree, seing that half of issues here are related to identity - a good dive-in video would be very popular
I agree, seing that half of issues here are related to identity - a good dive-in video would be very popular
If you're still stuck on this, let me know - I've managed to get it working on one of my projects and I can write up some notes that you might find useful...
@pauloneill - yeah, that would be great. I have a working solution - but I don't really like it.
@dmitry-selivanov I have a solution which I am currently trying to deploy to live but I'm having problems with IdentityServer4 I think... Once I have resolved this, I will be starting a new project also extending Identity so I'll make more extensive notes and can share a copy with you
@dmitry-selivanov I have a solution which I am currently trying to deploy to live but I'm having problems with IdentityServer4 I think... Once I have resolved this, I will be starting a new project also extending Identity so I'll make more extensive notes and can share a copy with you
Hi @pauloneill , Any news on this? Did you get to solve the issues? Is it possible to check your approach somewhere? Thanks!
@HybridSolutions here are the rough steps to extend Identity and integrate them into Jason's code base. Disclaimer: whilst I am a seasoned C#/.Net developer, I am new to dotNet Core, CQRS and Clean Architecture so this may not be the most elegant solution lol
I followed the tutorial here to extend User Identity.
Other tutorials say to extend ApplicationUser - which is already part of Jason's model - but I had trouble getting that to work so I created a new user model e.g. BookingsUser for the Bookings system I wrote. For this example lets call it CustomUser.
Make sure you add the [PersonalData] annotation to them so they get downloaded as part of the Identity Personal Data download.
Generate a migration and run it to apply these changes to your database. I had to use a "physical" database as the in-memory models don't support migrations. Took me a while to figure this out...
Now you will need to change all instances of ApplicationUser to CustomUser - this is where I expected extending ApplicationUser would work but alas no 😢
Next, change your Dependency Injection for Identity from:
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
to
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI()
.AddDefaultTokenProviders();
AddDefaultUI() adds back the login, logout, register screens, etc.
Next I had to scaffold out the Register and Personal Details pages per this page.
I did as they suggested and scaffolded into a new, blank ca-sln project so I could easily identify what changed.
Here are the Angular parts that I copied across for the 2 projects I've applied this to so far:
and
I also noticed what files had changed when I committed to BitBucket(/Git):
These are all the places where I changed ApplicationUser to CustomUser
I think that was everything. If it ever gets quiet here, I'll work through these steps again, document the whole process and maybe even create a YouTube video but for now feel free to contact me if you have any questions...
And if you'd prefer to chat "offline" send me a link to your socials and we can chat there 😄
Hope this helps 👍
@HybridSolutions so I'm starting another project using Jason's CA template and by simply extending Infrastructure - Identity - ApplicationUser.cs my model gets properly populated via a database migration so all that editing last time may not be necessary.
I'm expecting that I will still need to add scaffolding in order to edit my details (or use custom Application screens) as the default Download my Data link didn't pick up all the items I labelled as [PersonalData] using annotations but I'll report back when the admin section is complete...
Update: so in order to use CQRS classes against the User model within Jason's Clean Architecture project, I don't believe it's simply a case of extending Application User as I had hoped.
Instead, you have to create your own user class, in my case: BookingsUser, which is in the Domain layer (ApplicationUser is in the Infrastructure layer and so cannot be references by the Application Layer - and the extended fields are not accessible either).
And per my previous post (above), I changed all references of ApplicationUser to my BookingsUser so I could use CQRS classes to perform data management.
Now, maybe my CQRS classes should be using the Identity Server methods but that's an investigation for another day but for my purposes, this worked. (I even got Forgot Password, etc. code to work with SendGrid mailer so happy days).
If any of this helps anyone, or anyone has questions, feel free to hit me up
Update: so in order to use CQRS classes against the User model within Jason's Clean Architecture project, I don't believe it's simply a case of extending Application User as I had hoped.
Instead, you have to create your own user class, in my case: BookingsUser, which is in the Domain layer (ApplicationUser is in the Infrastructure layer and so cannot be references by the Application Layer - and the extended fields are not accessible either).
And per my previous post (above), I changed all references of ApplicationUser to my BookingsUser so I could use CQRS classes to perform data management.
Now, maybe my CQRS classes should be using the Identity Server methods but that's an investigation for another day but for my purposes, this worked. (I even got Forgot Password, etc. code to work with SendGrid mailer so happy days).
If any of this helps anyone, or anyone has questions, feel free to hit me up
Hi Paul! It would be interesting to see that approach in more detail . Any chance of that?
Hi Paul! It would be interesting to see that approach in more detail . Any chance of that?
Yeah I think I can manage something - I'll create a simple version of my project next week and ping you when it's done. I'll put it in a pubic Github repo....
Hi Paul! It would be interesting to see that approach in more detail . Any chance of that?
Yeah I think I can manage something - I'll create a simple version of my project next week and ping you when it's done. I'll put it in a pubic Github repo....
Hi pauloneill, Could you please share your project link?
I am also facing this issue. I have to use Id of ApplicationUser as a foreignKey to one of my domain.
And I am facing another issue, which is "I have to use UserManager
Please share your ideas to solve these issues. Thank you
Hi @hanushi I have to apologise but I've been slack and have only really started my write up/project. Once it's uploaded I will post again here
I haven't thought this through at all but is it possible to use Dependency Injection to inject the UserManager into your application as required?...
Hi @hanushi I have to apologise but I've been slack and have only really started my write up/project. Once it's uploaded I will post again here
I haven't thought this through at all but is it possible to use Dependency Injection to inject the UserManager into your application as required?...
Okay @pauloneill Thank you, The issue is not about using UserManager. The issue is calling ApplicationUser into Application layer.
ApplicationUser is inside Infrastructure layer.
Hi @hanushi I have to apologise but I've been slack and have only really started my write up/project. Once it's uploaded I will post again here I haven't thought this through at all but is it possible to use Dependency Injection to inject the UserManager into your application as required?...
Okay @pauloneill Thank you, The issue is not about using UserManager. The issue is calling ApplicationUser into Application layer.
ApplicationUser is inside Infrastructure layer.
Application layer defines interfaces that are implemented by outside layers so having a concrete implementation in there will be against the architecture. Referencing objects from Infrastructure layer also is not a good approach. Why do you need to do that? Can you give some more details?
Hi @hanushi I have to apologise but I've been slack and have only really started my write up/project. Once it's uploaded I will post again here I haven't thought this through at all but is it possible to use Dependency Injection to inject the UserManager into your application as required?...
Okay @pauloneill Thank you, The issue is not about using UserManager. The issue is calling ApplicationUser into Application layer. ApplicationUser is inside Infrastructure layer.
Application layer defines interfaces that are implemented by outside layers so having a concrete implementation in there will be against the architecture. Referencing objects from Infrastructure layer also is not a good approach. Why do you need to do that? Can you give some more details?
Could you please see this question. I have posted in this link. https://stackoverflow.com/questions/70597460/trouble-when-using-applicationuser-identityuser-usermanager-in-clean-architect.
I have to use ApplicationUser, inside GetAllQueryCommand ( namespace is Application.SiteCodes.Queries.GetAll )
namespace Infrastructure.Identity;
public class ApplicationUser : IdentityUser
{
}
@pauloneill meanwhile I managed to find a way to isolate Identity completely from Core and Application layers but still use an external database table to store additional info about an Identity User automatically by EF. I now have a single class in Infrastructure layer that is responsible for users CRUD and authorization (sign in). The "hack" is to use a IUser interface with all Identity User properties and a UserProfile type property.
public interface IUser
{
public int Id { get; set; }
public string UserName { get; set; }
public string NormalizedUserName { get; set; }
public string Email { get; set; }
public string NormalizedEmail { get; set; }
public bool EmailConfirmed { get; set; }
public string PasswordHash { get; set; }
public string SecurityStamp { get; set; }
public string ConcurrencyStamp { get; set; }
public string PhoneNumber { get; set; }
public bool PhoneNumberConfirmed { get; set; }
public bool TwoFactorEnabled { get; set; }
public DateTimeOffset? LockoutEnd { get; set; }
public bool LockoutEnabled { get; set; }
public int AccessFailedCount { get; set; }
public UserProfile UserProfile { get; set; }
public int UserProfileId { get; set; }
// ... Other commom properties between IdentityUser & User.
}
In Application layer I can't use ApplicationUser reference so I used my IUser:
public interface IApplicationUserService
{
Task<IList<IUser>> GetUsers();
ValueTask<IUser> GetCurrentUserAsync();
Task<IUser> FindByIdAsync(int userId);
Task<string> GetUserNameAsync(int userId);
Task<bool> IsInRoleAsync(int userId, string role);
Task<bool> AuthorizeAsync(int userId, string policyName);
Task<ApplicationSignInResult> PasswordSignInAsync(string userName, string password, bool isPersistent = false, bool lockoutOnFailure = false);
Task<(ApplicationResult result, int userId)> CreateUserAsync(IUser applicationUser, string password);
Task<ApplicationResult> DeleteUserAsync(int userId);
Task<ApplicationResult> AddToRoleAsync(IUser applicationUser, string roleName);
Task<ApplicationResult> UpdateAsync(IUser applicationUser);
}
public async Task<(ApplicationResult result, int userId)> CreateUserAsync(IUser user, string password)
{
ApplicationUser applicationUser = (ApplicationUser)user;
var result = await _userManager.CreateAsync(applicationUser, password);
return (result.ToApplicationResult(), user.Id);
}
In Register.cshtml.cs I can create a user using the regular ApplicationUser
public async Task<IActionResult>OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");
if (ModelState.IsValid)
{
var user = new ApplicationUser
{
UserName = Input.Email,
Email = Input.Email,
UserProfile = new UserProfile {
FirstName = "Myname",
LastName = "Mylastname",
DisplayName = "Myname Mylastname",
IsListed = true,
IsOnApproval = false,
JobPosition = "Administrator"
}
};
var result = await _applicationUserService.CreateUserAsync(user, Input.Password);
...
}
My ApplicationUser is defined like this so that EF deals with my additional user info automatically:
public class ApplicationUser: IdentityUser<int>, IUser
{
// Extend Identity user using a custom object with additional properties that will be stored in a separate database table
[PersonalData]
public virtual UserProfile UserProfile { get; set; }
public int UserProfileId { get; set; }
}
The only drawback is that in my ApplicationUserService class I have to specify when to include additional data in the user object. Here's an example:
public async Task<IUser> FindByIdAsync(int userId)
{
var user = await _userManager.Users.Include(p=>p.UserProfile).SingleOrDefaultAsync(u => u.Id == userId);
return user;
}
What do you think? Any suggestions for improvement?
Hi @hanushi I have to apologise but I've been slack and have only really started my write up/project. Once it's uploaded I will post again here I haven't thought this through at all but is it possible to use Dependency Injection to inject the UserManager into your application as required?...
Okay @pauloneill Thank you, The issue is not about using UserManager. The issue is calling ApplicationUser into Application layer. ApplicationUser is inside Infrastructure layer.
Application layer defines interfaces that are implemented by outside layers so having a concrete implementation in there will be against the architecture. Referencing objects from Infrastructure layer also is not a good approach. Why do you need to do that? Can you give some more details?
Could you please see this question. I have posted in this link. https://stackoverflow.com/questions/70597460/trouble-when-using-applicationuser-identityuser-usermanager-in-clean-architect.
I have to use ApplicationUser, inside GetAllQueryCommand ( namespace is Application.SiteCodes.Queries.GetAll )
namespace Infrastructure.Identity; public class ApplicationUser : IdentityUser { }
You shouldn't really be having GetAllQueryHandlerHandler() implementation in Application layer. Why don't you put the implementation in infrastructure layer and define an Interface for it in Application Layer? The way you're doing it, not only you will have to reference the Infrastructure layer but also Identity. Identity should be isolated in Infrastructure.
_applicationUserService
@HybridSolutions I think this solution is good.
And, GetAllQueryHandler() is a Query. Queries and command are inside the Application Layer. Implementing in infrastructure layer is not a good approach?
_applicationUserService
@HybridSolutions I think this solution is good.
And, GetAllQueryHandler() is a Query. Queries and command are inside the Application Layer. Implementing in infrastructure layer is not a good approach?
Sorry, didn't understand your question.
Hi all, great discussion and suggestions. As you know, within the template the approach isolates ApplicationUser
within Infrastructure, since it depends on IdentityUser
. IdentityUser
comes from ASP.NET Core Identity, and ideally, its best to avoid dependencies on other frameworks within Core.
If you are not concerned about a dependency on ASP.NET Core Identity, then simply move ApplicationUser
into the Domain. As @pauloneill mentioned, you can then extend ApplicationUser
, create migrations, and scaffold Identity pages. It will be relatively simple to extend Identity and access ApplicationUser
within Core.
If you would like to remain independent of ASP.NET Core Identity, then create a separate entity to represent your user within the domain model. Take a look at the following discussion to learn more: Can't map Application User with Domain entities.
@pauloneill meanwhile I managed to find a way to isolate Identity completely from Core and Application layers but still use an external database table to store additional info about an Identity User automatically by EF. I now have a single class in Infrastructure layer that is responsible for users CRUD and authorization (sign in). The "hack" is to use a IUser interface with all Identity User properties and a UserProfile type property.
public interface IUser { public int Id { get; set; } public string UserName { get; set; } public string NormalizedUserName { get; set; } public string Email { get; set; } public string NormalizedEmail { get; set; } public bool EmailConfirmed { get; set; } public string PasswordHash { get; set; } public string SecurityStamp { get; set; } public string ConcurrencyStamp { get; set; } public string PhoneNumber { get; set; } public bool PhoneNumberConfirmed { get; set; } public bool TwoFactorEnabled { get; set; } public DateTimeOffset? LockoutEnd { get; set; } public bool LockoutEnabled { get; set; } public int AccessFailedCount { get; set; } public UserProfile UserProfile { get; set; } public int UserProfileId { get; set; } // ... Other commom properties between IdentityUser & User. }
In Application layer I can't use ApplicationUser reference so I used my IUser:
public interface IApplicationUserService { Task<IList<IUser>> GetUsers(); ValueTask<IUser> GetCurrentUserAsync(); Task<IUser> FindByIdAsync(int userId); Task<string> GetUserNameAsync(int userId); Task<bool> IsInRoleAsync(int userId, string role); Task<bool> AuthorizeAsync(int userId, string policyName); Task<ApplicationSignInResult> PasswordSignInAsync(string userName, string password, bool isPersistent = false, bool lockoutOnFailure = false); Task<(ApplicationResult result, int userId)> CreateUserAsync(IUser applicationUser, string password); Task<ApplicationResult> DeleteUserAsync(int userId); Task<ApplicationResult> AddToRoleAsync(IUser applicationUser, string roleName); Task<ApplicationResult> UpdateAsync(IUser applicationUser); }
public async Task<(ApplicationResult result, int userId)> CreateUserAsync(IUser user, string password) { ApplicationUser applicationUser = (ApplicationUser)user; var result = await _userManager.CreateAsync(applicationUser, password); return (result.ToApplicationResult(), user.Id); }
In Register.cshtml.cs I can create a user using the regular ApplicationUser
public async Task<IActionResult>OnPostAsync(string returnUrl = null) { returnUrl ??= Url.Content("~/"); if (ModelState.IsValid) { var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email, UserProfile = new UserProfile { FirstName = "Myname", LastName = "Mylastname", DisplayName = "Myname Mylastname", IsListed = true, IsOnApproval = false, JobPosition = "Administrator" } }; var result = await _applicationUserService.CreateUserAsync(user, Input.Password); ... }
My ApplicationUser is defined like this so that EF deals with my additional user info automatically:
public class ApplicationUser: IdentityUser<int>, IUser { // Extend Identity user using a custom object with additional properties that will be stored in a separate database table [PersonalData] public virtual UserProfile UserProfile { get; set; } public int UserProfileId { get; set; } }
The only drawback is that in my ApplicationUserService class I have to specify when to include additional data in the user object. Here's an example:
public async Task<IUser> FindByIdAsync(int userId) { var user = await _userManager.Users.Include(p=>p.UserProfile).SingleOrDefaultAsync(u => u.Id == userId); return user; }
What do you think? Any suggestions for improvement?
@HybridSolutions Can you share some more details of your implementation? Is the UserProfile entity related to other domain models?
@HybridSolutions Can you share some more details of your implementation? Is the UserProfile entity related to other domain models?
I simplified the approach. I now have a User class for users with all the properties I need and a "UserName" property to be able to retrieve Identity User data. To be able to have both entities in sync, I use a UserService class. Here's an example when creating a new user that stores user info in Identity table and in my own user table:
public async Task<(ApplicationResult result, int userId)> CreateUserAsync(User user)
{
ApplicationResult appResult = new ApplicationResult();
ApplicationUser applicationUser = await _userManager.FindByNameAsync(user.UserName);
if (applicationUser == null)
{
using (IDbContextTransaction transaction = _context.Database.BeginTransaction())
{
try
{
applicationUser = new ApplicationUser
{
UserName = user.UserName,
Email = user.Email,
EmailConfirmed = true
};
var result = await _userManager.CreateAsync(applicationUser, user.Password);
// Remove sensible information
user.Password = "";
if (result.Succeeded)
{
object[] primaryKeys = await _repository.InsertAsync(user);
//object[] primaryKeys = await _repository.InsertAsync(user);
int userId = (int)primaryKeys[0];
user.Id = userId;
await transaction.CommitAsync();
appResult = result.ToApplicationResult();
_logger.LogInformation("New user inserted.", user);
}
else
{
await transaction.RollbackAsync();
_logger.LogError("Was not possible to create user. Error in Identity while adding new user!", user);
return (ApplicationResult.Failed(new ApplicationError { Description = "Was not possible to create user. Error in Identity while adding new user!" }), -1);
}
}
catch (System.Exception ex)
{
var message = ex.Message;
await transaction.RollbackAsync();
throw;
}
}
}
}
You always have to remember that if you change your User data, you must update Identity user as well. Here's the method to delete a user. I have to get my user data, check if it exists, retrieve Identity user and then delete them both in a single transaction.
public async Task<ApplicationResult> DeleteUserAsync(int userId)
{
User user = await FindByIdAsync(userId);
if (user != null)
{
var identityUser = await _userManager.FindByNameAsync(user.UserName);
using (IDbContextTransaction transaction = _context.Database.BeginTransaction())
{
var result = await _userManager.DeleteAsync(identityUser);
if (result.Succeeded)
{
await _repository.DeleteAsync(user);
transaction.Commit();
}
else
{
transaction.Rollback();
return ApplicationResult.Failed();
}
}
}
return ApplicationResult.Success;
}
That's it. I'm sure there's other ways to do it, but I found this is the less intrusive method and with less dependencies. Basically, the only dependency is the UserName property that is used to "connect" both entities.
If you have any other solution share it please.
@HybridSolutions Thank you for sharing your solution! I'm actually following the approach you proposed here. It has the advantage of an automatic synchronization between UserProfile and IdentityUser in Create and Update methods (with the approach proposed here I have to update all the common properties in the update method, for example Email, if I want it to be part of my User entity). Why do you propose this solution in place of the previous one?
Having a property in ApplicationUser was a dependency. Personally, didn't like or want to have any kind of change to the Identity builtin objects and tables. Since cascading delete did not work, it made no sense to go that way. Not saying its invalid, but I prefer not to have any attachments at all.
@HybridSolutions Ok, more work to do in the service, with updates to perform in transaction, but a cleaner solution. Do you have a repository to share with a full implementation of the UserService and related classes?
With your last approach the IdentityUser must be kept in sync with the Core User entity. The logic is all inside the UserService defined in Infrastructure, which necessarily contains logic written to handle the Domain entity. What kind of object is returned by the GetById /GetList methods of the UserService? If it contains only the Identity information, we need to add data saved to the Domain "User" entity somewhere, to add the missing information (for instance FirstName, LastName...)
@HybridSolutions Ok, more work to do in the service, with updates to perform in transaction, but a cleaner solution. Do you have a repository to share with a full implementation of the UserService and related classes?
I'm using a generic repository based on this project
With your last approach the IdentityUser must be kept in sync with the Core User entity. The logic is all inside the UserService defined in Infrastructure, which necessarily contains logic written to handle the Domain entity. What kind of object is returned by the GetById /GetList methods of the UserService? If it contains only the Identity information, we need to add data saved to the Domain "User" entity somewhere, to add the missing information (for instance FirstName, LastName...)
Identity Service and data from it is only required sometimes. I'm working mainly on my User Entity all the time. Remember that when needed, you can just use the regular way to access identity data:
public async Task<bool> IsInRoleAsync(int userId, string role)
{
User user = await FindByIdAsync(userId);
if (user != null)
{
var identityUser = await _userManager.FindByNameAsync(user.UserName);
if (identityUser != null)
{
bool _isInRole = await _userManager.IsInRoleAsync(identityUser, role);
return _isInRole;
}
}
return false;
}
The FindByIdAsync will return a User object.
public async Task<User> FindByIdAsync(int userId)
{
var user = await _repository.GetByIdAsync<User>(userId);
return user;
}
You have to inject some Identity classes into the UserService:
private readonly UserManager
I have been trying to extend the Identity model within my Clean Architecture application but keep running into problems.
Being relatively new to ASP Net Core, I have identified why my migrations didn't work (now using: "UseInMemoryDatabase": false) but now I seem to be getting a CORS error even though everything is on localhost and the only changes to your CleanArchitecture template are to extend ASPNetUsers by adding [PersonalData] fields FirstName and LastName.
The problem that has stopped me is that I cannot diagnose why the Login button doesn't go to the Login screen; it simply returns to https://localhost:5001/ as if there is no routing installed.
And I know I am not authenticated as the system tries to log in again when I go to the ToDo lists...
I have already read the Microsoft documentation and have extended the Identity model successfully by following both these tutorials:
https://www.yogihosting.com/aspnet-core-identity-setup/ and https://codewithmukesh.com/blog/user-management-in-aspnet-core-mvc/
but they are not using your Clean Architecture solution; they use MVC...
Your previous Clean Architecture videos have been extremely informative and I agree with your development philosophies relating to architecture and testing.
I would love to see a similar video that extends the Identity model to include, say, a mobile number or some other personal details that might be required in a real life application.