Search code examples
c#.net-coreabp-framework

Best way validate object in c# with "Manager" class


In my project I have an class named Precatorio

public class Precatorio : Pesquisa.Pesquisa
{ 
    public int FornecedorId { get; set; }
    public Fornecedor Fornecedor { get; set; }
    public int FundoId { get; set; }
    public Fundo Fundo { get; set; }
    public int DestinacaoRecursoId { get; set; }
    public DestinacaoRecurso DestinacaoRecurso { get; set; }
    public DateTime Data { get; set; }
    public int Natureza { get; set; }
    public long Tipo { get; set; }
    public decimal Valor { get; set; }
    public string Descricao { get; set; }
    private Precatorio()
    { 
    }

    internal Precatorio(
        int fornecedorId,
        Fornecedor fornecedor,
        int fundoId,
        Fundo fundo,
        int destinacaoRecursoId,
        DestinacaoRecurso destinacaoRecurso,
        DateTime data,
        int natureza,
        long tipo,
        decimal valor,
        string descricao
    )
    {   
        FornecedorId = fornecedorId;
        Fornecedor = fornecedor;
        FundoId = fundoId;
        Fundo = fundo;
        DestinacaoRecursoId = destinacaoRecursoId;
        DestinacaoRecurso = destinacaoRecurso;
        Data = data;
        Natureza = natureza;
        Tipo = tipo;
        Valor = valor;
        Descricao = descricao;
    }
}

And a PrecatorioManager class which validates the fields in order to return an object

public class PrecatorioManager : DomainService
{

    private readonly IPrecatorioRepository _precatorioRepository;

    public PrecatorioManager(IPrecatorioRepository precatorioRepository)
    {
        _precatorioRepository = precatorioRepository;
    }

    public async Task<Precatorio> CreateAsync(
        int fornecedorId,
        Fornecedor fornecedor,
        int fundoId,
        Fundo fundo,
        int destinacaoRecursoId,
        DestinacaoRecurso destinacaoRecurso,
        DateTime data,
        int natureza,
        long tipo,
        decimal valor,
        string descricao

    )
    {
        return new Precatorio(
            fornecedorId,
            fornecedor,
            fundoId,
            fundo,
            destinacaoRecursoId,
            destinacaoRecurso,
            data,
            natureza,
            tipo,
            valor,
            descricao
        );
    }

}

The problem is that PrecatorioManager.CreateAsync require to many fields, and it will be too much difficult to change some field and refactor in all the code that use this method. Second problem is that sometimes a developer may not use the PrecatorioManager.CreateAsync and pass throw de validation.Validations needs to pass in PrecatorioManager because some validations require repository injection and will be needed in all cases. Developers commonly use just:

var NewPrecatorio = ObjectMapper.Map<CreateUpdatePrecatorioDto,Precatorio>(input);

So, I'm looking for a way that internal Precatorio calls PrecatorioManager.CreateAsync or a best solution.


Solution

  • As I understand it, the PrecatorioManager class is responsible for validating new Precatorio objects and you have three design objectives:

    1. The only way to create a Precatorio object is to call PrecatorioManager.CreateAsync() to obtain one.

    2. The developer must not be allowed to bypass the validation of Precatorio.

    3. The pass-through initializer must be robust and expandable for the future when additional properties may be added to the Precatorio class.

    Similar to the other answer so far, this Precatorio has a private constructor that prevents direct instantiation. The static Create method has internal visibility to give access to PrecatorioManager, but checks the [CallerMemberName] attribute and provides "some" assurance by throwing an exception if not being called from a method named CreateAsync.

    One possibility for simplifying the arguments PrecatorioManager.CreateAsync() would be using a CollectionInitializer similar to the way many HTTP requests are formed.

    internal static Precatorio? Create(
        Dictionary<string, object> initializer, 
        [CallerMemberName] string? caller = null)
    {
        if (caller == "CreateAsync")
        {
            var created = new Precatorio();
            foreach (var kvp in initializer)
            {
                PropertyInfo? pi = typeof(Precatorio).GetProperty(kvp.Key);
                if (pi == null)
                {
                    System.Diagnostics.Debug.Assert(
                        false,
                        $"Expecting a property named {kvp}");
                }
                else
                {
                    pi.SetValue(created, kvp.Value);
                }
            }
            return created;
        }
        else throw new InvalidOperationException(
            $"Precatorio.Create cannot be called from {caller}."); 
    }
    

    The PrecatorioManager class is now greatly simplified:

    internal class PrecatorioManager
    {
        private PrecatorioRepository precatorioRepository;
        public PrecatorioManager(PrecatorioRepository precatorioRepository) =>
            this.precatorioRepository = precatorioRepository;
        internal async Task<Precatorio?> CreateAsync(Dictionary<string, object> init)
        {
            var created = Precatorio.Create(init);
            await ValidateAsync(created);
            return created;
        }
        private async Task<bool> ValidateAsync(Precatorio? created)
        {
            if(created == null) return false;
    
            Console.WriteLine("VALIDATING!");
            await Task.Delay(100); // It's up to you what is being awaited here.
    
            return true; // For example, return pass/fail result
        }
    }
    

    TESTBENCH

    Attempting to instantiate Precatorio won't even compile:

    // Illegal attempt to instantiate Precatorio();
    var doesNotCompile = new Precatorio();
    

    And attempting to bypass the validation would result in an exception being thrown:

    // Illegal attempt to create Precatorio directly
    try
    {
        var notAllowed = await Precatorio.Create(
            initializer: new Dictionary<string, object>
            {
                { "FornecedorId", 123 },
                { "FundoId", 456 },
                { "DestinacaoRecursoId", 798},
                { "Data", DateTime.UtcNow },
                { "Tipo", 0xeeeeffff },
                { "Valor", 1.234m },
                { "Descricao", "This is a test" },
            });
    }
    catch (Exception ex)
    {
        Console.WriteLine($"{ex.Message}{Environment.NewLine}");
    }
    

    A properly formed call will succeed. Console message confirms validation has taken place:

    // Normal create through manager
    var precatorio = await manager.CreateAsync(
        init: new Dictionary<string, object>
        {
            { "FornecedorId", 123 },
            { "FundoId", 456 },
            { "DestinacaoRecursoId", 798},
            { "Data", DateTime.UtcNow },
            { "Tipo", 0xeeeeffff },
            { "Valor", 1.234m },
            { "Descricao", "This is a test" },
        });
    
    Console.WriteLine(precatorio?.ToString());
    

    The console printout from the instantiation of a validated Precatorio object:

    console output