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 derivadoILazyLoader
– O serviço de carregamento lento. Confira a documentação sobre carregamento lento para obter mais detalhesAction<object, string>
– Um representante de carregamento lento. Confira a documentação sobre carregamento lento para obter mais detalhesIEntityType
– 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.