Tipos de entidades com construtores

É possível definir um construtor com parâmetros e fazer com que o EF Core chame esse construtor ao criar uma instância da entidade. Os parâmetros do construtor podem ser associados a propriedades mapeadas ou a vários tipos de serviços para facilitar comportamentos como o carregamento lento.

Observação

Atualmente, toda associação do construtor é feita por convenção. A configuração do uso de construtores específicos está prevista para uma versão futura.

Associação a propriedades mapeadas

Considere um modelo Blog/Post típico:

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

    public string Name { get; set; }
    public string Author { get; set; }

    public ICollection<Post> Posts { get; } = new List<Post>();
}

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

    public string Title { get; set; }
    public string Content { get; set; }
    public DateTime PostedOn { get; set; }

    public Blog Blog { get; set; }
}

Quando o EF Core criar instâncias desses tipos, por exemplo, para os resultados de uma consulta, ele primeiro chamará o construtor padrão sem parâmetros e, em seguida, definirá cada propriedade com o valor do banco de dados. No entanto, se o EF Core encontrar um construtor parametrizado com nomes de parâmetros e tipos que correspondam aos de propriedades mapeadas, ele chamará o construtor parametrizado com valores para essas propriedades e não definirá cada propriedade explicitamente. Por exemplo:

public class Blog
{
    public Blog(int id, string name, string author)
    {
        Id = id;
        Name = name;
        Author = author;
    }

    public int Id { get; set; }

    public string Name { get; set; }
    public string Author { get; set; }

    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public Post(int id, string title, DateTime postedOn)
    {
        Id = id;
        Title = title;
        PostedOn = postedOn;
    }

    public int Id { get; set; }

    public string Title { get; set; }
    public string Content { get; set; }
    public DateTime PostedOn { get; set; }

    public Blog Blog { get; set; }
}

Algumas coisas a serem observadas:

  • Nem todas as propriedades precisam ter parâmetros de construtor. Por exemplo, a propriedade Post.Content não é definida por nenhum parâmetro de construtor. Portanto, o EF Core a definirá depois de chamar o construtor da maneira habitual.
  • Os tipos de parâmetros e os nomes precisam corresponder aos tipos de propriedades e aos nomes, com exceção de que as propriedades podem ter maiúsculas e minúsculas Pascal enquanto os parâmetros têm minúsculas concatenadas.
  • O EF Core não pode definir as propriedades de navegação (como Blog ou Posts acima) usando um construtor.
  • O construtor pode ser público, privado ou ter qualquer outro tipo de acessibilidade. No entanto, os proxies de carregamento lento exigem que o construtor esteja acessível na classe proxy herdada. Normalmente, isso significa torná-lo público ou protegido.

Propriedades somente leitura

Como as propriedades estão sendo definidas por meio do construtor, pode fazer sentido tornar algumas delas somente leitura. O EF Core dá suporte a esse recurso, com algumas precauções:

  • As propriedades sem setters não são mapeadas por convenção. (Essa ação tende a mapear propriedades que não devem ser mapeadas, como propriedades computadas).
  • O uso de valores de chave gerados automaticamente exige uma propriedade de chave que seja de leitura/gravação, pois o valor da chave precisa ser definido pelo gerador de chaves ao inserir novas entidades.

Uma forma fácil de evitar essas situações é usar setters privados. Por exemplo:

public class Blog
{
    public Blog(int id, string name, string author)
    {
        Id = id;
        Name = name;
        Author = author;
    }

    public int Id { get; private set; }

    public string Name { get; private set; }
    public string Author { get; private set; }

    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public Post(int id, string title, DateTime postedOn)
    {
        Id = id;
        Title = title;
        PostedOn = postedOn;
    }

    public int Id { get; private set; }

    public string Title { get; private set; }
    public string Content { get; set; }
    public DateTime PostedOn { get; private set; }

    public Blog Blog { get; set; }
}

O EF Core vê uma propriedade com um setter privado como de leitura/gravação, o que significa que todas as propriedades são mapeadas como antes e a chave ainda pode ser gerada pelo repositório.

Uma alternativa ao uso de setters privados é tornar as propriedades realmente somente leitura e adicionar um mapeamento mais explícito em OnModelCreating. Da mesma forma, algumas propriedades podem ser removidas por completo e substituídas apenas por campos. Por exemplo, considere estes tipos de entidades:

public class Blog
{
    private int _id;

    public Blog(string name, string author)
    {
        Name = name;
        Author = author;
    }

    public string Name { get; }
    public string Author { get; }

    public ICollection<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    private int _id;

    public Post(string title, DateTime postedOn)
    {
        Title = title;
        PostedOn = postedOn;
    }

    public string Title { get; }
    public string Content { get; set; }
    public DateTime PostedOn { get; }

    public Blog Blog { get; set; }
}

E esta configuração em OnModelCreating:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>(
        b =>
        {
            b.HasKey("_id");
            b.Property(e => e.Author);
            b.Property(e => e.Name);
        });

    modelBuilder.Entity<Post>(
        b =>
        {
            b.HasKey("_id");
            b.Property(e => e.Title);
            b.Property(e => e.PostedOn);
        });
}

Itens a observar:

  • A “propriedade” da chave agora é um campo. Não é um campo readonly, ou seja, as chaves geradas pelo repositório podem ser usadas.
  • As outras propriedades são propriedades somente leitura definidas somente no construtor.
  • Se o valor da chave primária só for definido pelo EF ou lido do banco de dados, não será necessário incluí-lo no construtor. Isso mantém a “propriedade” da chave como um campo simples e deixa claro que ela não deve ser definida explicitamente ao criar blogs ou postagens.

Observação

Esse código resultará no aviso '169' do compilador indicando que o campo nunca é usado. Isso pode ser ignorado porque, na realidade, o EF Core está usando o campo de maneira extralinguística.

Como injetar serviços

O EF Core também pode injetar “serviços” no construtor de um tipo de entidade. Por exemplo, o seguinte pode ser injetado:

  • DbContext – A instância de contexto atual, que também pode ser tipada como o tipo DbContext derivado
  • ILazyLoader – O serviço de carregamento lento. Confira a documentação sobre carregamento lento para obter mais detalhes
  • Action<object, string> – Um representante de carregamento lento. Confira a documentação sobre carregamento lento para obter mais detalhes
  • IEntityType – Os metadados do EF Core associados a este tipo de entidade

Observação

Atualmente, somente os serviços conhecidos pelo EF Core podem ser injetados. O suporte para injeção de serviços de aplicativo está sendo considerado em uma versão futura.

Por exemplo, um DbContext injetado pode ser usado para acessar seletivamente o banco de dados a fim de obter informações sobre entidades relacionadas sem carregar todas elas. No exemplo abaixo, isso é usado para obter o número de postagens em um blog sem carregar as postagens:

public class Blog
{
    public Blog()
    {
    }

    private Blog(BloggingContext context)
    {
        Context = context;
    }

    private BloggingContext Context { get; set; }

    public int Id { get; set; }
    public string Name { get; set; }
    public string Author { get; set; }

    public ICollection<Post> Posts { get; set; }

    public int PostsCount
        => Posts?.Count
           ?? Context?.Set<Post>().Count(p => Id == EF.Property<int?>(p, "BlogId"))
           ?? 0;
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public DateTime PostedOn { get; set; }

    public Blog Blog { get; set; }
}

Algumas coisas a serem observadas quanto a isso:

  • O construtor é privado, pois é chamado apenas pelo EF Core, e há outro construtor público para uso geral.
  • O código que usa o serviço injetado (ou seja, o contexto) é defensivo em relação a ele ser null para lidar com casos em que o EF Core não cria a instância.
  • Como o serviço é armazenado em uma propriedade de leitura/gravação, ele será redefinido quando a entidade estiver anexada a uma nova instância de contexto.

Aviso

Em geral, injetar o DbContext dessa forma é considerado um antipadrão, pois ele associa seus tipos de entidades diretamente ao EF Core. Considere atentamente todas as opções antes de usar a injeção de serviço dessa maneira.