Search code examples
c#asp.netasp.net-corevalidationdependency-injection

Custom DataAnnotation and Dependency Injection


I have 2 model classes Batch and Coach as follows

Batch.cs

public class Batch
  {
    public int Id { get; set; }

    public string Name { get; set; }

     [NoBatchAfter9PM]
     public TimeSpan StartTime { get; set; }

     [NoBatchAfter9PM]
     public TimeSpan EndTime { get; set; }

     public string? Description { get; set; }

     [Display(Name="Is it a Weekend batch?")]
     public bool IsWeekendBatch { get; set; }

     [Display(Name = "Is it a Womens batch?")]
     public bool IsWomenBatch { get; set; }

     public ICollection<Learner>? Learners { get; set; }

     public Coach? Coach { get; set; }
}

Coach.cs

public class Coach
{
   public int Id { get; set; }

   public string Name { get; set; }

   [Display(Name="Gender")]
   public GenderId GenderId { get; set; }

   [Display(Name = "Batch")]
   public int BatchId { get; set; }

   [ForeignKey("BatchId")]
   public Batch Batch { get; set; }

   [ForeignKey("GenderId")]
   public Gender Gender { get; set; }
}

They both have one to many relationship.

In the CoachForm I have a dropdown for the batch field. When the form is submitted I have to check the gender of the coach and if gender is female and the batch is womens batch then only ModelValidation is to made successful for which I've created a custom DataAnnotation as in the below.

public class CheckForGenderAndBatch:ValidationAttribute
    {
        IBatchRepository batchRepository;
        static List<Batch> WomensBatch;

        public CheckForGenderAndBatch(IBatchRepository batchRepository)
        {
            this.batchRepository = batchRepository;
            WomensBatch = batchRepository.GetBatches().Where(x => x.IsWomenBatch).ToList();
        }
        protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
        {
            Coach coach = (Coach)validationContext.ObjectInstance;

            if (coach.GenderId == GenderId.Male && WomensBatch.Any(x => x.Id == coach.BatchId))
           {
            return new ValidationResult("Only Female Coach is allowed in womens batch");
           }
        return ValidationResult.Success;
         }
    }

For the IBatchRepository interface I've registered a concrete class as in the below in Program.cs

builder.Services.AddTransient<IBatchRepository, BatchProjectDbRepository>();

While I'm adding the attribute on top of the property BatchId in Coach.cs as in the below I'm getting the following error.

enter image description here

I know the there is no default constructor in the custom data annotation so I tried to pass the reference of the Repository class as in the below.

[CheckForGenderAndBatch(new BatchProjectDbRepository())]
[Display(Name = "Batch")]
public int BatchId { get; set; }

But the problem here is even the BatchProjectDbRepository class also does not have the default constructor.The consturctor of the BatchProjectDbRepository class is as in the below.

ProjectDbContext context;
 public BatchProjectDbRepository(ProjectDbContext context)
 {
    this.context = context;
 }  

So thought of passing the ProjectDbContext object to it as in the below way.

[CheckForGenderAndBatch(new BatchProjectDbRepository(new ProjectDbContext()))]
[Display(Name = "Batch")]
public int BatchId { get; set; }

But since ProjectDbContext class constructor also expects a parameter I'm getting the following error enter image description here

So my question is that is there anyways in which we can inject the object of concrete class into the class of the custom Data Annotation?

(Sorry for defining the problem in such a lengthy way)


Solution

  • I Get what you are trying to do. you don't need to inject the service using the constructor.

    you have to get the implementation at run time using the ValidationContext object and GetService method

    -The Steps

    1-make your validator class CheckForGenderAndBatch's contractor parameterless.

    2-inside of IsValid use this line

    var batchRepository = (IBatchRepository ) validationContext.GetService(typeof(IBatchRepository ));
    

    now you can use your repo by using the batchRepository object.

    one more thing always declarer your repository as scoped inside of the DI container.

    Side Note The constructor in such situations is used to get more data related to the validation domain like say you are trying to validate a balance threshold then in the constructor you send the value of the threshold needed yo make the validator as usable as possible.

    another example is the length validation you send in the constructor the value of the length that a string should surpass like 150 char for example.

    Good luck