Grouping data in a collection-view using DynamicData Xamarin forms prism - xamarin.forms

I'm working on a Xamarin forms app using Prism. I'm trying to manipulate(Sort, filter, etc..) and display grouped data using Dynamic data. I'm following this
https://www.xamboy.com/2021/01/20/using-dynamic-data-in-xamarin-forms-part-1/
My issue is that my Readonlyobservablecollection _getUsers is not getting populated please see below:
private SourceCache<User, string> _sourceCache = new SourceCache<User, string>(x =>x.Id);
private ReadOnlyObservableCollection<ObservableGroupedCollection<string,User, string>> _getUsers;
public ReadOnlyObservableCollection<ObservableGroupedCollection<string, User, string>> GetUser { get; set; }
public string Gender { get { return _gender; } set { SetProperty(ref _gender, value); OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs(nameof(Gender))); } }
private string _gender;
private async Task GetUsers()
{
try
{
var users = await _apiService.GetUsers();
_sourceCache.AddOrUpdate(users);
Func<User, bool> searchFilter(string text) => user =>
{
return string.IsNullOrWhiteSpace(text) || user.PhoneNumber.ToLower().Contains(text.ToLower()) || user.UserName.ToLower().Contains(text.ToLower()) || user.Name.ToLower().Contains(text.ToLower());
};
Func<User, bool> genderFilter(string gender) => user =>
{
return gender == "All" || user.Gender == gender;
};
Func<User, bool> statusFilter(string status) => user =>
{
if(status == "Live")
{
return user.Active == true;
}
if (status == "Suspended")
{
return user.Active == false;
}
return status == "None" || user.Payment.StatusMessage.Replace(" ","") == status;
};
var genderPredicate = this.WhenAnyValue(x => x.Gender)
.Select(genderFilter);
var filterPredicate = this.WhenAnyValue(x => x.SearchText)
.Select(searchFilter);
var statusPredicate = this.WhenAnyValue(x => x.Status)
.Select(statusFilter);
var sortPredicate = this.WhenAnyValue(x => x.SortBy)
.Select(x => SortExpressionComparer<User>.Ascending(a => a.Name));
_cleanUp = _sourceCache.Connect()
.Filter(genderPredicate)
.Group(x => x.Race)
.Transform(g => new ObservableGroupedCollection<string, User, string>(g, genderPredicate, sortPredicate))
.Bind(out _getUsers)
.DisposeMany()
.Subscribe();
Gender = "All";
OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs(nameof(Gender)));
}
catch (Refit.ApiException ex)
{
if(ex.StatusCode != System.Net.HttpStatusCode.NotFound)
{
await HandleExceptionAsync(ex);
}
}
catch (Exception ex)
{
await HandleExceptionAsync(ex);
}
}
The generic class
public class ObservableGroupedCollection<TGroupKey, TObject, TKey> : ObservableCollectionExtended<TObject>, IDisposable
{
public TGroupKey Key { get; }
public ObservableGroupedCollection(IGroup<TObject, TKey, TGroupKey> group, IObservable<Func<TObject, bool>> filter, IObservable<IComparer<TObject>> comparer)
{
this.Key = group.Key;
//load and sort the grouped list
var dataLoader = group.Cache.Connect()
.Filter(filter)
.Sort(comparer)
.ObserveOn(RxApp.MainThreadScheduler)
.Bind(this) //make the reset threshold large because xamarin is slow when reset is called (or at least I think it is #erlend, please enlighten me )
.Subscribe();
_cleanUp = new CompositeDisposable(dataLoader);
}
public void Dispose()
{
_cleanUp.Dispose();
}
private readonly IDisposable _cleanUp;
}
I'm really struggling with this. Please if anyone can help, I'd really appreciate it.

You can use this to populate _getUsers.
public ReadOnlyObservableCollection<ObservableGroupedCollection<string, User, string>> GetUser {
get
{
return _getUsers;
}
set
{
if (_getUsers != value)
{
_getUsers = value;
OnPropertyChanged(nameof(GetUser));
}
}
}

Related

Resolution failed with error: No public constructor is available for type IHttpContextAccessor

I recently upgraded my app to .net core 6 and now I am getting this error when trying to get a service using this code:
IUnityContainer container = HangfireUnityConfig.GetConfiguredContainer();
var authService = container.Resolve<IAuthService>();
I read some other posts that mentioned adding HttpContextAccessor in my ConfigureServices() method but none of the ways ive tried fixed the error.
services.AddHttpContextAccessor();
services.AddTransient<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
Another person mentioned adding the line in my Program.cs but still getting the error.
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
If I add RegisterType<IHttpContextAccessor, HttpContextAccessor>() to RegisterTypes() in my HangFireUnityConfig class the error goes away but throws a new error later on so Im not sure if thats the right fix.
public static void RegisterTypes(IUnityContainer container)
{
// register hangfire dependencies
container.RegisterType<IHttpContextAccessor, HttpContextAccessor>()
}
AuthService.cs
using MyApp.Entities.DTOs;
namespace MyApp.Service.Auth
{
public class AuthService : IAuthService
{
private UserDto currentUser = null;
private readonly IHttpContextAccessor _context;
public AuthService(IHttpContextAccessor ctx)
{
_context = ctx;
currentUser = parseClaimsUser();
}
public bool isInRole(string role, List<string> roleList)
{
return true;
}
public UserDto parseClaimsUser()
{
ClaimsPrincipal currentClaim = _context.HttpContext.User;
UserDto parsedUser = new UserDto();
bool isAdmin = false;
if (currentClaim == null || !currentClaim.Identity.IsAuthenticated)
{
return parsedUser;
}
//return user id from token properties
parsedUser.userID = currentClaim.Claims.Where(claim => claim.Type == ClaimTypes.NameIdentifier).Select(v => v.Value).FirstOrDefault<string>();
// retrieve groups from token properties --- this is only retrieved upon login. Users will have to log out and log back in to see any changes in groups
var currentGroupsIDs = currentClaim.HasClaim(claim => claim.Type == ClaimTypes.Role) ?
currentClaim.Claims.Where(t => t.Type == ClaimTypes.Role).Select(y => int.Parse(y.Value)).ToList<int>()
: new List<int>();
var adminString = currentClaim.Claims.Where(claim => claim.Type == ClaimTypes.AuthorizationDecision)
.Select(v => v.Value)
.SingleOrDefault<string>();
adminString = adminString == null ? "False" : adminString;
isAdmin = bool.Parse(adminString);
//parsedUser.userGrp = currentGroups;
parsedUser.userGrpIDs = currentGroupsIDs;
parsedUser.isAuthenticated = currentClaim.Identity.IsAuthenticated;
parsedUser.displayName = currentClaim.Identity.Name;
parsedUser.email = currentClaim.Claims.Where(w => w.Type == ClaimTypes.Email).Select(v => v.Value).SingleOrDefault<string>();
//parsedUser.currentToken = tokenExtract;
parsedUser.isAdmin = isAdmin;
var isUS = currentClaim.Claims.Where(claim => claim.Type == "us_citizen").Select(v => v.Value).SingleOrDefault<string>();
if (isUS != null)
{
parsedUser.isUSCitizenAndJPLEmployee = bool.Parse(isUS);
}
return parsedUser;
}
public void initUser()
{
currentUser = parseClaimsUser();
}
public UserDto getCurrentUser(bool includeToken = false)
{
if (currentUser == null || currentUser.userID == null)
{
currentUser = parseClaimsUser();
}
if (!includeToken)
{
currentUser.currentToken = null;
}
return currentUser;
}
public bool userIsAdmin()
{
return true;
}
}
}
I was able to figure out the error I was getting with my last implementation. The error was with not being able to cast my dbContext to type IObjectContextAdapter.
public List<KeyValuePair<string, long>> GetKeys(EntityEntry entry)
{
var keys = new List<KeyValuePair<string, long>>();
var objectStateEntry = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager.GetObjectStateEntry(entry.Entity);
if (objectStateEntry.EntityKey.EntityKeyValues != null)
{
keys.AddRange(objectStateEntry.EntityKey.EntityKeyValues.Select(key => new KeyValuePair<string, long>(key.Key, Convert.ToInt64(key.Value))));
}
return keys;
}
I refactored the code to look like this and got no errors.
public List<KeyValuePair<string, long>> GetKeys(EntityEntry entry)
{
//this gets an array of the key names
var keyNames = entry.Metadata.FindPrimaryKey()
.Properties
.Select(p => p.Name)
.ToArray();
var keys = new List<KeyValuePair<string, long>>();
if (keyNames != null)
{
//creates the KeyValuePairs
keys.AddRange(keyNames.Select(key => new KeyValuePair<string, long>(key, Convert.ToInt64(entry.Property(key).CurrentValue))));
}
return keys;
}

How do I prevent a duplicate entry for a Create/Edit Functionality in ASP.NET Core w/ EF?

I am trying to prevent a user from creating a form with a FirstName, LastName, and DOB that match an entry in the database and editing a form to match an existing entry. If you could also lead me to how I can show an error when this happens, that would be awesome.
My Model:
public class MRegForm
{
public int MRegFormId { get; set; }
[Display(Name = "First Name")]
public string FirstName { get; set; } = string.Empty;
[Display(Name = "Last Name")]
public string LastName { get; set; } = string.Empty;
public DateTime DOB { get; set; }
[I tried Index attribute. It did not work for me. I was able to create new duplicate forms with no issues.
[Index(nameof(FirstName), nameof(LastName), nameof(DOB), IsUnique = true)]
public class MRegForm
{
I also tried this. Same thing.
protected override void OnModelCreating(ModelBuilder modelbuilder)
{
base.OnModelCreating(modelbuilder);
modelbuilder.Entity<MRegForm>()
.HasIndex(x => new { x.FirstName, x.LastName, x.DOB})
.IsUnique();
}
public DbSet<MRegForm> MRegForm { get; set; } = default!;
I think that there is maybe a way to prevent this in the OnPostAsync()
This is my create OnPostAsync():
public async Task<IActionResult> OnPostAsync()
{
MRegForm.CreatorId = UserManager.GetUserId(User);
var isAuthorized = await AuthorizationService.AuthorizeAsync(User, MRegForm, RegFormOperations.Create);
if (isAuthorized.Succeeded == false)
return Forbid();
Context.MRegForm.Add(MRegForm);
await Context.SaveChangesAsync();
return RedirectToPage("./Index");
}
This is my Edit OnPostAsync():
public async Task<IActionResult> OnPostAsync(int id)
{
var mRegForm = await Context.MRegForm.AsNoTracking().SingleOrDefaultAsync(m => m.MRegFormId == id);
if (mRegForm == null)
return NotFound();
MRegForm.CreatorId = mRegForm.CreatorId;
var isAuthorized = await AuthorizationService.AuthorizeAsync(User, MRegForm, RegFormOperations.Update);
if (isAuthorized.Succeeded == false)
return Forbid();
MRegForm.Status = mRegForm.Status; // the Status is the current Status - Do Not Reset
Context.Attach(MRegForm).State = EntityState.Modified;
try
{
await Context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MRegFormExists(MRegForm.MRegFormId))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
private bool MRegFormExists(int id)
{
return (Context.MRegForm?.Any(e => e.MRegFormId == id)).GetValueOrDefault();
}
}
You can try to download the entity from the database if exists and make changes to it or creating new one if not exist.
Your clients can always make new MRegForm in the form, but you add or update in the back and. Bether option will be to pass the existing MRegForm to the form and the client see and change all props he need.
public async Task AddOrUpdate(MRegForm input)
{
var mRegForm = await Context.MRegForm
.FirstOrDefaltAsync(x => x.FirstName == input.FirstName && x.LastName == input.LastName && x.DOB == input.YourDate);
if(mRegForm != null)
{
//Make changes on mRegForm
mRegForm.SomeProp = input.SomeProp,
...
}
else
{
var newMRegForm = new MRegForm
{
//Set all props you need
}
await this.Context.AddAsync(newMRegForm );
}
await this.Context.SaveCangesAsync();
}

ASP.NET Core multiple (types) authorization requirements in single policy

Is there any way to have something like this?
options.AddPolicy("IsEducationOwner", policy =>
{
// Eather first OR second policy requirement needs to be true
policy.Requirements.Add(new EducationOwnerRequirement()); // My custom requirement that has one handler
policy.RequireRole("CatalogAdmin"); // Role based requirement
});
I found that this is working. Requirements needs to have additional handler that checks for role in user claim so the code looks like this.
Additional information can be found on this MSDN page or in this article
My example:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(options => {
options.AddPolicy("IsEducationOwner", policy =>
{
policy.Requirements.Add(new EducationOwnerRequirement());
});
});
services.AddTransient<IAuthorizationHandler, IsEducationOwnerHandler>();
services.AddTransient<IAuthorizationHandler, HasCatalogAdminRoleHandler>();
}
}
public class EducationOwnerRequirement : IAuthorizationRequirement
{
}
public class HasCatalogAdminRoleHandler : AuthorizationHandler<EducationOwnerRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, EducationOwnerRequirement requirement)
{
if (context.User.IsInRole("CatalogAdmin"))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
public class IsEducationOwnerHandler : AuthorizationHandler<EducationOwnerRequirement>
{
private PerformaContext _db;
public IsEducationOwnerHandler(PerformaContext db)
{
_db = db;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, EducationOwnerRequirement requirement)
{
var mvcContext = context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext;
if (mvcContext == null || !context.User.HasClaim(c => c.Type == ClaimTypeNaming.oid))
{
return Task.CompletedTask;
}
var path = mvcContext.HttpContext.Request.Path.Value;
var educationId = path.Substring(path.IndexOf("/api/educations/") + 16, path.Length - path.IndexOf("/api/educations/") - 16);
var userExternalId = context.User.FindFirst(ClaimTypeNaming.oid).Value;
var userId = _db.GetUserByExternalId(userExternalId).Select(x => x.Id).FirstOrDefault();
if(userId == Guid.Empty)
{
return Task.CompletedTask;
}
var educationOwners = _db.GetOwnersForEducation(Guid.Parse(educationId)).Select(x => x.UserId).ToList();
if (educationOwners.Contains(userId))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}

Entity Framework : A referential integrity constraint violation occurred when I do EntityState.Modified

I create a website in ASP.NET MVC 4.
It's been so long that I have this error : A referential integrity constraint violation occurred.
It's when i tried to update a entry in my db. Well when someone want to buy a product, i need to change the user who buy.
I have two one to many relationship.
EDIT
This my User class :
public class User
{
private string email, password, firstname, lastname;
private Adress shippingAdress, billingAdress;
private bool isConnected;
private List<Product> products;
//private List<Auction> auctions;
private long idShippingA, idBillingA;
public User()
{
products = new List<Product>();
}
/* public List<Auction> Auctions
{
get { return auctions; }
set { auctions = value; }
}
public void AddAuction(Auction auction)
{
if (auction != null)
auctions.Add(auction);
}*/
public long IdBillingA
{
get { return idBillingA; }
set
{
if (value < 0)
throw new ArgumentException("The id of the billing adress should not be negative");
idBillingA = value;
}
}
public long IdShippingA
{
get { return idShippingA; }
set
{
if (value < 0)
throw new ArgumentException("The id of the shipping adress should not be negative");
idShippingA = value;
}
}
public bool IsConnected
{
get { return isConnected; }
set { isConnected = value; }
}
public virtual List<Product> Products
{
get { return products; }
set
{
if (value == null)
throw new ArgumentNullException("The list of product should not be null");
products = value;
}
}
public Adress BillingAdress
{
get { return billingAdress; }
set
{
if (value == null)
throw new ArgumentNullException("The billing adress should not be null");
billingAdress = value;
}
}
public Adress ShippingAdress
{
get { return shippingAdress; }
set
{
if (value == null)
throw new ArgumentNullException("The shipping adress should not be null");
shippingAdress = value;
}
}
public string Password
{
get { return password; }
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("The password should not be null or empty");
password = value;
}
}
public string Email
{
get { return email; }
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("The email should not be null or empty");
email = value;
}
}
public string Lastname
{
get { return lastname; }
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("The lastname should not be null or empty");
lastname = value;
}
}
public string Firstname
{
get { return firstname; }
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("The fistname should not be null or empty");
firstname = value;
}
}
}
}
EDIT
This is my Product Class :
public class Product
{
private long id, strategyId;
private User userWhoSell;
private User userWhoBuy;
private string userWhoSellId, userWhoBuyId, name, description, urlPicture, isBought;
public string IsBought
{
get { return isBought; }
set { isBought = value; }
}
private SellStrategy strategy;
private float price;
private string strategyString;
public Product()
{
isBought = "F";
}
public float Price
{
get { return price; }
set
{
if (value < 0)
throw new ArgumentException("The price should not be negative");
price = value;
}
}
public string StrategyString
{
get { return strategyString; }
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("The strategy string should not be null, empty or with white space");
strategyString = value;
}
}
public long StrategyId
{
get { return strategyId; }
set
{
if (value < 0)
throw new ArgumentException("The strategy id should not be negative");
strategyId = value;
}
}
public SellStrategy Strategy
{
get { return strategy; }
set
{
if (value == null)
throw new ArgumentNullException("The strategy should not be null");
strategy = value;
}
}
public string SellerId
{
get { return userWhoSellId; }
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("The user id should not be null, empty or with white space");
userWhoSellId = value;
}
}
public string BuyerId
{
get { return userWhoBuyId; }
set
{
userWhoBuyId = value;
}
}
public string UrlPicture
{
get { return urlPicture; }
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("The picture's url should not be null, empty or with white space");
urlPicture = value;
}
}
public long Id
{
get { return id; }
set
{
if (value < 0)
throw new ArgumentException("The id should not be negative");
id = value;
}
}
public string Description
{
get { return description; }
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("The description should not be null, empty or with white space");
description = value;
}
}
public string Name
{
get { return name; }
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("The name should not be null, empty or with white space");
name = value;
}
}
public virtual User Buyer
{
get { return userWhoBuy; }
set
{
userWhoBuy = value;
}
}
public virtual User Seller
{
get { return userWhoSell; }
set
{
if (value == null)
throw new ArgumentNullException("The user should not be null");
userWhoSell = value;
}
}
}
EDIT
this is my context :
public class Context : DbContext
{
public Context(string connString)
: base(connString) { }
public DbSet<User> Users { get; set; }
public DbSet<Adress> Adress { get; set; }
public DbSet<Product> Products { get; set; }
//public DbSet<Auction> Auctions { get; set; }
public DbSet<SellStrategy> Strategies { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<User>().HasKey<string>(u => u.Email);
modelBuilder.Entity<Adress>().HasKey<long>(a => a.Id);
modelBuilder.Entity<Product>().HasKey<long>(p => p.Id);
modelBuilder.Entity<Auction>().HasKey<long>(au => au.Id);
modelBuilder.Entity<Product>()
.HasRequired(p => p.Seller)
.WithMany(u => u.Products)
.HasForeignKey(p => p.SellerId)
.WillCascadeOnDelete(false);
// Otherwise you might get a "cascade causes cycles" error
modelBuilder.Entity<Product>()
.HasOptional(p => p.Buyer)
.WithMany() // No reverse navigation property
.HasForeignKey(p => p.BuyerId)
.WillCascadeOnDelete(false);
// modelBuilder.Entity<Auction>().HasMany<User>(au => au.Users).WithMany(u => u.Auctions);
// modelBuilder.Entity<Auction>().HasRequired(au => au.Product).WithMany().WillCascadeOnDelete(false);
modelBuilder.Entity<User>().HasRequired(u => u.BillingAdress).WithMany().HasForeignKey(u => u.IdBillingA).WillCascadeOnDelete(false);
modelBuilder.Entity<User>().HasRequired(u => u.ShippingAdress).WithMany().HasForeignKey(u => u.IdShippingA).WillCascadeOnDelete(false);
modelBuilder.Entity<User>().Ignore(u => u.IsConnected);
modelBuilder.Entity<Product>().HasRequired<SellStrategy>(p => p.Strategy).WithMany().WillCascadeOnDelete(false);
modelBuilder.Entity<Product>().Ignore(p => p.StrategyString);
modelBuilder.Entity<Product>().Ignore(p => p.Price);
modelBuilder.Entity<SellStrategy>().Property(s => s.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
modelBuilder.Entity<AuctionSelling>().HasKey<long>(a => a.Id);
modelBuilder.Entity<AuctionSelling>().Map(m =>
{
m.MapInheritedProperties();
m.ToTable("AuctionSelling");
});
modelBuilder.Entity<DirectSelling>().HasKey<long>(d => d.Id);
modelBuilder.Entity<DirectSelling>().Map(m =>
{
m.MapInheritedProperties();
m.ToTable("DirectSelling");
});
modelBuilder.Entity<SellStrategy>().Property(s => s.SoldDate).IsOptional();
}
}
EDIT
And when I try to update my product :
public void UpDateProduct(Product product)
{
using(Context context = new Context(connectionString))
{
try
{
Product p = GetById(product.Id);
//context.Products.Attach(p);
p.BuyerId = product.BuyerId;
p.Buyer = product.Buyer;
p.IsBought = "T";
context.Entry(p).State = EntityState.Modified;
context.SaveChanges();
}
catch (Exception ex)
{
}
}
}
This is the line
context.Entry(p).State = EntityState.Modified;
that provoke the error.
This is the full error
A referential integrity constraint violation occurred: The property value(s) of 'User.Email' on one end of a relationship do not match the property value(s) of 'Product.BuyerId' on the other end.
When I do just context.saveChanges, nothing occurs...
I don't know what to do... I want to cry ^^...
Thank you a lot in advance !
This is caused by setting reference navigation properties in the constructor. Specifically:
userWhoSell = new User();
userWhoBuy = new User();
This means that a Product starts out having two dummy User objects that you have to replace for them to become meaningful. Otherwise EF may try to save these dummy objects. I'm pretty sure that by...
context.Entry(p).Entity.Buyer = product.Buyer;
...you actually set such an empty User object, which has a key value no email, not the key value Product.BuyerId you apparently set before.
I'd say: remove most of these initializations in the entities' constructors. Initializing collections makes sense. Sometimes setting default values does (but not primary key value), but reference navigation properties never. See also: EF codefirst : Should I initialize navigation properties?
Side note: neither would I use property setters for validations. That's what data annotations are for. These exceptions may interfere with EF when trying to materialize entities from the database.

How to overload UserManager.AddToRoleAsync(string userId, string role)

I'm using Asp.net Identity Framework 2.1. I implement customized ApplicatoinUser, ApplicationRole, ApplicationUserRole, because I want to add support to multi-tenant, that is each user belongs to different companies, but I have 3 roles among all these companies, they are User, Admin and Approver.
My ApplicationUserRole derived from IdentityUserRole, and have one more property: CompanyId. This property will indicate the user's role in this particular company. My code for these customized classes attached in bottom.
My question is when I try to override ApplicationUserManager(Yes, it derived from UserManager too)'s AddToRoleAsync , IsInRoleAsync , I don't know how to deal with the new CompanyId, looks like the existing function doesn't receive these companyId(or tenantId).
Then when I'm trying to overload these functions with companyId included, I can't find the db context either in ApplicatoinUserManager nor its base class.
Am I on the right track of adding tenantId/companyId to the application Role?
I've referenced this answer: SO linkes, and this blog.ASP.NET Web Api and Identity 2.0 - Customizing Identity Models and Implementing Role-Based Authorization
My IdentityModels:
public class ApplicationUserLogin : IdentityUserLogin<string> { }
public class ApplicationUserClaim : IdentityUserClaim<string>
{
}
public class ApplicationUserRole : IdentityUserRole<string>
{
public string CompanyId { get; set; }
}
// You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more.
public class ApplicationUser : IdentityUser<string, ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>//, IAppUser
{
public ApplicationUser()
{
this.Id = Guid.NewGuid().ToString();
}
public virtual string CompanyId { get; set; }
public virtual List<CompanyEntity> Company { get; set; }
public DateTime CreatedOn { get; set; }
public async Task<ClaimsIdentity> GenerateUserIdentityAsync(ApplicationUserManager manager, string authenticationType)
{
// Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
var userIdentity = await manager.CreateIdentityAsync(this, authenticationType);
// Add custom user claims here
return userIdentity;
}
}
// Must be expressed in terms of our custom UserRole:
public class ApplicationRole : IdentityRole<string, ApplicationUserRole>
{
public ApplicationRole() {}
public ApplicationRole(string name) : this()
{
this.Name = name;
}
// Add any custom Role properties/code here
public string Description { get; set; }
}
// Most likely won't need to customize these either, but they were needed because we implemented
// custom versions of all the other types:
public class ApplicationUserStore: UserStore<ApplicationUser, ApplicationRole, string,ApplicationUserLogin, ApplicationUserRole,ApplicationUserClaim>, IUserStore<ApplicationUser, string>, IDisposable
{
public ApplicationUserStore()
: this(new IdentityDbContext())
{
base.DisposeContext = true;
}
public ApplicationUserStore(DbContext context)
: base(context)
{
}
}
public class ApplicationRoleStore
: RoleStore<ApplicationRole, string, ApplicationUserRole>,
IQueryableRoleStore<ApplicationRole, string>,
IRoleStore<ApplicationRole, string>, IDisposable
{
public ApplicationRoleStore()
: base(new IdentityDbContext())
{
base.DisposeContext = true;
}
public ApplicationRoleStore(DbContext context)
: base(context)
{
}
}
My IdentityConfig:
public class ApplicationUserManager
: UserManager<ApplicationUser, string>
{
public ApplicationUserManager(IUserStore<ApplicationUser, string> store)
: base(store) { }
public static ApplicationUserManager Create(
IdentityFactoryOptions<ApplicationUserManager> options,
IOwinContext context)
{
var manager = new ApplicationUserManager(
new UserStore<ApplicationUser, ApplicationRole, string,
ApplicationUserLogin, ApplicationUserRole,
ApplicationUserClaim>(context.Get<ApplicationDbContext>()));
// Configure validation logic for usernames
manager.UserValidator = new UserValidator<ApplicationUser>(manager)
{
AllowOnlyAlphanumericUserNames = false,
RequireUniqueEmail = false
};
// Configure validation logic for passwords
manager.PasswordValidator = new PasswordValidator
{
RequiredLength = 6,
//RequireNonLetterOrDigit = true,
//RequireDigit = true,
//RequireLowercase = true,
//RequireUppercase = true,
};
var dataProtectionProvider = options.DataProtectionProvider;
if (dataProtectionProvider != null)
{
manager.UserTokenProvider =
new DataProtectorTokenProvider<ApplicationUser>(
dataProtectionProvider.Create("ASP.NET Identity"));
}
// add sms and email service provider
manager.SmsService = new EMaySmsServiceProvider();
manager.EmailService = new ConcordyaEmailServiceProvider();
return manager;
}
public string GetCurrentCompanyId(string userName)
{
var user = this.FindByName(userName);
if (user == null)
return string.Empty;
var currentCompany = string.Empty;
if (user.Claims.Count > 0)
{
currentCompany = user.Claims.Where(c => c.ClaimType == ConcordyaPayee.Core.Common.ConcordyaClaimTypes.CurrentCompanyId).FirstOrDefault().ClaimValue;
}
else
{
currentCompany = user.CurrentCompanyId;
}
return currentCompany;
}
public override Task<IdentityResult> AddToRoleAsync(string userId, string role, string companyId)
{
return base.AddToRoleAsync(userId, role);
}
#region overrides for unit tests
public override Task<bool> CheckPasswordAsync(ApplicationUser user, string password)
{
return base.CheckPasswordAsync(user, password);
}
public override Task<ApplicationUser> FindByNameAsync(string userName)
{
return base.FindByNameAsync(userName);
}
#endregion
}
public class ApplicationRoleManager : RoleManager<ApplicationRole>
{
public ApplicationRoleManager(IRoleStore<ApplicationRole, string> roleStore)
: base(roleStore)
{
}
public static ApplicationRoleManager Create(
IdentityFactoryOptions<ApplicationRoleManager> options,
IOwinContext context)
{
return new ApplicationRoleManager(
new ApplicationRoleStore(context.Get<ApplicationDbContext>()));
}
}
First of all, I would like to say thanks for taking it this far. It gave me a great start for my multi-tenant roles solution. I'm not sure if I'm 100% right, but this works for me.
Firstly, you cannot override any of the "RoleAsync" methods, but you can overload them. Secondly, the UserStore has a property called "Context" which can be set to your DbContext.
I had to overload the "RoleAsyc" methods in both my UserStore and UserManager extended classes. Here is an example from each to get you going:
MyUserStore
public class MyUserStore : UserStore<MyUser, MyRole, String, IdentityUserLogin, MyUserRole, IdentityUserClaim> {
public MyUserStore(MyDbContext dbContext) : base(dbContext) { }
public Task AddToRoleAsync(MyUser user, MyCompany company, String roleName) {
MyRole role = null;
try
{
role = Context.Set<MyRole>().Where(mr => mr.Name == roleName).Single();
}
catch (Exception ex)
{
throw ex;
}
Context.Set<MyUserRole>().Add(new MyUserRole {
Company = company,
RoleId = role.Id,
UserId = user.Id
});
return Context.SaveChangesAsync();
}
}
MyUserManager
public class MyUserManager : UserManager<MyUser, String>
{
private MyUserStore _store = null;
public MyUserManager(MyUserStore store) : base(store)
{
_store = store;
}
public Task<IList<String>> GetRolesAsync(String userId, int companyId)
{
MyUser user = _store.Context.Set<MyUser>().Find(new object[] { userId });
MyCompany company = _store.Context.Set<MyCompany>().Find(new object[] { companyId });
if (null == user)
{
throw new Exception("User not found");
}
if (null == company)
{
throw new Exception("Company not found");
}
return _store.GetRolesAsync(user, company);
}
}
From here a couple scary things happen and I don't know a better way to manage them.
The User "IsInRole" method in the HttpContext will work but it will not be tenant-sensitive so you can no longer use it.
If you use the "Authorize" attribute, the same idea for "scary thing 1" applies, but here you can just extend it and make things happy for your system. Example below:
MyAuthorizeAttribute
public class MyAuthorizeAttribute : AuthorizeAttribute {
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (null == httpContext)
{
throw new ArgumentNullException("httpContext");
}
HttpSessionStateBase session = httpContext.Session;
IList<String> authorizedRoleNames = Roles.Split(',').Select(r => r.Trim()).ToList();
if (!httpContext.User.Identity.IsAuthenticated)
{
return false;
}
if (null == session["MyAuthorize.CachedUsername"])
{
session["MyAuthorize.CachedUsername"] = String.Empty;
}
if (null == session["MyAuthorize.CachedCompanyId"])
{
session["MyAuthorize.CachedCompanyId"] = -1;
}
if (null == session["MyAuthorize.CachedUserCompanyRoleNames"])
{
session["MyAuthorize.CachedUserCompanyRoleNames"] = new List<String>();
}
String cachedUsername = session["MyAuthorize.CachedUsername"].ToString();
int cachedCompanyId = (int)session["MyAuthorize.CachedCompanyId"];
IList<String> cachedUserAllRoleNames = (IList<String>)session["MyAuthorize.CachedUserAllRoleNames"];
IPrincipal currentUser = httpContext.User;
String currentUserName = currentUser.Identity.Name;
int currentCompanyId = (int)session["CurrentCompanyId"];//Get this your own way! I used the Session in the HttpContext.
using (MyDbContext db = MyDbContext.Create())
{
try
{
MyUser mUser = null;
ICollection<String> tmpRoleIds = new List<String>();
if (cachedUsername != currentUserName)
{
session["MyAuthorize.CachedUsername"] = cachedUsername = String.Empty;
//Reload everything
mUser = db.Users.Where(u => u.Username == currentUserName).Single();
session["MyAuthorize.CachedUsername"] = currentUserName;
session["MyAuthorize.CachedCompanyId"] = cachedCompanyId = -1; //Force Company Reload
cachedUserCompanyRoleNames.Clear();
}
if (cachedUserCompanyRoleNames.Count != db.Users.Where(u => u.Username == currentUserName).Single().Roles.Select(r => r.RoleId).ToList().Count)
{
cachedUserCompanyRoleNames.Clear();
if (0 < currentCompanyId)
{
if(null == mUser)
{
mUser = db.Users.Where(u => u.Username == cachedUsername).Single();
}
tmpRoleIds = mUser.Roles.Where(r => r.Company.Id == currentCompanyId).Select(r => r.RoleId).ToList();
session["MyAuthorize.CachedUserCompanyRoleNames"] = cachedUserCompanyRoleNames = db.Roles.Where(r => tmpRoleIds.Contains(r.Id)).Select(r => r.Name).ToList();
session["MyAuthorize.CachedCompanyId"] = cachedCompanyId = currentCompanyId;
}
}
if (cachedCompanyId != currentCompanyId)
{
cachedUserCompanyRoleNames.Clear();
//Reload company roles
if (0 < currentCompanyId)
{
if(null == mUser)
{
mUser = db.Users.Where(u => u.Username == cachedUsername).Single();
}
tmpRoleIds = mUser.Roles.Where(r => r.Company.Id == currentCompanyId).Select(r => r.RoleId).ToList();
session["MyAuthorize.CachedUserCompanyRoleNames"] = cachedUserCompanyRoleNames = db.Roles.Where(r => tmpRoleIds.Contains(r.Id)).Select(r => r.Name).ToList();
session["MyAuthorize.CachedCompanyId"] = cachedCompanyId = currentCompanyId;
}
}
}
catch (Exception ex)
{
return false;
}
}
if (0 >= authorizedRoleNames.Count)
{
return true;
}
else
{
return cachedUserCompanyRoleNames.Intersect(authorizedRoleNames).Any();
}
}
}
In closing, as I said, I'm not sure if this is the best way to do it, but it works for me. Now, throughout your system, make sure you used your overloaded methods when dealing with Roles. I am also thinking about caching the Roles in a MVC BaseController that I wrote so that I can get similar functionality to User.IsInRole in all of my MVC Views.

Resources