Blazor WASM ViewModel - asp.net-core

I did a lot of Razor pages the past year, and a couple of weeks ago I started to transform all to a ViewModel for my Blazor Server App.
Now I thought it's time to make a new Blazor WebAssembly App.
But I struggle to build a POC with a ViewModel, based on the WeatherForecast example.
But whatever I do, I have errors. And so far I did not find a a good basic example.
Unhandled exception rendering component: Unable to resolve service for type 'fm2.Client.Models.IFetchDataModel' while attempting to activate 'fm2.Client.ViewModels.FetchDataViewModel'.
System.InvalidOperationException: Unable to resolve service for type 'fm2.Client.Models.IFetchDataModel' while attempting to activate 'fm2.Client.ViewModels.FetchDataViewModel'.
Example: https://github.com/rmoergeli/fm2
namespace fm2.Client.ViewModels
{
public interface IFetchDataViewModel
{
WeatherForecast[] WeatherForecasts { get; set; }
Task RetrieveForecastsAsync();
Task OnInitializedAsync();
}
public class FetchDataViewModel : IFetchDataViewModel
{
private WeatherForecast[] _weatherForecasts;
private IFetchDataModel _fetchDataModel;
public WeatherForecast[] WeatherForecasts
{
get => _weatherForecasts;
set => _weatherForecasts = value;
}
public FetchDataViewModel(IFetchDataModel fetchDataModel)
{
Console.WriteLine("FetchDataViewModel Constructor Executing");
_fetchDataModel = fetchDataModel;
}
public async Task RetrieveForecastsAsync()
{
_weatherForecasts = await _fetchDataModel.RetrieveForecastsAsync();
Console.WriteLine("FetchDataViewModel Forecasts Retrieved");
}
public async Task OnInitializedAsync()
{
_weatherForecasts = await _fetchDataModel.RetrieveForecastsAsync();
}
}
}
namespace fm2.Client
{
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<IFetchDataViewModel, FetchDataViewModel>();
await builder.Build().RunAsync();
}
}
}
Additional note:
Here how I did it previously for Blazor Server App: https://github.com/rmoergeli/fm2_server
Here I try the same for the Blazor WebAssembly App:
https://github.com/rmoergeli/fm2_wasm (Constructor is not initialized).
This POC is different comapred to the first link at the top. Here I tried to just do the same like I did for the Blazor Server App.

I pulled the latest code from Github. It looks like the wrong api was getting called.
When I changed from this:
WeatherForecast[] _weatherForecast = await _http.GetFromJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts");
to this:
WeatherForecast[] _weatherForecast = await _http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
in WeatherViewModel.cs
I could get the weather data to be displayed.

Related

HTTP Errors When Using POST Methods

I can't quite seem to figure out how to call HTTP POST functions from my Blazor WASM project hosted with ASP.NET. I am having trouble finding any examples of using POST methods past .NET 6 likely because it's so new. I've tried setting content-headers to JSON and many different ways of retrieving the request body from the actual controller function, but I just get 500, 415, and 400 errors. I've also tried not using model binding the the controller function, but to no avail. I do not believe this is the issue though, as using the [ApiController] attribute infers proper model binding as far as I know. I can only imagine the issue stems from the HTTP call.
The service that calls the method:
public async Task CreateUser(User user)
{
await _httpClient.PostAsJsonAsync("users", user);
}
The controller function:
[HttpPost]
public async Task PostUser(User user)
{
_context.Users.Add(user);
await _context.SaveChangesAsync();
}
The given from the above code is just a simple 400 error.
Also, I've added a test user into the database manually, and I'm able to retrieve it without any issues.
Here's some code from one of my demo projects showing API calls to get WeatherForecast records.
Here's the Web Assembly project DataBroker:
public class WeatherForecastAPIDataBroker : IWeatherForecastDataBroker
{
private readonly HttpClient? httpClient;
public WeatherForecastAPIDataBroker(HttpClient httpClient)
=> this.httpClient = httpClient!;
public async ValueTask<bool> AddForecastAsync(WeatherForecast record)
{
var response = await this.httpClient!.PostAsJsonAsync<WeatherForecast>($"/api/weatherforecast/add", record);
var result = await response.Content.ReadFromJsonAsync<bool>();
return result;
}
public async ValueTask<bool> DeleteForecastAsync(Guid Id)
{
var response = await this.httpClient!.PostAsJsonAsync<Guid>($"/api/weatherforecast/delete", Id);
var result = await response.Content.ReadFromJsonAsync<bool>();
return result;
}
public async ValueTask<List<WeatherForecast>> GetWeatherForecastsAsync()
{
var list = await this.httpClient!.GetFromJsonAsync<List<WeatherForecast>>($"/api/weatherforecast/list");
return list!;
}
}
And here's the controller it's calling:
namespace Blazr.Demo.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private IWeatherForecastDataBroker weatherForecastDataBroker;
public WeatherForecastController(IWeatherForecastDataBroker weatherForecastDataBroker)
=> this.weatherForecastDataBroker = weatherForecastDataBroker;
[Route("/api/weatherforecast/list")]
[HttpGet]
public async Task<List<WeatherForecast>> GetForecastAsync()
=> await weatherForecastDataBroker.GetWeatherForecastsAsync();
[Route("/api/weatherforecast/add")]
[HttpPost]
public async Task<bool> AddRecordAsync([FromBody] WeatherForecast record)
=> await weatherForecastDataBroker.AddForecastAsync(record);
[Route("/api/weatherforecast/delete")]
[HttpPost]
public async Task<bool> DeleteRecordAsync([FromBody] Guid Id)
=> await weatherForecastDataBroker.DeleteForecastAsync(Id);
}
The Repo for the Demo Project Blazor.Demo
Controller Code
Data Broker Code

How to inject a service in my DbContext class and have host.MigrateDatabase() still working

I've got a working EFCore, .NET5, Blazor WASM application.
I call await host.MigrateDatabase(); in my Program.Main() to have my database always up-to-date.
public static async Task<IHost> MigrateDatabase(this IHost host)
{
using var scope = host.Services.CreateScope();
try
{
// Get the needed context factory using DI:
var contextFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<AppDbContext>>();
// Create the context from the factory:
await using var context = contextFactory.CreateDbContext();
// Migrate the database:
await context.Database.MigrateAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
throw;
}
return host;
}
In my AppDbContext I've overridden SaveChangesAsync() to add and update CreatedOn en UpdatedOn.
I mentioned this in DbContext.SaveChanges overrides behaves unexpected before.
I also want to fill CreatedBy and UpdatedBy with the userId.
I have an IdentityOptions class to hold the user data:
public class IdentityOptions
{
public string UserId => User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
public ClaimsPrincipal User { get; set; }
}
I've registered this class in StartUp like this:
services.AddScoped(sp =>
{
var context = sp.GetService<IHttpContextAccessor>()?.HttpContext;
var identityOptions = new IdentityOptions();
if (context?.User.Identity != null && context.User.Identity.IsAuthenticated)
{
identityOptions.User = context.User;
}
return identityOptions;
});
I inject this IdentityOptions class into several other services, without any problem.
But when I inject it in my AppDbContext:
public AppDbContext(DbContextOptions<AppDbContext> options, IdentityOptions identityOptions)
: base(options)
{
...
}
I get an error in MigrateDatabase():
"Cannot resolve scoped service 'IdentityOptions' from root provider."
I've been trying numerous options I found googling but can't find a solution that works for me.
Please advice.
Update:
services.AddDbContextFactory<AppDbContext>(
options => options.UseSqlServer(Configuration.GetConnectionString("DbConnection"),
b => b.MigrationsAssembly("DataAccess"))
#if DEBUG
.LogTo(Console.WriteLine, new [] {RelationalEventId.CommandExecuted})
.EnableSensitiveDataLogging()
#endif
);
Thanks to the great help of #IvanStoev (again), I found the answer.
Adding lifetime: ServiceLifetime.Scoped to AddDbContextFactory in Startup solved my problem.
Now I can use my IdentityOptions class in SaveChanges and automatically update my Created* and Updated* properties.

WebAssembly's IAuthenticationTokenProvider crashes when requesting a token

I am trying to authenticate the user in my WASM Blazor app using google's OIDC.
I have managed to retrieve the token by following this article: https://learn.microsoft.com/en-gb/aspnet/core/blazor/security/webassembly/standalone-with-authentication-library?view=aspnetcore-3.1&tabs=visual-studio
I am trying to retrieve the AccessToken to pass it to the SignalR hub using the injected instance of IAccessTokenProvider when building an instance of HubConnection:
public RemoteCombatListener(ITokenCache tokenCache)
{
_connection = new HubConnectionBuilder()
.WithUrl("https://localhost:44364/combat", opts => {
opts.AccessTokenProvider = tokenCache.GetToken;
})
.Build();
}
Here is the implementation of my TokenCache:
public class TokenCache : ITokenCache
{
private readonly IAccessTokenProvider _tokenProvider;
private readonly NavigationManager _navManager;
public string CachedToken { get; private set; }
public TokenCache(IAccessTokenProvider tokenProvider, NavigationManager navManager)
{
_tokenProvider = tokenProvider;
_navManager = navManager;
}
public async Task<string> GetToken()
{
if (string.IsNullOrEmpty(CachedToken))
{
var requestedToken = await _tokenProvider.RequestAccessToken();
if (requestedToken.TryGetToken(out var accessToken))
{
CachedToken = accessToken.Value;
}
else
{
throw new AccessTokenNotAvailableException(_navManager, requestedToken, Enumerable.Empty<string>());
}
}
return CachedToken;
}
}
The problem I am facing right now is that when calling the _tokenProvider.RequestAccessToken() method, I get the following exception:
An exception occurred executing JS interop: The JSON value could not be converted to System.DateTimeOffset. Path: $.token.expires | LineNumber: 0 | BytePositionInLine: 80.. See InnerException for more details.
I am unable to figure out what is wrong with my setup as debugging stopped working for me randomly and the only option I have is Console.Log debugging.
It turns out that default configuration for the Oidc doesn't request access_token, only id_token. Had to add the following:
builder.Services.AddOidcAuthentication(options => {
// Rest of configs ...
options.ProviderOptions.ResponseType = "id_token token";
});

Generic string router with DB in Asp.net Core

I am creating an internet store. And I want to add short URLs for products, categories and so on.
For example:
store.com/iphone-7-plus
This link should open the page with iPhone 7 plus product.
The logic is:
The server receives an URL
The server try it against existent routes
If there is no any route for this path - the server looks at a DB and try to find a product or category with such title.
Obvious solutions and why are they not applicable:
The first solution is a new route like that:
public class StringRouter : IRouter
{
private readonly IRouter _defaultRouter;
public StringRouter(IRouter defaultRouter)
{
_defaultRouter = defaultRouter;
}
public async Task RouteAsync(RouteContext context)
{
// special loggic
await _defaultRouter.RouteAsync(context);
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return _defaultRouter.GetVirtualPath(context);
}
}
The problem is I can't provide any access to my DB from StringRouter.
The second solution is:
public class MasterController : Controller
{
[Route("{path}")]
public IActionResult Map(string path)
{
// some logic
}
}
The problem is the server receive literally all callings like store.com/robots.txt
So the question is still open - could you please advise me some applicable solution?
For accessing DbContext, you could try :
using Microsoft.Extensions.DependencyInjection;
public async Task RouteAsync(RouteContext context)
{
var dbContext = context.HttpContext.RequestServices.GetRequiredService<RouterProContext>();
var products = dbContext.Product.ToList();
await _defaultRouter.RouteAsync(context);
}
You also could try Middleware to check whether the reuqest is not exist, and then return the expected response.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider)
{
app.Use(async (context,next) => {
await next.Invoke();
// add your own business logic to check this if statement
if (context.Response.StatusCode == 404)
{
var db = context.RequestServices.GetRequiredService<RouterProContext>();
var users = db.Users.ToList();
await context.Response.WriteAsync("Request From Middleware");
}
});
//your rest code
}

Testing ASP.NET 5 with Entity Framework 7 using in memory database

I am wanting to get ahold of the Context that I am injecting into the controllers during testing and modify the data in the "in memory" version of the database context.
So the controller looks like this
[Route("api/[controller]")]
public class TestController : Controller
{
private readonly TestContext _testContext;
public TestController(TestContext testContext)
{
_testContext = testContext;
}
[HttpGet]
public IActionResult Get()
{
return Ok(new { _testContext.Users });
}
}
The test looks like this
public class SiteTests
{
[Fact]
public async Task GetIt()
{
var server = TestServer.Create(app => { app.UseMvc(); }, services =>
{
services.AddMvc();
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<TestContext>(options => options.UseInMemoryDatabase());
services.AddScoped<TestContext, TestContext>();
});
var client = server.CreateClient();
var response = await client.GetAsync("http://localhost/api/test");
var content = await response.Content.ReadAsStringAsync();
Assert.True(response.IsSuccessStatusCode);
}
}
I would love to somehow get ahold of the context before the client gets the request and modify what data will be coming back from the database context.
I have the test project in GitHub
If you're targeting .NET Core, you won't be able to make use of any automatic mocking frameworks.
The best you can do is make all your methods in TestContext virtual, then extend it in your unit tests.
public class IntegrationTestContext : TestContext
{
// override methods here
}
You can then use
var context = new IntegrationTestContext();
services.AddInstance<TestContext>(context);
You can also capture any extra information you want in IntegrationTestContext and access it from within your test.