How to reuse lambda expression on entities with different property names in EF Core with MS Data Sync?

125 views Asked by At

In ASP.Net EF Core I have various entity models with corresponding controllers which inherit from Microsoft.AspNetCore.DataSync.TableController and each model inherits EntityTableData from the same DataSync library so as to facilitate the Offline Sync from a MAUI client App.

The question (I think) is for EF Core (but perhaps the options are limited/directed by the Offline sync requirement both on Server/Client)... how do I reuse Lambda business logic in EF considering that entity models may not have the same property name to use in that reusable lambda.
Details below... but in short I have a User entity with an Id property and other entities with a foreign key property of UserId and would like to reuse the following logic on all of those entities... Or an alternative that achieves the same reuse aim?

/// <summary>
/// Limit data returned to client
/// </summary>
/// <returns></returns>
public override System.Linq.Expressions.Expression<Func<T, bool>> GetDataView()
{
    Logger.LogInformation($"GetDataView for {Authenticatable.DbUserId}");

    //Only records which belong to this user should ever be visible
    return Authenticatable.DbUserId == null
        ? _ => false
        //the following doesn't work for a clean 
        //user entity because it does not have 
        //a UserId property but instead an Id property!
        : model => model.UserId == Authenticatable.DbUserId;
}

The user entity cannot simply have a UserId instead of Id property because MS Azure DataSync needs an Id property/column as the unique identifier for Sync.

What I've tried... I tried a bunch of newbie mistake things such as a UserId property on the User entity which also maps to the Id column of the DB. But this results in an error because you cannot have multiple EF properties mapping to the same table column and because I must have Id inherited from MS DataSync EntityTableData.

My Hack solution My workaround is a HACK and I would prefer something that feels cleaner (and doesn't affect the client).

In this hack solution the reusable lamba (aforementioned) stipulates that T inherits from IUserAttributableEntity allowing for reuse.

public interface IUserAttributableEntity{string UserId { get; set; }}

The User entity itself gets around the problem of needing an Id property but also needing a UserId property mapping to the DB table Id like this...

public partial class User : EntityTableData,  IUserAttributableEntity
{
    // In order to map correctly the client model has
    // [JsonProperty("UserId")] on the Id property
    // This resolves 'Could not find a property named 'id' on type'
    [NotMapped]
    public override string Id { get => base.Id; set => base.Id = value; }

    /// <summary>
    /// HACK: this enables us to reused generic Access Control Provider
    ///       which expects UserId and NOT Id
    /// </summary>
    [Key]
    [Column(nameof(Id))]
    public string UserId { get => Id; set { Id = value; } }

It is essentially making the Id property from the base EntityTableData dto not mapped to the DB table Id column and instead UserId does this for me. It does work fine, but apart from feeling like a HACK it has consequences on the MAUI client which is where it feels really dirty...

The client is also using offline MS DataSync and so must also adhere to the Id property rule like the server. But, because the server DTO now maps UserId in place of Id, issues occur such as when executing a queryable e.g. await _userRepo.PullAsync(condition: f=>f.Id == userId); results in an error similar to "'id' not found". This is just an example!

Therefore, we now need to HACK the client...

public class User : BaseDTO, ILoggedInUser
{
    // JsonProperty fixes 'Could not find a property named 'id' on type'
    // when doing a PullAsyncWhere on Id.
    // Only required where corresponding server model
    // has '[NotMapped]' attribute on Id
    [JsonProperty("UserId")]
    public new string Id { get { return base.Id; } set { base.Id = value; } }

Finally, this all works. But it feels really dirty to be making changes on the server which then result in hacks on the client.

A preferred alternative would have been to leave the client completely vanilla as it should be and instead somehow force Id when it comes across the wire from the client to the server to be mapped to UserId.

I did try this on the server user entity...

...
[Key]
[Column(nameof(Id))]
[JsonProperty("Id")]
public string UserId { get => Id; set { Id = value; } }
...

... but this just failed with errors similar to 'id not found'. I also tried with lowercase...[JsonProperty("id")]. but still no cigar :(

1

There are 1 answers

12
Svyatoslav Danyliv On

You can silently correct Expression Tree by LINQKit (select appropriate EF Core version). Mark property UserId as Expandable and provide implementation function which returns Expression:

public partial class User : EntityTableData,  IUserAttributableEntity
{
    /// <summary>
    /// Make UserId translatable
    /// </summary>
    [NotMapped]
    [Expandable(nameof(UserIdIml))]
    public string UserId { get => Id; set { Id = value; } 

    static Expression<Func<User, string>> UserIdIml() => e => e.Id;

    ...
}

Before usage you have to activate LINQKit while building DbContextOptions:

builder
    .UseSqlServer(connectionString) // or any other provider
    .WithExpressionExpanding();     // enabling LINQKit extension