m.IsProperty() is always false
jjavierdguezas opened this issue · comments
I am creating this issue for future reference in case someone else encounters the same problem I had while using MailBody
.
Scenario: I extended the default MailBody
template in a different library from where I was using it. In addition to modifying the HTML of the template, I also allowed passing CSS attributes through the dynamic attributes
parameter of MailBody
.
|-- MyProj
|-- MyProj.Shared.Services.csproj
|-- Email
|-- Brand
|-- MailBodyExtensions.cs
|-- MyProj.App.csproj
|-- Program.cs
// MyProj.Shared.Services library
namespace MyProj.Shared.Services.Email.Brand
{
public static class MailBodyExtensions
{
public static MailBlockFluent GetTemplate()
{
var template = MailBodyTemplate.GetDefaultTemplate();
// ...
template.Link(m =>
{
var withColorStyles = m.IsProperty(() => m.Attributes.color);
var withBackgroundColorStyles = m.IsProperty(() => m.Attributes.backgroundColor);
var withFontWeightStyles = m.IsProperty(() => m.Attributes.fontWeight);
var withFontSizeStyles = m.IsProperty(() => m.Attributes.fontSize);
var withMarginBottomStyles = m.IsProperty(() => m.Attributes.marginBottom);
return $"<a href='{m.Link}' style='line-break: anywhere;" +
(withColorStyles ? $"color:{m.Attributes.color};" : string.Empty) +
(withBackgroundColorStyles ? $"background-color:{m.Attributes.backgroundColor};" : string.Empty) +
(withFontWeightStyles ? $"font-weight:{m.Attributes.fontWeight};" : string.Empty) +
(withFontSizeStyles ? $"font-size:{m.Attributes.fontSize};" : string.Empty) +
(withMarginBottomStyles ? $"Margin-bottom:{m.Attributes.marginBottom};" : string.Empty) +
$"'>{m.Content}</a>";
});
// ...
return template;
}
}
}
// MyProj.App > Program.cs
using MyProj.Shared.Services.Email.Brand;
namespace MyProj.App
{
public class Program
{
public static async Task Main()
{
// ...
var body = MailBodyExtensions
.GetTemplate()
// ...
.Link(callbackUrl, callbackUrl, new { color = "#7F88A1", fontSize = "12px" });
await _emailService.SendEmailAsync(/*...*/, body, /*...*/);
// ...
}
}
}
Problem: However, I encountered a problem where m.IsProperty(() => m.Attributes.prop)
always returned false
, even though I passed the argument correctly and could see that the object had the field while debugging.
I spent a considerable amount of time resolving this issue. If someone else encounters the same problem, I can share the solution I found. While researching the issue, I came across two relevant questions on Stack Overflow: Q 8465121 and Q 19936397.
Explanation: Anonymous objects are internal
, which means their members are very restricted outside of the assembly that declares them. dynamic
respects accessibility, so pretends not to be able to see those members. If the call was in the same assembly, it would work.
Solution 1: The obvious one: move the MailBody
extension code to the same assembly/library
Solution 2: Use InternalsVisibleTo
to expose internal
values to other lib
// MyProj.App.csproj
<ItemGroup>
<InternalsVisibleTo Include="MyProj.Shared.Services" />
</ItemGroup>
And it works like a charm
This issue can be closed without taking any further action. However, it may be helpful to include a comment about this in the README file.
Hi,
I didn't know about the limitation of dynamic..
I have another solution in mind.
I can change the code to use a Dictionary<string, string> instead of a dynamic object. It will be a small breaking change. But, I think it is worth it.
What do you think?
Hi,
I didn't know about the limitation of dynamic..
I have another solution in mind.
I can change the code to use a Dictionary<string, string> instead of a dynamic object. It will be a small breaking change. But, I think it is worth it.
What do you think?
I think we should avoid making breaking changes and investigate first by trying to maintain the current structure. I love using anonymous objects because their syntax is much simpler than creating a dictionary. If an alternative way is found to check if the dynamic object has the property, the problem would be solved without breaking changes. From what I saw in the answers on Stack Overflow, it could be accessed using reflection. If this is the case, we would have to think of a way for the user to use it easily. I will think about it and tell you more later.
@doxakis I checked that using Reflection works. This is the initial proposal that comes to mind. It is true that you have to pass the property name as a string
and there would be no intellisense to prevent errors, but the same thing happens with dynamic
.
public bool IsProperty(string prop)
{
try
{
var val = (Attributes as object)?.GetType().GetProperty(prop)?.GetValue(Attributes);
return val != null;
}
catch
{
return false;
}
}
// ...
template.Link(m =>
{
var withColorStyles = m.IsProperty("color");
// ...
}
I continued investigating and there are very interesting solutions using Expression
but unfortunately dynamic
operations cannot be used with Expression
. So I think the previous proposal is the best one that comes to mind.
Is it possible for you to submit a PR with what work best for you ?
After that, I will simply update the documentation in the readme and push the new version on nuget.
Is it possible for you to submit a PR with what work best for you ?
yes of course
Hi,
I merged your PR and did few modifications after that on the project:
- I noticed that you had to do specific code for NETSTANDARD14 and NETSTANDARD16. I want to keep the code simple. So, I decided to simply drop .net standard 1.4, 1.6 since .net standard 2.0 should be fine for most projects. It will work on both .net framework and .net core. I also updated the other projects as well to target the current LTS version of .net (.net 6)
- I adjust the added unit tests to only check one action at a time.
- I did few minor code cleanup
- I removed the vb project given as an example since I don't want to maintain it. It will still remain in the git history for reference.
- I also added a new method GetAttribute in ContentElement.
- I created a changelog file to help other migrate to the latest version.
- The readme has been updated to show the new usage patterns.
Thank you for your contribution!
Philip