Question: OData with Blazor Wasm
Hi, Where can someone find the starting point to fully implement OData with Blazor Wasm?
I've seen many videos and articles regarding this but using a simple HttpClient, which I like. For example: https://github.com/hassanhabib/ODataWithEDMAndBlazor/blob/master/BlazorPagination/Data/StudentsService.cs
var response = await client.GetAsync($"{baseUrl}/api/students?$orderby=Name&$count=true&$skip={skip}&$top={top}");
The urls are constructed with the parameters passed, and obviously this is very limited. Is there a solution for building OData queries dynamically? Or how would someone approach multiple $filter and $orderby requested by the client? I've seen these libraries, but both fall short in certain aspects. https://github.com/ZEXSM/OData.QueryBuilder https://github.com/simple-odata-client/Simple.OData.Client
I've glanced over the Connected Service and haven't tried it yet, but if this is what's officially supported, where could someone find an existing example with Blazor? And more importantly, is it capable of building queries dynamically?
Can you try with the latest version of OData.Client. It uses httpclient (setting HttpRequestTransportMode) rather than webclient and should be able to work with Blazor..
Details: We've added an enum option called HttpRequestTransportMode in the DataServiceContext class that will allow users to select between HttpWebRequest and HttpClient in processing HttpRequests and HttpResponses.
The use of HttpWebRequest will be the default. So, if you want to use HttpClient with Microsoft.OData.Client then you will be required to do the following:
DefaultContainer context = new DefaultContainer(new Uri("https://localhost:44307/odata/")); context.HttpRequestTransportMode = HttpRequestTransportMode.HttpClientRequestMessage;
See PR https://github.com/OData/odata.net/pull/1953 for more ref
Hi again, I'm testing it out with the HttpRequestTransportMode.HttpClient.
I want to avoid all the code generation and the connected service.
I relied on this repo: https://github.com/tomasfabian/Joker
And created the context:
public partial class ODataServiceContext : DataServiceContext
{
#region Constructors
public ODataServiceContext(Uri serviceRoot, HttpClient httpClient) : base(serviceRoot, ODataProtocolVersion.V4)
{
HttpRequestTransportMode = HttpRequestTransportMode.HttpClient;
if (EdmModel == null)
{
Format.LoadServiceModel = () => GetServiceModelAsync(httpClient).Result;
Format.UseJson();
}
else
{
Format.UseJson(EdmModel);
}
}
#endregion
public static IEdmModel EdmModel { get; set; }
// https://stackoverflow.com/questions/56946976/consuming-odata-in-blazor-client-app-using-odata-connected-services/62552327#62552327
// WASM support https://github.com/tomasfabian/Joker/blob/master/Samples/Blazor.WebAssembly/Joker.BlazorApp.Sample/Factories/OData/ODataServiceContextFactory.cs
public static async Task<IEdmModel> GetServiceModelAsync(HttpClient httpClient)
{
using (var stream = await httpClient.GetStreamAsync("odata/$metadata"))
using (var reader = XmlReader.Create(stream))
{
return EdmModel = CsdlReader.Parse(reader);
}
}
public DataServiceQuery<TElement> Query<TElement>()
{
return base.CreateQuery<TElement>(typeof(TElement).Name);
}
}
This works for me:
var result = await ODataServiceContext.Query<Part>()
.AddQueryOption("$orderby", "Number asc")
.ExecuteAsync();
This doesn't:
var result = ODataServiceContext.Query<Part>()
.OrderBy(x => x.Number)
.ToList();
The error message:
Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] Unhandled exception rendering component: Cannot wait on monitors on this runtime. System.PlatformNotSupportedException: Cannot wait on monitors on this runtime.
Also, I couldn't get around to use my OData response class wrapping @odata.metadata, @odata.count and value, but I found the QueryOperationResponse<T> which basically does the same and works:
var query = ODataServiceContext
.Query<Part>()
.IncludeCount()
.AddQueryOption("$skip", 0)
.AddQueryOption("$top", 10);
var response = (await query.ExecuteAsync()) as QueryOperationResponse<Part>;
Console.WriteLine($"TotalCount: {response.Count}");
foreach (var item in response)
{
Console.WriteLine($"{item.Number}");
}
In conclusion, I would prefer
- To use methods with Expressions (
Where, OrderBy, Select, ...) instead ofAddQueryOption. - A better way to construct the context.
- Full control over the response object, maybe like
GetJsonAsync<T>
Hi! I would appreciate an answer, but just came up with a different approach. For anybody else struggling to find a straightforward way, like me.
What about just using the regular HttpClient and using the DataServiceContext to build the query?
Here's a working example (I'm using MudBlazor Table):
@code {
[Inject]
public HttpClient HttpClient { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
DataServiceContext ODataContext;
MudTable<Part> table;
int currentPageSize;
string search;
bool expand;
protected override async Task OnInitializedAsync()
{
Uri serviceRoot = NavigationManager.ToAbsoluteUri("/odata");
ODataContext = new DataServiceContext(serviceRoot);
await base.OnInitializedAsync();
}
async Task<TableData<Part>> ServerReload(TableState state)
{
var skip = state.PageSize == currentPageSize ? state.Page * state.PageSize : 0;
var top = state.PageSize;
currentPageSize = state.PageSize;
var query = ODataContext.CreateQuery<Part>(nameof(Part)).IncludeCount();
if (expand)
{
// TODO: query = query.Expand(x => x.PartBoxType.Select(y => y.BoxType)) as DataServiceQuery<Part>;
query = query.AddQueryOption("$expand", "PartBoxType($expand=BoxType)");
}
if (!string.IsNullOrWhiteSpace(search))
{
query = query.Where(x =>
x.Description.ToLower().Contains(search.ToLower()) ||
x.Number.ToLower().Contains(search.ToLower())) as DataServiceQuery<Part>;
}
if (state.SortDirection != SortDirection.None)
{
// TODO: query = query.OrderBy(x => x.Description)
var direction = state.SortDirection == SortDirection.Ascending ? "asc" : "desc";
var orderby = $"{state.SortLabel} {direction}";
query = query.AddQueryOption("$orderby", orderby);
}
query = query.Skip(skip).Take(top) as DataServiceQuery<Part>;
var url = query.ToString();
var response = await HttpClient.GetFromJsonAsync<OdataApiResponse<Part>>(url);
IEnumerable<Part>? items = response?.Value;
int count = response?.Count ?? 0;
return new TableData<Part>() { TotalItems = count, Items = items };
}
}
What are the advantages of using the DataServiceContext and the EdmModel to retrieve data?
On a separate note, I still can't find an alternative for not writing the parameters with strings.
- how would someone use OrderBy for multiple columns with different directions combination? Expected result:
$orderby=Number asc, Description desc - what is the approach for nested collections with Expand? Expected result:
$expand=PartBoxType($expand=BoxType)I came across an issue on Simple.OData.Client with their solution being simply:.Expand(x => x.Category.Products.Select(y => y.Category))
@RicardoValero95 you can use expressions as follows:
DataServiceQuery query = (DataServiceQuery)ODataServiceContext
.CreateQuery("Part")
.Where();
.OrderBy()
.Expand()
etc etc
QueryOperationResponse response = await query.ExecuteAsync() as QueryOperationResponse;
Use this link https://docs.microsoft.com/en-us/odata/client/basic-crud-operations#using-with-pocos to get an idea on how to construct the context.
Hi, thanks but I kinda answered myself on how to use expressions, but not on the specific cases I mentioned:
- OrderBy for multiple columns with different directions combination?
Expected result:
Student/?$orderby=FirstName asc,LastName desc - Nested collections with Expand?
Expected result:
Teacher/?$expand=Course($expand=CourseType)
Also I kinda get that using the connected service is the prefered way as mentioned in the link you sent.
But it isn't clear for me how to construct it yourself. I'd be very glad if someone linked me a sample Blazor Wasm with OData Client project (without the connected service).
@RicardoValero95 you already provided a way in which to create the context above... But you can also try this:
public partial class Connector : DataServiceContext
{
public Connector(Uri serviceRoot) : base(serviceRoot)
{
HttpRequestTransportMode = HttpRequestTransportMode.HttpClient;
this.Books = base.CreateQuery("Books");
this.Format.LoadServiceModel = () => GetEDMModel();
this.Format.UseJson();
}
private IEdmModel GetEDMModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
//builder.EntitySet
builder.EntitySet("Books").EntityType.HasKey(p => p.Id).Count().Select().Page(null, 100).Expand().Filter();
return builder.GetEdmModel();
}
public DataServiceQuery Books { get; }
Then in the OnInitializedAsync method of Blazor's Razor page, do this or any query that you may be interested in.
var dataserviceContect = new Connector(new Uri("https://localhost:44353/odata"));
var result = await dataserviceContect.Books.ExecuteAsync();
foreach (var book in result)
{
Console.WriteLine(book.Author);
Console.WriteLine(book.ISBN);
}
I'm with the same problem. ExecuteAsync() works, but ToList(), FirstOrDefault(), LastOrDefault() and so on doesn't. Has anyone found a way to do it?
// Works
var result = ODataServiceContext.Query<UserDto>()
.ExecuteAsync();
// Doesn't Work
var result = ODataServiceContext.Query<UserDto>()
.OrderBy(x => x.Id)
.ToList();
Are there any plans to release a version of the OData client that works with Blazor Wasm?
@Robelind which issue are you facing?
The same as above, e.g.
// Doesn't Work
var result = ODataServiceContext.Query<UserDto>()
.OrderBy(x => x.Id)
.ToList();
The workarounds are cumbersome and ugly, IMHO.
I know this is probably not the answer to the question you were looking for, but speaking as an outside developer (and not in my role as an OData contributor) I stopped using the official OData client a long time ago, and now use Simple.OData.Client.V4 in my Blazor apps. It works really well with HttpClientFactory and using Bearer tokens in request headers, and I haven't had any issues with it.
HTH!
Also, I noticed this exception.
Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] Unhandled exception rendering component: Cannot wait on monitors on this runtime. System.PlatformNotSupportedException: Cannot wait on monitors on this runtime.
The issue here is the way you load your service model (IEdmModel). Calling .Result will make the main thread wait for the result of the asynchronous method, and Blazor does not allow that. Fix this, and your exception will be gone.
I copy/paste my response from another issue may help others...
sorry in my case I was getting that error when I did something like this
ctx.Users.Where(....).FirstOrDefault()
But I found that using Microsoft.OData.Extensions.Client...I can do
(await ctx.Users.Where(....).ExecuteAsync()).FirstOrDefault()
And I no longer have that issue....of course you can do manually the same logic that ExecuteAsync does...is an extension method.
Closing this. Feel free to create a new issue if there are any more questions to be answered.
@rblanca your suggestion works well for orderby, did you manage to solve Select? as when using it we create a dynamic type that is not castable with the original type.
.Select(a => new { a.NickName, a.Likes }).ExecuteAsync<Person>();
That throws errors saying it can't cast it.
In order to get mine to work in Blazor, I had to cast to DataServiceQuery<T> then call the ExecuteAsync. Would be nice if it implemented the IAsyncEnumerable so you could call ToListAsync instead.
var results = (DataServiceQuery<PLDashboardDataQualityOvertime_LineChartItem>)context.DashboardQualityOvertime
.GroupBy(
d => new { d.ProjectKey, d.TimeStamp },
(d1, d2) => new PLDashboardDataQualityOvertime_LineChartItem
{
Category = d1.ProjectKey,
TimeStamp = d1.TimeStamp,
Passes = d2.Sum(x => x.Passes),
Fails = d2.Sum(x => x.Fails),
NoValues = d2.Sum(x => x.NoValues),
NotApplicables = d2.Sum(x => x.NotApplicables)
});