Moin,
wie in meinen beiden anderen Threads bereits angesprochen beschäftige ich mich im Moment mit C#, MVVM, WPF und SQLite (Ich selber habe nur ein wenig VBA (Excel) und Access Erfahrung, sprich dies ist mein erstes Projekt mit C# überhaupt)
Zu meinem "Projekt":
Als Backend dient mir eine SQLite Datei mit 2 Tabellen (Mitarbeiter und Team) und ich möchte jedem Mitarbeiter (ID, Vorname, Nachname) ein Team zuordnen. Dieses Team soll mir im späteren Verlauf eine mandantenfähige Lösung dienlich sein (dazu weiter unten mehr). Das Anlegen ieines neuen Mitarbeiters oder Teams kann via Menu oben vorgenommen warden, das Löschen funktioniert via "Delete". De Änderungen warden allesamt in die SQLite Datei zurück gespielt.
Bitte unter "settings.settings" den Link zur Test.db anpassen bevor das Projekt debuggt wird 😉
Was ich mir wünsche:
Wie bereits gesagt stehe ich ganz am Anfang mit C# und würde mich freuen wenn ihr euch mein kleines Projekt ansehen könntet. Denke da ist eine ganze Menge an Verbesserungspotential vorhanden. Würde mir natürlich auch wünschen das Verbesserungsvorschläge ev. direct an meinem Code aufgezeigt würden.
Zum weiteren Verlauf:
Ich möchte dieses "Miniprojekt" nutzen um eine bestehende Accesslösung abzulösen. Es wird noch 2-3 weitere solche Projekte geben die dann schlussendlich in ein grosses Ganzes übernommen warden … davon bin ich aber 1.) noch weit weit entfernt und 2.) möchte ich so gut als mir möglich Best-Practice betreiben (klar, ein Profi würde das ganz anders angehen)
Link:
<Link entfernt>
Sonstiges:
An dieser Stelle schon mal vielen Dank an alle die meinen Post gelesen haben 😃
Bitte stell Dein Projekt auf einer Plattform wie GitHub, GitLab oder Azure DevOps (Public Repository) zur Verfügung. Den Link hab ich entfernt.
Besten Dank!
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
au weia, hoffe ich habe das nun richtig gemacht mit GitHub … falls net bitte melden (musste mir erstmal n Account anlegen)
--> https://github.com/MoritzJuergensen/SQLite (Datenbank befindet sich im Database Ordner --- Pfad muss wie oben angegeben angepasst warden)
Ich hab nur mal ganz grob reingeschaut. Ein nächster logischer Schritt wäre ggf. deine Datenabfragen aus deinem Model rauszuholen und somit zu entkoppeln.
Desweiteren ist es empfehlenswert ein Lightweight ORM wie z.B. "Dapper" ( Hier gibt es auch Extensions für SQLite ) einzusetzen damit du nicht selbst den DataReader nutzen musst um die Daten aus der DB in deine Anwendung zu holen.
Hier ein paar Tips:
[Artikelserie] SQL: Parameter von Befehlen
[Artikel] Drei-Schichten-Architektur
[Artikel] MVVM und DataBinding
[Artikel] C#: Richtlinien für die Namensvergabe
Ansonsten macht sowas hier wenig Sinn:
public void MyMethod(object parameter)
{
if ((string)paramter == "Team")
CreateTeam();
// etc.
}
Erstens sollte man schon den richtigen Datentyp verwenden (in dem Fall string
statt object
), und zweitens kann man auch direkt die CreateTeam-Methode aufrufen. Magic Strings sind aber auf keinen Fall zu empfehlen.
Weeks of programming can save you hours of planning
erstmal danke euch Beiden!
@KroaX
Muss ich mir anschauen wie das mit dem ORM funktioniert, lese das hier zum ersten Mal. Yep habe einfach mal "alles" ins ViewModel gepackt, funktioniert aber def. keine optimale Lösung. Hier dran muss ich defitiniv arbeiten
@MrSparkle
denke du sprichst von dieser Methode, oder?
private void CreateNewRow(object param)
{
string Parameter = (string) param;
if (Parameter == "Mitarbeiter")
{
OC_Mitarbeiter.Add(new Mitarbeiter(99999, "?", "?", 1));
}
else if (Parameter == "Team")
{
OC_Team.Add(new Team(99999, "?"));
}
else
{
MessageBox.Show("falscher Parameter");
}
}
Die Methode hängt ja am Menu, sollte ich die beiden "Create" Methoden als einzelne Methoden definieren? (Hatte mir das irgendwie über die Zeit unter VBA angewöhnt alles zu verschachteln wenn es ähnliche Sachen sind --- hier wäre in dem Fall eine "CreateTeam" und Eine "CreateMitarbeiter" Methode sinnvoll?)
Zu den Links, Nr. 1 und 4 sind toller Lesestoff aber für 2 und 3 wäre es echt net wenn du mir praktische Beispiele anhand meines Beispiels geben würdest. Denke das würde mir helfen Theorie und Praxis besser miteinander zu verknüpfen. (Natürlich nur wenns net all zu grosse Mühe macht 😃 )
Im Prinzip brauchst Du hier die Basics von OOP.
Dann kannst Du einfach mit einer generischen Methode sowohl Team wie auch Mitarbeiter darstellen.
Was Du da mit object machst ist im Prinzip völlig untypisiertes Programmieren und daher im Ansatz schon nicht gut.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Habe nun 2 Dinge gemacht:
1.) Ich hab mir endlich n C# Buch (Schrödinger programmiert…) gekauft (und werde nach dem Abschluss dieses MiniProjekts nochmal von 0 anfangen mit dem Buch)
und
2.) Ich habe versucht den Teil mit den Mitarbeitern mit Dapper umzusetzen. Hier erstmal der Code dazu (komplett neu aufgebaut):
Employee.cs
namespace SQLiteDapper.Model
{
public class Employee
{
public int ID_Mitarbeiter { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int Team { get; set; }
}
}
IEmployeeRepository.cs
using System.Collections.Generic;
namespace SQLiteDapper.Model
{
public interface IEmployeeRepository
{
List<Employee> GetAll();
Employee GetById(int id);
bool Update(Employee employee);
}
}
EmployeeRepository.cs
using Dapper;
using System.Collections.Generic;
using System.Data.SQLite;
using System.Linq;
namespace SQLiteDapper.Model
{
class EmployeeRepository : IEmployeeRepository
{
private SQLiteConnection db = new SQLiteConnection(Properties.Settings.Default.connString);
// Get Employee record by Id
public Employee GetById(int id)
{
return this.db.Query<Employee>("SELECT * FROM Mitarbeiter WHERE ID_Mitarbeiter=@Id", new { Id = id }).FirstOrDefault();
}
// Retreives the data from the table.
public List<Employee> GetAll()
{
return this.db.Query<Employee>("SELECT * FROM Mitarbeiter").ToList();
}
// Update the employee record
public bool Update(Employee employee)
{
string query = "UPDATE Mitarbeiter SET LastName = @LastName WHERE ID_Mitarbeiter = @ID_Mitarbeiter";
var count = this.db.Execute(query, employee);
return count > 0;
}
}
}
ViewModel.cs
using SQLiteDapper.Model;
using System.Collections.Generic;
namespace SQLiteDapper.ViewModels
{
public partial class ViewModel
{
public IEnumerable<Employee> Employees { get; set; }
public ViewModel ()
{
// Get data for View Datagrid
IEmployeeRepository employeeRepository = new EmployeeRepository();
Employees = employeeRepository.GetAll(); //per Binding ins XAML File
Employee emp = employeeRepository.GetById(1);
emp.LastName = "Blödsinn";
employeeRepository.Update(emp);
}
}
}
So, es funktioniert soweitber nun stehe ich aber vor einem Problem das ich ohne Hilfe def. net sauber lösen kann:
Für eine Liste funktioniert onpropertychanged ja nicht. Lohnt es sich die Liste in eine ObservableCollection umzuwandeln (Dapper unterstützt soweit ich weiss OC nicht direkt) oder soll ich diese sehr statischen Daten einfach als "Liste" belassen? (Meinen Datagrid kann ich ja trotzdem ändern und dann per Button oder so den Update Prozess anstossen) --> Falls Ja, wie mache ich das am "Besten" habs versucht aber das klappt bei mir nicht 😦 )?
Was mir auf die Schnelle auffällt:
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Danke für die fixe Antwort!
Das mit der Liste, ID_Mitarbeiter, TeamID und der OOP Benennung habe ich bereits umgesetzt, danke dafür!
Dependency Injection muss ich später mal nachgucken, denke das ist aber imho zweitrangig. Mein grösstes Problem ist das Thema "Update":
In einem Beispiel habe ich folgendes gefunden:
private int UpdateStudent(Student student)
{
using (var connection = new SqlConnection(sqlConnectionString))
{
connection.Open();
var affectedRows = connection.Execute("Update Student set Name = @Name, Marks = @Marks Where Id = @Id", new { Id = studentId, Name = txtName.Text, Marks = txtMarks.Text });
connection.Close();
return affectedRows;
}
}
Sehe ich das richtig das ich jede Eigenschaft überspielen soll? (Beispiel: Ich ändere "LastName" und überspiele dann trotzdem "FirstName" und "TeamID" mit?) Könnte ja rechts das Datagrid einblenden und links dann 2 Textboxen und Eine Combobox mit den Daten füllen. Nach dem Aktualisieren könnte ich via Button den Update Prozess starten.
Oder könnte man nach dem Editieren eines Feldes im Datagrid den Updateprozes starten?
PS:
die grösste Tabelle hat im Moment (nach 2 Jahren) ca. 2000 Einträge, denke GetAll() dürfte da noch funktionieren, oder?
Es macht keinen Sinn 2000 Einträge auf einmal zu laden; Du wirst diese niemals gleichzeitig anzeigen können.
Prinzipiell ja: bei SQL musst Du alle Eigenschaften manuell angeben, die Du aktualisieren willst.
Es gibt aber Dapper Extensions (Dapper.Contrib), die Methoden in vereinfachter Form zur Verfügung stellen, dass Du das SQL nicht manuell schreiben musst.
Projekt ist aber nicht sonderlich aktiv.
Bitte übernehm das Beispiel nicht in dieser Form 1:1.
Das Öffnen von Verbindungen gehört NICHT in eine Abfragemethode.
Wie Du die UI umsetzt ist völlig unabhängig von der Datenbank.
[Artikel] Drei-Schichten-Architektur
Prinzipiell verwendet man auch andere Klassen in der UI, Logik- und Datenschicht. Selten, dass in der realen Welt alle 3 Layer exakt die gleiche Prseäntation eines Modells haben.
Es gibt auch sehr moderne Ansätze, die überhaupt keine spezifischen Modelle mehr für UI und Logik kennen.
Hier werden jeweils nur die Eigenschaften geladen, die benötigt und in Form von Projektionsklassen bereitgestellt werden.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Ne das mit den 2000 war eher auf das Thema Performance bezogen. In der heutigen Lösung werden alle 2000 in einer scrollbaren Liste dargestellt, würde ich so nie wiede realisieren wollen 😃
Über das Contrib Projekt bin ich beim googlen auch gestolpert, hatte aber bzgl. der Aktivität ein ähnliches Gefühl wie du … aber bei den paar Eigenschaften spielt es imho noch keine Rolle.
Das Beispiel war eher auf die einzelnen SQL Eigenschaften bezogen, der Rest ist selbst aus meiner Sicht suboptimal gelöst.
Die UI hatte ich nur ins Spiel gebracht weil ja beim vorherigen Beispiel die Inotify Schnittstelle zusammen mit der ObservableCollection alles "selbst" gemacht hat. Ohne die Schnittstelle habe ich ja das Problem das beispielsweise die Daten die dem Team hinterlegt sind nicht mehr automatisch aktualisiert werden sondern ich ja eigentlich bei einer Änderung die Initialisierung erneut anstossen muss (zumindest bei dem was ich bisher zur List gelesen habe)
stehe jetzt komplett aufm Schlauch 😦
Kann mir jemand anhand des neuen Codes zeigen wie mein Code aussehen muss damit bei Änderungen im View (nehmen wir an ich ändere bei einem Mitarbeiter den Nachnamen direkt im Datagrid) die Änderung via OOP in die Datenbank zurück gespeichert werden?
Habs nach langem Würgen hingekriegt, allerdings glaube ich das es net so "gut" ist.
Hier nur die 3 aktualisierten Dateien:
ViewModel.cs
using SQLiteDapper.Model;
using System;
using System.Collections.Generic;
using System.Windows.Input;
namespace SQLiteDapper.ViewModels
{
public partial class ViewModel
{
public RelayCommand Update { get; set; }
public List<Employee> Employees { get; set; }
public Employee MySelectedEmployee { get; set; }
public ViewModel ()
{
// Get data for View Datagrid
IEmployeeSQLiteRepository employeeRepository = new EmployeeSQLiteRepository();
Employees = employeeRepository.GetAll(); //per Binding ins XAML
this.Update = new RelayCommand(_ => UpdateRecord());
}
void UpdateRecord()
{
IEmployeeSQLiteRepository employeeRepository = new EmployeeSQLiteRepository();
Employee emp = employeeRepository.GetById(MySelectedEmployee.ID);
emp.FirstName = MySelectedEmployee.FirstName;
emp.LastName = MySelectedEmployee.LastName;
emp.TeamID = MySelectedEmployee.TeamID;
employeeRepository.Update(emp);
}
}
public class RelayCommand : ICommand
{
private Action<object> action;
public RelayCommand(Action<object> action)
{
this.action = action;
}
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
action(parameter);
}
public event EventHandler CanExecuteChanged;
}
}
IEmployeeSQLiteRepository.cs
using System.Collections.Generic;
namespace SQLiteDapper.Model
{
public interface IEmployeeSQLiteRepository
{
List<Employee> GetAll();
//bool Add(Employee employee);
Employee GetById(int id);
Employee Update(Employee employee);
}
}
EmployeeSQLiteRepository.cs
using System;
using Dapper;
using System.Collections.Generic;
using System.Data.SQLite;
using System.Linq;
using Dapper.Contrib.Extensions;
namespace SQLiteDapper.Model
{
class EmployeeSQLiteRepository : IEmployeeSQLiteRepository
{
private SQLiteConnection db = new SQLiteConnection(Properties.Settings.Default.connString);
// Get Employee record by Id
public Employee GetById(int id)
{
try
{
return this.db.Query<Employee>("SELECT * FROM employees WHERE ID=@Id", new { Id = id }).FirstOrDefault();
}
catch (Exception exception)
{
Console.WriteLine(exception);
return null;
}
}
// Retrieves the data from the table.
public List<Employee> GetAll()
{
try
{
return this.db.Query<Employee>("SELECT * FROM employees LIMIT 20").ToList();
}
catch (Exception exception)
{
Console.WriteLine(exception);
return null;
}
}
// Update the employee record
void Update(Employee employee)
{
try
{
SqlMapperExtensions.Update(db, new Employee { ID = employee.ID, FirstName = employee.FirstName, LastName = employee.LastName, TeamID = employee.TeamID });
}
catch (Exception exception)
{
Console.WriteLine(exception);
}
}
Employee IEmployeeSQLiteRepository.Update(Employee employee)
{
throw new NotImplementedException();
}
}
}
So oder so habe ich hierzu ein paar Sachen die mir nicht klar sind:
Ist es "sinnvoll" die von Dapper genutzte Liste zu verwenden oder sollte man diese in eine ObservableCollection umwandeln da man dann "INotifyPropertyChanged" nutzen kann? (Würde aber irgendwie Dapper ad absurdum führen in meinen Augen) --> Falls ja, gibt es da eine einfache Möglichkeit?
Ich führe jetzt das Update eines Mitarbeiters via Button im WPF Formular aus, ist der Code so korrekt mit der ICommand Schnittstelle?
Generell, ist der Code und der Aufbau so in Ordnung oder gibt es irgendwas "schwerwiegendes" was ich noch ändern sollte?
Datenbank-Objekte werden prinzipiell nicht in UIs verwendet -> Schichtentrennung.
[Artikel] Drei-Schichten-Architektur
Es ist selten, dass Du alle Felder aus einer Datenbank 1:1 auch so in der UI hast.
Zusätzlich stellt das natürlich eine Abhängigkeit dar, dass jede Änderung an der DB sich durch alle Schichten durchschlägt.
Zusätzlich hast Du keinerlei Logikschickt.
Du hast direkt die Datenbank-Schicht in der UI-Schicht.
Ist pragmatisch; aber natürlich auch risikoreich bei Anpassungen etc.
PS: Select * gilt als Bad Practise, weil Du eben alle Spalten liest - die Du aber evtl. gar nicht brauchst.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Danke dir für das Feedback.
Habe heute nochmal alles über den Haufen geworfen und wieder von vorne angefangen da ich nicht zufrieden bin .... Macht 0 Sinn weiter zu machen wenn die grundlegendsten Sachen nicht "stimmen".
Habe jetzt ne Möglichkeit gefunden mit Dapper einigermassen ne ObersavbleCollection raus zu kriegen und damit kann ich wieder die Inotify Schnittstelle nutzen.
PS: Ich glaube je länger je mehr das so ein Projekt anfangs vielleicht doch 2-3 Nummern zu gross ist...
Den Namen deines Repository Interface würde ich generischer halten - warum steht das SQLite im Namen, das kann durchaus Technologie Unabhängig sein!
ObservableCollection kannst du in der GUI schicht durchaus verwenden. An einer DB macht es nur dann Sinn, wenn die DB dich über Änderungen informieren würde.
Habe die letzten Tagen viel gelesen und versucht so viel als möglich umzusetzen und das ganze Projektlein etwas "seriöser" umzusetzen. (DAL, etc) Dapper wurde (entgegen des Titels) noch nicht implementiert, das mache ich erst wenn die Methoden und Co so passen.
Hier der Azurelink --> https://moritzjuergensen.visualstudio.com/SQL_V1/SQL_V1%20Team/_git/SQL_V1
Ich habe dennoch einige offenen Punkte die mir nicht einleuchten und bei denen mir vielleicht jemand helfen kann:
Wäre echt froh wenn Verbesserungen anhand meines Codes dargestellt würden, dann kann ich gleich vergleichen wo ich Fehler gemacht habe.
Vielen Dank an Alle die mir helfen 😃
wie bestimme ich ob es beispielsweise ein Bool oder ein Wert ist?
Prinzipiell mit dem Rückgabetyp von Methoden.
Im Falle von Datenbank-Operationen gibst Du i.d.R. jedoch das aktualisierte Objekt zurück.
Wenn etwas schief geht, dann wirfst Du eine Exception.
Nur dass etwas schief gegangen ist; das reicht Dir ja nicht.
macht man dann ein neues Projekt oder "sollte" man Sachen die logisch zusammen passen (Beispiel: Alles was im Konfigurationsmenu ist) in einem Projekt vereinen?
[Artikel] Drei-Schichten-Architektur
Was genau in einzelnen Projekten liegt, das kommt auf das Projekt an.
Wenn es für alles immer eine pauschale Lösung geben würde, bräuchte man keine Entwickler und Architekten. 😉
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Würdest du mir anhand meiner Update Employee Methode zeigen wie es aussehen sollte? Dann könnte ich analog die anderen Methoden entsprechend ändern.
Werde den 3 Schichten Artikel nochmals lesen und mir überlegen wie ich das in grösserem Ausmasse umsetzen könnte
Erstmal rundum:
Du hast hier eine Mini-Anwendung; im Prinzip ist es sehr einfach, wenn Du Dich an die Grundlagen der Guidelines von C# hälst - dazu musst Du sie Dir aber mal durchlesen 😃
Leider machen das die wenigsten, weswegen strukturelle Probleme oft hier schon die Ursache haben.
Wenn man sich an die wichtigsten Puntke (zB .NET Namespaces und Projekte) hält, dann kommt zB sowas bei raus:
https://github.com/BenjaminAbt/Sample.DotNetWPFStructure (einfach als Zip ziehen oder mit Hilfe der OctoTree Browserweriterung bequem anschauen).
Du hast eine Klassenbibliothek mit dem erfundenen Namen "Moritz.Mitarbeiterverwaltung", in der die gesamte Logik Deiner Anwendung untergebracht ist - ohne Abhängigkeiten an die Runtime (hier WPF) zu haben.
Hinzu kommt eben die WPF Anwendung hier als Name DesktopApp - die Runtime gehört schließlich nicht in den Projektnamen.
Die Klassenbibliothek enthält hierbei also die Logik und die Datenbankschicht (DAL) - ohne die Schichten namentlich zu nennen.
Die Namen der Klassen und Interfaces helfen Dir beim Faktor Modularisierung.
Die Logik interessiert nur, dass es ein Repository gibt, mit dem Mitarbeiter veraltet werden können (IEmployeeRepository) und ist hierbei so platziert, dass sie auch im Namespace neutral liegt.
Die konkrete Implementierung liegt nun in einem spezifischen Namespace (Sqlite) und trägt auch einen spezifischen Namen (EmployeeSqliteRepository).
Entscheidest Du Dich später statt Sqlite eben zB MSSQL zu verwenden, dann ist dies sehr einfach durch einen eigenen Namespace erreichbar inkl. den spezifischen Implementierungen (EmployeeMssqlRepository).
Die Namespaces haben dabei die Struktur, dass Du sie jederzeit in extra Projekte auslagern könntest, um eine höhere Unabhängigkeit von Dependencies (eben Mssql, Sqlite...) zu haben.
Lohnt sich dann aber erst wenn man es wirklich braucht.
Dieses Basis ermöglicht dann die Anwendung von Prinzipien wie Dependency Injection und erfüllt die 3- Schicht Architectur.
Du kannst nun nämlich die Logik aus einer WPF-App, Konsolen-App oder Web-App ansprechen.
Zur Update Methode: sofern es sich um eine einfache Anwendung handelt:
Im Endeffekt müssten aber alle Deine Repositories komplett umgeschrieben werden.
Du hast ein Repository, in dem eigentlich die Datenbank-Operationen liegen sollte, dann aber noch eine DAL Klasse. Erschließt sich mir nicht.
//Add a new employee
public ObservableCollection<Employee> AddEmployee(ObservableCollection<Employee> OCEmployees)
Die Methode hat namentlich "Füge einen Employee hinzu" - Du gibst ihr aber eine ganze Liste.
Es reicht völlig (mit Dapper, anders macht es keinen Sinn - keiner will Sql Code schreiben)
class EmployeeSqliteRepository : IEmployeeRepository
{
IDbConnection _connection; // Sqlite implementiert IDbConnection. Siehe ADO.NET
public EmployeeRepository (IDbConnection connection) // Die Connection wird übergeben und nicht im Repository erstellt! Eine Connection können sich mehrere Repository-Instanzen teilen.
{
_connection = connection;
}
public IList<EmployeeEntity> GetAll()
{
_connection.GetAll<EmployeeEntity>().ToList(); // GetAll = Dapper
}
public bool Update(EmployeeEntity e)
{
return connection.Update(e);
}
}
Und hier sieht man direkt, dass man das ganze generische Schreiben kann (OOP)
class SqlRepository<TEntity> : ISqlRepository<TEntity>
{
IDbConnection _connection;
public SqlRepository (IDbConnection connection)
{
_connection = connection;
}
public IList<TEntity> GetAll()
{
_connection.GetAll<TEntity>().ToList(); // GetAll = Dapper
}
public bool Update(TEntity e)
{
return connection.Update(e);
}
}
public class EmployeeRepository : SqlRepository<EmployeeEntity>
{
public EmployeeRepository (IDbConnection connection) : base(connection)
{
}
}
public class TeamRepository : SqlRepository<TeamEntity>
{
public TeamRepository (IDbConnection connection) : base(connection)
{
}
}
Man also den strukturellen Code nur ein mal schreiben muss.
Im Endeffekt ist das also nur eine Mischung aus den C# bzw. .NET Guidelines und Grundlagen von Objekt-orientierter Programmierung 😃
Aber ja, man muss sich diese Dinge durchlesen, im Kopf sortieren und abstrahieren. Kommt alles mit der Zeit.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Vorab:
Vielen lieben Dank für das mehr als ausführliche Feedback!!
Die Quintessenz daraus ist für mich ganz klar: Nochmals über die Bücher und ganz von vorne anfangen. Irgendwie sind viele Sachen zwar in den Kopf rein aber leider auch wieder raus; hast du mit deinem letzten Satz auch wunderbar getroffen
Aber ja, man muss sich diese Dinge durchlesen, im Kopf sortieren und abstrahieren. Kommt alles mit der Zeit.
Nun zum detaillierten Feedback:
Das mit dem Projektnamen habe ich ehrlich gesagt ignoriert weil ich erst mit Dapper gespielt hatte. Wird natürlich so auch nicht mehr gemacht. Mit meiner Interpretation von DAL war ich wohl auf dem Holzweg, hatte das so in einem Lernvideo gesehen und dachte das es sich so gehört.
Mit dem Einchecken muss ich mich explizit nochmals befassen, hatte einfach den Hauptordner angeklickt und dann veröffentlicht.
(Zum Thema Guidelines durchlesen: Witzig ist das ich mein Accessprogramm erst auch kreuz und quer aufgebaut hatte und erst später eine "organisierte und aufgeräumte" Version erstellt habe .... anstatt direkt von Anfang an ... neige zur Chaostheorie)
Das mit der DAL Klasse ist simpel (wenngleich auch total falsch): Ich dachte ich müsse sämtliche Datenbankzugriffe (den SQL Code) in eine eigene Datei auslagern, deshalb dieser "Murks".
Ich muss mir nochmals ganz in Ruhe die Grundlagen durchlesen und dann deinen Code neu interpretieren.
Der Teil mit den Listen holen und die Entity updaten ist logisch (auch wenn ich den Codesyntax noch nicht 100% verstehe) aber bei "public EmployeeRepository (IDbConnection connection) : base(connection)" ist Schluss. Vermute mit ":base(Connection)" implementierst du etwas wo die Verbindungsdaten geregelt sind, liege ich damit richtig?
Auf alle Fälle, vielen vielen Dank für deine Bemühungen ... weiss es echt zu schätzen 🙂
aber bei "public EmployeeRepository (IDbConnection connection) : base(connection)" ist Schluss. Vermute mit ":base(Connection)" implementierst du etwas wo die Verbindungsdaten geregelt sind, liege ich damit richtig?
EmployeeRepository erbt von der Basisklasse SqlRepository<EmployeeEntity>. Der Aufruf am Ende des Konstruktors bewirkt, dass der Konstruktor der Basisklasse aufgerufen wird.
Auf diese weise wird ganz gut redundanter Code vermieden und viel wichtiger: Die abgeleitete Klasse muss die Datenzugriffsschicht außer im Konstruktor gar nicht kennen.
Wissen ist nicht alles. Man muss es auch anwenden können.
PS Fritz!Box API - TR-064 Schnittstelle | PS EventLogManager |
Danke für die Erklärung 😃
@Abt
vielen Dank für den Github Link!
Ich habe die letzten Tage versucht das Beispiel von Abt (OOP) in das von ihm netterweise zur Verfügung gestellte Rahmenprojekt einzubauen. Mir erschliessen sich noch einige Sachen nicht so wirklich, darum brauche ich (mal wieder) euren Rat.
Die Struktur hat sich leicht verändert im Gegensatz zum Originalprojekt (siehe Bild im Anhang)
Der Inhalt der einzelnen Dateien sieht nun wie folgt aus:
EmployeeEntity.cs
namespace Toolbox.Employee.Database.Entities
{
class EmployeeEntity
{
}
}
EmployeeRepository.cs
using System.Data;
using Toolbox.Employee.Database.Entities;
namespace Toolbox.Employee.Database.Sqlite.Repositories
{
class EmployeeRepository : SqlRepository<EmployeeEntity>
{
public EmployeeRepository(IDbConnection connection) : base(connection)
{
GetAll(); //BEISPIEL
}
}
}
IEmployeeRepository.cs
namespace Toolbox.Employee.Database.Sqlite.Repositories
{
class IEmployeeRepository
{
}
}
ISqlRepository.cs
namespace Toolbox.Employee.Database.Sqlite.Repositories
{
internal interface ISqlRepository<TEntity>
{
}
}
SqlRepository.cs
using Dapper.Contrib.Extensions;
using System.Collections.Generic;
using System.Data;
using System.Linq;
namespace Toolbox.Employee.Database.Sqlite.Repositories
{
class SqlRepository<TEntity> : ISqlRepository<TEntity> where TEntity : class
{
IDbConnection _connection;
public SqlRepository(IDbConnection connection)
{
_connection = connection;
}
public IList<TEntity> GetAll()
{
return _connection.GetAll<TEntity>().ToList(); // GetAll = Dapper
}
public bool Update(TEntity e)
{
return _connection.Update(e);
}
}
}
DbSqliteContext.cs
namespace Toolbox.Employee.Database.Sqlite
{
class DbSqliteContext
{
}
}
IDbContext.cs
namespace Toolbox.Employee.Database
{
class IDbContext
{
}
}
Employee.cs
using System.ComponentModel;
namespace Toolbox.Employee.Models
{
public class Employee
{
//Employee ID -- Auto Increment (Database)
public int Id { get; set; }
private string _firstName;
public string FirstName
{
get => _firstName;
set
{
if (_firstName != null && _firstName != value)
{
_firstName = value;
}
OnPropertyChanged("FirstName");
}
}
private string _lastName;
public string LastName
{
get => _lastName;
set
{
if (_lastName != null && _lastName != value)
{
_lastName = value;
}
OnPropertyChanged("LastName");
}
}
//TeamId -- Foreign Key
private int _teamId;
public int TeamId
{
get => _teamId;
set
{
if (_teamId != value)
{
_teamId = value;
}
else
{
return;
}
OnPropertyChanged("TeamId");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Team.cs
namespace Toolbox.Employee.Models
{
}
EmployeeService.cs
using System;
using System.Collections.Generic;
using System.Text;
namespace Toolbox.Employee.Services
{
class EmployeeService
{
}
}
Soweit die Theorie, nun habe ich mir dazu ein paar Gedanken gemacht, recherchiert und mir sind einige Dinge unklar und bräuchte weitere Hilfe:
um eine generische Methode nutzen zu können muss ich ja eine Entity definieren. Ist es richtig das ich in der Datei "EmployeeEntity.cs" definiere aus was diese Entity besteht (beispielsweise ID, Vorname, Nachname) und dann in der Datei "Employee.cs" beschreibe das der Vorname ein String ist und wie get/set definiert sind?
damit ich die IDbConnection "connection" nutzen kann muss ich ja irgendwie definieren wohin sie connecten soll. Wo wird das definiert (in welcher Datei) und vor Allem, die Verbindung sollte ja nach erfolgtem Update oder was auch immer wieder geschlossen werden.
wenn ich in der Datei "EmployeeRepository.cs" "GetAll" (gekennzeichnet mit Beispiel) aufrufen will, warum ist dort keine Angabe der Entity notwendig? (oder ist es so wie ich vermute und ich starte von dort gar nicht den Aufruf?)
ausgehend von der MVVM Idee, entspricht "EmployeeService.cs" dem ViewModel für die Employee Sachen?
Ist es richtig das ich in der Datei "EmployeeEntity.cs" definiere aus was diese Entity besteht (beispielsweise ID, Vorname, Nachname) und dann in der Datei "Employee.cs" beschreibe das der Vorname ein String ist und wie get/set definiert sind?
Si.
public class EmployeeEntity : Entity
{
public int Id {get;set;}
public string Vorname {get;set;}
public string Nachname {get;set;}
// ...
}
Wo wird das definiert (in welcher Datei) und vor Allem, die Verbindung sollte ja nach erfolgtem Update oder was auch immer wieder geschlossen werden.
Eigentlich übernimmt das eben die Dependency Injection für Dich.
services.AddTransient<IDbConnection>(_ => return new SqliteConnection("connection string..)".
Verwendest Du keine Dependency Injection aktuell, dann musst Du das selbst instantiieren und auch selbst verwalten (Disposen=>Closen).
Oft wrappt man die Verbindung selbst in einem "DbContext".
D.h. der DbContext kennt die Verbindung und das Repository der DbContext.
Der DbContext hat darüber hinaus die Kenntnis über alle Tabellen, die die Repositories nutzen können.
Das ist das, was Du da im Screenshot auch siehst.
warum ist dort keine Angabe der Entity notwendig
Generics => Generic Classes (C# Programming Guide)
- ausgehend von der MVVM Idee, entspricht "EmployeeService.cs" dem ViewModel für die Employee Sachen?
Nein. Ein Service ist Bestandteil der Logik, nicht der View.
Das ViewModel kann/darf aber den Service kennen und kann drauf zugreifen.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Woher kommt denn das ": Entity"?
public class EmployeeEntity : Entity
Müsste ich das nicht auf das Employee.cs File im Model Ordner referenzieren?
Ah, ist hier gar nicht drin - sorry.
Man kann eben Vererbung nutzen, um generische Einschränkunge zu realisieren.
Wenn man folgendes schreibt...
class SqlRepository<TEntity> : ISqlRepository<TEntity> where TEntity : class
.. dann kann man hier als TEntity alles einwerfen, was eine Klasse ist.
Wenn man schreibt
class SqlRepository<TEntity> : ISqlRepository<TEntity> where TEntity : class, Entity
schreibt, dann man kann nur Klassen als Generic übergeben, die von Entity erben (bzw. kann man auch einfach als Interface deklarieren).
public interface IEntity {}
public abstract class Entity : IEntity {}
public class EmployeeEntity: Entity {}
class SqlRepository<TEntity> : ISqlRepository<TEntity> where TEntity : class, IEntity // oder Entity wenn man kein Interface hat
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
irgendwas geht sich bei mir net aus.
Versuche mal aufzuzeigen wo meine Denkblockade ist:
In der Datei "SqlRepository.cs" sind die Datenbankaugaben wie "alle Einträge holen" oder "Eintrag updaten" abgelegt. Der Code
class SqlRepository<TEntity> : ISqlRepository<TEntity> where TEntity : class, Entity
{
//BLIBLABLUB
}
besagt das es sich um eine Klasse "SqlRepository" handelt die als Parameter eine TEntity erfordert (stellt in diesem Fall Mitarbeiter oder Team dar), die vom Interface "ISqlRepository" abhängig ist und der Parameter nur gültig ist wenn es eine Klasse ist die von Entity erbt -- in deinem Beispiel
public class EmployeeEntity: Entity {}
Die Klasse "EmployeeEntity" erbt von Entity und sie wie folgt aus:
public class EmployeeEntity : Entity
{
public int Id {get;set;}
public string Vorname {get;set;}
public string Nachname {get;set;}
// ...
}
Sprich hier ist definiert welche Eigenschaften vorhanden sind
Widerspruch 1 für mich: Wieso brauche ich denn ein Model "Employee.cs" in dem fast das Gleiche steht? Kann es ja net doppelt definieren --> Annahme: Employee.cs fällt weg
Widerspruch 2 für mich: Das mit dem " : Entity" Zusatz verstehe ich gar nicht. Was soll denn da drin stehen?--> Annahme: Gemäss folgendem Link (abstrakte Klasse) vermute ich das diese Klasse Sachen beinhaltet die für beide Entities (Team oder Mitarbeiter) relevant sind, wobei sich mir hier net erschliesst was das sein soll
Die ganz ganz groben Züge erschliessen sich mir langsam aber irgendwie bleibt jedesmal ein Fragezeichen, daher vielen Dank das Ihr meine "blöden" Fragen so umfassend beantwortet 😃
Hoffe das ich mir eurer Hilfe bald eine funktionierende Klassenbibliothek habe mit der ich das ganze nochmals durchgehen kann; sprich den kompletten Workflow innerhalb der Klasse abarbeiten kann.
Widerspruch 1 für mich: Wieso brauche ich denn ein Model "Employee.cs" in dem fast das Gleiche steht?
Das mag in Deinem sehr einfachen Beispiel so sein; die Regel ist, dass Entität und Modell nicht identisch sind.
Beispiel: Du hast in einer UserEntity zB. E-Mail und Password.
Das willst Du mit Sicherheit nicht in Deinem Model "User" haben.
Aber wie bereits gesagt: pragmatisch bleiben ist legitim.
Das mit dem " : Entity" Zusatz verstehe ich gar nicht. Was soll denn da drin stehen?
Das sind generische Einschränkungen.
Siehe Erklärung oben. Das ist ein wichtiges Element in OOP von/mit .NET.
Damit schränkst Du eben ein, dass eine Klasse nur mit den generischen Klassen funktioniert, die Du definierst.
Du musst Dich wirklich ein wenig mit den Themen beschäftigen...
Aber hier nochmal:
class SqlRepository<TEntity> : ISqlRepository<TEntity> where TEntity : class, IEntity
class EmployeeRepository : SqlRepository<EmployeeEntity>, IEmployeeRepository
Damit definierst Du, dass TEntity eine Klasse sein muss, die das Interface IEntity implementiert.
public class EmployeeEntity : Entity // Entiy implementiert IEntity
wird nun funktionieren, weil Du die Constrainst erfüllst.
public class EmployeeEntity {}
wird ein Fehler verwerfen, weil Du nirgends IEntity implementierst.
In diesem Fall führen die Constraints eben dazu, dass Du garantiert in einem Repository eine Klasse verwenden musst, die zwangsweise eine Entität in Form der Implementierung von IEntity ist.
Das ganze kann man natürlich jetzt noch weiter Spinnen, dass Du IEntity gewisse Inhalte gibst.
public interface IEntity
{
int Id {get;}
}
Du gibst also zB Deiner Entität immer vor, dass sie eine Eigenschaft id
hat, die immer ein int
ist.
Das sorgt dafür, dass Du in Deinem SqlRepository
bereits auf die Eigenschaft id
zugreifen kannst ohne die konkrete Klasse zB Employee zu kennen.
Damit kannst Du allen ableitenden Repositories zB ein GetById
() zur Verfügung stellen ohne das jedes Mal im spezifischen Repository schreiben zu müssen - was aber eben die Gemeinkeit jeder Entität voraussetzt, dass es immer eine Id-Spalte gibt.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Aber wie bereits gesagt: pragmatisch bleiben ist legitim.
Ja ich muss irgendwo ne Grenze ziehen, ansonsten habe ich zu viel drin was ich nicht wirklich verstehe. Habe jetzt schon Schwierigkeiten den ganzen Sachen zu folgen.
Du musst Dich wirklich ein wenig mit den Themen beschäftigen...
und damit wären wir bei Problem Nummer 2: Ich für meinen Teil finde es extrem schwierig anhand von abstrakten Erklärungen daraus die richtigen Schlussfolgerungen zu ziehen, zumal mir hier im RL auch niemand wirklich helfen kann. Beispielsweise dein Link zu den generischen Klassen.
Klar steht dort sehr viel und vermutlich auch sehr gut erklärt aber für mich als Laien doch schwer zu interpretieren und dann in ein funktionierendes Beispiel einzusetzen.
Wenn ich ein konkretes Beispiel habe (wie jetzt in meinem Fall die aus deiner Sicht sicherlich sehr simple Anwendung) dann kann ich anhand meines Codes die Theorie nachvollziehen, resp. ich kann es vermutlich besser als andersrum.
Die Idee mit "GetByID" greife ich auf, zumal ich in meiner Anwendung in jeder Tabelle eine eindeutige ID habe und auch haben werde.
Vielen Dank für deine Geduld! 😃 Ich denke ich werde jetzt erstmal wieder über die Bücher und versuche deinen umfangreichen Input irgendwie zu interpretieren und dann umzusetzen bevor ich auch nur daran denken sollte weiter zu machen. Werde auch im Buch weiter machen und parallel die Grundlagen nochmals durchlesen und vertiefen.
Die Idee mit "GetByID" greife ich auf, zumal ich in meiner Anwendung in jeder Tabelle eine eindeutige ID habe und auch haben werde.
Nicht nur eine eindeutige Id, sondern die Spalte muss auch überall identisch heißen (eben Id).
Am Ende vom Tag kann das so aussehen:
public interface IDbContext
{
}
public class DbContext : IDbContext
{
}
public interface IEntity
{
int Id { get; }
}
public abstract class Entity : IEntity
{
public int Id { get; set; }
}
public interface ISqlRepository<TEntity> where TEntity : class, IEntity
{
IList<TEntity> GetAll();
int Count();
TEntity GetById(int id); // Wir wissen ja, Id muss int sein
bool Delete(TEntity entity);
bool Delete(int id);
}
public abstract class SqlRepository<TEntity> : ISqlRepository<TEntity> where TEntity : class, IEntity
{
protected IDbContext DbContext { get; }
protected abstract string TableName { get; }
protected abstract string IdFIeld { get; }
protected SqlRepository(IDbContext dbContext)
{
DbContext = dbContext;
}
public IList<TEntity> GetAll()
{
return DbContext.Connection.GetAll<TEntity>();
}
public TEntity GetById(int id)
{
return DbContext.Connection.Get<TEntity>(id);
// wenn man kein Dapper.Contrib hat/will:
string sql = $"SELECT * FROM {TableName} WHERE {IdFIeld} = @Id";
TEntity entity = DbContext.Connection.Query<TEntity>(sql, new { Id = entity.Id }); // hier braucht man eben nun die definierte Id von IEntity.Id
return entity;
}
public int Count()
{
string sql = $"SELECT COUNT(*) FROM [{TableName}];";
return DbContext.Connection.ExecuteScalar<int>(sql)
}
public bool Delete(TEntity entity)
{
return DbContext.Connection.Delete(entity)
// wenn man kein Dapper.Contrib hat/will:
var sql = $"DELETE FROM {TableName} WHERE {IdFIeld} = @Id";
var affectedrows = DbContext.Connection.Execute(sql, new { Id = entity.Id }); // hier braucht man eben nun die definierte Id von IEntity.Id
}
public bool Delete(int id)
{
var sql = $"DELETE FROM {TableName} WHERE {IdFIeld} = @Id";
var affectedrows = DbContext.Connection.Execute(sql, new { Id = id });
return affectedrows > 0;
}
}
public class EmployeeEntity : Entity
{
public string Vorname { get; set; }
public string Nachname { get; set; }
}
public interface IEmployeeRepository : ISqlRepository<EmployeeEntity>
{
}
public class EmployeeRepository : SqlRepository<EmployeeEntity>, IEmployeeRepository
{
public EmployeeRepository(IDbContext dbContext) : base(dbContext)
{
}
// Tabellenname für selbst geschriebene SQL Commands, die Dapper.Contrib nicht unterstützt
// ansonsten muss eben in Dapper.Contrib via Attribute oder Mapper Configuration der Tabellenname hinterlegt werden
protected override string TableName { get; } = "Employees";
protected override string IdField { get; } = "Id";
}
Ein mal die richtige OOP Basis und Du hast nur noch den Aufwand für das Definieren der Enitäten und das Anlegen des Repositories.
Die Standard SQL Methoden bekommst Du dann durch die Vererbung einfach geschenkt.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Entschuldige die verspätete Antwort, krankheitsbedingt ist dies leider völlig in Vergessenheit geraten 😠
Ich habe die letzten Wochen versucht mittels des gekauften Buches die Grundlagen zu erlangen und mittlerweile leuchtet mir einiges ein was vorher unklar war. Habe mir jetzt dein OOP Beispiel vorgenommen und versuche im Moment jeden Schritt zu dokumentieren (warum und wieso) sowie nachzuvollziehen, was da genau abläuft, sprich wer gibt wem die Anweisung oder wer erbt von wem und was erbt er (und so weiter)
Mein jetziger Stand ist folgender:
Die Grundsatz Idee hinter deinem Ansatz ist die Definition einer generischen Struktur, sprich ich definiere die Funktionen (Löschen, Auslesen, etc) und je nachdem ob es ein Team oder ein Mitarbeiter ist wird immer dieselbe Funktion aufgerufen, nur die "Parameter" (wie zum Beispiel TableName) werden entsprechend geändert.
Um zu Prüfen ob alles funktioniert wollte ich im CodeBehind der XAML Datei eine Messagebox einbinden welche den Vor- und Nachnamen des Mitarbeiters mit der Id 1 ausgibt. Wäre jetzt über das EmployeeRepository eingestiegen aber ich kriege den Befehl net hin. (Vermute der Schluss heisst dann irgendwas a la GetById(1).FirstName😉
PS: der Kram im CodeBehind dient nur zum Testen, habe miich noch nicht mit XAML beschäftigt
PSS:
Hab das ganze Projekt neu hochgeladen, ist einfacher da was nachzugucken
--> https://moritzjuergensen.visualstudio.com/_git/Calendar?path=%2FCalendar&version=GBmaster
Den Connection String habe ich unter "Projekteigenschaften - Ressourcen" definiert. Die DbContext Datei habe ich erstellt, aber das "Set" fehlt mir noch, vielleicht kannste mir hier auch noch auf die Sprünge helfen damit ich anhand eines funktionierenden Modells das ganze mittels Buch nochmals rekapitulieren kann...Falls du mal in der Gegend um Lindau oder der Ostschweiz bist lade ich dich für die Hilfe mal auf ein Bier ein 👍
hab versucht anhand von einem Tutorial die "Dependency Injection" einzubauen, allerdings stimmt da etwas noch nicht.
Wenn ich es start wirft die Datei "SqlRepository" einen "System.NullReferenceException: "Der Objektverweis wurde nicht auf eine Objektinstanz festgelegt." Fehler aus. Hab da wohl etwas falsch eingebaut 😭
Null Reference kann aber nich vom SqlRepository kommen.
Von SqlRepository kann auch gar keine Instanz erzeugt werden, weil es eine abstrakte Klasse ist.
Wo wird die NullReference genau geworfen?
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Die Return Zeile des folgenden Codes wirft die Exception aus (in der Dateil SqlRepository.cs):
public TEntity GetById(int id)
{
return DbContext.Connection.Get<TEntity>(id);
}
Ich habe die DbContext um folgenden Code ergänzt:
using System.Data.SQLite;
namespace Calendar.Database.DbContext
{
public class DbContext : IDbContext
{
public SQLiteConnection Connection
{
get
{
return null;
}
set
{
using (var connection = new SQLiteConnection("Data Source=C:\\Database\\Database.db;Version=3;"))
{
connection.Open();
}
}
}
}
}
Eine Datei namens ContainerConfig.cs beinhaltet das NuGet Paket Autofac für die Dependency Injection und der Code sieht so aus:
using Autofac;
using Calendar.Database.DbContext;
using Calendar.Database.Repositories;
namespace Calendar
{
public static class ContainerConfig
{
public static IContainer Configure()
{
var builder = new ContainerBuilder();
builder.RegisterType<DbContext>().As<IDbContext>();
builder.RegisterType<EmployeeRepository>().As<IEmployeeRepository>();
return builder.Build();
}
}
}
Ich war nach dem Tutorial der Meinung das der Code (vor Allem der Teil in der Klammer)
public EmployeeRepository(IDbContext dbContext) : base(dbContext)
{
}
automatisch die Injection triggert aber ich bin da wohl auf dem Holzweg und geh da heute Abend nochmal über die Bücher.
Der genaue Fehlercode lautet übrigends:
System.NullReferenceException
HResult=0x80004003
Nachricht = Der Objektverweis wurde nicht auf eine Objektinstanz festgelegt.
Quelle = Dapper
Stapelüberwachung:
bei Dapper.SqlMapper.<QueryImpl>d__140`1.MoveNext() in C:\projects\dapper\Dapper\SqlMapper.cs: Zeile1066
bei System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
bei System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
bei Dapper.SqlMapper.Query[T](IDbConnection cnn, String sql, Object param, IDbTransaction transaction, Boolean buffered, Nullable`1 commandTimeout, Nullable`1 commandType) in C:\projects\dapper\Dapper\SqlMapper.cs: Zeile721
bei DapperExtensions.DapperImplementor.GetList[T](IDbConnection connection, IClassMapper classMap, IPredicate predicate, IList`1 sort, IDbTransaction transaction, Nullable`1 commandTimeout, Boolean buffered)
bei DapperExtensions.DapperImplementor.Get[T](IDbConnection connection, Object id, IDbTransaction transaction, Nullable`1 commandTimeout)
bei System.Dynamic.UpdateDelegates.UpdateAndExecute5[T0,T1,T2,T3,T4,TRet](CallSite site, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4)
bei DapperExtensions.DapperExtensions.Get[T](IDbConnection connection, Object id, IDbTransaction transaction, Nullable`1 commandTimeout)
bei Calendar.Database.Repositories.SqlRepository`1.GetById(Int32 id) in C:\Users\mj\Source\Repos\Calendar_Azure\Calendar\Calendar\Database\Repositories\SqlRepository.cs: Zeile27
bei Calendar.MainWindow..ctor() in C:\Users\mj\Source\Repos\Calendar_Azure\Calendar\Calendar\ViewModels\MainWindow.xaml.cs: Zeile24
public SQLiteConnection Connection
{
get
{
return null;
}
set
{
using (var connection = new SQLiteConnection("Data Source=C:\\Database\\Database.db;Version=3;"))
{
connection.Open();
}
}
}
Das kann nicht klappen; das macht auch null sinn und im gleichen Zug kann der Zugriff hier nur null ergeben und eine Exception werfen.
Siehe auch [FAQ] NullReferenceException: Der Objektverweis wurde nicht auf eine Objektinstanz festgelegt - kann man auch super debuggen ( [Artikel] Debugger: Wie verwende ich den von Visual Studio? )
public SQLiteConnection Connection
{
get
{
var connection = new SQLiteConnection("Data Source=C:\\Database\\Database.db;Version=3;");
connection.Open();
return connection;
}
}
Hier darf auch kein using() verwendet werden, sonst ist die Verbindung natürlich sofort wieder verworfen.
usings() verwendet man so, dass sie um die eigentliche Operation liegen.
Noch besser natürlich
public class SqliteContext : IDbContext
{
private IDbConnection _connection = null;
private string _connectionString;
public SqliteContext(string connectionString)
{
_connectionString = connectionString;
}
public IDbConnection Connection
{
get
{
if (_connection is null)
{
_connection = new SQLiteConnection(_connectionString);
_connection.Open();
}
return _connection;
}
}
}
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Das mit dem Debugging hatte ich mittels Einzelschritt versucht (und eben auch um heraus zu finden, wann er wohin springt) aber wenn natürlich der Code an sich zwar ok aber trotzdem Blödsinn ist dann bringt dir das nicht viel.
Deine 2. Variante erinnert mich stark an mein Dependency Injection Beispiel von gestern, guck es mir daheim mal an.
A propos, den Teil mit "GetAll()" in SqlRepository.cs musste ich deaktivieren weil GetAll ein IEnumerable zurück gibt und daher
public IList<TEntity> GetAll()
{
return DbContext.Connection.GetAll<TEntity>();
}
nicht funktionieren kann (wenn ich mich nicht komplett täusche).
Nochmals vielen Dank für die tatkräftige Unterstützung!
Eine kleine Randnotiz noch:
Der Code funktioniert soweit, allerdings habe ich die lustige Eigenschaft dass die Tabelle im db File auf Entity enden muss, sprich "EmployeeEntity". Das finde ich aber raus warum das so ist. 😉
A propos, den Teil mit "GetAll()" in SqlRepository.cs musste ich deaktivieren weil GetAll ein IEnumerable zurück gibt und daher..
Das eine hat mit dem anderen nichts zutun.
Schau Dir an wie Listen funktionieren, dann siehst Du, dass IList von IEnumerable erbt.
Dann verstehst Du auch, wie hier der korrekte Weg wäre.
Intermediate Materialization (C#)
Darüber hinaus ist es aber nur sehr selten eine gute Idee wirklich alles aus der Datenbank zu laden.
Daher sollte man GetAll eh vermeiden, wenn möglich.
Das finde ich aber raus warum das so ist. 😉
Steht in der Doku.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Sodele, mittlerweile habe ich die "Add" und die "Update" Variante auch eingebaut und es klappt wunderbar!
Add (hier musste ich allerdings "long" anstatt "int" nehmen - int wurde in der Dapper Doku genannt - warum kann ich nicht sagen, aber long funktioniert)
public long Add(TEntity entity)
{
return DbContext.Connection.Insert(entity);
}
Update
public bool Update(TEntity entity)
{
return DbContext.Connection.Update<TEntity>(entity);
}
Das 2. Beispiel für den Inhalt der DbContext habe ich mal angeguckt:
"noch besser weil die Connection zuerst geprüft wird (sprich ist bereits eine offene Verbindung vorhanden?) und weil es nun als SqLite Verbindungsdatei erkennbar ist?
Mir ist noch aufgefallen das nun die Datei "Employee.cs" (mein ursprüngliches Model) gar nicht mehr eingebunden ist. Gehe ich richtig in der Annahme das nun die Definition (als Beispiel)
public string FirstName
{
get => _firstName;
set
{
if (_firstName != null && _firstName != value)
{
_firstName = value;
}
OnPropertyChanged("FirstName");
}
}
direkt in der EmployeeEntity gemacht wird und ich die "normale" Model Datei nicht mehr brauche?
"noch besser weil die Connection zuerst geprüft wird (sprich ist bereits eine offene Verbindung vorhanden?) und weil es nun als SqLite Verbindungsdatei erkennbar ist?
Gehe ich richtig in der Annahme..
Das eine hat mir dem anderen nicht viel zutun.
Das sind zwei getrennte Schichten.
[Artikel] Drei-Schichten-Architektur
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
ach so, ne den Connection String habe ich auch im 2. Beispiel bereits nicht mehr hard im Code. Hab den in der App.config verstaut (kopiere im Moment bei jedem Debug eine Datei in den bin Ordner zum ausprobieren)
<connectionStrings>
<add name="SQLite" connectionString="Data Source=Database\\DatabaseFile\\Database.db;Version=3;" />
</connectionStrings>
Also gehört eine Entity gemäss Link zu Datenzugriffsschicht da damit ja eigentlich nur verwaltet werden, richtig? (Es wird ja nix verarbeitet sondern nur "verwaltet", resp. aus der DB ausgelesen - das SqlRepository gehört demnach auch dazu, oder?)
Was sich mir nicht ganz ausgeht:
Wenn ich nun in der Entity Definition
public string FirstName { get; set; }
schreibe und gleichzeitig in der Employee Model Datei
public string FirstName
{
get => _firstName;
set
{
//blub blub
}
}
steht so ist das doch doppelt gemoppelt, oder nicht? Ich mein ich definiere 2x das es einen String namens "FirstName" gibt.
Viel wichtiger ist, wo wird denn die Model Datei eingeklinkt? In der Entity steht ja
public class EmployeeEntity : Entity
sprich "FirstName" und "LastName" hole ich aus der EmployeeEntity und die "Id" aus der "normalen" Entity. Eigentlich müsste sich doch das Employee Model auf die Entität stützen, oder?
--> Habe nur folgendes Beispiel gefunden, hab aber keine Ahnung ob das so korrekt ist:
https://stackoverflow.com/questions/30319584/how-to-map-entity-framework-model-classes-with-business-layer-class-in-n-tier-ar
Damit dieses Thema nicht endlos ausufert, mache ich jetzt hier zu. Das hat alles nichts mehr mit einem Code-Rieview zu tun. Bitte beachte die Regeln für dieses Forum: Code-Review Regeln
Bei konkreten Fragen bitte im passenden Unterforum posten, und auch da bitte [Hinweis] Wie poste ich richtig? beachten, besonders 1.2 Nur ein Thema pro Thread.
Weeks of programming can save you hours of planning