mercredi 10 novembre 2021

Child object validation, how and when?

i've a doubt on how and when validate child objects. Let's assume i've two entities Invoice and InvoiceDetails, where invoice contains a list of InvoiceDetails.

    public class Invoice
    {
       public int id {get; set;}
       public DateTime CreationDate {get; set;}
       public List<InvoiceDetails> Details { get; set; } = new List<InvoiceDetails>();
    }

    public class InvoiceDetails
    {
       public int id {get; set;}
       public string Description { get; set; }
       public decimal Amount { get; set; }
       public List<InvoiceDetails> Details { get; set; } = new List<InvoiceDetails>();
    }

Now let's assume that in the service layer i've an InvoiceService and InvoiceDetailsService that take care of saving and, before that, do proper validation, something like:

public class InvoiceService
    {
       public Invoice Save(Invoice invoice, User user)
        {
            Validate();

            if (invoice.Id == 0)
            {
                context.Invoices.Add(invoice);
            }

            await context.SaveChangesAsync();
            return invoice;
        }

       public void Validate(Invoice invoice, User user)
        {
            if (!user.canSaveInvoices)
            {
               throw new Exception("User cannot save invoices!!")
            }

            await context.SaveChangesAsync();
            return invoice;
        }
    }

and

public class InvoiceDetailService
    {
       public InvoiceDetail Save(InvoiceDetail invoiceDetails)
        {
            Validate();

            if (invoiceDetails.Id == 0)
            {
                context.InvoiceDetails.Add(invoiceDetails);
            }

            await context.SaveChangesAsync();
            return invoiceDetails;
        }

       public void Validate(InvoiceDetail invoiceDetails)
        {
            if (invoiceDetails.Amount<0)
            {
               throw new Exception("Amount cannot be less than 0!!")
            }

            await context.SaveChangesAsync();
            return invoiceDetails;
        }
    }

Finally in the controller i'll have something like:

public async Task<IActionResult> CreateInvoice([FromBody] InvoiceRequest model)
{
   var invoiceDetails = new InvoiceDetails()
   {
     //fill properties
   };

   var invoice = new Invoice()
   {
     //fill properties
     InvoiceDetails = new List<InvoiceDetails>() { invoiceDetails };
   };

   invoiceService.Save(invoice, currentUser);
    
}

Now the point is that if i do this, the invoice service is going to validate the invoice, but since i'm not calling the invoiceDetailService.Save() i'm skipping the validation on the child ( invoiceDetails ).

Clearly in this simple case i might do the validation within the invoiceService.Validate() because invoiceDetails are probably only gonna live within this context, but for a moment let's put this apart and assume that invoiceDetails might live also outside of this context so that i cannot rely on the fact that the "parent" validate his child ( also because at some point, Invoice might become child of a bigger class, and it would make no sense to rewrite the same validation in the parent.. ).

An idea could be to go down one layer and manage the validation at the dal level, something like overriding the beforeSave of entity framework but this sound terribly wrong to me... it makes no sense at all to validate a business rule in the Data layer...

Another idea might be to validate in the controller.. but... NO! the controller should not have this responsability and also i should always remember to validate before saving... doesn't look right to me..

I'm sure there is a correct and clean solution but i cannot see it yet... can someone point me in the right direction?

Thanks to everyone!

ps. there's another thing that bothers me, and it's the fact that since the save methods of the services call the context.SaveChanges, it actually means that every change in the context will be saved, not only the one releated to the "service" called.. clearly i could force myself to save always as soon as possible but in any case it leaves a door open to possible "difficult to troubleshoot behaviors". If someone has opinions also on this it would be great!

Aucun commentaire:

Enregistrer un commentaire