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
Related
We're using EF core and I just learned that using the DbContext as long lived object is not a good idea. The DbContext is not designed to be a long lived object.
So I now inject a new instance for every transaction, which I thought worked great. But now I see that child entities are not being populated after disposing the DbContext and creating a new one.
Some example code to show the problem
public InitialTestData(Func<IMachineRepository> machineRepositoryFactory)
{
{
using var machineRepository = machineRepositoryFactory();
if (!machineRepository.FindAll().Any())
{
machineRepository.Add("411-01", "https://localhost:5002/411-01");
machineRepository.Add("411-02", "https://localhost:5002/411-02");
machineRepository.Add("411-03", "https://localhost:5002/411-03");
machineRepository.Add("413-01", "https://localhost:5002/413-01");
machineRepository.Add("413-02", "https://localhost:5002/413-02");
machineRepository.Add("413-03", "https://localhost:5002/413-03");
machineRepository.Save();
foreach (var machineEntity in machineRepository.FindAll())
{
var machineStatusChangeEntity = new MachineStatusChange
{
DateTime = DateTime.Now,
State = MachineStateDataModel.Idle
};
machineEntity.StatusChanges.Add(machineStatusChangeEntity);
machineRepository.Save();
}
}
var bla = machineRepository.FindAll().ToList();
foreach (var machine in bla)
{
// machine entity has a list of one child entity as expected
}
}
{
using var machineRepository = machineRepositoryFactory();
var bla = machineRepository.FindAll().ToList();
foreach (var machine in bla)
{
// machine entity has zero child entities, why? it's present in the database
}
}
}
In the above example I (autofac) creates the machine repository twice. Each repository gets a DbContext injected. The first time, I add some entities and save the changes (persisted in database successfully). I can (obviously) still query on this repository, as everything is still cached as well.
But when I then recreate the machine repository for the second time, it is able to query the Machine entities (the "parent" entities) but it no longer is able to find the related child elements. As mentioned, they are stored in the database, I double checked the Guids of parent and childs, the all match perfectly. Yet EF code doesn't "remake" the relation somehow.
Am I missing something important in the OnModelConfiguring (posted below) or did I design the entities wrong?
See below the remaining classes that make the "database layer"
Repository pattern:
public class MachineRepository : RepositoryBase<Machine>, IMachineRepository
{
public MachineRepository(PatDatabase repositoryContext) : base(repositoryContext)
{
}
}
public abstract class RepositoryBase<T> : IRepository<T> where T : class
{
protected PatDatabase RepositoryContext { get; set; }
protected RepositoryBase(PatDatabase repositoryContext)
{
RepositoryContext = repositoryContext;
}
public IQueryable<T> FindAll() => RepositoryContext.Set<T>();
public IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression) =>
RepositoryContext.Set<T>().Where(expression);
public EntityEntry<T> Refresh(T entity) => RepositoryContext.Entry(entity);
public void Create(T entity) => RepositoryContext.Set<T>().Add(entity);
public void Update(T entity) => RepositoryContext.Set<T>().Update(entity);
public void Delete(T entity) => RepositoryContext.Set<T>().Remove(entity);
}
The two entity classes
[Table("Machine")]
public class Machine
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
public string BaseUrl { get; set; }
public ICollection<MachineStatusChange> StatusChanges { get; set; } = new List<MachineStatusChange>();
}
[Table("MachineStatusChange")]
public record MachineStatusChange
{
[Key]
public Guid Id { get; set; }
[ForeignKey(nameof(Machine))]
public Guid MachineId { get; set; }
public DateTime DateTime { get; set; }
public MachineStateDataModel State { get; set; }
public Machine Machine { get; set; }
}
The "PatDatabase" / DbContext
public class PatDatabase : DbContext
{
private readonly string _connectionString;
// TODO, discuss/investigate
// Any purpose to define DbSet<T> as they did in tutorial?
//public DbSet<Machine> Machines { get; set; }
/// <summary>
/// Used by dotnet ef migrations
/// NOTE: Autofac will always prefer the constructor with most arguments
/// </summary>
public PatDatabase()
{
var assembly = Assembly.GetExecutingAssembly();
using var stream = assembly.GetManifestResourceStream("IAI.ToolSuite.PAT.Server.appsettings.json");
var configuration = new ConfigurationBuilder().AddJsonStream(stream).Build();
// TODO BP: replace "LocalMySqlConnection" with "MySqlConnection" when figured out migrations in docker properly
// It seems that 'dotnet ef migrations remove' requires access to the database(??)
// In my case, my docker-compose is down, I would expect that's not a problem.... To be investigated...
_connectionString = configuration["MySqlConnection:connectionString"];
}
/// <summary>
/// Constructor used by autofac (autofac will always prefer constructor with most arguments if resolvable)
/// </summary>
public PatDatabase(IConfiguration configuration, IWebHostEnvironment env)
{
if (env.IsLocal())
{
_connectionString = configuration["MySqlConnection:connectionString"];
}
else
{
_connectionString = configuration["MySqlConnection:connectionString"];
}
}
/// <summary>
/// IMPORTANT
/// override the OnConfiguring allows us to keep a parameterless constructor
/// ef migrations tool requires a parameterless constructor
/// </summary>
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseMySql(_connectionString, MySqlServerVersion.LatestSupportedServerVersion,
builder =>
{
builder.EnableRetryOnFailure(20, TimeSpan.FromSeconds(10), null);
});
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Machine>().HasMany(m => m.StatusChanges);
modelBuilder.Entity<MachineStatusChange>()
.HasOne(m => m.Machine)
.WithMany(m => m.StatusChanges);
}
public void MigrateIfNeeded()
{
if (Database.GetPendingMigrations().Any())
{
Database.Migrate();
}
// I think calling ensure created before migrations can cause issues
// when migrations exist but the database or tables were dropped
Database.EnsureCreated();
}
}
Argh... I wasn't including the child entities at all after disposing it...
My apologies for wasting anybodies time.
The obvious fix
var bla = machineRepository.FindAll().Include(m => m.StatusChanges).ToList();
My actual problem which I had was this
using var machineRepository = _machineRepositoryFactory();
var machines = machineRepository
.FindAll();
// Here, it would've had been a GREAT idea to
// capture the resulting IQueryable return by `Include`
machines
.Include(m => m.StatusChanges)
// Iterating over machines now, will obviously not include the child entities.
So the fix...
machine = machines
.Include(m => m.StatusChanges)
Or even simpler of course...
using var machineRepository = _machineRepositoryFactory();
var machines = machineRepository
.FindAll()
.Include(m => m.StatusChanges);
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 attempting to implement the Unit of Work and Repository Pattern in my ASP.NET MVC app as described here.
I was receiving the following error:
Value cannot be null.
Parameter name: entitySet
during a query. After doing some debugging, I noticed that my DBSet<T> classes were throwing the following error:
{
"The context cannot be used while the model is being created. This exception may be
thrown if the context is used inside the OnModelCreating method or if the same context
instance is accessed by multiple threads concurrently. Note that instance members of
DbContext and related classes are not guaranteed to be thread safe."
}
System.SystemException { System.InvalidOperationException }
I have looked around Stack Overflow but cannot find a solution. I used the "Code First from Database Approach." I checked my connection string and it seems right. My version of Entity, as defined in the packages.config file, is 6.1.3. I have tried commenting out some of the relationships within the DbContext class. The definition of which is as follows:
public partial class BKTrainerContext : DbContext
{
public BKTrainerContext()
: base("name=BKTrainerContext")
{
}
public virtual DbSet<AccessLevel> AccessLevels { get; set; }
public virtual DbSet<BuildCardCategory> BuildCardCategories { get; set; }
public virtual DbSet<BuildCard> BuildCards { get; set; }
public virtual DbSet<Manager> Managers { get; set; }
public virtual DbSet<SliderImage> SliderImages { get; set; }
public virtual DbSet<StoreMessage> StoreMessages { get; set; }
public virtual DbSet<Store> Stores { get; set; }
public virtual DbSet<Video> Videos { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<AccessLevel>()
.HasMany(e => e.Managers)
.WithRequired(e => e.AccessLevel1)
.HasForeignKey(e => e.AccessLevel)
.WillCascadeOnDelete(false);
modelBuilder.Entity<BuildCardCategory>()
.HasMany(e => e.BuildCards)
.WithRequired(e => e.BuildCardCategory1)
.HasForeignKey(e => e.BuildCardCategory)
.WillCascadeOnDelete(false);
modelBuilder.Entity<Manager>()
.HasMany(e => e.StoreMessages)
.WithRequired(e => e.Manager)
.HasForeignKey(e => e.MessageAuthor)
.WillCascadeOnDelete(false);
modelBuilder.Entity<Manager>()
.HasMany(e => e.Stores)
.WithRequired(e => e.Manager)
.WillCascadeOnDelete(false);
modelBuilder.Entity<StoreMessage>()
.Property(e => e.MessageBody)
.IsUnicode(false);
modelBuilder.Entity<StoreMessage>()
.HasMany(e => e.Stores)
.WithMany(e => e.StoreMessages)
.Map(m => m.ToTable("MessagesStores").MapLeftKey("MessageID").MapRightKey("StoreID"));
}
}
This is my GenericRepo class
public class GenericRepo<T> : IGenericRepo<T> where T : class
{
internal BKTrainerContext dbContext;
internal DbSet<T> db;
public GenericRepo(BKTrainerContext context)
{
this.dbContext = context;
this.db = context.Set<T>();
}
public virtual IEnumerable<T> Get(Expression<Func<T, bool>> filter = null, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null, string includeProperties = "")
{
IQueryable<T> query = db;
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
}
public virtual T GetEntity(object id)
{
return db.Find(id);
}
public virtual void Insert(T newEntity)
{
db.Add(newEntity);
}
public virtual void Delete(object id)
{
T enity = db.Find(id);
Delete(enity);
}
public virtual void Delete(T entity)
{
if (dbContext.Entry(entity).State == EntityState.Detached)
{
db.Attach(entity);
}
db.Remove(entity);
}
public virtual void Update(T entity)
{
db.Attach(entity);
dbContext.Entry(entity).State = EntityState.Modified;
}
}
The original error is thrown by the lines within Get(); those that call a query.xx method.
My Unit of Work class is as follows:
public class UnitOfWork : IUnitOfWork
{
private BKTrainerContext dbContext = new BKTrainerContext();
private GenericRepo<BuildCard> buildCardRepo;
private GenericRepo<BuildCardCategory> buildCardCategoryRepo;
private GenericRepo<Manager> managerRepo;
private GenericRepo<StoreMessage> messageRepo;
private GenericRepo<SliderImage> sliderRepo;
private GenericRepo<Store> storeRepo;
private GenericRepo<Video> videoRepo;
private bool disposed = false;
public GenericRepo<BuildCard> BuildCardRepo
{
get
{
if (this.buildCardRepo == null) { buildCardRepo = new GenericRepo<BuildCard>(dbContext); }
return buildCardRepo;
}
}
public GenericRepo<BuildCardCategory> BuildCardCategoriesRepo
{
get
{
if (this.buildCardCategoryRepo == null) { buildCardCategoryRepo = new GenericRepo<BuildCardCategory>(dbContext); }
return buildCardCategoryRepo;
}
}
public GenericRepo<Manager> ManagerRepo
{
get
{
if (this.managerRepo == null) { managerRepo = new GenericRepo<Manager>(dbContext); }
return managerRepo;
}
}
public GenericRepo<StoreMessage> MessageRepo
{
get
{
if (this.messageRepo == null) { messageRepo = new GenericRepo<StoreMessage>(dbContext); }
return messageRepo;
}
}
public GenericRepo<SliderImage> SliderRepo
{
get
{
if (this.sliderRepo == null) { sliderRepo= new GenericRepo<SliderImage>(dbContext); }
return sliderRepo;
}
}
public GenericRepo<Store> StoreRepo
{
get
{
if (this.storeRepo == null) { storeRepo= new GenericRepo<Store>(dbContext); }
return storeRepo;
}
}
public GenericRepo<Video> VideoRepo
{
get
{
if (this.videoRepo == null) {videoRepo = new GenericRepo<Video>(dbContext); }
return videoRepo;
}
}
public void Save()
{
dbContext.SaveChanges();
}
protected virtual void Dispose(bool disposing) {
if (!this.disposed)
{
if (disposing)
{
dbContext.Dispose();
}
}
this.disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
I am using Unity to inject it into my Controller.
public class HomeController : Controller
{
private IUnitOfWork worker;
public HomeController(IUnitOfWork unit)
{
this.worker = unit;
}
public ActionResult Index(bool failedLogin = false)
{
IndexViewModel vm = new IndexViewModel();
vm.invalidLogin = failedLogin;
return View(vm);
}
[HttpPost]
public ActionResult AttemptLogin(string userName, string password)
{
List<Store> stores = worker.StoreRepo.Get().ToList();
Store store = worker.StoreRepo.Get(u => u.Username == userName && u.Password == password).FirstOrDefault();
if (stores != null)
{
Session["UserLevel"] = 0;
Session["UserID"] = store.ID;
return RedirectToAction("Index", "User");
}
else
{
Manager manager = worker.ManagerRepo.Get(m => m.Username == userName && m.Password == password).FirstOrDefault();
if (manager != null)
{
Session["UserLevel"] = manager.AccessLevel;
Session["UserID"] = manager.ID;
return RedirectToAction("Index", "Admin");
}
else
{
return RedirectToAction("Index", new { failedLogin = true });
}
}
}
}
I would really appreciate any help. Thanks!
EDIT:
Injection configuration:
public class UnityConfig
{
#region Unity Container
private static Lazy<IUnityContainer> container = new Lazy<IUnityContainer>(() =>
{
var container = new UnityContainer();
RegisterTypes(container);
return container;
});
/// <summary>
/// Gets the configured Unity container.
/// </summary>
public static IUnityContainer GetConfiguredContainer()
{
return container.Value;
}
#endregion
/// <summary>Registers the type mappings with the Unity container.</summary>
/// <param name="container">The unity container to configure.</param>
/// <remarks>There is no need to register concrete types such as controllers or API controllers (unless you want to
/// change the defaults), as Unity allows resolving a concrete type even if it was not previously registered.</remarks>
public static void RegisterTypes(IUnityContainer container)
{
// NOTE: To load from web.config uncomment the line below. Make sure to add a Microsoft.Practices.Unity.Configuration to the using statements.
// container.LoadConfiguration();
container.RegisterType<IUnitOfWork, UnitOfWork>();
// container.RegisterType<IProductRepository, ProductRepository>();
}
}
}
The UnityMVCActivator class
[assembly: WebActivatorEx.PreApplicationStartMethod(typeof(BKBuildCard.App_Start.UnityWebActivator), "Start")]
[assembly: WebActivatorEx.ApplicationShutdownMethod(typeof(BKBuildCard.App_Start.UnityWebActivator), "Shutdown")]
namespace BKBuildCard.App_Start
{
/// <summary>Provides the bootstrapping for integrating Unity with ASP.NET MVC.</summary>
public static class UnityWebActivator
{
/// <summary>Integrates Unity when the application starts.</summary>
public static void Start()
{
var container = UnityConfig.GetConfiguredContainer();
FilterProviders.Providers.Remove(FilterProviders.Providers.OfType<FilterAttributeFilterProvider>().First());
FilterProviders.Providers.Add(new UnityFilterAttributeFilterProvider(container));
DependencyResolver.SetResolver(new UnityDependencyResolver(container));
}
/// <summary>Disposes the Unity container when the application is shut down.</summary>
public static void Shutdown()
{
var container = UnityConfig.GetConfiguredContainer();
container.Dispose();
}
}
}
EDIT
I attempted to run the application by using a concrete implementation of the class, but the problem persists.
private UnitOfWork worker;
public HomeController() {
this.worker = new UnitOfWork();
}
Just change one line in your RegisterTypes method:
container.RegisterType<IUnitOfWork, UnitOfWork>(new TransientLifetimeManager());
or try changing to
container.RegisterType<IUnitOfWork, UnitOfWork>(new PerThreadLifetimeManager());
What is happening here is that multiple threads are trying to share the same DbContext. The second one tries querying the model while the model is still being built on first thread. The solution would be to make sure that no 2 threads share the same DbContext. If the DbContext would be different on different threads, each one would build and query model on its own.
Finally figured it out. I neglected to mark a customer property on my class as [NotMapped].
public HttpPostedFileBase cardImg { get; set; }
The correction simply involved adding the attribute.
[NotMapped]
public HttpPostedFileBase cardImg { get; set; }
I was under the impression that the following error:
Value cannot be null.
Parameter name: entitySet
was caused by the context issues. Evidently, it was the reverse; the null parameter issue was causing "Context cannot be used" error. This is a good summary of resolving issues related to null entitySet parameters (which can cause the context issues noted above).
The solution was simple, but, it would not have been my initial guess based upon the error message. As such, I hope somebody may be able to gain some use insight from my foolish mistake.
I have build a self hosted WCF service which consumes a unit of work with all my repositories in it. The repositories use code first EF to connect to the database. I am using the Ninject.Extensions.Wcf.SelfHost package to start the service and get the injection working.
Everything works just fine until i want to commit something to the database. I can read records from the database, but writing does not work. After digging and debugging i found that my db context is not shared between the unit of work and the repositories. So when i commit in my unit of work the context has no changes to commit.
any advice?
And here the code:
Startup code for the service
private static void StartNinjectSelfHosted(string address)
{
var service =
NinjectWcfConfiguration.Create<SecurityService, NinjectServiceSelfHostFactory>(
serviceHost =>
serviceHost.AddServiceEndpoint(typeof(ISecurityService), new BasicHttpBinding(), address));
selfHosted = new NinjectSelfHostBootstrapper(CreateKernel, service);
selfHosted.Start();
serviceAddress = address;
}
private static StandardKernel CreateKernel()
{
var kernel = new StandardKernel();
ConfigurationAction scope = bind => bind.InRequestScope();
kernel.Load((new NinjectModule[]
{
new ContextBinder(scope),
new ServiceBinder(scope) ,
new UnitOfWorkBinder(scope),
new RepositoryBinder(scope),
}));
return kernel;
}
Binders
public class ContextBinder : NinjectModule
{
private readonly ConfigurationAction _bindInScope;
public ContextBinder(ConfigurationAction bindInScope)
{
_bindInScope = bindInScope;
}
public override void Load()
{
Kernel.Bind(typeof(SecurityContext)).ToSelf().InSingletonScope();
}
}
public class ServiceBinder : NinjectModule
{
private readonly ConfigurationAction _configurationAction;
public ServiceBinder(ConfigurationAction configurationAction)
{
_configurationAction = configurationAction;
}
public override void Load()
{
Kernel.Bind(
x => x.FromAssembliesMatching("WcfInterfaces*")
.SelectAllInterfaces()
.Join.FromAssembliesMatching("*Facade*")
.SelectAllClasses()
.BindDefaultInterface()
.Configure(_configurationAction));
}
}
public class UnitOfWorkBinder : NinjectModule
{
private readonly ConfigurationAction _configurationAction;
public UnitOfWorkBinder(ConfigurationAction configurationAction)
{
_configurationAction = configurationAction;
}
public override void Load()
{
Kernel.Bind(x => x
/** Select all unit of work interfaces */
.FromAssembliesMatching("SecurityDomain*")
.SelectAllUnitOfWorkInterfaces()
/** Select all unit of work implementations */
.Join.FromAssembliesMatching("SecurityImplementation*")
.SelectAllUnitOfWorkImplementations()
/** Bind interfaces to implementations */
.BindDefaultInterface()
/** Configure the scope */
.Configure(_configurationAction));
}
}
public class RepositoryBinder : NinjectModule
{
private readonly ConfigurationAction _configurationAction;
public RepositoryBinder(ConfigurationAction configurationAction)
{
_configurationAction = configurationAction;
}
public override void Load()
{
Kernel.Bind(x => x
/** Select all default repository interfaces */
.FromAssembliesMatching("SecurityDomain*")
.SelectAllRepositoryInterfaces()
/** Select all repository implementations */
.Join.FromAssembliesMatching("SecurityImplementation*")
.SelectAllRepositoryImplementations()
/** Bind interfaces to implementations */
.BindDefaultInterface()
/** Configure the scope */
.Configure(_configurationAction));
}
}
Unit of work
public class UnitOfWork : IUnitOfWork
{
private readonly SecurityContext _context;
public UnitOfWork(SecurityContext context, ISecurityUnitOfWork security)
{
Console.WriteLine("*** Unit Of Work ContextHash: {0}***", context.Hash);
_context = context;
Security = security;
}
public void Commit(int userId)
{
Console.WriteLine("Context hash {0}", _context.Hash);
using (var transaction = _context.Database.BeginTransaction())
{
try
{
DateTime now = DateTime.Now;
foreach (var entry in _context.ChangeTracker.Entries<Entity>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreationDate = now;
entry.Entity.CreationUserId = userId;
break;
case EntityState.Modified:
entry.Entity.ModificationDate = now;
entry.Entity.ModificationUserId = userId;
break;
case EntityState.Deleted:
entry.State = EntityState.Modified;
entry.Entity.Deleted = true;
break;
}
}
_context.SaveChanges();
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
throw;
}
}
}
public ISecurityUnitOfWork Security { get; private set; }
}
Security Unit of work
public class SecurityUnitOfWork : ISecurityUnitOfWork
{
public SecurityUnitOfWork(IAccountRepository accounts, IRoleRepository roles, IRightRepository rights, IUserRepository users, IApplicationRepository applications)
{
Applications = applications;
Users = users;
Rights = rights;
Roles = roles;
Accounts = accounts;
}
public IAccountRepository Accounts { get; private set; }
public IRoleRepository Roles { get; private set; }
public IRightRepository Rights { get; private set; }
public IUserRepository Users { get; private set; }
public IApplicationRepository Applications { get; private set; }
}
Repositories
public class AccountRepository : GenericRepository<SecurityContext, Account>, IAccountRepository
{
public AccountRepository(SecurityContext context)
: base(context)
{
}
}
public class GenericRepository<TContext, TEntity> : IGenericRepository<TEntity>
where TContext : DbContext
where TEntity : class, IDeletable, IIdentifiable
{
private readonly TContext _context;
private readonly DbSet<TEntity> _entitySet;
private IQueryable<TEntity> _entities;
public GenericRepository(TContext context)
{
_context = context;
_entitySet = context.Set<TEntity>();
_entities = _entitySet;
}
/// <summary>
/// Gets the DbContext
/// </summary>
protected virtual TContext Context
{
get { return _context; }
}
/// <summary>
/// Gets the entities
/// </summary>
protected virtual IQueryable<TEntity> Entities
{
get { return _entities; }
set { _entities = value; }
}
/// <summary>
/// Gets the editable dbset
/// </summary>
public virtual IDbSet<TEntity> EntitySet
{
get { return _entitySet; }
}
/// <summary>
/// Gets the entities
/// </summary>
protected virtual IQueryable<TEntity> Process(IEntityFilter<TEntity> filter = null, IEntitySorter<TEntity> sorter = null, IEntityIncluder<TEntity> includer = null)
{
var entities = _entities.Where(x => !x.Deleted);
if (includer != null)
entities = includer.AddInclusions(entities);
if (filter != null)
entities = filter.Filter(entities);
if (sorter != null)
entities = sorter.Sort(entities);
return entities;
}
public virtual IQueryable<TEntity> List(IEntitySorter<TEntity> sorter = null, IEntityFilter<TEntity> filter = null, int? page = null, int? pageSize = null, IEntityIncluder<TEntity> includer = null)
{
if ((page.HasValue || pageSize.HasValue) && sorter == null)
{
throw new ArgumentException("You have to define a sorting order if you specify a page or pageSize! (IEntitySorter was null)");
}
if (page.HasValue && !pageSize.HasValue)
{
throw new ArgumentException("You have to define a pageSize if you specify a page!");
}
var entities = Process(filter, sorter, includer);
if (page != null)
entities = entities.Skip(pageSize.Value * page.Value);
if (pageSize != null)
entities = entities.Take(pageSize.Value);
return entities;
}
public virtual int Count(IEntityFilter<TEntity> filter = null)
{
return Process(filter).Count();
}
public bool Any(IEntityFilter<TEntity> filter = null)
{
return Process(filter).Any();
}
public TEntity SingleOrDefault(IEntityFilter<TEntity> filter = null, IEntityIncluder<TEntity> includer = null)
{
return Process(filter, includer: includer).SingleOrDefault();
}
public TEntity Single(IEntityFilter<TEntity> filter = null, IEntityIncluder<TEntity> includer = null)
{
return Process(filter, includer: includer).Single();
}
public TEntity FirstOrDefault(IEntityFilter<TEntity> filter = null, IEntitySorter<TEntity> sorter = null, IEntityIncluder<TEntity> includer = null)
{
return Process(filter, sorter, includer).FirstOrDefault();
}
public TEntity First(IEntityFilter<TEntity> filter = null, IEntitySorter<TEntity> sorter = null, IEntityIncluder<TEntity> includer = null)
{
return Process(filter, sorter, includer).First();
}
public virtual TEntity Find(int id)
{
var entity = EntitySet.FirstOrDefault(x => x.Id == id);
if (entity != null && entity.Deleted)
{
return null;
}
return entity;
}
public virtual void AddOrUpdate(TEntity entity)
{
if (entity.Id == 0)
{
Add(entity);
}
else
{
Update(entity);
}
}
public virtual void Delete(TEntity entity)
{
entity.Deleted = true;
Update(entity);
}
public virtual void Delete(IEnumerable<TEntity> entities)
{
foreach (TEntity entity in entities)
{
Delete(entity);
}
}
public virtual void Delete(int id)
{
TEntity entity = Find(id);
if (entity != null)
Delete(entity);
}
public virtual void HardDelete(TEntity entity)
{
DbEntityEntry entry = Context.Entry(entity);
if (entry.State != EntityState.Deleted)
{
entry.State = EntityState.Deleted;
}
else
{
EntitySet.Attach(entity);
}
}
public virtual void HardDelete(int id)
{
TEntity entity = Find(id);
if (entity != null)
HardDelete(entity);
}
public TResult Query<TResult>(Func<IQueryable<TEntity>, TResult> query)
{
return query(Entities);
}
/// <summary>
/// Gets the queryable entities
/// </summary>
public IQueryable<TEntity> QueryableEntities
{
get
{
return _entitySet;
}
}
protected virtual void Add(TEntity entity)
{
DbEntityEntry entry = Context.Entry(entity);
if (entry.State != EntityState.Detached)
{
entry.State = EntityState.Added;
}
else
{
EntitySet.Add(entity);
}
}
protected virtual void Update(TEntity entity)
{
DbEntityEntry entry = Context.Entry(entity);
if (entry.State == EntityState.Detached)
{
EntitySet.Attach(entity);
}
entry.State = EntityState.Modified;
}
}
when i start the service this is the output
Starting service
**** CONTEXT CONSTRUCTED, HASH:63174400 ****
**** CONTEXT CONSTRUCTED, HASH:24275713 ****
**** CONTEXT CONSTRUCTED, HASH:34631232 ****
**** CONTEXT CONSTRUCTED, HASH:66590816 ****
**** CONTEXT CONSTRUCTED, HASH:24695352 ****
**** CONTEXT CONSTRUCTED, HASH:11985038 ****
*** Unit Of Work ContextHash: 63174400***
--------------------------------
Security service is running # http://localhost/security
So after some more debugging and testing i managed to solve this myself. here's a what i did and what i found:
I started looking at the ninject scope and tried all the available options, none of them worked. next step was skip the binderClasses and manually link all my Interfaces and implementations. At first this was also no go, so i started playing with the scope setting again.
I got the whole thing working with the manual binding an in RequestScope. Of course manual binding was not what i wanted.
after some more testing i have this
private static StandardKernel CreateKernel()
{
var kernel = new StandardKernel();
ConfigurationAction scope = bind => bind.InRequestScope();
/* this works*/
scope(
kernel.Bind(typeof(SecurityContext))
.ToSelf());
/*
* This works
*
* kernel.Bind(typeof(SecurityContext))
.ToSelf()
.InRequestScope();*/
/*
* This does not work
kernel.Load(new ContextBinder(scope));
*/
kernel.Load(new UnitOfWorkBinder(scope));
kernel.Load(new RepositoryBinder(scope));
kernel.Load(new ServiceBinder(scope));
return kernel;
}
I have no Idea why binding the context in the contextbinder create a separate context for every instance it needs. So if anyone could clarify.
I marked this as resolved because the code above is working for me.
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.