Identity Server 4 - saving refresh token in datatabase - c#

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.

Related

When I saving an object, NHibernate tries to save object in database with id=0

I have NHibernate --version 2.1.2.4000 and use NHibernate.Mapping.Attributes
When i want to save object, like that
session.SaveOrUpdate(car);
session.Flush();
I have an exception "Cannot insert explicit value for identity column in table 'Car' when IDENTITY_INSERT is set to OFF." This happens when NHibernate tries set sql script with ID=0, like that
"exec sp_executesql N'INSERT INTO Car (...., Id) VALUES (.., #p12)',N'...,#p12 int', #p12=0 go"
My class has the following attribute
public class Car: ILifecycle {
[Generator(Class = "identity")]
[Id(0, Name = "Id", Column = "Id")]
public virtual int Id { get; set; }
public virtual LifecycleVeto OnDelete(ISession s);
public virtual void OnLoad(ISession s, object id);
public virtual LifecycleVeto OnSave(ISession s);
public virtual LifecycleVeto OnUpdate(ISession s);
}
Nhibernate helper has the following setting
public class NhibernateHelper {
private readonly string _configurePath;
/// <param name="path"></param>
public NhibernateHelper(string path) {
_configurePath = path;
}
/// <summary>The session factory.</summary>
private static ISessionFactory SessionFactory;
/// <summary>Gets current session.</summary>
/// <returns>The current session.</returns>
public static ISession GetCurrentSession() {
return SessionFactory.OpenSession();
}
/// <summary>Connects to database 1.</summary>
public void ConnectToDb() {
var configuration = new Configuration();
configuration.Configure(_configurePath);
HbmSerializer.Default.Validate = true;
Stream s = HbmSerializer.Default.Serialize(Assembly.GetExecutingAssembly());
configuration.AddInputStream(s);
var assembly = Assembly.Load("DAL");
s = HbmSerializer.Default.Serialize(assembly);
configuration.AddInputStream(s);
SessionFactory = configuration.BuildSessionFactory();
}
public void CloseSessionFactory() {
if (SessionFactory != null)
SessionFactory.Close();
}
}
I forgot about the numbers before the attributes.[Generator(1, Class = "identity")]

How to return an empty column with iQueryable

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);
}
}

Set default value when saving changes

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

Writing a generic FluentValidation custom validator to check unique constraint

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.

How to correctly update partially submitted (to a REST API) objects with LINQ to SQL

Our C# application communicates with a database through a LINQ-to-SQL Database Model, specifically using the MVC4 libraries.
I've been assigned the task of implementing a RESTful API. We thought it would be a good idea to version the API. That way, changes to the API can be introduced in a new version, and existing API clients won't break. To support this, every version of the API has its own set of Data Transfer Objects (DTOs) that it exposes and accepts. Some mapping is done (using AutoMapper) to translate between the API and Database Model.
Currently I'm working on updating and creating functionality. That is, if a client POSTs an Item object to the ItemsController, and the Item does not exist in the database yet (given its unique identifier), a new Item should be created. If the identifier is already present in the database, the existing data should be updated. So far so good.
Now, I'm converting a legacy code base to communicate with the RESTful API instead of with the database directly. Some parts of this codebase update a single property on a resource, and send just the identifier and the new value of that single property. The rest of the object should remain as it is in the database.
I'm having trouble implementing this using LINQ-to-SQL, specifically because of the DTO layer. This is the controller method:
[HttpPut]
[HttpPost]
public void UpdateOrCreateItem(ItemDTO data)
{
Item submittedItem = Map(data);
ItemRepository.UpdateOrCreateItem(submittedItem);
}
Now, instead of receiving a fully filled data object, only the identifier and one other property are filled. When the LINQ-to-SQL processes this data as follows:
public static void UpdateOrCreateItem(Item submittedItem)
{
if (submittedItem.Id > 0)
{
// update
using (DatabaseAccessor db = new DatabaseAccessor())
{
db.context.Items.Attach(submittedItem);
db.context.Refresh(RefreshMode.KeepCurrentValues, submittedItem);
db.context.SubmitChanges();
}
} else {
// create
// omitted...
}
}
Refreshing marks all the empty (missing) properties as changed, and its all saved to the database. Instead, only the properties that were submitted at the REST API level should be stored. What would be an elegant solution to this problem?
In the end I wrote some code to accept json-patch requests (see https://www.rfc-editor.org/rfc/rfc6902).
You need to add the media type "application/json-patch" to the collection of accepted formats.
You need to accept an identifier and an array of JsonPatchOperation objects as input to a HTTP PATCH method on your ApiController
The ApiController method:
[HttpPatch]
public void UpdatePartially(int id, JsonPatchOperation[] patchOperations)
{
if (id > 0)
{
// DatabaseAccessor is just a wrapper around my DataContext object
using (DatabaseAccessor db = new DatabaseAccessor())
{
SetDataLoadOptions(db); // optional of course
var item = db.context.Items.Single(i => i.id == id);
foreach (JsonPatchOperation patchOperation in patchOperations)
{
// when you want to set a foreign key identifier, LINQ-to-SQL throw a ForeignKeyReferenceAlreadyHasValueException
// the patchOperation will then use GetForeignKeyObject to fetch the object that it requires to set the foreign key object instead
patchOperation.GetForeignKeyObject = (PropertyInfo property, object identifier) =>
{
// this is just example code, make sure to correct this for the possible properties of your object...
if (property == typeof(Item).GetProperty("JobStatus", typeof(JobStatus)))
{
return db.context.JobStatus.Single(js => js.StatusId == (int)identifier);
}
else if (property == typeof(Item).GetProperty("User", typeof(User)))
{
return db.context.Users.Single(u => u.UserId == (Guid)identifier);
}
throw new ArgumentOutOfRangeException("property", String.Format("Missing getter for property '{0}'.", property.Name));
};
patchOperation.ApplyTo(item);
}
db.context.SubmitChanges();
}
}
}
And here are the dependencies of the above method:
/// <summary>
/// Add this to the global configuration Formatters collection to accept json-patch requests
/// </summary>
public class JsonPatchMediaTypeFormatter : JsonMediaTypeFormatter
{
public JsonPatchMediaTypeFormatter() : base()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json-patch"));
}
}
/// <summary>
/// All possible values for the "op" property of a json-patch object
/// docs: https://www.rfc-editor.org/rfc/rfc6902#section-4
/// </summary>
public enum JsonPatchOperationType
{
add,
remove,
replace,
move,
copy,
test
}
/// <summary>
/// json-patch is a partial update format for HTTP PATCH requests
/// docs: https://www.rfc-editor.org/rfc/rfc6902
/// </summary>
public class JsonPatchOperation
{
public string op { get; set; }
public string from { get; set; }
public string path { get; set; }
public string value { get; set; }
public Func<PropertyInfo, object, object> GetForeignKeyObject { get; set; }
public JsonPatchOperationType Operation
{
get
{
return (JsonPatchOperationType)Enum.Parse(typeof(JsonPatchOperationType), op);
}
}
public void ApplyTo(object document)
{
switch (Operation)
{
case JsonPatchOperationType.add:
Add(document, path, value);
break;
case JsonPatchOperationType.remove:
Remove(document, path);
break;
case JsonPatchOperationType.replace:
Replace(document, path, value);
break;
case JsonPatchOperationType.move:
Move(document, path, from);
break;
case JsonPatchOperationType.copy:
Copy(document, path, from);
break;
case JsonPatchOperationType.test:
Test(document, path, value);
break;
}
}
private void Add(object document, string path, string value)
{
Type documentType = document.GetType();
PathInfo pathInfo = GetPathInfo(documentType, path);
object convertedValue = ConvertToType(value, pathInfo.PropertyInfo.PropertyType);
pathInfo.PropertyInfo.SetValue(document, convertedValue, pathInfo.Indexes);
}
private void Replace(object document, string path, string value)
{
Type documentType = document.GetType();
PathInfo pathInfo = GetPathInfo(documentType, path);
object convertedValue = ConvertToType(value, pathInfo.PropertyInfo.PropertyType);
try
{
pathInfo.PropertyInfo.SetValue(document, convertedValue, pathInfo.Indexes);
}
// gnarly hack for setting foreign key properties
catch (TargetInvocationException tie)
{
if (tie.InnerException is ForeignKeyReferenceAlreadyHasValueException)
{
PropertyInfo matchingProperty = documentType.GetProperties().Single(p => p.GetCustomAttributes(typeof(AssociationAttribute), true).Any(attr => ((AssociationAttribute)attr).ThisKey == pathInfo.PropertyInfo.Name));
matchingProperty.SetValue(document, GetForeignKeyObject(matchingProperty, convertedValue), null);
}
else
{
throw tie;
}
}
}
private void Remove(object document, string path)
{
Type documentType = document.GetType();
PathInfo pathInfo = GetPathInfo(documentType, path);
pathInfo.PropertyInfo.SetValue(document, GetDefaultValue(pathInfo.PropertyInfo.PropertyType), pathInfo.Indexes);
}
private void Copy(object document, string path, string from)
{
throw new NotImplementedException();
}
private void Move(object document, string path, string from)
{
throw new NotImplementedException();
}
private void Test(object document, string path, string value)
{
throw new NotImplementedException();
}
#region Util
private class PathInfo
{
public PropertyInfo PropertyInfo { get; set; }
public object[] Indexes { get; set; }
}
private PathInfo GetPathInfo(Type documentType, string path)
{
object[] indexes = null;
PropertyInfo propertyInfo = documentType.GetProperty(path);
return new PathInfo { PropertyInfo = propertyInfo, Indexes = indexes };
}
private object GetDefaultValue(Type t)
{
if (t.IsValueType)
return Activator.CreateInstance(t);
return null;
}
private object ConvertToType(string value, Type type)
{
TypeConverter typeConverter = TypeDescriptor.GetConverter(type);
return typeConverter.ConvertFromString(value);
}
#endregion
}
It should be obvious that this is not finished, mature or elegant. But it works.

Categories

Resources