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

added basic sample to reproduce composite error #834

Conversation

shainegordon
Copy link

This reproduces the bug described in #238

To recreate this, all you need to do is start the application and goto https://localhost:7208/acme/create-client

You'll be present with the standard .NET development exception UI with the error InvalidOperationException: Unable to track an entity of type 'Client' because its primary key property 'TenantId' is null.

This was referenced May 27, 2024
@alexTr3
Copy link

alexTr3 commented Jun 7, 2024

We need this bug to be fixed NOW!

settings TenantSetMode.OverWrite is not working

@AndrewTriesToCode
Copy link
Contributor

AndrewTriesToCode commented Jun 7, 2024

@alexTr3 , I agree with you. Please understand that I work on this library in my spare time and without compensation and make about $35 a month in donations. I will be taking a look into this issue but feel free to provide a PR or any suggestions to fix the issue. It’s a deep non trivial one and the first area I am looking at is if shadow properties are the cause or if it is the property value generators specific to Postgres.

@shainegordon
Copy link
Author

@alexTr3 , I agree with you. Please understand that I work on this library in my spare time and without compensation and make about $35 a month in donations. I will be taking a look into this issue but feel free to provide a PR or any suggestions to fix the issue. It’s a deep non trivial one and the first area I am looking at is if shadow properties are the cause or if it is the property value generators specific to Postgres.

I don't think this is a Postgres specific issue, as this is reproducable in SQLite 👍

@AndrewTriesToCode
Copy link
Contributor

AndrewTriesToCode commented Jun 7, 2024

@shainegordon
This isn't the cleanest solution, but as a workaround will an approach like this work for you?

app.MapGet("/create-client", (ApplicationDbContext applicationDbContext, HttpContext httpContext) =>
{
    var client = new Client { Name = "Client 1" };
    var tenantId = httpContext.GetMultiTenantContext<AppTenantInfo>().TenantInfo?.Id ?? "null";
    applicationDbContext.Entry(client).Property("TenantId").CurrentValue = tenantId;
    applicationDbContext.Add(Client);
   
   // set here or somewhere else if desired...
   applicationDbContext.TenantMismatchMode = TenantMismatchMode.Overwrite;

   applicationDbContext.SaveChanges();
    
    return Results.Ok("Client created");
});

You can even just skip the http context stuff and assign it to a dummy tenant id -- the overwrite mode should put in the correct tenant on saving.

A real solution would make use of efcore events of interceptors which didn't yet exist at the time this was created but which I plan to utilize eventually.

@shainegordon
Copy link
Author

shainegordon commented Jun 7, 2024 via email

@AndrewTriesToCode
Copy link
Contributor

AndrewTriesToCode commented Jun 7, 2024 via email

@shainegordon
Copy link
Author

shainegordon commented Jun 7, 2024 via email

@AndrewTriesToCode
Copy link
Contributor

I appreciate your willingness to help. I honestly only know the basics on events and interceptors. I got the feeling events might work but interceptors might enable some more sophisticated features--but that's just a gut feeling. If they only fire with change tracking I'm still in a rut. Maybe settings a default value for the tenant id shadow property to something that isn't null but also is reserved not to be a tenant name so that the overwrite mode kicks in by default.

I do recommend adding your own TenantId column if possible and using the shadow property only for situations where you can't add your own properties to your entities (E.g. they are outside your control). Then you can explicitly make any such decisions and set keys with AdjustKeys, etc.

@shainegordon
Copy link
Author

shainegordon commented Jun 7, 2024

@AndrewTriesToCode

“I do recommend adding your own TenantId column if possible”

Now that you said this out loud, if you own the entity and can change it, then implementing something like IMultiTenant isn't the worst idea in the world. No different to IArchivable, or IAuditable 👍

It would be actually quite interesting to see if this can be solved automatically with a library-provided interface/attribute & source generator.

If developer is able to change their entity from something like

public class Customer {
   public int Id { get;set; }
   public string Name { get; set; }
}

to (add partial and interface)

public partial class Customer implements IMultiTenantEntity {
   public int Id { get;set; }
   public string Name { get; set; }
}

it would be possible to source generate

public partial class Customer {
   private const string PLACEHOLDER_TENANT_ID = "Invalid-Tenant";

   public string TenantId { get;set; } = PLACEHOLDER_TENANT_ID;
}

This would then be valid code

   Console.WriteLine("Current tenant: " + customer.TenantId);`

now that I have written this out, I think you might have to use an Attribute, because source generators run in the compilation stage, so I think they need compilable code, which wouldn't happen if you have an unimplemented interface

[MultiTenantEntity]
public partial class Customer {
   public int Id { get;set; }
   public string Name { get; set; }
}

I've built a bit of internal tooling with source generators (generate controllers from Mediatr queries/commands), so this is something that interests me - https://www.nuget.org/packages?q=realmdigital

@shainegordon
Copy link
Author

shainegordon commented Jun 7, 2024

This source-generated solution is definitely something I would be willing to contribute, but I cannot offer any commitments, but I think you know how it is :)

Realistically this would be something I could tackle after July 2024

edit: This would actually be really simple to do, I have most of the code to achieve this already

@AndrewTriesToCode
Copy link
Contributor

AndrewTriesToCode commented Jun 7, 2024 via email

@shainegordon
Copy link
Author

shainegordon commented Jun 7, 2024

Now I feel like an idiot, because source generators or interfaces here is completely pointless when all you actually need to do is add

    public string TenantId { get;set; } = "SOME_TEMPORARY_VALUE";

which is a whole lot simpler than spending time implementing the source generator and a developer using it 😆

@shainegordon
Copy link
Author

This still brings me bad to my original quandry

Customer

Id TenantId Name
1 tenant_1 Andrew
2 tenant_1 Shaine

Do I ACTUALLY want my DB tables that have foreign keys to look like this

Order

Id TenantId DatePlaced CustomerId CustomerTenantId
34 tenant_1 2024-01-02 1 tenant_1
46 tenant_1 2024-01-05 2 tenant_1

vs

Order

Id TenantId DatePlaced CustomerId
34 tenant_1 2024-01-02 1
46 tenant_1 2024-01-05 2

@shainegordon
Copy link
Author

shainegordon commented Jun 27, 2024

I've actually ended up with a solution that I am very happy with, but it did require quite a bit of manual configuration, not sure if this can be avoided.

I'll see if I can share this in a viable way, maybe via updating the PR

Interestingly enough, adding proper TenantId property solves my issue with the table structure above.

Using a shadow property results in
Order

Id TenantId DatePlaced CustomerId CustomerTenantId
34 tenant_1 2024-01-02 1 tenant_1
46 tenant_1 2024-01-05 2 tenant_1

While adding your own TenantId column results in (what I would expect)
Order

Id TenantId DatePlaced CustomerId
34 tenant_1 2024-01-02 1
46 tenant_1 2024-01-05 2

@AndrewTriesToCode
Copy link
Contributor

@shainegordon I'm glad you have a working solution. I'm going to close this PR but happy to have it as a reference for others and potentially for future potential changes.

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

Successfully merging this pull request may close these issues.

3 participants