Really new to C#, ASP.NET MVC and FluentValidation.
i have a user model like:
public class UserDetails{
public int ID { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
}
for now, i've been validating the UserName and Email using FluentValidation, something like:
public AdminDetailsValidator(){
RuleFor(ad => ad.UserName).NotNull().Must(UniqueUserName(UserName)).WithMessage("UserName not Available");
RuleFor(ad => ad.Email).NotNull().Must(UniqueEmail(Email)).WithMessage("This Email id has already been registered"); ;
}
public bool UniqueUserName(string un)
{
if (UserDbContext.userDetails.SingleOrDefault(p => p.UserName == un) == null)
{
return true;
}
else
{
return false;
}
}
public bool UniqueEmail(string em)
{
if (UserDbContext.userDetails.SingleOrDefault(p => p.Email == em) == null)
{
return true;
}
else
{
return false;
}
}
But i'd rather want a more generic UniqueValidator, that i can use with multiple classes and properties. Or Atleast, i don't have to make a separate function for each property. So i looked into the custom validators. But i have no idea, how i can use that feature for my needs.
I want to do something like this:
RuleFor(ad => ad.Email).NotNull().SetValidator(new UniquePropertyValidator<UserDbContext>(userDetails.Email).WithMessage("This Email id has already been registered");
Is that even possible to do that? I want to pass the DbContext as type parameter and property as an argument(or some variation of it, whichever works). and the method can check the property against the table and return if it's unique or not.
Have you looked into using lambdas and generics? I haven't used FluentValidation so this might not be the correct method for a validator.
var dbContext = new UserDbContext();
RuleFor(ud => ud.Email)
.NotNull()
.SetValidator(
new UniquePropertyValidator<UserDetails>
(ud, ud => ud.Email, () => dbcontext.userDetails)
.WithMessage("This Email id has already been registered");
public class UniquePropertyValidator<T> {
public UniquePropertyValidator(T entity, Func<T,string> propertyAccessorFunc, Func<IEnumerable<T>> collectionAccessorFunc) {
_entity = entity;
_propertyAccessorFunc = propertyAccessorFunc;
_collectionAccessorFunc =collectionAccessorFunc;
}
public bool Validate(){
//Get all the entities by executing the lambda
var entities = _collectionAccessorFunc();
//Get the value of the entity that we are validating by executing the lambda
var propertyValue = _propertyAccessorFunc(_entity);
//Find the matching entity by executing the propertyAccessorFunc against the
//entities in the collection and comparing that with the result of the entity
//that is being validated. Warning SingleOrDefault will throw an exception if
//multiple items match the supplied predicate
//http://msdn.microsoft.com/en-us/library/vstudio/bb342451%28v=vs.100%29.aspx
var matchingEntity = entities.SingleOrDefault(e => _propertyAccessorFunc(e) == propertyValue);
return matchingEntity == null;
}
}
I have also been trying to find an elegant solution for this validator, but the solution provided so far seems to fetch all the data and then check for uniqueness. That is not very good in my opinion.
When trying to use the implementation proposed below, I get an error that LINQ to Entities does not support Invoke (i.e. executing a Func<> inside the Where clause). Is there any workaround?
public class UniqueFieldValidator<TObject, TViewModel, TProperty> : PropertyValidator where TObject : Entity where TViewModel : Entity
{
private readonly IDataService<TObject> _dataService;
private readonly Func<TObject, TProperty> _property;
public UniqueFieldValidator(IDataService<TObject> dataService, Func<TObject, TProperty> property)
: base("La propiedad {PropertyName} tiene que ser unica.")
{
_dataService = dataService;
_property = property;
}
protected override bool IsValid(PropertyValidatorContext context)
{
var model = context.Instance as TViewModel;
var value = (TProperty)context.PropertyValue;
if (model != null && _dataService.Where(t => t.Id != model.Id && Equals(_property(t), value)).Any())
{
return false;
}
return true;
}
}
public class ArticuloViewModelValidator : AbstractValidator<ArticuloViewModel>
{
public ArticuloViewModelValidator(IDataService<Articulo> articuloDataService)
{
RuleFor(a => a.Codigo).SetValidator(new UniqueFieldValidator<Articulo, ArticuloViewModel, int>(articuloDataService, a => a.Codigo));
}
}
We can solve this problem simply by working with LINQ to Entities.
Here is a static method used to determine whether the given value is unique in the specified DbSet:
static class ValidationHelpers
{
/// <summary>
/// Determines whether the specified <paramref name="newValue"/> is unique inside of
/// the given <paramref name="dbSet"/>.
/// </summary>
/// <param name="dbSet"></param>
/// <param name="getColumnSelector">
/// Determines the column, with which we will compare <paramref name="newValue"/>
/// </param>
/// <param name="newValue">
/// Value, that will be checked for uniqueness
/// </param>
/// <param name="cancellationToken"></param>
/// <typeparam name="TEntity"></typeparam>
/// <typeparam name="TColumn"></typeparam>
/// <returns></returns>
public static async Task<bool> IsColumnUniqueInsideOfDbSetAsync<TEntity, TColumn>(DbSet<TEntity> dbSet,
Expression<Func<TEntity, TColumn>> getColumnSelector,
TColumn newValue,
CancellationToken cancellationToken)
where TEntity : class
{
return !await dbSet
.Select(getColumnSelector)
.AnyAsync(column => column.Equals(newValue), cancellationToken);
}
}
Example of using
We have the following entity:
public class Category
{
// ...
public string Title { get; set; }
// ...
}
And a DbContext class:
public interface ApplicationDbContext
{
// ...
public DbSet<Category> Category { get; set; }
// ...
}
Let's say a user wants to create a new category. We want to validate the title of this category for uniqueness:
RuleFor(c => c.Title)
.MustAsync
(
(newTitle, token) => ValidationHelpers.IsColumnUniqueInsideOfDbSetAsync
(_context.Category, c => c.Title, newTitle, token)
)
.WithMessage("{PropertyName} must be unique");
Note: _context is an object of type ApplicationDbContext.
Related
I want validate a request model with some ids. I try to preload all required data with a bulk request.
The problem is the RuleForEach inside my WhereAsync is called before the LoadUserGroupsAsync is done or started. I start the validation with TestValidateAsync(request).
Is there a better solution for this I have unfortunately not found any solutions for it. Also I have no access to the model from outside a RuleFor, RuleForEach, Where, ...
private readonly List<UserGroup> _userGroups;
WhenAsync(async (request, cancellationToken) => await this.LoadUserGroupsAsync(request.Items, cancellationToken), () =>
{
RuleForEach(o => o.Items).SetValidator(new UserUpdateValidator(this._userGroups));
});
private async Task<bool> LoadUserGroupsAsync(UserUpdateDto[] userUpdates, CancellationToken cancellationToken)
{
var ids = userUpdates.Select(o => o.userGroupId);
this._userGroups = await this._userGroupService.GetByIdsAsync(ids, cancellationToken);
return true;
}
public class UserUpdateValidator : AbstractValidator<UserUpdateDto>
{
public UserUpdateValidator(
UserGroup[] groups)
{
RuleFor(item => item.UserGroupId).Must(userGroupId =>
{
var group = groups.SingleOrDefault(o => o.Id == userGroupId);
if (group == null)
{
return false;
}
return true;
}).WithMessage("Group is invalid");
RuleFor(item => item.UserGroupId).Must(userGroupId =>
{
var group = groups.SingleOrDefault(o => o.Id == userGroupId);
return group.Active;
}).WithMessage("Group is inactive");
RuleFor(item => item.Password).Must((context, password) =>
{
var group = groups.SingleOrDefault(o => o.Id == context.UserGroupId);
if (group.Permissions.Contains("AllowPasswordChange"))
{
return true;
}
return false;
}).WithMessage("It is now allowed to change the password for your user");
}
}
Update 2021-04-28 - Add more Informations to example
You can use lazy loading and a wrapper object for the users. This would require calling sync methods instead of async methods to load the users, however.
You could use a Lazy<IEnumerable<User>> object, but that would probably require refactoring your child validator. I like creating a wrapper class for Lazy<IEnumerable<User>> just to make the code backwards-compatible for any other code accepting IEnumerable<T> objects:
/// <summary>
/// Represents a lazy loaded enumerable of type T
/// </summary>
public class LazyEnumerable<T> : IEnumerable<T>
{
private readonly Lazy<IEnumerable<T>> items;
/// <summary>
/// Initializes a new lazy loaded enumerable.
/// </summary>
/// <param name="itemFactory">A lambda expression that returns the items to be iterated over.</param>
public LazyEnumerable(Func<IEnumerable<T> itemFactory)
{
items = new Lazy<T>>(itemFactory);
}
/// <summary>
/// Initializes a new lazy loaded enumerable.
/// </summary>
/// <param name="itemFactory">The Lazy<T> object used to lazily retrieve the items to be iterated over.</param>
public LazyEnumerable(Lazy<IEnumerable<T>> items)
{
this.items = items;
}
public IEnumerator<T> GetEnumerator()
{
return items.Value.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return items.Value.GetEnumerator();
}
}
This is actually a general purpose class that can be used for any type. I wish .NET had this class in its base library, to be honest. I tend to copy and paste this in to every project, because it is so easy to use. In any event...
Then modify your validator class. Since you did not post enough code for your validator, I took a few guesses about the names of things and structure of your code:
public class YourValidator : AbstractValidator<X>
{
private UserService _userService;
private IEnumerable<UserGroup> _userGroups;
public YourValidator(UserService userService)
{
_userService = userService;
When((request, cancellationToken) => PreloadUserGroups(request.Items, cancellationToken), () =>
{
RuleForEach(o => o.Items).SetValidator(new UserUpdateValidator(_userGroups));
});
}
private bool PreloadUserGroups(UserUpdateDto[] userUpdates, CancellationToken cancellationToken)
{
var ids = userUpdates.Select(o => o.userGroupId);
_userGroups = new LazyEnumerable<UserGroup>(() => _userService.GetByIds(ids, cancellationToken));
return true;
}
}
This will lazy load the users, and since you pass the same object to all child validators it will load the users only once, regardless of how many times the collection is iterated.
Lastly, modify your child validator class to accept an IEnumerable<UserGroup> object instead of an array:
public class UserUpdateValidator : AbstractValidator<UserUpdateDto>
{
public UserUpdateValidator(IEnumerable<UserGroup> groups)
I've a method what obtains a list of Users filtering by an specific field. This method is useful for a lot of functionalities of my application, but in other cases is too slow.
The user's table have a field what contains data of pictures, so it's a heavy field. Now i'm developing a functionality what not need this field and i'm searching the way to don't return it, or return it as empty for streamline the process
I'm working with c#, obtaining a filteredList of Users from UnitOfWork repository with "GetByFilter" function.
UserController.cs
/// <summary>
/// Get by Filter
/// </summary>
/// <param name="filter">user filters</param>
/// <returns></returns>
[Route("functionRoute")]
[HttpPost]
public IHttpActionResult GetUsersByFilter([FromBody] UserFilter filter)
{
try
{
UserService service= new UserService ();
List<User> list = service.GetByFilter(filter).ToList();
List<UserCE> listCE = Mapper.Map<List<UserCE>>(list);
return Ok(listCE);
}
catch (Exception ex)
{
TraceManager.LogError(ex);
return InternalServerError(ex);
}
}
UserService.cs
public List<User> GetByFilter(UserFilter filter)
{
return _unitOfWork.UserRepository.GetByFilter(filter).ToList();
}
UserRepository.cs
public IQueryable<User> GetByFilter(UserFilter filter)
{
return Get_internal(filter);
}
private IQueryable<User> Get_internal(UserFilter filter)
{
IQueryable<User> users = _context.Users;
if (filter.Deleted != null)
{
users = users.Where(u => u.Deleted == filter.Deleted );
}
return users;
}
I try to clear column later, but the process continues being too heavy. How i can streamline this process?
I would change your Get_internal() function like so:
(have not tested this, I wrote the code purely from memory, but it should probably work with a few minor tweaks)
private IQueryable<User> Get_internal(UserFilter filter)
{
IQueryable<User> users = _context.Users;
if (filter.Deleted != null)
{
users = users.Where(u => u.Deleted == filter.Deleted );
}
return users.Select(x => new User() {
Id = x.Id,
Name = x.Name,
//every property except your image property
});
}
This will return a new User object for every row, but will only fill the properties you are selecting explicitly. So if you don't select your image property, this will not get selected.
If you check the generated SQL, you should also see that this column is never selected.
By convention, all public properties with a getter and a setter will be included in the model.
To exclude properties you will need to either use Data Annotations or the Fluent API
Data Annotations:
public class User
{
public int Id { get; set; }
[NotMapped]
public string IgnoredField { get; set; }
}
Fluent API:
public class MyContext : DbContext
{
public DbSet<User> Users{ get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.Ignore(b => b.IgnoredField);
}
}
Finnaly after try some things, I decided to apply the solution proposed by Steven, but with few minor tweaks. It gave problems in "Get_Internal", so I decided (although it's not the best option) do it in the controller.
the code looks (more or less) like this:
/// <summary>
/// Get by Filter
/// </summary>
/// <param name="filter">user filters</param>
/// <returns></returns>
[Route("functionRoute")]
[HttpPost]
public IHttpActionResult GetUsersByFilter([FromBody] UserFilter filter)
{
try
{
UserService service= new UserService ();
List<User> list = service.GetByFilter(filter).Select(x => new User() {
Id = x.Id,
Name = x.Name,
//every property except your image property
}).ToList();
List<UserCE> listCE = Mapper.Map<List<UserCE>>(list);
return Ok(listCE);
}
catch (Exception ex)
{
TraceManager.LogError(ex);
return InternalServerError(ex);
}
}
All my entities extend BaseEntity which has those (relevant) properties:
namespace Sppd.TeamTuner.Core.Domain.Entities
{
public abstract class BaseEntity
{
/// <summary>
/// Unique identifier identifying a single instance of an entity.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Specifies when the entity instance has been created.
/// </summary>
public DateTime CreatedOnUtc { get; set; }
/// <summary>
/// Specifies by whom the entity instance has been created.
/// </summary>
public Guid CreatedById { get; set; }
/// <summary>
/// Specifies when the entity instance has been last updated.
/// </summary>
public DateTime ModifiedOnUtc { get; set; }
/// <summary>
/// Specifies by whom the entity instance has been last modified.
/// </summary>
public Guid ModifiedById { get; set; }
protected BaseEntity()
{
Id = Guid.NewGuid();
}
}
}
I want to let ef set the created/modified properties before saving. For this, I've added following when configuring the DbContext:
private void ConfigureBaseEntity<TEntity>(EntityTypeBuilder<TEntity> builder)
where TEntity : BaseEntity
{
// Constraints
builder.Property(e => e.CreatedOnUtc)
.HasDefaultValueSql(_databaseConfig.Value.SqlUtcDateGetter)
.ValueGeneratedOnAdd();
builder.Property(e => e.ModifiedOnUtc)
.HasDefaultValueSql(_databaseConfig.Value.SqlUtcDateGetter)
.ValueGeneratedOnAddOrUpdate()
.IsConcurrencyToken();
builder.Property(e => e.CreatedById)
.HasValueGenerator<CurrentUserIdValueGenerator>()
.ValueGeneratedOnAdd();
builder.Property(e => e.ModifiedById)
.HasValueGenerator<CurrentUserIdValueGenerator>()
.ValueGeneratedOnAddOrUpdate();
}
And this ValueGenerator:
internal class CurrentUserIdValueGenerator : ValueGenerator<Guid>
{
public override bool GeneratesTemporaryValues => false;
public override Guid Next(EntityEntry entry)
{
return GetCurrentUser(entry).Id;
}
private static ITeamTunerUser GetCurrentUser(EntityEntry entry)
{
var userProvider = entry.Context.GetService<ITeamTunerUserProvider>();
if (userProvider.CurrentUser != null)
{
return userProvider.CurrentUser;
}
if (entry.Entity is ITeamTunerUser user)
{
// Special case for user creation: The user creates himself and thus doesn't exist yet. Use him as the current user.
return user;
}
throw new BusinessException("CurrentUser not defined");
}
}
When persisting the changes by calling SaveChanges() on the DbContext, I get following exception:
Microsoft.EntityFrameworkCore.DbUpdateException
HResult=0x80131500
Message=An error occurred while updating the entries. See the inner exception for details.
Source=Microsoft.EntityFrameworkCore.Relational
StackTrace:
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection)
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(DbContext _, ValueTuple`2 parameters)
at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(IEnumerable`1 commandBatches, IRelationalConnection connection)
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChanges(IReadOnlyList`1 entries)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IReadOnlyList`1 entriesToSave)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
at Sppd.TeamTuner.Infrastructure.DataAccess.EF.TeamTunerContext.SaveChanges(Boolean acceptAllChangesOnSuccess) in E:\dev\Sppd.TeamTuner\Backend\Sppd.TeamTuner.DataAccess.EF\TeamTunerContext.cs:line 48
Inner Exception 1:
SqlException: Cannot insert the value NULL into column 'ModifiedById', table 'Sppd.TeamTuner-DEV.dbo.CardType'; column does not allow nulls. UPDATE fails.
The statement has been terminated.
When checking the contents of the ChangeTracker all entities have the ModifiedById set:
Side note: The ToList() are required, otherwise it didn't enumerate correctly
Beside the fact that the IDs contain the value I expect, the ModifiedById property is not Nullable and thus should never be null (it might contain default(Guid)).
Any idea what's going on?
[Edit] Code to add:
Seeder:
internal class CardTypeDbSeeder : IDbSeeder
{
private readonly IRepository<CardType> _cardTypeRepository;
public CardTypeDbSeeder(IRepository<CardType> cardTypeRepository)
{
_cardTypeRepository = cardTypeRepository;
}
public int Priority => SeederConstants.Priority.BASE_DATA;
public void Seed()
{
_cardTypeRepository.Add(new CardType
{
Id = Guid.Parse(TestingConstants.CardType.ASSASSIN_ID),
Name = "Assassin"
});
}
[...]
}
Repository:
namespace Sppd.TeamTuner.Infrastructure.DataAccess.EF.Repositories
{
internal class Repository<TEntity> : IRepository<TEntity>
where TEntity : BaseEntity
{
protected DbSet<TEntity> Set => Context.Set<TEntity>();
protected TeamTunerContext Context { get; }
protected virtual Func<IQueryable<TEntity>, IQueryable<TEntity>> Includes { get; } = null;
public Repository(TeamTunerContext context)
{
Context = context;
}
public async Task<TEntity> GetAsync(Guid entityId)
{
TEntity entity;
try
{
entity = await GetQueryWithIncludes().SingleAsync(e => e.Id == entityId);
}
catch (InvalidOperationException)
{
throw new EntityNotFoundException(typeof(TEntity), entityId.ToString());
}
return entity;
}
public async Task<IEnumerable<TEntity>> GetAllAsync()
{
return await GetQueryWithIncludes().ToListAsync();
}
public void Delete(Guid entityId)
{
var entityToDelete = GetAsync(entityId);
entityToDelete.Wait();
Set.Remove(entityToDelete.Result);
}
public void Add(TEntity entity)
{
Set.Add(entity);
}
public void Update(TEntity entity)
{
Set.Update(entity);
}
protected IQueryable<TEntity> GetQueryWithIncludes()
{
return Includes == null
? Set
: Includes(Set);
}
}
}
Commit the changes:
if (isNewDatabase)
{
s_logger.LogDebug($"New database created. Seed data for SeedMode={databaseConfig.SeedMode}");
foreach (var seeder in scope.ServiceProvider.GetServices<IDbSeeder>().OrderBy(seeder => seeder.Priority))
{
seeder.Seed();
s_logger.LogDebug($"Seeded {seeder.GetType().Name}");
}
// The changes are usually being saved by a unit of work. Here, while starting the application, we will do it on the context itself.
context.SaveChanges();
}
As discussed in the comments and a lot of browsing in GitHub issues, it turned out that it isn't possible to use value generators for this. I've solved it by implementing a PrepareSaveChanges() in an override of Datacontext.SaveChanges which calls following code:
private void SetModifierMetadataProperties(EntityEntry<BaseEntity> entry, DateTime saveDate)
{
var entity = entry.Entity;
var currentUserId = GetCurrentUser(entry).Id;
if (entity.IsDeleted)
{
entity.DeletedById = currentUserId;
entity.DeletedOnUtc = saveDate;
return;
}
if (entry.State == EntityState.Added)
{
entity.CreatedById = currentUserId;
entity.CreatedOnUtc = saveDate;
}
entity.ModifiedById = currentUserId;
entity.ModifiedOnUtc = saveDate;
}
For the full implementation, follow the exectution path from the override of SaveChangesAsync
I am using IdentityServer4 in .Net Core 2.0 and I am successfully generating access tokens and refresh tokens. I just need to be able to "see" the refresh token on the server side when it's being generated, so that I can save it in a database for some specific purposes.
How can I access the refresh token's value while it is being generated on the server?
According to the comments, I think that this will be a useful solution for you, and for others with your case.
I'm starting with the things around IdentityServer itself. As it is highly recommended to use your own PersistedGrant store for production environments we need to override the default one.
First - in the Startup.cs:
services.AddTransient<IPersistedGrantStore, PersistedGrantStore>();
This will implement their IPersistedGrantStore interface, with our own PersistedGrantStore class.
The class itself:
public class PersistedGrantStore : IPersistedGrantStore
{
private readonly ILogger logger;
private readonly IPersistedGrantService persistedGrantService;
public PersistedGrantStore(IPersistedGrantService persistedGrantService, ILogger<PersistedGrantStore> logger)
{
this.logger = logger;
this.persistedGrantService = persistedGrantService;
}
public Task StoreAsync(PersistedGrant token)
{
var existing = this.persistedGrantService.Get(token.Key);
try
{
if (existing == null)
{
logger.LogDebug("{persistedGrantKey} not found in database", token.Key);
var persistedGrant = token.ToEntity();
this.persistedGrantService.Add(persistedGrant);
}
else
{
logger.LogDebug("{persistedGrantKey} found in database", token.Key);
token.UpdateEntity(existing);
this.persistedGrantService.Update(existing);
}
}
catch (DbUpdateConcurrencyException ex)
{
logger.LogWarning("exception updating {persistedGrantKey} persisted grant in database: {error}", token.Key, ex.Message);
}
return Task.FromResult(0);
}
public Task<PersistedGrant> GetAsync(string key)
{
var persistedGrant = this.persistedGrantService.Get(key);
var model = persistedGrant?.ToModel();
logger.LogDebug("{persistedGrantKey} found in database: {persistedGrantKeyFound}", key, model != null);
return Task.FromResult(model);
}
public Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId)
{
var persistedGrants = this.persistedGrantService.GetAll(subjectId).ToList();
var model = persistedGrants.Select(x => x.ToModel());
logger.LogDebug("{persistedGrantCount} persisted grants found for {subjectId}", persistedGrants.Count, subjectId);
return Task.FromResult(model);
}
public Task RemoveAsync(string key)
{
var persistedGrant = this.persistedGrantService.Get(key);
if (persistedGrant != null)
{
logger.LogDebug("removing {persistedGrantKey} persisted grant from database", key);
try
{
this.persistedGrantService.Remove(persistedGrant);
}
catch (DbUpdateConcurrencyException ex)
{
logger.LogInformation("exception removing {persistedGrantKey} persisted grant from database: {error}", key, ex.Message);
}
}
else
{
logger.LogDebug("no {persistedGrantKey} persisted grant found in database", key);
}
return Task.FromResult(0);
}
public Task RemoveAllAsync(string subjectId, string clientId)
{
var persistedGrants = this.persistedGrantService.GetAll(subjectId, clientId);
logger.LogDebug("removing {persistedGrantCount} persisted grants from database for subject {subjectId}, clientId {clientId}", persistedGrants.Count(), subjectId, clientId);
try
{
this.persistedGrantService.RemoveAll(persistedGrants);
}
catch (DbUpdateConcurrencyException ex)
{
logger.LogInformation("removing {persistedGrantCount} persisted grants from database for subject {subjectId}, clientId {clientId}: {error}", persistedGrants.Count(), subjectId, clientId, ex.Message);
}
return Task.FromResult(0);
}
public Task RemoveAllAsync(string subjectId, string clientId, string type)
{
var persistedGrants = this.persistedGrantService.GetAll(subjectId, clientId, type);
logger.LogDebug("removing {persistedGrantCount} persisted grants from database for subject {subjectId}, clientId {clientId}, grantType {persistedGrantType}", persistedGrants.Count(), subjectId, clientId, type);
try
{
this.persistedGrantService.RemoveAll(persistedGrants);
}
catch (DbUpdateConcurrencyException ex)
{
logger.LogInformation("exception removing {persistedGrantCount} persisted grants from database for subject {subjectId}, clientId {clientId}, grantType {persistedGrantType}: {error}", persistedGrants.Count(), subjectId, clientId, type, ex.Message);
}
return Task.FromResult(0);
}
}
As you can see in it I have an interface and the logger.
The IPersistedGrantService interface:
public interface IPersistedGrantService
{
void Add(PersistedGrantInfo persistedGrant);
void Update(PersistedGrantInfo existing);
PersistedGrantInfo Get(string key);
IEnumerable<PersistedGrantInfo> GetAll(string subjectId);
IEnumerable<PersistedGrantInfo> GetAll(string subjectId, string clientId);
IEnumerable<PersistedGrantInfo> GetAll(string subjectId, string clientId, string type);
void Remove(PersistedGrantInfo persistedGrant);
void RemoveAll(IEnumerable<PersistedGrantInfo> persistedGrants);
}
As you can see, There is an object called PersistedGrantInfo. This is my DTO that I use for the mapping between the db entity, and the IDS4 entity (you are not forced to use it, but I'm doing it for a better abstraction).
This Info object is mapped to the IDS4 entity with AutoMapper:
public static class PersistedGrantMappers
{
internal static IMapper Mapper { get; }
static PersistedGrantMappers()
{
Mapper = new MapperConfiguration(cfg => cfg.AddProfile<PersistedGrantMapperProfile>())
.CreateMapper();
}
/// <summary>
/// Maps an entity to a model.
/// </summary>
/// <param name="entity">The entity.</param>
/// <returns></returns>
public static PersistedGrant ToModel(this PersistedGrantInfo entity)
{
return entity == null ? null : Mapper.Map<PersistedGrant>(entity);
}
/// <summary>
/// Maps a model to an entity.
/// </summary>
/// <param name="model">The model.</param>
/// <returns></returns>
public static PersistedGrantInfo ToEntity(this PersistedGrant model)
{
return model == null ? null : Mapper.Map<PersistedGrantInfo>(model);
}
/// <summary>
/// Updates an entity from a model.
/// </summary>
/// <param name="model">The model.</param>
/// <param name="entity">The entity.</param>
public static void UpdateEntity(this PersistedGrant model, PersistedGrantInfo entity)
{
Mapper.Map(model, entity);
}
}
And the mapper profile:
public class PersistedGrantMapperProfile:Profile
{
/// <summary>
/// <see cref="PersistedGrantMapperProfile">
/// </see>
/// </summary>
public PersistedGrantMapperProfile()
{
CreateMap<PersistedGrantInfo, IdentityServer4.Models.PersistedGrant>(MemberList.Destination)
.ReverseMap();
}
}
Going back to the IPersistedGrantService - the implementation is up to you. Currently as a DB entity I have an exact copy of the IDS4 entity:
public class PersistedGrant
{
[Key]
public string Key { get; set; }
public string Type { get; set; }
public string SubjectId { get; set; }
public string ClientId { get; set; }
public DateTime CreationTime { get; set; }
public DateTime? Expiration { get; set; }
public string Data { get; set; }
}
But according to your needs, you can do something different (store this data in different table, use different column names etc.). Then in my service implementation, I'm just using the data that comes from the `IPersistedGrantStore' implementation, and I'm CRUD-ing the entities in my db context.
As a conclusion - the main thing here is to override\implement their IPersistedGrantStore interface according to your needs. Hope that this helps.
I'm trying to apply [BsonRepresentation(BsonType.ObjectId)] to all ids represented as strings withoput having to decorate all my ids with the attribute.
I tried adding the StringObjectIdIdGeneratorConvention but that doesn't seem to sort it.
Any ideas?
Yeah, I noticed that, too. The current implementation of the StringObjectIdIdGeneratorConvention does not seem to work for some reason. Here's one that works:
public class Person
{
public string Id { get; set; }
public string Name { get; set; }
}
public class StringObjectIdIdGeneratorConventionThatWorks : ConventionBase, IPostProcessingConvention
{
/// <summary>
/// Applies a post processing modification to the class map.
/// </summary>
/// <param name="classMap">The class map.</param>
public void PostProcess(BsonClassMap classMap)
{
var idMemberMap = classMap.IdMemberMap;
if (idMemberMap == null || idMemberMap.IdGenerator != null)
return;
if (idMemberMap.MemberType == typeof(string))
{
idMemberMap.SetIdGenerator(StringObjectIdGenerator.Instance).SetSerializer(new StringSerializer(BsonType.ObjectId));
}
}
}
public class Program
{
static void Main(string[] args)
{
ConventionPack cp = new ConventionPack();
cp.Add(new StringObjectIdIdGeneratorConventionThatWorks());
ConventionRegistry.Register("TreatAllStringIdsProperly", cp, _ => true);
var collection = new MongoClient().GetDatabase("test").GetCollection<Person>("persons");
Person person = new Person();
person.Name = "Name";
collection.InsertOne(person);
Console.ReadLine();
}
}
You could programmatically register the C# class you intend to use to represent the mongo document. While registering you can override default behaviour (e.g map id to string):
public static void RegisterClassMap<T>() where T : IHasIdField
{
if (!BsonClassMap.IsClassMapRegistered(typeof(T)))
{
//Map the ID field to string. All other fields are automapped
BsonClassMap.RegisterClassMap<T>(cm =>
{
cm.AutoMap();
cm.MapIdMember(c => c.Id).SetIdGenerator(StringObjectIdGenerator.Instance);
});
}
}
and later call this function for each of the C# classes you want to register:
RegisterClassMap<MongoDocType1>();
RegisterClassMap<MongoDocType2>();
Each class you want to register would have to implement the IHasIdField interface:
public class MongoDocType1 : IHasIdField
{
public string Id { get; set; }
// ...rest of fields
}
The caveat is that this is not a global solution and you still have to manually iterate over your classes.