Dann werde ich wohl auf XUnit umsteigen und werde mir die HttpHandler genauer ansehen.
Ans Mocken hatte ich gar nicht gedacht, aber muss man ja, wenn man die externe API nicht testen will.
@Abt ich habe noch keine Idee wie ich das Exception Handling mit Refit machen soll. Die Snippets auf der Github Seite helfen mir da bedingt weiter.
Was ich damit meine ist, was ist der Way2Go für Exceptions in einer Library? Die ApiException sollte ich ja schon behandeln oder nicht?
Werfe ich dann eine neue custom Exception?
Nabend,
falls dies nicht der richtige Bereich sein sollte, gerne verschieben.
Ich habe eine Library für die API von TMDB geschrieben in .NET 6.
API Doku ist hier zu finden.
Dabei nutze ich Refit um die Requests zu senden.
Für die Library würde ich gerne Tests schreiben um die Funktionalität der Library zu gewährleisten.
Ich nutze das Test Framework von Microsoft "MSTest".
Ich möchte gerne Wissen ob mein Vorgehen dabei das richtige ist.
Es gibt mehrere Endpunkte wo man Informationen holen kann, zB. von Filmen/Serien/Personen.
Da würde ich einen Test schreiben wo der Service authentifiziert ist und eine richtige Film Id übergibt. Sowie der Service nicht authentifiziert ist und eine Exception wirft und ich eine Id übergebe die nicht existiert.
Es gibt auch einen Endpunkt wo man Filme und Serien als Favorit markieren kann. Meine Library Implementation sieht wie folgt aus:
AccountApi ist das Interface um den API Call mittels Refit zu senden.
Die Methode wird von dieser hier aufgerufen:
public async Task<Response> MarkMovieAsFavoriteAsync(int accountId, int movieId, bool favorite)
{
return await this.MarkAsFavoriteAsync(accountId, movieId, AccountMediaType.Movie, favorite).ConfigureAwait(false);
}
Die 2. Methode ist in einem Interface angegeben und das Interface benutze ich in der Test Klasse.
Jetzt weiß ich nicht, wie ich diese Methode am besten teste.
Wenn ich einen Film als Favorit markiere, bekomme ich die Rückmeldung, dass ich den Film erfolgreich als Favorit markiert habe.
Sende ich den Request jetzt nochmal, bekomme ich die Rückmeldung, dass der Eintrag geupdated worden ist.
Müsste ich die Markierung nach dem Ende des Tests nicht zurücksetzen? Wie handhabt man das bei einer öffentlichen API?
Und wie teste ich das in diesem konkreten Fall am besten?
Das sind zwei Tests die ich für die Methode geschrieben habe:
ich schreibe mir gerade eine Library mit .NET 6 um entspannter mit einer API zu kommunizieren.
Dahingehend, erstelle ich für jede Response, explizite Klassen/Records, damit ich die direkt typisieren kann.
Bin noch nicht ganz auf den grünen Zweig mit Records gekommen.
Bei meiner Recherche, bin ich auch auf die Top Antwort von dem Stackoverflow Eintrag gestoßen.
Also habe ich meine Models/DTOs als Records definiert.
Funktioniert auch soweit so gut. Nur ist mir dann ein Problem aufgefallen.
Ich habe als Beispiel diesen Record:
public record CreateSessionRequest([property: JsonPropertyName("request_token")] string RequestToken);
Den Record nutze ich bspw. für ein Request.
Für ein zweiten Request brauche ich zu der genannten Property aber auch noch Username und Password. Also Vererbung. Um auch weniger schreiben zu müssen, aber auch die Übersichtlichkeit zu behalten.
Also ein 2. Record, der den oberen Record erbt.
//Ohne Vererbung
public record CreateSessionWithCredentialsRequest(
[property: JsonPropertyName("username")] string Username,
[property: JsonPropertyName("password")] string Password
);
//Mit Vererbung
public record CreateSessionWithCredentialsRequest(
[property: JsonPropertyName("username")] string Username,
[property: JsonPropertyName("password")] string Password
) : CreateSessionRequest(); //Hier in den Klammern muss die Property RequestToken angegeben werden
Wie im Kommentar erwähnt, kommt hier meine Frage?/Problem auf.
Ich müsste dem 2. Record ebenfalls eine Property 'RequestToken' hinzufügen, damit ich den Wert zum Basis Record übergeben kann.
Das ist jetzt eine Property. Aber ich habe auch Fälle, da sind es gleich mal 20 Properties und da lohnt sich die Vererbung halt nicht, wenn ich jede Property neu definieren muss.
Ich könnte den Record jetzt genau so definieren, wie eine Klasse, weil ich dann einen parameterlosen Konstruktor habe, aber damit entfällt zB. auch die Funktionalität des Deconstructs (sinnig oder nicht, sei dahingestellt).
Und da stelle ich mir die Frage, macht ein Record überhaupt Sinn?
Da kann ich auch gleich normale Klassen verwenden. Und dahingehend, fällt mir dann auch kein Beispiel ein, wann ich denn Records nutzen soll.
Sie sind immutable und reichen Werte weiter. Aber sobald man mit Vererbung arbeiten möchte, würde nach meinem jetzigen Kenntnisstand, Records wegfallen, weil man eben die Properties alle neu definieren muss.
Ich freue mich auf auf Ideen, womit ich das besser verstehen kann oder auch, wenn ich etwas übersehe.
Im Endeffekt also, wenn ich bei Eager Loading bleibe, muss ich für unterschiedliche Includes, jeweils eine seperate Methode haben, weil ich die Expressions für Includes nicht mappen kann. Korrekt?
Da ich eine BLL Ebene und eine Repository Ebene besitze, wären die Projections in der Repository Ebene.
Oder verstehe ich das falsch? Da .ProjectTo auf IQueryable setzt und IQueryable in der Repository Ebene bleiben sollte.
Ich weiß aber noch nicht so ganz, wie ich auf das Query Specification Pattern verzichten kann.
Nehmen wir ein Beispiel:
Schüler, Klassen, Schule.
Ich brauche im Frontend eine Klasse und auch nur die dazugehörigen Schüler, nicht die Schule. (Eager Loading wäre ein Include auf Schüler)
Rufe eine generische GET Methode (die nur die Klasse zurückgeben würde ohne die Schüler) in der BLL auf.
Die BLL-Methode ruft die GET-Methode im Repository auf und dann mappe ich den Result in der BLL Methode und gib es zum Frontend zurück. (Als Bsp. im Repository: context.Classes.ToList()).
Wie halte ich das denn jetzt generisch, dass ich bis zum Repository weiterleiten kann, dass ich die Schüler auch brauche?
Also würdest du das Query Specification Pattern nicht empfehlen und im DAL eine extra Methode dann dafür haben, die mir explizit die Pages zurück gibt, mit den Includes die ich benötige?
Edit:
Bzw. das Pattern nur für alles andere verwenden außer Includes?
funktioniert das Mapping. Reicht auch für Criteria.
Aber bei dem Include funktioniert das Mapping nicht.
Sobald ich das gemappte Include zum Repository übergebe und EF Core das Statement verarbeitet, bekomme ich diese Fehlermeldung:
Fehler
Error: System.InvalidOperationException: The expression 'Convert(x.Topic, Object)' is invalid inside an 'Include' operation, since it does not represent a property access: 't => t.MyProperty'. To target navigations declared on derived types, use casting ('t => ((Derived)t).MyProperty') or the 'as' operator ('t => (t as Derived).MyProperty'). Collection navigation access can be filtered by composing Where, OrderBy(Descending), ThenBy(Descending), Skip or Take operations. For more information on including related data, see http://go.microsoft.com/fwlink/?LinkID=746393.
Kann man die Navigation Property überhaupt Mappen? Oder kann ich mit dem Include gar nicht auf DTO's gehen und muss direkt auf die Entitäten zurück greifen?
Hatte ich im nachhinein erstellt, weil die englische .NET Community nicht in diesem Forum vertreten ist.
Ist aber gelöscht.
Zitat von Abt
Dadurch hab ich mir durchgelesen, wie das aktuell funktioniert; und es ist wohl so, dass es derzeit keine Streaming Möglichkeit von WebAssembly gibt (egal ob Blazor oder was anderes).
Ja läuft^^
Wie erwähnt, habe ich das dann mit Javascript umgesetzt.
zur Info, dass Github Issue wurde ins Backlog verschoben.
Falls da was neues kommen sollte, sag ich hier Bescheid.
Dann dachte ich, löse ich das nicht über Streaming der ganzen Datei, sondern transferiere die Datei in Chunks.
buffer = new byte[8120];
using Stream stream = file.OpenReadStream(long.MaxValue); //file = IBrowserFile vom <InputFile> Element
await stream.ReadAsync(buffer, 0, buffer.Length);
Der Code-Schnipsel sorgt ebenfalls für die gleiche Exception. (3. Zeile)
Also gibt es derzeit mit der Blazor Componente keine Möglichkeit, Dateien über 2? GB zu handlen.
Für meinen Test war es eine Datei mit 9,7 GB.
Werde das ganze über Plain JS lösen, weil da funktioniert das..
Grüße
Edit:
Habe gerade nochmal gegooglet und spontan vom 15.11.2020 das Github Issue gefunden. Gleicher Fehler, ist aber auch nicht gefixt.
Lande im Catch. Siehe UI Info im Anhang. Und aus dem Stacktrace, kann ich nicht erkennen, woran es liegt.
Ich denke es liegt an dem IBrowserFile Interface bzw. der OpenReadStream Methode die aufgerufen wird.
Die Datei die hochgeladen wurde, war 8,7GB groß. Eine TextDatei von 0 Bytes konnte ich ohne Probleme hochladen bzw. ist der Call zur API durch gegangen.
Aber woran kann es liegen?
Hier noch einmal der Stacktrace von der Exception:
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Threading.Tasks.ValueTask`1.get_Result()
at Microsoft.AspNetCore.Components.Forms.RemoteBrowserFileStream.<FillBuffer>d__9.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.IO.Pipelines.PipeCompletion.ThrowLatchedException()
at System.IO.Pipelines.Pipe.GetReadResult(ReadResult& result)
at System.IO.Pipelines.Pipe.ReadAsync(CancellationToken token)
at System.IO.Pipelines.Pipe.DefaultPipeReader.ReadAsync(CancellationToken cancellationToken)
at Microsoft.AspNetCore.Components.Forms.RemoteBrowserFileStream.<CopyFileDataIntoBuffer>d__10.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ValueTaskAwaiter`1.GetResult()
at Microsoft.AspNetCore.Components.Forms.BrowserFileStream.<ReadAsync>d__22.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.IO.Stream.<CopyToAsyncInternal>d__29.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Net.Http.HttpContent.<<CopyToAsync>g__WaitAsync|56_0>d.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Net.Http.MultipartContent.<SerializeToStreamAsyncCore>d__23.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Net.Http.HttpContent.<<CopyToAsync>g__WaitAsync|56_0>d.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Net.Http.HttpConnection.<SendRequestContentAsync>d__62.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Net.Http.HttpConnection.<SendAsyncCore>d__56.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Net.Http.HttpConnection.<SendAsyncCore>d__56.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
at System.Net.Http.HttpConnectionPool.<SendWithRetryAsync>d__72.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Threading.Tasks.ValueTask`1.get_Result()
at System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable`1.ConfiguredValueTaskAwaiter.GetResult()
at System.Net.Http.RedirectHandler.<SendAsync>d__4.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
at System.Net.Http.DiagnosticsHandler.<SendAsyncCore>d__5.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
at Microsoft.Extensions.Http.Logging.LoggingHttpMessageHandler.<SendAsync>d__5.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.<SendAsync>d__5.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
at System.Net.Http.HttpClient.<SendAsyncCore>d__85.MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
at Refit.RequestBuilderImplementation.<>c__DisplayClass14_0`2.<<BuildCancellableTaskFuncForMethod>b__0>d.MoveNext() in /_/Refit/RequestBuilderImplementation.cs:line 296
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
at RD.Intelligence.Web.Components.AddFilesComponent.<UploadFilesAsync>d__14.MoveNext() in D:\Repositories\RD.Intelligence\RD.Intelligence.Web\Components\AddFilesComponent.razor:line 87
Aber Feedback zum Code:
- Du hast viel obsolete Code vermutlich aus verschiedenen Quellen zusammenkopiert, selbst die Error Code haste übernommen (???)
Stimmt. Derzeit aber nur zum testen, bis es funktioniert, danach wird der Code deutlich verbessert.
Zitat von Abt
- Man sollte IFormFile verwenden
Warum? Werden die Files dann nicht im Arbeitsspeicher zwischen gespeichert? Warum nutzt die Microsoft Doku das ebenfalls nicht um große Dateien hochzuladen?
Zitat von Abt
- Datenbank-Operationen darf man auch ruhig auslagern, muss man nich mit Logik mischen ;-)
Das ganze funktioniert nur bedingt.
Der Code-Block der nicht ausgeführt wird auf diesem Weg ist der:
int bytesRead = 0;
byte[] buffer = new byte[8192];
while ((bytesRead = await section.Body.ReadAsync(buffer.AsMemory(0, buffer.Length))) != 0)
{
await uploadStream.WriteAsync(buffer.AsMemory(0, bytesRead));
}
await uploadStream.CloseAsync();
await section.Body.DisposeAsync();
Er überspringt die while Schleife, weil der Body anscheinend leer ist.
Aber was mache ich beim Post Request falsch? Durch googlen bin ich bis jetzt auch nicht schlauer geworden.
Ob ich HttpClient.PostAsync oder HttpClient.SendAsync aufrufe, ändert nichts.
ich meine die Standard implementierte Authentifizierung von Microsoft, wenn man bei der Erstellung den Authentication Type auf Individual Accounts setzt.
Der dadurch erzeugte ApplicationDbContext der vom IdentityDbContext erbt, sowie die Standard Login/Logout etc. Seiten von Identity, brauche ich alles nicht.
Bei Blazor Server-Side gibt es keinen direkten HttpContext, weil die Kommunikation über SignalR läuft.
Dafür ist ja dann der AuthenticationStateProvider da.
Mit dem Code kann ich mich auch anmelden (Razor Page, User Verifizierung seitens DB ausgeschlossen)
Was mir aber jetzt fehlt, dass der State gespeichert wird, was ja über das Cookie laufen würde, wenn ich es richtig verstehe.
Und wie dieser dann wieder dazu führt, dass der User weiterhin angemeldet ist.
Aber wie geht es jetzt weiter mit dem Cookie?
Was man noch machen kann, ist eine eigene Implementierung vom AuthenticationStateProvider, aber das hilft mir auch nicht weiter.
Alle Beispiele die ich finde nehmen irgendein HttpContext um sich anzumelden etc. (Bspw. hier: how-to-authenticate-a-user-with-blazor-server)
Aber ich habe noch keine Lösung ohne Identity gefunden bzw. auch keine mit Identity die ich auf meinen Fall adaptieren könnte.
ich bin gerade am überlegen, wie ich das folgende Problem löse.
Ich habe eine
- Web-Anwendung (Blazor, Server-Side, .NET5)
- Razor Class Library (.NET5) (RCL)
Ich habe in der RCL mehrere Komponenten, die auch den CSS Scope Identifier von Blazor nutzen.
Die hinterlegte css Datei wird von einer scss Datei generiert.
Die genannte scss Datei importiert eine "_mixins.scss" Datei, um mixins zu nutzen, die das Leben vereinfachen. (_mixins.scss Datei liegt auch in der RCL)
Funktioniert alles soweit.
Problem ist jetzt, dass ich die "_mixins.scss" Datei auch gerne in der Web-Anwendung in eine scss Datei importieren würde, nur weiß ich nicht wie.
Ein einfacher Import über einen relativen Pfad funktioniert nicht, weil Web Compiler, die Datei nicht findet.
Dann hatte ich gegooglet und bin auf "Shared Project" gestoßen.
Habe ich dann mal ein Projekt erstellt, die scss Datei hinzugefügt und in der Web-Anwendung hinzugefügt. Konnte die Datei ohne Probleme in einer anderen scss Datei importieren.
Nur kann ich die RCL nicht in das Shared Project migrieren, weil da die CSS Isolation fehlt.
Und ein drittes Projekt für nur ein paar scss Dateien finde ich, ist ein zu großer Overhead.
Andere Möglichkeit die ich noch gesehen hatte, die scss Datei zu linken in einen Ordner in der Web-Anwendung. Funktioniert aber ebenfalls auch nicht, weil der die Datei ebenfalls nicht findet.
Als Nachtrag, hab es über Automapper geschafft.
Aber nicht das ganze Objekt direkt, sondern die Properties einzelnt zu mappen.
Man braucht dafür das Nuget Package von Automapper 'AutoMapper.Extensions.ExpressionMapping'.
Und dann muss man noch beim hinzufügen des Automappers im Service die Methode aufrufen
automapper.AddExpressionMapping();
//So sieht das dann ganz aus
services.AddAutoMapper((serviceProvider, automapper) =>
{
automapper.AddExpressionMapping();
automapper.AddProfile<AutoMapping>();
}, typeof(ApplicationDbContext).Assembly);
Das hatte ich davor auch schon alles.
Was ich nun mache, das ich die einzelnen Properties vom Specification Objekt durch gehe und diese mappe:
Mit Automapper schaffe ich es nicht, es zu mappen. Der Wert ist immer null. (Criteria)
Deshalb manuell mappen.
Wie mappt man eine Expression? I dont know => google.
Wobei ich bei der _parameter Variable das 'c' entfernt habe.
Mapping erfolgreich.
1. Bild im Anhang: So sieht es dann im Debugger aus.
Lass ich die Expression nun gegen die Db laufen, erhalte ich eine InvalidOperationException, sobald ich auf den Entites mit Where und dem Criteria filtere.
Message:
The LINQ expression 'DbSet<Category>()
.Where(c => x.Id == __idToGet_0)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See [url]https://go.microsoft.com/fwlink/?linkid=2101038[/url] for more information.
In der Message steht "c => x.Id....".
Ich gehe davon aus, dass es an dem "c" liegt. Aber woher kommt das?
P.S.: Ich weiß wie Expressions oberflächlich funktionieren, aber so genau, wie man diese mappt, woraus die konkret bestehen weiß ich nicht. Deswegen habe ich vorerst die Methoden aus dem SO Beitrag 1:1 übernommen.