2
\$\begingroup\$

I'm working on a side project. One of the design decisions is that components shouldn't do any database calling (if this is wrong or impractical then I'm open to disregarding this design policy). I'm also using radzen component library, but has no bearing on this.

I have a button component that is basically two buttons. one for save (a sub part of an object), and a dropdown/context button to save all (all aspects of a more complex object). this button component is a component and can be used in multiple places. so my idea is to pass the save methods to the button from a parent page. but what's happening in practice is that the save component is a child in another component. this is no big deal, the page defines the save methods, passes it to the component which then passes to the child. where things get hairy is that the method definition is not really definable on the button. sometimes I'm passing a func<Task> other times i'm passing func<myType,Task>. so to account for this is I'm setting the method as a dynamic and calling invoke on that. Here's how that looks.

@inject ContextMenuService ContextMenuService
<RadzenStack Gap="0px" Orientation="Orientation.Horizontal">

    <RadzenButton Text=@SaveText Click="SaveAction" class="rz-border-radius-0" ButtonStyle="ButtonStyle" />
    <RadzenButton Icon="expand_more" class="rz-border-radius-0" ButtonStyle="ButtonStyle" Click="@(args=>ShowDropdownMenu(args))" Visible=hasContextItems />
</RadzenStack>

@code{
[Parameter]
public Func<Task> SaveAction { get; set; }
[Parameter]
public List<ContextMenuItem> ContextMenuList { get; set; } = new List<ContextMenuItem>();
[Parameter]
public ButtonStyle ButtonStyle { get; set; }
[Parameter]
public string SaveText { get; set; }
private bool hasContextItems => ContextMenuList.Count > 0;

void ShowDropdownMenu(MouseEventArgs args)
{
  ContextMenuService.Open(args, ContextMenuList, OnMenuItemClick);
}

async void OnMenuItemClick(MenuItemEventArgs args)
{
    dynamic method = args.Value;
    await method.Invoke();
    ContextMenuService.Close();
}
}

usage looks like this:

...
<RadzenColumn>
        <ContextSaveComponent SaveText="Save Board" ButtonStyle="ButtonStyle.Success" ContextMenuList="saveContextMenu"
            SaveAction="(async()=>await SaveBoardChanges(Model, Model.Boards))" />
</RadzenColumn>
...
@code{
[Parameter]
public Func<PricingModel, List<Board>, Task> SaveBoardChanges { get; set; }

private List<ContextMenuItem> saveContextMenu = new List<ContextMenuItem>();
protected override async Task OnInitializedAsync()
{
    saveContextMenu.Add(new ContextMenuItem
    {
        Text = "Save All Pricing Model Changes",
        Value = (async()=> await SaveAllPricingModelChanges(Model))
    });

}
} 

in the snippet above, the SaveBoardChanges is a parameter and is a child component of a page. The page defines these methods and passes them in like so:

@page "/myPage"
<RadzenTabs RenderMode="TabRenderMode.Client" TabPosition="TabPosition.Left" u/bind-SelectedIndex=@selectedPricingModelIndex>
    <Tabs>
        
        @foreach(var model in pricingModels)
        {
            int i = pricingModels.IndexOf(model);
            <RadzenTabsItem [email protected]>
                <PricingModelComponent Model=@pricingModels[i]
                    SaveAllPricingModelChanges="SavePricingModelAsync"
                    SaveBoardChanges="SaveBoardChangesAsync"
                />
            </RadzenTabsItem>
        }
    </Tabs>
</RadzenTabs>
@code{
private async Task SavePricingModelAsync(PricingModel model)
{
    await DataSource.UpdatePricingModelAsync(model);
}

private async Task SaveBoardChangesAsync(PricingModel model, List<Board> boards)
{
    model.Boards = boards;
    await DataSource.UpdatePricingModelAsync(model);
}
}

This all works, but having to use the dynamic type to circumvent strong typing generates a code smell from a (potentially bad) design policy caused by not wanting components to make out of process calls (database updates), and only having pages define methods that make that make out of process calls. Is my design policy fine and this is just one of the things I need to do, or should I abandon the policy (on this early project)?

\$\endgroup\$
4
  • \$\begingroup\$ you could simply try cast dynamic to Func<Task>, you could also use generics to do a method extension so it can be reused across project. \$\endgroup\$
    – iSR5
    Commented May 15 at 19:01
  • \$\begingroup\$ the methods being sent are always Func<Task> but are sometimes Func<MyType, Task> so that's why it's a dynamic and not a particular Func implementation. \$\endgroup\$
    – xtreampb
    Commented May 15 at 19:11
  • \$\begingroup\$ you can use type testing. if(method is Func<Task> func) and if(method is Func<MyType, Task> func) or you could just use reflection to cover all Func<,>. \$\endgroup\$
    – iSR5
    Commented May 15 at 22:14
  • \$\begingroup\$ sure I could use reflection, but does it give me any meaningful type safety than if I just use dynamic. I guess it would ensure that the Invoke method exists, but at this point is it meaningful, or worth the cost of having to reflect all the different types of Func<,> with my custom types as well. this code review was mainly to ask if my policy of "no out of process calls in a component, only on pages" was okay if it forced me to write code like this. \$\endgroup\$
    – xtreampb
    Commented May 15 at 22:50

1 Answer 1

2
\$\begingroup\$

Needing to access a distant RDBMS across the WAN can induce noticeable latencies. Fast unit tests may need a high fidelity mock of a database.

Let's leave aside the matter of whether "no DB access in this layer!" is the right tradeoff for your use case, and just take it as a given.


having to use the dynamic type to circumvent strong typing generates a code smell

Consider serializing DB rows as JSON, and uniformly passing such datastructures into this layer in string form. Now you get to introduce appropriate types upon deserializing.

\$\endgroup\$
1
  • \$\begingroup\$ so you're saying that this design policy, while may force me to bypass type casting, is similar to deserializing JSON data to type dynamic when reading from a DB before hydrating an object. So it isn't really a code smell. \$\endgroup\$
    – xtreampb
    Commented May 15 at 20:33

Not the answer you're looking for? Browse other questions tagged or ask your own question.