Skip to content

Instantly share code, notes, and snippets.

@haritha99ch
Last active October 19, 2023 09:27
Show Gist options
  • Save haritha99ch/f7f3e90562395c5f95f3898e0570f041 to your computer and use it in GitHub Desktop.
Save haritha99ch/f7f3e90562395c5f95f3898e0570f041 to your computer and use it in GitHub Desktop.
Entity Framework: Selector pattern - Enhancing Generic Repository Pattern

Selector Pattern: Enhancing Generic Repository Pattern with Type-Safe Data Selection in Entity Framework

Introduction

The Selector Pattern is a novel approach that provides a type-safe solution for selecting properties in Entity Framework queries. It avoids the use of anonymous types, which can lead to unpredictable return types and potential issues with type safety.

Problem

When working with Entity Framework (EF), it is often necessary to select specific properties from entities. A common approach is to use anonymous types:

var result =
    await _context.Set<Member>().Select(e => new { e.FirstName, e.LastName }).FirstOrDefaultAsync();

This method works well for some scenario, but it has some limitations:

  1. Type Safety: The type of result is an anonymous type, which is not known at compile time. This can lead to issues with type safety and can make the code harder to understand and maintain.
  2. Readability: If many properties need to be selected, the Select clause can become very long and hard to read.
  3. Reusability: The same anonymous type cannot be reused in different parts of the code. If the same properties need to be selected in multiple queries, the same Select clause has to be repeated each time.

Existing Solution

To overcome these limitations, a named object can be defined and the selector can be mapped directly in the EF query or in the repository method:

NamedObject result =
    await _context.Set<Member>().Select(e => new NamedObject { FirstName = e.FirstName, LastName = e.LastName })
    .FirstOrDefaultAsync();

This approach solves the issue with type safety, as result now has a known type (NamedObject). However, it introduces new problems as well as some problems of using anonymous types.

  1. Readability: The Select clause is still long and can become even longer if many properties need to be selected. Each property needs to be explicitly mapped to a new object in the Select clause, which can make the code hard to read.
  2. Maintainability: If the structure of NamedObject changes (e.g., if a property is added or removed), all queries that use this object need to be updated. This can be time-consuming and error-prone.

Novel Solution

The Selector Pattern provides a solution to these problems by allowing properties to be selected in a type-safe way without the need of anonymous types or explicit object mapping in either Select clause or repository method. It makes the code more readable, maintainable, flexible, and performant. It addresses this problem by using a generic EntitySelector object that defines a selection expression. This allows to use the Select() method in a type-safe way, avoiding the downsides of using anonymous types.

Implementation

Here's of how to implement the Selector Pattern:

public abstract record EntitySelector<TEntity, TResult> where TResult : EntitySelector<TEntity, TResult>
{
    protected abstract Expression<Func<TEntity, TResult>> Select();

    public static Expression<Func<TEntity, TResult>> Selector
        => ((TResult)RuntimeHelpers.GetUninitializedObject(typeof(TResult))).Select();
}

In this code snippet, EntitySelector is a generic record that takes two type parameters: TEntity, which is the type of the entity, and TResult, which is the type of the object that returns from the select query. The Select() method is abstract and must be implemented in derived classes to define the selection expression.

The Selector property uses RuntimeHelpers.GetUninitializedObject() to create an instance of the derived class without calling its constructor, and then calls the Select() method on this instance. This allows to get the selector from derived classes in a static context.

In the repository's methods/ EF queries, An EntitySelector can be passed as a parameter and use its selector in queries. This ensures that the return type of the query is always a specific, named type.

TEntitySelectProps selectProps =
    await _context.Set<TEntity>().Where(e => e.Id == id).Select(TEntitySelectProps.Selector).FirstOrDefaultAsync();

Practical Examples

Here are some practical examples of how to use the Selector Pattern:

The following examples are derived from the sample project Entity Framework: Advanced Generic Repository pattern. For a comprehensive understanding of the repository implementation, it is recommended to review the project in detail as going though this document. Prior knowledge of Entity Framework and the commonly used repository pattern will be beneficial for a thorough comprehension of the subject matter.

Usage

The simplest way to use an EntitySelector is to define a selection expression for a single property. For example, to select only the email of a Member, define an EntitySelector like this:

public record MemberEmail(string Email) : EntitySelector<Member, MemberEmail>
{
    protected override Expression<Func<Member, MemberEmail>> Select() => e => new(e.Account!.Email);
}

To use the selector, a new method should be introduced in the repository:

public async Task<TResult?> GetByIdAsync<TResult>(Guid id, Expression<Func<TEntity, TResult>> selector)
    where TResult : EntitySelector<TEntity, TResult>
        => await _context.Set<TEntity>().Where(e => e.Id == id).Select(selector).FirstOrDefaultAsync();

The method select from an entity from the database based on its ID and transforms it into a result object. The transformation is defined by a selector expression passed as a parameter to the method

Use this selector in repository methods like this:

MemberEmail memberEmail = await memberRepository.GetByIdAsync<MemberEmail>(member.Id, MemberEmail.Selector);
SELECT TOP(1) [a].[Email]
FROM [Members] AS [m]
INNER JOIN [Accounts] AS [a] ON [m].[AccountId] = [a].[Id]
WHERE [m].[Id] = @__id_0
{
  "Email": "Lee84@hotmail.com"
}

Selecting Multiple Properties

EntitySelector can be also used to select multiple properties at once. For example, to select the caption, author name, and creation date of a Blog post, define an EntitySelector like this:

public record BlogListItemDetails(string Caption, string Author, DateTime DateTime)
    : EntitySelector<Blog, BlogListItemDetails>
{
    protected override Expression<Func<Blog, BlogListItemDetails>> Select() =>
        blog => new(blog.Caption, $"{blog.Member!.FirstName} {blog.Member!.LastName}", blog.CreatedAt);
}

To use the selector, a new method should be introduced in the repository:

public async Task<IEnumerable<TResult>> GetAllAsync<TResult>(Expression<Func<TEntity, TResult>> selector)
    where TResult : EntitySelector<TEntity, TResult>
        => await _context.Set<TEntity>().Select(selector).ToListAsync();

Use this selector in repository methods like this:

BlogListItemDetails allBlogListItemDetails =
    await blogRepository.GetAllAsync<BlogListItemDetails>(BlogListItemDetails.Selector);
SELECT [b].[Caption], [m].[FirstName], [m].[LastName], [b].[CreatedAt]
FROM [Blogs] AS [b]
INNER JOIN [Members] AS [m] ON [b].[MemberId] = [m].[Id]
{
  "Caption": "Quidem aperiam vel magni voluptatem non amet.\nMolestiae dolores voluptas et.\nMagnam dicta temporibus earum.",
  "Author": "Ernie Kuhn",
  "DateTime": "2023-10-18T02:48:20.7150822"
},
{
  "Caption": "Ut ad facere.\nEsse earum consequatur est ut omnis.\nPlaceat qui et odit minus et id est.",
  "Author": "Ernie Kuhn",
  "DateTime": "2023-10-18T02:48:20.7185971"
}

Some Complex Cases

The Following example uses specification pattern.

As the queries become more complex, so as the selectors. For example, to select detailed information about a Blog post including its caption, author name and email, creation date, description and whether it's age restricted or not, define an EntitySelector like this:

public record BlogContentDetails(
    string Title,
    string Author,
    DateTime DateTime,
    string Description,
    string AgeRestricted
    ) : EntitySelector<Blog, BlogContentDetails>
{
    protected override Expression<Func<Blog, BlogContentDetails>> Select() =>
        e => new(
            e.Caption,
            $"{e.Member!.FirstName} {e.Member.LastName} - {e.Member.Account!.Email}",
            e.CreatedAt,
            e.Description,
            e.AgeRestricted ? "Age Restricted" : "Not Age Restricted"
        );
}

To use the selector, a new method should be introduced in the repository:

Use this selector in repository methods like this:

public async Task<TResult?> GetOneAsync<TResult, TSpecification>(
    TSpecification specification,
    Expression<Func<TEntity, TResult>> selector)
    where TResult : EntitySelector<TEntity, TResult> where TSpecification : Specification<TEntity>
    => await _context.Set<TEntity>().AddSpecification(specification).Select(selector).FirstOrDefaultAsync();
BlogContentDetails blogContentDetails =
    await blogRepository.GetOneAsync(new BlogWithMember("Some caption"), BlogContentDetails.Selector);
SELECT TOP(1) [b].[Caption], [m].[FirstName], [m].[LastName], [a].[Email], [b].[CreatedAt], [b].[Description], CASE
    WHEN [b].[AgeRestricted] = CAST(1 AS bit) THEN N'Age Restricted'
    ELSE N'Not Age Restricted'
END
FROM [Blogs] AS [b]
INNER JOIN [Members] AS [m] ON [b].[MemberId] = [m].[Id]
INNER JOIN [Accounts] AS [a] ON [m].[AccountId] = [a].[Id]
WHERE [b].[Caption] = @__caption_0
{
  "Title": "Some caption",
  "Author": "Ernie Kuhn - Olive84@gmail.com",
  "DateTime": "2023-10-18T02:48:20.7150822",
  "Description": "Omnis quia placeat sapiente mollitia reprehenderit laboriosam. Autem totam minima. Et rerum voluptatem dicta.",
  "AgeRestricted": "Age Restricted"
}

Benefits

The Selector Pattern provides several benefits:

  1. Utilizing specific, named types instead of anonymous types ensures type safety in queries.
  2. Code Reusability: The generic EntitySelector record can be reused for different entities, reducing code duplication.
  3. Defining different selection expressions for each entity provides flexibility in how data is queried.
  4. Predictability: Because the return type of the queries is always a specific, named type, the code becomes more predictable and easier to understand.

In conclusion, the Selector Pattern is a great example of how advanced features of C# such as records, generics, and expression trees can be used to solve complex problems in elegant ways. Well done on coming up with this solution!

Enhanced Generic Repository

public interface IRepository<TEntity> where TEntity : Entity
{

    Task<TEntity> AddAsync(TEntity record, CancellationToken? cancellationToken = null);
    Task<TEntity?> GetByIdAsync(Guid id, CancellationToken? cancellationToken = null);
    Task<IEnumerable<TEntity>> GetAllAsync(CancellationToken? cancellationToken = null);
    Task<TEntity> UpdateAsync(TEntity record, CancellationToken? cancellationToken = null);
    Task<bool> DeleteAsync(Guid id, CancellationToken? cancellationToken = null);
    Task<bool> ExistsAsync(Guid id, CancellationToken? cancellationToken = null);



    Task<TEntity?> GetByIdAsync<TSpecification>(Guid id, CancellationToken? cancellationToken = null)
        where TSpecification : Specification<TEntity>;

    Task<IEnumerable<TEntity>> GetAllAsync<TSpecification>(CancellationToken? cancellationToken = null)
        where TSpecification : Specification<TEntity>;

    Task<TEntity?> GetOneAsync<TSpecification>(TSpecification specification,
        CancellationToken? cancellationToken = null) where TSpecification : Specification<TEntity>;

    Task<IEnumerable<TEntity>> GetManyAsync<TSpecification>(TSpecification specification,
        CancellationToken? cancellationToken = null) where TSpecification : Specification<TEntity>;

    Task<bool> ExistsAsync<TSpecification>(TSpecification specification,
        CancellationToken? cancellationToken = null) where TSpecification : Specification<TEntity>;



    Task<TResult?> GetByIdAsync<TResult>(Guid id, Expression<Func<TEntity, TResult>> selector,
        CancellationToken? cancellationToken = null) where TResult : EntitySelector<TEntity, TResult>;

    Task<IEnumerable<TResult>> GetAllAsync<TResult>(Expression<Func<TEntity, TResult>> selector,
        CancellationToken? cancellationToken = null) where TResult : EntitySelector<TEntity, TResult>;

    Task<TResult?> GetOneAsync<TResult, TSpecification>(TSpecification specification,
        Expression<Func<TEntity, TResult>> selector,
        CancellationToken? cancellationToken = null)
        where TResult : EntitySelector<TEntity, TResult>
        where TSpecification : Specification<TEntity>;

    Task<IEnumerable<TResult>> GetManyAsync<TResult, TSpecification>(TSpecification specification,
        Expression<Func<TEntity, TResult>> selector,
        CancellationToken? cancellationToken = null)
        where TResult : EntitySelector<TEntity, TResult>
        where TSpecification : Specification<TEntity>;

}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment