Klasse mit Berechnungen zur Laufzeit Compilen und nutzen?

  • C#

Es gibt 5 Antworten in diesem Thema. Der letzte Beitrag () ist von xChRoNiKx.

    Klasse mit Berechnungen zur Laufzeit Compilen und nutzen?

    Hallöchen,

    ich habe 2 Funktionen ausgelagert in einer Static Class wegen der Übersicht.
    Nun sind dort in den 2 Funktionen Berechnungen vorhanden die mir einen Preis ausrechnen Anhand von
    Einkaufspreisen / Steuern / Größe eines Kartons.

    Da es nun so ist wenn sich eine Formel ändert das ich das Programm neu kompilieren muss würde nun ich das gerne "dynamischer" haben.
    Dachte ich erst daran das man ja irgendwie das ganze parsen könnte vom String doch dann viel mir ein ich kenne ein Tool das auch
    eine C# Klasse im Verzeichnis liegen hat in dem man Änderungen vornehmen kann und die dann auch im Programm da sind.

    Wie kann ich also eine C# Klasse die ich habe als .cs Datei neben dem Programm liegen haben und diese wird dann beim start mit "einkompiliert" und ich
    greife dann auf die neu einkompilierte Class zu?
    Also quasi das er meine vorhanden Class ersetzt?

    Ist das überhaupt Sinnvoll? Gibt es bessere Wege dem User zu ermöglichen Formeln mit Variablen aufzuschreiben?
    Wobei das Problem ist wie ich finde das es auch noch If-Abfragen gibt...

    Spoiler anzeigen

    C#-Quellcode

    1. if (Einkaufspreis < 20)
    2. {
    3. switch (size)
    4. {
    5. case 1:
    6. return ((EinkaufspreisSteuern / 0.90) * 1.19) / 0.9;
    7. case 6:
    8. return ((((EinkaufspreisSteuern * 6 + 10) * 1.19) / 0.88) - 10) / 0.8;
    9. case 12:
    10. return (((EinkaufspreisSteuern * 12 + 15) * 1.19 / 0.76) - 9) / 0.4;
    11. case 18:
    12. return (((EinkaufspreisSteuern * 18 + 20) * 1.19 / 0.85) - 8) / 0.6;
    13. }
    14. }


    Das sind noch nicht alle Abfragen da folgen noch mehre Abfragen wieder mit verschiedenen Formeln pro size.
    Daher denke ich wäre die beste Möglichkeit das mit der Klasse und der einkompilierung zu machen.

    Bin über jeden Denkanstoß glücklich.
    Grüße , xChRoNiKx

    Nützliche Links:
    Visual Studio Empfohlene Einstellungen | Try-Catch heißes Eisen
    also für so arithmetische Ausdrücke, auch mit Variablen, hab ich ja mal einen Formelparser gebastelt, und iwo bei die Tuts eingestellt.
    Ist aber sehr kompliziert, das Teil.
    Tatsächlich ists programmiererisch einfacher, wenn man eine Eingabe entgegennimmt, mit String-Operationen eine Klasse drumrumfrickelt, die man dann mit CodeDom kompiliert, und per Reflection aufrufen kann.
    activevb.de/cgi-bin/tippupload/preview.pl?id=34&sid=0%0D%0A
    guck - ich kann sogar den Code davon kopieren:
    Spoiler anzeigen

    VB.NET-Quellcode

    1. ' Dieser Source stammt von http://www.activevb.de
    2. ' und kann frei verwendet werden. Für eventuelle Schäden
    3. ' wird nicht gehaftet.
    4. ' Um Fehler oder Fragen zu klären, nutzen Sie bitte unser Forum.
    5. ' Ansonsten viel Spaß und Erfolg mit diesem Source!
    6. '
    7. ' Beachten Sie, das vom Designer generierter Code hier ausgeblendet wird.
    8. ' In den Zip-Dateien ist er jedoch zu finden.
    9. ' ----------- Anfang Projektgruppe Calculator.sln -----------
    10. ' ---------- Anfang Projektdatei Calculator.vbproj ----------
    11. ' ---------------- Anfang Datei Calculator.vb ----------------
    12. ' Projekteinstellungen:
    13. ' Option Strict On
    14. ' Option Explicit On
    15. ' Imports Microsoft.VisualBasic.ControlChars
    16. Imports System.Reflection
    17. Imports System.CodeDom.Compiler
    18. Imports System.Text.RegularExpressions
    19. Public Class Calculator
    20. Implements IDisposable
    21. Private _prov As New Microsoft.VisualBasic.VBCodeProvider
    22. Private _tpClass1 As Type
    23. Private Shared ReadOnly _bindFlags As BindingFlags = BindingFlags.DeclaredOnly Or _
    24. BindingFlags.Public Or BindingFlags.InvokeMethod Or BindingFlags.Static
    25. ' mit diesem Regex wird bei mathematische Multiplikation-Schreibweise das im VB-Code
    26. ' erforderliche '*' eingefügt
    27. Private _rgx As New Regex("(?<=\d)\s*\*?\s*(?=[a-zA-Z\(])", RegexOptions.Compiled)
    28. Public Function TrySetExpression(ByVal Value As String) As Boolean
    29. Static expression As String
    30. If Not AssignSave(expression, _rgx.Replace(Value, "*")) Then Return _tpClass1 IsNot Nothing
    31. _tpClass1 = Nothing
    32. Dim sl As New StringList
    33. ' vollständige kleine Klasse mit einer Function
    34. sl.AddLine("Imports System.Math")
    35. sl.AddLine("Public Class Class1")
    36. sl.AddLine("Public Shared Function Calc(ByVal X As Double) As Object")
    37. sl.AddLine("Return ", expression) ' expression, vom User eingegeben, wird einkompiliert
    38. sl.AddLine("End Function")
    39. sl.AddLine("End Class")
    40. Dim cp As New CompilerParameters
    41. cp.GenerateInMemory = True
    42. Dim cr As CompilerResults = _prov.CompileAssemblyFromSource(cp, sl.Flush)
    43. If cr.Errors.Count > 0 Then
    44. sl.AddLines("Die Expression konnte nicht kompiliert werden.", "")
    45. For Each err As CompilerError In cr.Errors
    46. sl.AddLine(err.ErrorText)
    47. Next
    48. MsgBox(sl.Flush)
    49. Return False
    50. End If
    51. ' aus der Assembly interessiert nur der Reflection-Type "Class1"
    52. _tpClass1 = cr.CompiledAssembly().GetType("Class1")
    53. Return True
    54. End Function
    55. Public Function Calc(ByVal x As Double) As Single
    56. ' "gerechnet" wird, indem die Class1.Calc()-Function per Reflection aufgerufen wird
    57. Dim args() As Object = {x}
    58. Return CSng(_tpClass1.InvokeMember("Calc", _bindFlags, Nothing, Nothing, args, Nothing))
    59. End Function
    60. Public Sub Dispose() Implements IDisposable.Dispose
    61. If _prov Is Nothing Then Return
    62. _prov.Dispose()
    63. _prov = Nothing
    64. GC.SuppressFinalize(Me)
    65. End Sub
    66. End Class
    67. ' ----------------- Ende Datei Calculator.vb -----------------
    68. ' -------------- Anfang Datei frmCalculator.vb --------------
    69. Public Class frmCalculator
    70. Private _calculator As New Calculator
    71. Private Shared _culture As Globalization.CultureInfo = _
    72. Globalization.CultureInfo.InvariantCulture
    73. Private Sub Button1_Click(ByVal sender As Object, ByVal e As EventArgs) Handles Button1.Click
    74. If _calculator.TrySetExpression(Me.txtFunction.Text) Then
    75. Dim x As Double
    76. If Double.TryParse(txtXValue.Text, Globalization.NumberStyles.Float, _culture, x) Then
    77. Me.lbResult.Text = _calculator.Calc(x).ToString(_culture)
    78. Me.ErrorProvider1.Clear()
    79. My.Computer.Audio.PlaySystemSound(Media.SystemSounds.Asterisk)
    80. Else
    81. Dim err As String = "Ungültige Eingabe"
    82. If txtXValue.Text.Contains(",") Then
    83. err &= NewLine & "(Dezimaltrenner ist ""."")"
    84. End If
    85. Me.ErrorProvider1.SetError(txtXValue, err)
    86. End If
    87. End If
    88. End Sub
    89. Private Sub frmCalculator_Disposed(ByVal sender As Object, ByVal e As EventArgs) Handles _
    90. Me.Disposed
    91. _calculator.Dispose()
    92. End Sub
    93. End Class
    94. ' --------------- Ende Datei frmCalculator.vb ---------------
    95. '...
    Der Spass ist aber hoch-riskant, weil ein User kann da sowas wie Sql-Injection betreiben, und beliebig umfangreichen Schad-Code eingeben.
    Ein anderer Nachteil ist, dass der Vorgang vermutlich beachtliche Resourcen verbraucht (son Compiler wiegt schon einiges).
    Da müsste man mindestens mal testen, ob die auch wirklich wieder freigegeben werden.

    @Bluespide: Ja, guck, derselbe Ansatz.
    Nur du solltest unbedingt die disposablen Komponenten auch disposen.

    Dieser Beitrag wurde bereits 7 mal editiert, zuletzt von „ErfinderDesRades“ ()

    Ich hatte auch schon mal so ein Problem und habe dieses auch mit dem nach kompilieren von C#-Code gelöst. Dabei wollte ich einfach nur den Inhalt von einer Funktion angeben müssen. In diesem Beispiel habe ich 2 parameter: int i und string value. Als return Wert muss ich dann einen string zurück geben. Aber das kann natürlich angepasst werden:

    C#-Quellcode

    1. class Program {
    2. static void Main(string[] args) {
    3. string myCustomCode = "return (i * 3) + value;";
    4. Func<int, string, string> myFunc = CompileScript(myCustomCode);
    5. Console.WriteLine(myFunc(2, "Test"));
    6. Console.ReadKey();
    7. }
    8. private static Func<int, string, string> CompileScript(string script) {
    9. CompilerParameters parms = new CompilerParameters() { GenerateExecutable = false, GenerateInMemory = true };
    10. parms.ReferencedAssemblies.AddRange(new[] { "System.dll", "System.Core.dll", "System.Data.dll", "System.Data.DataSetExtensions.dll", "System.Deployment.dll", "System.Drawing.dll", "System.Web.dll", "System.Windows.Forms.dll", "System.Xml.dll", "System.Xml.Linq.dll", Assembly.GetExecutingAssembly().Location }.ToArray());
    11. CompilerResults compilerResults = new CSharpCodeProvider().CompileAssemblyFromSource(parms, new string[] { "using System; using System.CodeDom.Compiler; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using Microsoft.CSharp; public class Script { public static string Run(int i, string value) { " + script + "}}" });
    12. if (compilerResults.Errors.HasErrors) {
    13. foreach (CompilerError error in compilerResults.Errors) {
    14. Console.WriteLine(error.ErrorText);
    15. }
    16. return null;
    17. } else {
    18. return (Func<int, string, string>)(compilerResults.CompiledAssembly.GetType("Script").GetMethod("Run", BindingFlags.Public | BindingFlags.Static).CreateDelegate(typeof(Func<int, string, string>)));
    19. }
    20. }
    21. }
    Heyho,

    danke euch beiden.

    Ich habe mal das Beispiel aus dem Post vom @ErfinderDesRades genommen und folgend bei mir verwendet:

    C#-Quellcode

    1. using (Microsoft.CSharp.CSharpCodeProvider _prov = new Microsoft.CSharp.CSharpCodeProvider())
    2. {
    3. CompilerParameters cp = new CompilerParameters();
    4. cp.GenerateExecutable = false;
    5. cp.GenerateInMemory = true;
    6. CompilerResults cr = _prov.CompileAssemblyFromFile(cp, Path.Combine(Environment.CurrentDirectory, "Klassen", "PreisKalkulation.cs"));
    7. if (cr.Errors.HasErrors)
    8. {
    9. MessageBox.Show("Fehler");
    10. Environment.Exit(9);
    11. }
    12. Assembly ass = cr.CompiledAssembly;
    13. calcClass = ass.GetType("Preis.PreisKalkulation");
    14. cr = null;
    15. cp = null;
    16. }


    C#-Quellcode

    1. private double calcPreis(double ekpreis, double ekpreisWithTax, int size)
    2. {
    3. object[] args = { ekpreis, ekpreisWithTax, size };
    4. return Convert.ToDouble(calcClass.InvokeMember("calculatePreis", _bindFlags, null, null, args));
    5. }

    Kann die Funktionen aus der Class Abrufen und es funktioniert.

    Ist mir auch ganz akzeptabel so. Viel passieren kann da nicht auf deine bedenken hin EDR da nur ein andere jemand das benutzt.
    Und der ist da nicht dran interessiert das Tool kaputt zu machen damit.

    Falls es keine wirklich viel bessere Lösung gibt würde ich die nun so nehmen und verwenden.
    Ich markiere das Thema mal von meiner Seite aus als Erledigt. Sollte es natürlich was besseres geben immer her damit.
    Grüße , xChRoNiKx

    Nützliche Links:
    Visual Studio Empfohlene Einstellungen | Try-Catch heißes Eisen