Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supporting Key names other than "Id" #285

Open
zachrybaker opened this issue Jan 6, 2022 · 2 comments
Open

Supporting Key names other than "Id" #285

zachrybaker opened this issue Jan 6, 2022 · 2 comments

Comments

@zachrybaker
Copy link

First off, this is a very convenient library, especially combined with the EFC Generator, especially when your database design is "ideal."

@pwelter34 I'm curious if you have considered making it work for tables whose PK is not named "Id." Let's ignore compound keys for a moment. While Id is the EF convention, it is not an EF requirement. Wouldn't it be nice if it wasn't a requirement here?

From what I can tell, the only place that we run into trouble is in the EntityDataContextHandlerBase's Read method. The Where clause has a linq expression which is server-evaluated looking for an "Id" field. While I could add a view or a computed column to get around this hard-coded property name, folks probably prefer to keep their db as pure as possible.

One thought is that if all these EFC-facing handlers and even EntityDataContextHandlerBase were themselves implementations of generic interfaces, perhaps we could substitute other implementations to get us the desired key name in Read implentation. Just a thought.

Otherwise, the "Id" name is not an issue without code workarounds. The create/delete/update handlers don't suffer this above issue on the EF side. On the model-facing side, this is a viable workaround at the entity level to avoid issues:

    [NotMapped]
    public string Id { get => MyActualKey; set => MyActualKey= value; }
@pwelter34
Copy link
Member

Unfortunately, Id is required and used all over the framework. The interface IHaveIdentifier<TKey> is used to deal with entities in a common way. When the db pkey is different, i rename the key to Id. efg parses the source code and will keep the rename.

@zachrybaker
Copy link
Author

I have noticed that it is indeed baked in at a few spots. And I see the reasoning. Without being able to require the key is a singular "Id" property it creates more challenges leaning on generics and more specification required when wiring up services. The issue being that you then need to pass some linq expression function to instruct it how to find the item by its key.

The thing is, this can also be supplied via open generics. So I don't think that it is actually that much change. Here's what I've come up with to demonstrate

public static class DataContextExtensions
{
    // read by an expression rather than by a hardcoded key property name.
    public static async Task<TReadModel> Read<TKey, TEntity, TReadModel>(
        this DbContext context,
        Expression<Func<TEntity, bool>> WhereIdExpression, 
        IMapper mapper, 
        CancellationToken cancellationToken = default)
        where TEntity : class, new()
    {
        var model = await context.Set<TEntity>()
            .AsNoTracking()
            .Where(WhereIdExpression)
            .ProjectTo<TReadModel>(mapper.ConfigurationProvider)
            .FirstOrDefaultAsync(cancellationToken)
            .ConfigureAwait(false);

        return model;
    }
}

With this bit, you can ride generics a bit further...

public interface IHaveIdExpressionFn<TEntity, TKey> 
    where TEntity : class, IHaveIdExpression<TKey>, new()
{
    Expression<Func<TEntity, bool>> WhereIdExpression(TKey id);
}
// Define a default for situations where we will use this for things like hard deletes of a soft-deletable entity, that don't actually need a different key name.
public class DefaultIdExpressionFn<TEntity, TKey> : IHaveIdExpressionFn<TEntity, TKey>
    where TEntity :  class, IHaveIdExpression<TKey>, IHaveIdentifier<TKey>, new()
{
    public Expression<Func<TEntity, bool>> WhereIdExpression(TKey id)
        => x => Equals(x.Id, id);
}

// Define a generic create handler.
public class EntityCreateWithIdExprHandler<TContext, TEntity, TKey, TCreateModel, TReadModel, TIdExpression>
    : IRequestHandler<EntityCreateCommand<TCreateModel, TReadModel>, TReadModel>
    where TContext : DbContext
    where TEntity : class, IHaveIdentifier<TKey>, IHaveIdExpression<TKey>, new()
    where TIdExpression : IHaveIdExpressionFn<TEntity, TKey>
{
    protected TIdExpression ExpressionClass { get; }
    public EntityCreateWithIdExprHandler(ILoggerFactory loggerFactory, TContext dbContext, IMapper mapper, TIdExpression expressionClass){...}

    public async Task<TReadModel> Handle(EntityCreateCommand<TCreateModel, TReadModel> request, CancellationToken cancellationToken)
    {
        ... 
        ///cribs the Create handler from your EntityCreateCommand work, but uses the above read function via the TIdExpression supplied....
        var readModel = await DataContext.Read<TKey, TEntity, TReadModel>(
            ExpressionClass.WhereIdExpression(entity.Id),
            Mapper, cancellationToken)
            .ConfigureAwait(false);
        ...
    }
}

// Here's a particular IHaveIdExpressionFn for an entity:
public class FrequencyHaveIdExpressionFn : IHaveIdExpressionFn<Frequency, string>
{
    public Expression<Func<Frequency, bool>> WhereIdExpression(string id)
            => x => Equals(x.Description, id);
}

// and lastly, the registrations:
services
    .AddSingleton<FrequencyHaveIdExpressionFn, FrequencyHaveIdExpressionFn>(s => new FrequencyHaveIdExpressionFn())
    .AddSingleton(typeof(IHaveIdExpressionFn<, >), typeof(DefaultIdExpressionFn<,>))
    .RegisterCreateWithIdExprFor<FamisRequestsContext, Domain.Entities.Frequency, string, FrequencyReadModel, FrequencyCreateModel, FrequencyHaveIdExpressionFn>()
    .RegisterHardDeleteFor<FamisRequestsContext, Domain.Entities.Program, string, ProgramReadModel, IHaveIdExpressionFn<Domain.Entities.Program, string>> ();

And this works without having to take things further! It all hinges, on being able to express a where function. Doing this as part of the generics is the win here. I even managed to use IHaveIdExpressionFn on the hard delete registration.

RegisterCreateWithIdExprFor is basically a clone of your AddEntityCreateCommand in your EFCore project, and RegisterHardDeleteFor a clone of AddEntityDeleteCommand. Tweaks for handler types of course.

Extrapolating this further, one can see how MediatR.CommandQuery.EntityFrameworkCore.DomainServiceExtensions really could be modified to have overloaded or side-by-side helpers for AddEndityCommands etc that give this flexibility.

Admittedly in my entities where I need this custom identifier, I have retained them as IHaveIdentifier but I have modified them to have the Id be decorated with [NotMapped] and made it a getter and setter for my actual identifier. A bit dirty, but it leaves less to work around and still leverage your work. I literally just had to swap out the set read method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants