0% found this document useful (0 votes)
2 views

ADO.NET

The document explains ADO.NET, a framework for accessing databases in .NET applications, detailing its components like SqlConnection, SqlCommand, and DataAdapter for CRUD operations. It emphasizes the importance of a data access layer to connect the front end and back end of applications, and discusses connection management, including the use of connection strings and error handling. Additionally, it highlights best practices such as using 'using' statements for resource management and storing connection strings in configuration files.

Uploaded by

Sanjeev Kumar
Copyright
© © All Rights Reserved
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views

ADO.NET

The document explains ADO.NET, a framework for accessing databases in .NET applications, detailing its components like SqlConnection, SqlCommand, and DataAdapter for CRUD operations. It emphasizes the importance of a data access layer to connect the front end and back end of applications, and discusses connection management, including the use of connection strings and error handling. Additionally, it highlights best practices such as using 'using' statements for resource management and storing connection strings in configuration files.

Uploaded by

Sanjeev Kumar
Copyright
© © All Rights Reserved
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 24

//----------------------------------------------------------- ADO.

NET ------------------------------------------------------------//

// If we need to access a database in your application , we need a persistence framework (a tool that manages the process of moving data between a
program and a database, and the mapping between the two) , using which we can load objects from or save them to a database. We can write our own
persistence framework using ADO.NET classes like SqlConnection() , SqlCommand(), SqlDataReader() ,etc . But it is very costly, since we have to write from
scratch a lot of Stored Procedures, read data using ADO.NET objects and manually map the database tables and records to the domain objects in our
application. Entity Framework does all this on our behalf.
// We can have numerous types of .NET applications – Web App, Console App, Windows App(Winforms, WPF), etc. All such applications will have two
components : Front End and Back End.
//Front End refers to the user interface – which could either be your Windows interface, or Web Application,etc through which the user interacts and inputs
data. We will need to store those user- input data somewhere permanently. This is done in some DataSources or Database – which comprises the Back
End in this case. Again, Database are of many types as well – MySQL, SQL Server, Oracle, XML data sources.
// To connect/ communicate between the above two components we need a data access layer called Data Provider – like a bridge between our .NET
application and the Data sources. This Data Provider is supplied to us by ADO.NET ( ADO or ADO.NET). ADO is a Microsoft technology which stands for
ActiveX Data Objects. It is a programming interface to access data in a database from an application. ADO.NET specifically is a module of .NET framework
containing classes that can be used to Create, retrieve, insert and delete data (CRUD operations on a data source). ADO.NET mainly uses System.data.dll
and System.xml.dll where its classes are located.
// Since there are many types of databases, ADO.NET provides us with different data providers – one for each database type. We use the data provider
SQLClient for SQL Server database ; data provider OracleClient for Oracle database ; data provider OleDb for databse MS Access, etc. To use a given data
provider, we will need to include the corresponding namespace in our Application, which are provided by ADO.NET. For example to work with SQLClient,
we use the namespace System.Data.SQLClient. Similarly, we must use the namespace System.Data.OracleClient to work with corresponding OracleClient
data provider and database.
// However, any data provider we use, there will always be some common functionalities they all need to perform – like Connecting to Database,
executing Queries or Commands, Reading data from tables of database, etc. As such ADO.NET provides some common classes among all data providers
(albeit with different prefixes for corresponding Db). These common classes are Connection, Command, DataReader and DataAdapter. For example,
consider the Connection class. While working with SQLClient , we will use SQLConnection class, SQLCommand class, etc. But for OracleClient, we would use
OracleConnection class, OracleCommand class,etc. The rest of the code would be same while working with either/any database.
// ADO.NET is a collection of software components that we can use to access data and data services from a database or database system (relational or non-
relational). This is used obviously with .NET and C# (not with Python or Java). ADO.NET works in both connected or disconnected environment.The in-
memory representation of data is “DataSet”. A DataSet contains one or more table. The tables between DataSet are known as DataTable. Here, the
Database records can be accessed like any other collection by iterating over them or by using the Primary Key index. Here, we open connections only long
enough to perform a database operation, such as Select or Update. We can read rows into a dataset and then work with them without staying connected
to the data source (disconnected environment). We communicate with a Database using a DataAdapter which makes calls to a DB provider or the APIs
provided by the underlying data source. To transmit data in ADO.NET , we use a dataset which can transmit an XML stream. So XML is used for passing data.

// Components of ADO.NET refer to classes which are designed for data manipulation and faster data access. These components are Connection,
Command, DataReader, DataAdapter, DataSet & DataView. First four components are probably sub components of DataProvider.
// .NET application cannot directly execute our SQL code, since it understands only C#. Data Provider enables this by providing the above classes. Step by
step, a .NET application would need to Connect to the database, Prepare an SQL Command, Execute the Command & Retrieve the results and display
them in the UI.
// Now to connect to the Database, we must first create an instance/object of SQLConnection class (provided to use by SQLClient Data Provider). Think of
it as a blueprint or configuration file for how the connection should be established but not the connection itself.This is the entry point of our .NET
Application into SQL Server Database. Next, we must first call the Open method of SQLConnection object. If we could successfully establish the Connection,
then the State property of our SQLConnection object is set to Open, otherwise
it remains Close. This must be checked before we start wrting our queries.
Obviously, after completing our queries, we must close the Connection by
calling the Close method on the SQLConnection object.
// Consider the example where we open SQL Management Server Studio 
Initially it asks for login (we come to that later), after which we can Select ‘New
Query’ in the ribbon above. The new window ‘SQLQuery1.sql….’ is actually a
Connection to the selected Database (in this case FirstDatabase). On clicking
the ‘New Query’ button again, a new window will open; implying a new
Connection object has been created to the same database.
// To summarize : SQLConnection Class is used to eatablish an open
connection to the SQL Server database. This connection object represents a
unique session to a SQL Server data source. Moreover, this is a sealed class, so
it cannot be inherited. However, this SQLConnection class itself is a child of
System.Data.Common.DbConnection and implements ICloneable interface.
This class is within System.Data.SqlClient namespace, which itself is within the Assembly System.Data.SqlClient.dll.
// While creating object of SQLConnection Class, we will need to pass an argument
called ‘connection string’. This string variable includes :
1) Source database server name/ Data Source : Identifies the server name, could be
local machine, domain name or IP address.
2) Initial Catalog: Basically the database name within server name mentioned above.
3) Integrated Security: Acts like a security layer for accessing database. Set it to True in
case we have started database authentication login with Windows Authentication,
else we set it to False , when we start the database authentication with SQL server
authentication. In this case, we supply (User Id) Username and Password (matching
SQL Server User
ID).

// Always remember to close Connection after queries have been performed, otherwise
it keep consuming memory resources of our system.
// We create a new project in Visual Studio -> Console App (.NET) -> Name our solution
and project SqlConnectionADO -> Also go to Sql Server Management Studio -> Copy the
Server name at time of login -> Right click on navigation and create a new database ->
name it ado_db.
// Now within Visual Studio -> Program.cs:
using System;
using System.Data; // (4.1) Include this to be able to use ConnectionState.
using System.Data.SqlClient; // (3) Include this namespace to be able to use the classes supplied by data provider SqlClient. Before…
namespace SQLConnectionADO //…working with a database, we must import a data provider namespace at the top.
{
internal class Program
{
static void Main(string[] args)
{ // (5) We are going to call the Connection method from within the Main method but by…
Program.Connection(); //…using only the class name Program, since we declared the Connection method as static.
Console.ReadLine();
}
static void Connection() // (1) First, we create a separate method, Connection, that is going to contain out connection logic
{ // (2) Next we create a connection string ‘cs’ , hardcoding some values. Paste the Data Source which was earlier copied at login.
string cs = "Data Source=DESKTOP-HA6VMFQ; Initial Catalog=ado_db;Integrated Security=true;"; //
(2.1) The initial catalog is set to the db name we create earlier; Set the Security to ‘true’(lower case)
// (2.2) Note the way of writing connection string: type of key value pair separated by ; entirely within “” “”
SqlConnection con = new SqlConnection(cs); // (3) We create an instance(con) of SqlConnection class with the…
try // …above connection string as parameter.
{ // (4.3)We include those statements within try block which could throw exception/error.
con.Open(); // (4) Next we call Open method on this instance, and would later Close it as well, after all queries are done.
if (con.State == ConnectionState.Open) // (4.1) Next we check whether our connection string is correct or…
{ //….using the State property of the connection class, which is of type ConnectionState (itself an enum). In case the connection..
Console.WriteLine("Connection Successful!!"); //…is successfully opened, we display a message.
} // (4.2) But an error/exception would be thrown, if our connection could not be established e.g, cs is wrong. So we go for…
} //….exception handling, using try-catch-finally block.
catch(SqlException ex) // (4.4) The errors belong to the SqlException class (recall the syntax; ex is the exception object)
{ Console.WriteLine(ex.Message); // (4.5) The message property of the exception object is then displayed at…
} //…console itself rather than the app crashing.
finally // (4.6) This block always executes whether exception is thrown or not – because we must close the connection.
{ con.Close();// (4), (4.6) If the Open method fails, the SqlConnection object does not establish a valid…
} //…connection to the database, and there is no need to explicitly close it. The Close method is only relevant when connection…
} //…is successfully opened. If the connection never opens due to an exception, there’s nothing to close.
}
}
(4) Open() method is the point where a physical connection to the database is established , involves network communication with the SQL Server to
authenticate and set up the connection. SQL Server verifies Credentials (username/password or integrated security), checks whether the specified database
exists – after which SQL Server allocates resources for the connection, such as memory buffers. The connection becomes live, and the application is now
ready to execute SQL commands against the database. Until Open() is called, the database server remains unaware of the SqlConnection object.
This separation of object creation and connection provides (i) Efficiency: You can create multiple SqlConnection objects with different connection strings,
and only open them when they're actually needed, (ii) Error Management: If a database server is down or credentials are incorrect, the Open() method will
throw exceptions you can handle. This avoids potential confusion by delaying errors until you're ready to use the connection. (iii) Pooling: ADO.NET uses
connection pooling by default, which means the physical database connections are reused for efficiency. When you call Open(), it might retrieve an already-
open connection from the pool instead of making a new one.
(4.1) ConnectionState is an enum in the System.Data namespace. It has values Closed (0), Open (1), Connecting (2), Executing(4) and Fetching (8) .

// Now we modify some things in the above code. First note that we must always ensure that connections are always closed. We can always achieve this by
opening the connection inside of a using block. In this case, the connection is automatically closed/resources are disposed of properly once the code exits
the block. The using statement in C# ensures that objects that implement the IDisposable interface are properly disposed of, even if an exception occurs.
In other words, the connection is released back to the connection pool or properly disposed at the end of the block.
This could be the code below string cs = “….” assignment statement within Connection method:
using (SqlConnection con = new SqlConnection(cs)) //using Statement: Declares a scope for the object inside the parentheses.
{ //SqlConnection inherits from DbConnection , which itself implements IDisposable interface – so it has a Dispose() method.
con.Open(); // Using block works with any type that implements the IDisposable interface.
if(con.State == ConnectionState.Open)
{
Console.WriteLine("Connection Successful!!");
}
} //Using automatically calls the Dispose() method of the object when the block is exited, freeing resources.

// But the above statement may still throw an error(due to problems in connection string ,etc) which has not been handled. So we nest the using within try
catch block. Note this could be the other way round as well. We could put try-catch block completely within using block.
SqlConnection con = null; // Declare the connection object outside try block since it may be later used by finally block outside using.
try
{ using (con = new SqlConnection(cs)) // Set the connection object properly within using.
{
con.Open(); // If our Open method succeds, the connection will be auto closed once we exit using.
if (con.State == ConnectionState.Open)
{ Console.WriteLine("Connection Successful!!"); } }
}
catch (SqlException ex)
{ Console.WriteLine(ex.Message); }
finally // This is actually not required. We can check the connection state using console writeline and passing con.state.
{
Console.WriteLine("Within finally: {0}", con.State); // This outputs Closed, implying that connection already closed…
con.Close(); //….once outside using block.
}
// In following case, we put try catch within using . No need to put finally for closing connection. These lines follow after SqlConnection con = null;

using (con = new SqlConnection(cs))


{ try
{
con.Open();
Console.WriteLine("Success!!");
Console.WriteLine(con.State);
}
catch (SqlException ex)
{
Console.WriteLine("Error!!");
Console.WriteLine(ex.Message);
}
Console.WriteLine(con.State);
}
Constructors of SqlConnection Class of ADO.NET : // Default constructor doesn’t take any arguments – initializes a new instance of the
SqlConnection class.
// SqlConnection(string) – takes only a single parameter string which is the connection string and then initializes a new instance of the class.
// SqlConnection(string, SqlCredential) – first parameter is the connection string, 2nd argument is the object containing the User id and password when using
Sql Authentication. So when using this constructor we won’t have Integrated Security = true in the connection string. This constructor is not used if we pass
our user id and password already within the connection string.

// SqlConnection class uses SqlDataAdapter and SqlCommand classes together to increase performance when connecting to a Microsoft SQL Server Db.
// The connection string is not usually stored in a variable. Rather it is stored in the
Web.config file or App.config file (already present) of an application.
// We first go to App.config file. Within this file we will find an xml script(not HTML). We add
the <connectionStrings> element ; this <connectionStrings> section provides a reusable
connection string that the application can use to connect to a database without hardcoding
connection details in the code. Within this section we create an <add> tag : in its opening tag
we provide three attributes:
1) name : This is the name of the connection string; we use this name in your code to retrieve
the connection string.
2) connectionString : This is the actual string used to establish a connection to the database.
3) providerName : This specifies the data provider used to connect to the database.
// Note that ConnectionStrings is a specialized collection of type ConnectionStringSettingsCollection (has a parent class which implements ICollection).
ConnectionStrings collection can be indexed using a key (the name attribute from the configuration file), as in ConnectionStrings["dbcs"]. The key (case-
insensitive) corresponds to the name of the connection string, and the value is a ConnectionStringSettings object.

// Now, back within Program.cs file, we want to get the connectionString. We first add the namespace System.Configurationat the top, but it won’t work
without adding a
reference to it in our
project. Go to Solution
Explorer -> right click
References -> Add
Reference -> Click on
Assemblies on top left
side -> Check box for
System.Configuration.
Next, we remove the
hardcoded value
within string cs and
get the connection
string in this variable.
This is done via a
static class ConfigurationManager within System.Configuration. This class exposes a collection ConnectionStrings of type
ConnectionStringSettingsCollection. The ConnectionStringSettings class has a string property ConnectionString – can be used to get or set the string used
to open the SQL Server database. Thus, in our previous code, we change only the string cs = “….” line:
string cs = ConfigurationManager.ConnectionStrings["dbcs"].ConnectionString;
// Now, we need to store the data from our .NET application into a database – not just store/Insert, but also Update and Delete data from Database using
our .NET application. All these operations are provided by the SqlCommand class – without this class we cannot execute the SQL queries/statements for
any of those aforementioned operations. This SqlCommand is obviously provided by the Data Provider SqlClient, which in turn is inbuilt in ADO.NET.
// SqlCommand is thus used to both prepare an SQL statement or StoredProcedure and then execute it on a SQL server database.
// SqlCommand class is a sealed class, inherits from System.Data.Common.DbCommand and implements parents interfaces ICloneable and IDisposable.

// SqlCommand has five overloaded constructors:


1) public SqlCommand() : used to initialize a new instance of the Sql.Data.SqlClient.SqlCommand class.
2) public SqlCommand(string cmdText) : does the same as above but with the text of the query we want to execute (cmdText).
3) public SqlCommand(string cmdText, SqlConnection con) : takes the query text as well as the connection object/instance of SQL server – mostly used.
4) public SqlCommand(string cmdText, SqlConnection con, SqlTransaction transaction) : useful if we want ability to rollback or commit our queries.
5) public SqlCommand(string cmdText, SqlConnection con, SqlTransaction transaction, SqlCommandColumnEncryptionSetting encryptSetting ) : if we have
applied some sort of encryption on the columns.

// Most Common Properties in SqlCommand class:


1)CommandText : Suppose we use the parameterless constructor of SqlCommand to create its instance. Then, we can still supply the string containing
query using the CommandText property on the SqlCommand object.
2)Connection : Similarly, we can supply the connection object using this property.
3)CommandType : This is an enum, which specifies how a command string is interpreted. If our string is a simple text referring to SQL Query, we use the
default value of Text( = 1). Other enum values of this property are StoredProcedure (= 4) and TableDirect (= 512). This CommandType is actually a
property within IDbCommand (base class for SqlCommand), hence this property is accessed on our SqlCommand object. When the
IDbCommand.CommandType property is set to StoredProcedure, the IDbCommand.CommandText property should be set to the name of the stored
procedure to be accessed.

// Most common methods of the SqlCommand class are :


1)ExecuteReader() : Used for executing SELECT query on our Database. The Database actually fetches required data and returns to our .NET application.
Next, we need to be able to read this returned data. But this is returned as an object of SqlDataReader – so we need to create an instance of
SqlDataReader to be able to get it. The ExecuteReader() in the SqlCommand object sends the SQL command text/statements to the SqlConnection object
and populates a SqlDataReader object based on the SQL statement or Stored Procedures.
2)ExecuteNonQuery() : Used for Insert, Update and Delete from .NET application on our Database.
3)ExecuteScalar() : Used for queries with Aggregrate functions/also called scalar functions. These aggregrate functions are often used with SELECT and they
aggregrate multiple values or rows(do some operation on an aggregrate of them) but return only one value(hence scalar).

// SqlDataReader is a part of ADO.NET and is used to read data from a SQL Server database. This DataReader object is used for accessing data from the
data source(database). This class is provided by other data providers also. The DataReader object in C# ADO.NET allows us to retrieve data from data bases
in read-only and forward-only mode. So we cannot modify the data of the database through this DataReader object – cannot Update or Delete. We can
read the rows of the table sequentially one by one from first till last ( forward ). We cannot move backward (say from 3rd record to 2nd record).

// DataReader is highly efficient for retrieving large datasets when you don't need to modify them or move backward.
// To make modifications we use DataAdapter (learn later).
// So SqlDataReader inherits from abstract class DbDataReader, which in turn inherits from MarshalByRefObject which uses platform specific pointers. So
I think, this is how it moves from one record to another. Thus SqlDataReader provides a way of reading a forward-only stream of rows from a SQL Server
database. This data reader retrieves a read-only, forward-only stream of data from a database. Results are returned as the query executes, and are stored
in the network buffer on the client until you request them using the Read method of the DataReader. Using the DataReader can increase application
performance both by retrieving data as soon as it is available, and (by default) storing only one row at a time in memory, reducing system overhead.
[A network buffer is a temporary storage space for data that's being sent between devices or between a device and an app.]
Using the DataReader can increase application performance and reduce system overhead because only one row at a time is ever in memory. It retrieves
rows from the database one at a time. it doesn’t load the entire dataset into memory (unlike a DataSet or DataTable) and is suitable for large datasets.

// It requires an open SqlConnection to fetch data and closes it after the operation.SqlDataReader object provides a connection oriented data access to the
SQL Server data sources from C# applications. So we need to explicitly Open our connection before using this SqlDataReader object. If we try to use
SqlDataReader object, while the connection state is closed, it will throw an error. Always close the SqlDataReader (explicity or with using) and
SqlConnection after usage to release resource (resource management). Always call the Close method ( reader.Close(); ) or use ‘Using’ when you have
finished using the DataReader object. If your Command contains output parameters or return values, those values are not available until the DataReader is
closed. While a DataReader is open, the Connection is in use exclusively by that DataReader. You cannot execute any commands for the Connection,
including creating another DataReader, until the original DataReader is closed.
Microsoft’s Official Doc : The DataReader provides an unbuffered stream of data that allows procedural logic to efficiently process results from a data
source sequentially.
Unbuffered Stream: The data is not fully loaded into memory (as a DataTable or DataSet would). Instead, it reads one record at a time directly from the
database. This is efficient when working with large datasets because it avoids the memory overhead of loading all records at once. Stream of Data: It
sequentially processes data as it is retrieved, similar to reading a file line by line, rather than getting all content at once.
Procedural Logic: This means the data is processed programmatically using loops or other control flow constructs (like while or for). With a DataReader, you
commonly use a while loop to process one record at a time.
Efficient process: Because it retrieves only one record at a time and avoids buffering, it reduces memory consumption and is faster for operations where
data is consumed as it is read. It is especially useful for reading large result sets where only one row is needed at a time or where partial data is sufficient.

After using the DataReader.Read method to obtain a row from the query results, we need to access each column data. There are two things to consider –
the column data itself and the data type of that data. We can access each column of the returned row/record by passing the name or ordinal number of
the column to the DataReader. So , we could write reader[0] OR reader[“id”] for accessing the first column. But this does not taking into account the data
type. So, for best performance, the DataReader provides a series of methods that allow you to access column values in their native data types
(GetDateTime() , GetDouble() , GetGuid() , GetInt32(), GetString() and so on). Thus we can write reader.GetInt32(0) OR reader["Name"].ToString() . Using
these typed methods provides type safety - instead of casting to ensure correct types.

// Some Properties of the SqlDataReaderClass :


reader.HasRows(): Checks if any data is returned; Returns true if the SqlDataReader has one or more rows, otherwise false.
reader.Read(): Moves the reader to the next row. Hence we can use this Read() method to iterate through the results. Also returns true if there are more
rows remaining, otherwise false. Can be used to check whether we reach the end of the fetched data.
reader.FieldCount() : outputs the number of columns within the table
reader.IsClosed() : returns true or false depending upon whether the reader is open or closed.

//By default, synchronous ExecuteReader does block execution, and the program won’t move to the next line until data fetching completes. So there are
Asynchronous Many of the properties mentioned above, are also available in their Async form.

Open Late, Close Early : Another connection-related item I look for when performing a code review is whether or not a connection is held open for the
shortest period of time that it is required. The basic rule is to keep the connection open only as long as you need it. You should only open a connection
immediately before you need to access the database and then close it as soon as you are done accessing the database. Be wary of any nondatabase-related
code that executes while the connection is open. Connections hold open valuable resources to the database, consume memory, and can lock data that
could cause other queries to slow down. So it's best to open connections late and close them as early as possible.
// Below are the ‘students’ & ‘employees’ table respectively. Created this in SSMS, for working with ADO.NET.
In context of the above tables (employees and students) that we designed, we create the following methods:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration; //For using ConfigrationManager, to acess connection strings from Configuration element of App.config
namespace SQLConnectionADO
{
internal class Program
{
static void Main(string[] args)
{
Program.ConnectionForRead(); // We create a method ConnectionForRead to execue the sql connection and command…
Console.ReadLine(); //…and call it from thee Main method of our Program
}
static void ConnectionForRead()
{ // Below, we access the connection string using Configuration manager
string connectionString = ConfigurationManager.ConnectionStrings["dbcs"].ConnectionString;
SqlConnection connection = null; // We declare an object of type SqlConnection and set the initial value to null
try // To catch any possible errors, SqlConnection or otherwise we use a try catch block
{
using (connection = new SqlConnection(connectionString))
{
connection.Open(); // To be able to use SqlDataReader ,we must open connection first – connection oriented
if (connection.State == ConnectionState.Open)
{
Console.WriteLine("Connection Successful!!");
}
using (SqlCommand command = new SqlCommand()) // (1),(4)
{ // Since we are using a parameterless constructor for SqlCommand we must use its properties to set values.
command.Connection = connection;
command.CommandText = "SELECT * FROM employees";
command.CommandType = (CommandType)1; // (2)
using (SqlDataReader reader = command.ExecuteReader()) // (1), (3)
{
while (reader.Read()) // Procedural logic, Moves reader to next row.
{
Console.WriteLine("Id: " + reader["emp_id"] + " Name: " + reader.GetString(1) + " " + reader[2]);
// Access value of column – indexing through column name, column ordinal number
} } }// We can obviously access them in any order irrespective of how they appear in actual table.
}
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine("Connection State: " + connection.State); // Obviously displays Closed.
} } }
}
(1) It’s a good practice to use a using block for SqlCommand objects because, like SqlConnection and SqlDataReader, SqlCommand implements the
IDisposable interface. This means it can allocate unmanaged resources (e.g., memory, database handles, etc.) during its lifetime, which should be released
explicitly when the command is no longer needed.
(2) Since CommandType is an enum, we have used casting to convert integer to CommandType. It would throw error if the integer is not correct, for
example 2, since there is no enum value corresponding to integer 2. Also, we can simply write command.CommandType = CommandType.Text , since
value 1 corresponds to enum type Text – used for simple SELECT query. Also we may omit this altogether, since it is the default value.
(3) We cannot create an object of SqlDataReader using new keyword. In case we try to do this, it shows a warning : SqlDataReader has no constructor that
takes 0 arguments. Rather, we have to always use ExecuteReader on the SqlCommand instance to get the SqlDataReader object.
(4) It is possible to use other constructors of SqlCommand, like the one where we pass the query string and the connection object (defined previously).
Obviously, in this case we need not assign value to the properties CommandText, Connection and CommandType separately -making our code concise.
Consider the next snippet – alternative for the above second using(). We add a string queryString just outside the try-catch block. The first using() remains
same:

string queryString = "SELECT * FROM employees";


.....
using (SqlCommand command = new SqlCommand(queryString,connection)) // Note we supply the string …
{ //….for query as a parameter, followed by connection object. Hence no need to set respective properties within this using.
using (SqlDataReader reader = command.ExecuteReader())
{
while (reader.Read())
{
Console.WriteLine("Id: " + reader["emp_id"] + " Name: " + reader.GetString(1) + " " + reader[2]);
}
}
}
…. . . . .
//We now try to execute a Stored Procedure and multiple query commands from within the
same connection. We first create a stored procedure separately in SQL Server Management
Studio, on our employees table in our ado_db databse. We store this procedure as a 
separate file. Our procedure has a name : spGetEmployees.
// Also note in the following code how the reader cannot be reused without closing it first
(either through Using or explicitly.)
static void ReadEmployeeRecord()
{ try{
string connectionString = ConfigurationManager.ConnectionStrings["dbcs"].ConnectionString;
SqlConnection connection = new SqlConnection(connectionString);
using (connection)
{ connection.Open();
if (connection.State == ConnectionState.Open)
{ Console.WriteLine("Connection Successful!!"); }
string queryLessThan = "SELECT * FROM employees WHERE salary < 70000";
string storedProc = "spGetEmployees"; // (1)When we set CommandText property to name of the stored procedure…
using (SqlCommand command = new SqlCommand(storedProc, connection))
{
command.CommandType = CommandType.StoredProcedure; //(1)…set CommandType property to StoredProcedure
using (SqlDataReader dataReader = command.ExecuteReader())
{ //(1)….The command executes this stored procedure when you call one of the Execute methods.
while (dataReader.Read())
{ Console.WriteLine("Id: " + dataReader["emp_id"] + " Name: " +
dataReader.GetString(1) + " " + dataReader[2]); }
Console.WriteLine(dataReader.IsClosed); // Outputs False, since dataReader is still open, and will….
} //…..close automatically only when we move out of using block. Obviously can’t access the local variable dataReader after }.
command.CommandText = queryLessThan; // We can set the CommandText and CommandType to another value.
command.CommandType = (CommandType)1; // Thus we can execute another query on same ‘connection’ over same..
using (SqlDataReader reader = command.ExecuteReader()) //…’command’, but we must use new…..
{ //...SqlDataReader. If the previous data reader is not closed, due to resource management problems, it throws an error:
//There is already an Open DataReader associated with this Command which must be closed first
while (reader.Read())
{ Console.WriteLine("Name: " + reader["name"]); }
Console.WriteLine(reader.Read()); // returns False : since no more reows remains outside while loop
Console.WriteLine(reader.HasRows()); // returns True since this current DataReader has non-zero rows.
} // The second SqlDataReader is also closed after this point. Note the normally this execution is Synchronous. So ADO.NET ..
//…must close all resources related to the SqlDataReader before moving onto next line.

command.CommandText = "SELECT * FROM students"; // Now we want to access a different table.


command.CommandType = (CommandType)1; // not necessary since default value, but must override if last type...
using (SqlDataReader reader = command.ExecuteReader()) //…was a stored procedure.
{ Console.WriteLine(reader.FieldCount); // output no of records/rows in this table - 5
while (reader.Read())
{ Console.WriteLine("Name " + reader["name"]); }
Console.WriteLine(reader.IsClosed); // False – still within using
reader.Close(); // we can explicitly close the reader
Console.WriteLine(reader.IsClosed); // True – still within using

} } } }
catch (Exception ex) // Always use Debugging tool to help understand which line throws errors – since this catch block will give…
{ Console.WriteLine(ex.Message); } //…the error message but we may not be able to see which using threw the error.
}
//Output of previous code looks somewhat like this:
// We get the result of all three data readers: 

# To use MySql database :


// First download & install MySql.Data package: Project -> Mannage Nuget Packages.
// Now our file Program.cs must have another using for this client,besides previous clients:
using MySql.Data.MySqlClient;
// We add another connnectionString within App..config as shown. Note the different parameters to be
passed. This can be obtained, simply internet search MySql ConnectionStrings.

// For MySql, the attribute values can also be obtained by


going into the top ribbon of MySql -> Database -> Manage
Connections -> select Local instance MySQL80 . Here we
will find all those values for hostname,port,username,etc.
// The MySqlClient provides almost all the classes and
corresponding constructors as that of SqlClient. For
example, the MySqlCommand has four different
overloaded constructors – parameterless, string query
only, string query and connection object, and the one
involving transaction.

static void DemoForMySql()


{ try
{
string connectionString = ConfigurationManager.ConnectionStrings["MySQLConnection"].ConnectionString;
MySqlConnection connection = null;
using(connection = new MySqlConnection(connectionString))
{ connection.Open();
Console.WriteLine("Connection : " + connection.State);
string queryText = "SELECT * from questions";
using (MySqlCommand command = new MySqlCommand(queryText,connection))
{
using (MySqlDataReader reader = command.ExecuteReader())
{ while (reader.Read())
{
Console.WriteLine("Id: " + reader["question_id"]); // Gets question id and text using…
Console.WriteLine("Text: {0}", reader.GetString(1)); //…column name and ordinal number
} } } }
Console.WriteLine("Connection : " + connection.State); // Outside the root ‘using’ – returns closed
}
catch(Exception e) { Console.WriteLine(e.Message); } // Any error like incorrect value of Pwd throws exception
}

Architecture for data access from database: Two ways to access data – Connected Data Access/Architecture and Disconnected Data
Access.
SqlDataReader falls under Connected Data Access, while SqlDataAdapter class follows disconnected data access.
In connected data access, the connection to the database must be explicitly opened before using the reader, and connection is explicitly closed after
reading is done. In disconnected data access, there is no need to explicitly open or close the connection – the DataAdapter class automatically does this
job, when we use its Fill() method. When working with SqlDataAdapter, we combine it with any of two other classes – DataSet and DataTable. Once data is
read from the database by the SqlDataAdapter, we can pass that data to the DataSet or DataTable. DataSet is used when working with multiple tables
having some relationships between them. In case we are working with a single table,without any relationships we use DataTable.
DataSet is Data Provider independent – it is not in the System.Data.SqlClient or OledbClient. Rather it is present in System.Data, and hence common to all
data providers. This is unlike SqlCommand which was provided explicitly by SqlClient OR OracleCommand by OracleClient.

// DataTable represents a single Table – used to store a single table of our Database in this DataTable object.
// The DataSet object is central to supporting disconnected, distributed data scenarios with ADO.NET. The DataSet is a memory-resident representation
of data that provides a consistent relational programming model regardless of the data source. It can be used with multiple and differing data sources,
with XML data, or to manage data local to the application. The DataSet represents a complete set of data, including related tables, constraints, and
relationships among the tables. The methods and objects in a DataSet are consistent with those in the relational database model. The DataSet can also
persist and reload its contents as XML, and its schema as XML schema definition language (XSD) schema.
// DataSet represents multiple tables as DataTable objects and creates a set of these DataTable(s) . It contains the copy of the data we fetched – a local
copy that gets populated in client PC. Since its independent of Data Source and exists locally (in memory), it makes our application fast and reliable.
Both the DataTable and DataSet classes reside in disconnected architecture. They do not require an explicitly open or active connection to the database,
since its done automatically. To fill either of them with data, we need to use the SqlAdapter class and its Fill method.

DataSet object model :


An ADO.NET DataSet contains a collection of zero or more tables represented by
DataTable objects. The DataTableCollection contains all the DataTable objects in
a DataSet. A DataTable is also defined in the System.Data namespace and
represents a single table of memory-resident data. It is a tabular representation
of data – in row and column format. It contains a collection of columns
represented by a DataColumnCollection, and constraints represented by a
ConstraintCollection, which together define the schema of the table. A DataTable
also contains a collection of rows represented by the DataRowCollection, which
contains the data in the table. Along with its current state, a DataRow retains
both its current and original versions to identify changes to the values stored in
the row.

A DataSet contains relationships in its DataRelationCollection object. A


relationship, represented by the DataRelation object, associates rows in one
DataTable with rows in another DataTable. A relationship is analogous to a join
path that might exist between primary and foreign key columns in a relational
database. A DataRelation identifies matching columns in two tables of a
DataSet.
Relationships enable navigation from one table to another in a DataSet. The
essential elements of a DataRelation are the name of the relationship, the name
of the tables being related, and the related columns in each table. Relationships can be built with more than one column per table by specifying an array of
DataColumn objects as the key columns. When you add a relationship to the DataRelationCollection, you can optionally add a UniqueKeyConstraint and a
ForeignKeyConstraint to enforce integrity constraints when changes are made to related column values.

The DataSet, DataTable, and DataColumn all have an ExtendedProperties property. ExtendedProperties is a PropertyCollection where you can place custom
information, such as the SELECT statement that was used to generate the result set, or the time when the data was generated. The ExtendedProperties
collection is persisted with the schema information for the DataSet.
To illustrate the above concepts, we create another method SqlDataAdapterDemo(), below the previous Main() method , within same namespace and
Program class. Then we call this method from within Main() :
static void SqlDataAdapterDemo()
{
string connectionString = ConfigurationManager.ConnectionStrings["dbcs"].ConnectionString;
SqlConnection connection = new SqlConnection(connectionString);
string command = "SELECT * FROM employees"; // Up till this point almost everything is same as our previous methods.
SqlDataAdapter adapter = new SqlDataAdapter(command, connection); // Rather than using a DataReader, we use a..
//…DataAdapter. We create an instance of DataAdapter using its constructor which takes both query command and connection as parameters. (1)
DataSet dataset = new DataSet(); // Instantiate a DataSet object
adapter.Fill(dataset); // Note how we don’t explicitly use Connection.Open() before this line. (2)
Console.WriteLine(dataset.Tables); // Returns System.Data.DataTableCollection – see the picture in previous page
foreach (DataRow row in dataset.Tables[0].Rows) //(3), Since there is a DataRowCollection, which implements IEnumerable..
{ // ….and ICollection at some level -we can loop over this collection using foreach. Note only one Table was available and loaded (Tables[0]).
Console.WriteLine("{0} {1} {2}", row[0], row[1], row[“city”]); // As we go over each row, we access…
} //…each element using index – implying DataRow implements a C# indexer in its definition. Note we can index via column ordinal number…
} //…or column name : row[“columnName”]
(1) SqlDataAdapter has overloaded constructors – to accept one or more arguments.
SqlDataAdapter() :- Initializes a new instance of SqlDataAdapter class
SqlDataAdapter(SqlCommand command) :- initializes a new instance with the specified SqlCommand as the SelectCommand property
SqlDataAdapter(string selectCommandText, SqlConnection connection) :- takes a string to be used by the SqlCommand property of SqlDataAdapter and a
SqlConnection object as well.
SqlDataAdapter(string selectCommandText, string connectionString) :- takes a string for the connection string ,rather than the Sql Connection object.
(1.5) The SqlDataAdapter, serves as a bridge between a DataSet and SQL Server for retrieving and saving data. The SqlDataAdapter provides this bridge by
mapping Fill, which changes the data in the DataSet to match the data in the data source, and Update, which changes the data in the data source to match
the data in the DataSet, using the appropriate Transact-SQL statements against the data source. When the SqlDataAdapter fills a DataSet, it creates the
necessary tables and columns for the returned data if they do not already exist.
(2) The Fill method implicitly opens the Connection that the DataAdapter is using if it finds that the connection is not already open. If Fill opened the
connection, it will also close the connection when Fill is finished. This can simplify your code when dealing with a single operation such as a Fill or an
Update. Similarly, when we call Update() method on the SqlDataAdapter, it opens the connection to the database (if it's not already open), performs the
required operation, and then closes the connection. If the connection is already open (e.g., explicitly opened earlier), SqlDataAdapter will use the open
connection and not close it after the operation. f you want more control over the connection (e.g., keeping it open for multiple operations), you can open it
manually (connection.Open()) before calling SqlDataAdapter methods. However, in this case, you will be responsible for closing it.
The Fill method has many overloads. But in all cases, the Fill method Adds or refreshes rows in the DataSet or DataTable. The Fill method retrieves the
data from the data source using a SELECT statement. The IDbConnection object associated with the select command must be valid, but it does not need to
be open. If the IDbConnection is closed before Fill is called, it is opened to retrieve data and then closed. If the connection is open before Fill is called, it
remains open. If an error or an exception is encountered while populating the data tables, rows added prior to the occurrence of the error remain in the
data tables. The remainder of the operation is aborted. If a command does not return any rows, no tables are added to the DataSet, and no exception is
raised.
(3)Note that Fill() has closed the connection by now. Our foreach is being done during disconnected state – Disconnected Data Access. Once DataSet or
DataTable is filled, then no active connection is required to read the data.
(4) The output given by the above code can also be obtained only by using a DataTable object and filling it – since we are still loading only one table.
static void DemoForDataTable()
{
string connectionString = ConfigurationManager.ConnectionStrings["dbcs"].ConnectionString;
SqlConnection connection = new SqlConnection(connectionString);
string command = "SELECT * FROM employees";
SqlDataAdapter adapter = new SqlDataAdapter(command, connection);
DataTable table = new DataTable(); // (4)
adapter.Fill(table);
foreach (DataRow row in table.Rows)
{
Console.WriteLine("{0} {1} {2}", row[0], row[1], row["city"]);
}
}

/PaperGenerator

├── /Data
│ ├── DatabaseHelper.cs # Handles MySQL database operations
│ └── QuestionRepository.cs # Fetches questions from the database

├── /Models
│ └── Question.cs # Represents a question entity

├── /Services
│ ├── PdfGenerator.cs # Handles PDF generation using pdflatex
│ └── LatexTemplateService.cs # Manages LaTeX template creation

├── /Templates
│ └── base_template.tex # Base LaTeX template for PDF generation

├── /Output
│ └── generated_pdfs # Folder to store generated PDFs

├── /ViewModels
│ └── MainViewModel.cs # ViewModel for the main UI

├── /Views
│ └── MainWindow.xaml # Main WPF UI window

├── /Utilities
│ └── FileHelper.cs # Utility methods for file operations

└── App.config # Configuration file for database connection
# We want to set the parameters of a query depending upon user input. Right now, I am concerned only regarding the
user input- make a class UserInput, a method within this to take input through Console.ReadLine for number of
questions, the subject, the difficulty level and a boolean for deciding whether the user needs the answer key as well.
Through this method I set the value of corresponding public properties like NumberOfQuestions, Subject,
DifficultyLevel and NeedAnswerKey. I plan on using these values for creating a query string (using Concat or
Stringbuilder.Add) in another class by passing these parameters or accessing them otherwise.

namespace databaseTrial.Input
{
public class UserInput
{
public int NumberOfQuestions { get; private set; }
public string DifficultyLevel { get; private set; }
public string Subject { get; private set; }
public bool NeedAnswerKey { get; private set; }

public void GetParameters()


{
Console.Write("Enter the number of Questions required : ");
string? numberInput = Console.ReadLine();
if(int.TryParse(numberInput, out int number))
{
NumberOfQuestions = number;

}
else
{
Console.WriteLine("Invalid Input. Using Default Value of 5");
NumberOfQuestions = 5;
}

Console.Write("Enter the Difficulty Level of Questions required (Options- Easy, Medium, Hard) : ");
DifficultyLevel = Console.ReadLine() ?? "Easy";
Console.Write("Enter the Subject concerned : ");
Subject = Console.ReadLine() ?? "Physics";

Console.Write("Do you need an Answer Key ? (yes/no) : ");


string? answerKeyInput = Console.ReadLine()?.ToLower();
NeedAnswerKey = answerKeyInput == "yes";
}
}
}

Visual Studio showing a warning with the Console.ReadLine - Deference of a possible null reference/ Possible null
reference assignment? is related to nullable reference types, a feature introduced in C# 8.0. By default, reference
types (like string) are considered non-nullable, meaning they are expected to never hold a null value. However,
Console.ReadLine() can return null if the input stream is closed (e.g., if the user presses Ctrl+Z or the input is
redirected and ends unexpectedly).Console.ReadLine() returns a string? (a nullable string), which means it can return
either a string or null.When you assign the result of Console.ReadLine() to a non-nullable string (like Subject or
DifficultyLevel), the compiler warns you because it cannot guarantee that the value is not null.

You can explicitly tell the compiler that you are sure the value will not be null by using the null-forgiving operator (!).

You can use the null-coalescing operator (??) to provide a default value if Console.ReadLine() returns null.

You can explicitly check if the result of Console.ReadLine() is null and handle it appropriately

update your properties to allow null values:- but you'll need to handle null values elsewhere in your code

warning on the string properties- Non nullabble value must contain no null when exiting constructor

warning "Non-nullable property must contain a non-null value when exiting constructor" occurs because of the nullable
reference types feature in C#. When nullable reference types are enabled, the compiler enforces that non-nullable
reference types (like string) must be initialized with a non-null value before they are accessed or used. If the compiler
cannot guarantee that the property is initialized with a non-null value, it raises this warning.

You can initialize the properties with default values at the point of declaration. This ensures that they are
never null.

You can initialize the properties in the constructor. This ensures that they are assigned a value when the
object is created

you can mark the properties as nullable by using string?.

If you are confident that the properties will always be initialized before they are used, you can suppress
the warning using the null-forgiving operator (!). This tells the compiler to ignore the warning.
Pipeline Steps

Here’s the step-by-step pipeline for your project:

1. Database Setup

 Use MySQL to store the question table with columns like id, question_text (LaTeX format), and difficulty_level.

 Example table:

sql

Copy

CREATE TABLE question (

id INT PRIMARY KEY AUTO_INCREMENT,

question_text TEXT NOT NULL,

difficulty_level VARCHAR(50)

);

2. Fetch Questions from MySQL

 Use ADO.NET to connect to the MySQL database and fetch questions based on user input (e.g., number of
questions).

 Example code in QuestionRepository.cs:

csharp

Copy

public List<Question> GetQuestions(int numberOfQuestions)

List<Question> questions = new List<Question>();

using (MySqlConnection connection = new MySqlConnection(connectionString))


{

connection.Open();

string query = "SELECT question_text FROM question LIMIT @count";

using (MySqlCommand command = new MySqlCommand(query, connection))

command.Parameters.AddWithValue("@count", numberOfQuestions);

using (MySqlDataReader reader = command.ExecuteReader())

while (reader.Read())

questions.Add(new Question

QuestionText = reader["question_text"].ToString()

});

return questions;

3. Create LaTeX Document

 Use a base LaTeX template (base_template.tex) and dynamically insert the fetched questions into it.

 Example template (base_template.tex):

latex

Copy

\documentclass{article}

\begin{document}

\title{Generated Questions}

\maketitle

QUESTIONS_PLACEHOLDER

\end{document}

 Replace QUESTIONS_PLACEHOLDER with the actual questions in LatexTemplateService.cs:

csharp
Copy

public string GenerateLatexDocument(List<Question> questions)

string templatePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Templates", "base_template.tex");

string templateContent = File.ReadAllText(templatePath);

string questionsContent = string.Join("\n\n", questions.Select(q => q.QuestionText));

string finalLatexContent = templateContent.Replace("QUESTIONS_PLACEHOLDER", questionsContent);

return finalLatexContent;

4. Generate PDF Using pdflatex

 Save the generated LaTeX content to a .tex file and use the Process class to run pdflatex.

 Example code in PdfGenerator.cs:

csharp

Copy

public void GeneratePdf(string latexContent, string outputDirectory)

string latexFilePath = Path.Combine(outputDirectory, "temp.tex");

File.WriteAllText(latexFilePath, latexContent);

ProcessStartInfo processStartInfo = new ProcessStartInfo

FileName = "pdflatex",

Arguments = $"-output-directory={outputDirectory} {latexFilePath}",

RedirectStandardOutput = true,

RedirectStandardError = true,

UseShellExecute = false,

CreateNoWindow = true

};

using (Process process = new Process())

{
process.StartInfo = processStartInfo;

process.Start();

process.WaitForExit();

if (process.ExitCode == 0)

Console.WriteLine("PDF generated successfully!");

else

string error = process.StandardError.ReadToEnd();

Console.WriteLine("Error: " + error);

5. Display Success Message in WPF

 Use data binding and commands in WPF to trigger the pipeline and display a success message.

 Example in MainViewModel.cs:

csharp

Copy

public ICommand GeneratePdfCommand { get; }

public string StatusMessage { get; set; }

public MainViewModel()

GeneratePdfCommand = new RelayCommand(GeneratePdf);

private void GeneratePdf()

var questions = _questionRepository.GetQuestions(5); // Fetch 5 questions

string latexContent = _latexTemplateService.GenerateLatexDocument(questions);

_pdfGenerator.GeneratePdf(latexContent, Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Output",


"generated_pdfs"));
StatusMessage = "PDF generated successfully!";

OnPropertyChanged(nameof(StatusMessage));

6. Clean Up

 Delete temporary .tex and .log files after PDF generation.

 Example in FileHelper.cs:

csharp

Copy

public void CleanUpTempFiles(string directory)

foreach (var file in Directory.GetFiles(directory, "temp.*"))

if (!file.EndsWith(".pdf"))

File.Delete(file);

}
Using ADO.NET to connect to Postgresql:

First target Console Application for all platforms , not just for windows. There you get option for .NET 8 and above.
In .NET Framework 4.8 it creates problem, cannot install the psotgres driver. Then don’t check the box for don’t enable
top level statements – created problem even after npgsql was installed – need to look into it further.

Download npgsql from Nuget package manager. Include using npgsql at top. Follwing code works. Similar to the other.
In pgAdmin 4, right click Postgres and check properties for the connection parameters – or google.
Dbhsdb

Zdhdbbz

Understanding Processes in General

A process is an instance of a program that is being executed. It contains the program code, its current activity, and
the resources allocated to it (such as memory, file handles, etc.). Processes are managed by the operating system,
which allocates resources, schedules execution, and ensures isolation between processes.

Key concepts related to processes:

1. Process ID (PID): A unique identifier assigned to each process by the operating system.

2. Parent and Child Processes: A process can create another process, known as a child process. The creating
process is called the parent process.

3. Process State: A process can be in various states, such as running, waiting, or terminated.

4. Inter-Process Communication (IPC): Mechanisms for processes to communicate with each other (e.g.,
pipes, sockets, shared memory).

Using Processes in C#

In C#, you can interact with processes using the System.Diagnostics.Process class. This class allows you to start, stop,
and manage external processes from your application.

Key Methods and Properties of the Process Class:

 Start(): Starts a process.

 Kill(): Terminates a process.

 WaitForExit(): Waits for the process to exit.

 StandardInput: Provides a stream to write input to the process.

 StandardOutput: Provides a stream to read output from the process.

 StandardError: Provides a stream to read error messages from the process.

 ExitCode: Gets the exit code of the process after it terminates.

Example: Running pdflatex from C#

To run pdflatex (a command-line tool for generating PDFs from LaTeX files) from your C# application, follow these
steps:
Step 1: Set Up the ProcessStartInfo Object

The ProcessStartInfo class is used to specify the process to start and its configuration (e.g., arguments, working
directory, etc.).

csharp

Copy

using System;

using System.Diagnostics;

class Program

static void Main()

// Create a new ProcessStartInfo object

ProcessStartInfo processStartInfo = new ProcessStartInfo

FileName = "pdflatex", // The command to run

Arguments = "example.tex", // The LaTeX file to compile

RedirectStandardOutput = true, // Redirect standard output

RedirectStandardError = true, // Redirect standard error

UseShellExecute = false, // Do not use the OS shell

CreateNoWindow = true, // Do not create a window

WorkingDirectory = @"C:\path\to\latex\files" // Set the working directory

};

Step 2: Start the Process

Use the Process class to start the process and capture its output.

csharp

Copy

using (Process process = new Process())

process.StartInfo = processStartInfo;

// Start the process


process.Start();

// Read the output (if any)

string output = process.StandardOutput.ReadToEnd();

string error = process.StandardError.ReadToEnd();

// Wait for the process to exit

process.WaitForExit();

// Display the output and error

Console.WriteLine("Output: " + output);

Console.WriteLine("Error: " + error);

// Check the exit code

if (process.ExitCode == 0)

Console.WriteLine("PDF generated successfully!");

else

Console.WriteLine("Failed to generate PDF.");

}
public LaTeXPreamble(string docType, string author, string title, string subject)

_docType = docType;

_author = author;

_title = title;

_subject = subject;

public StringBuilder Preamble()

StringBuilder preamble = new StringBuilder();

preamble.Append(

$"\\documentclass{{{_docType}}}\r\n\\usepackage[a4paper, total={{7in, 9in}}]{{geometry}}\r\n\\


usepackage{{amsmath}}\r\n\\usepackage{{graphicx}}\r\n\\usepackage{{enumitem}}\r\n\\usepackage{{multicol}}\
r\n\\usepackage{{adjustbox}}\r\n\\usepackage{{tasks}}\r\n\\title{{\r\n {_title}\\\\\r\n \\vspace{{0.2cm}}\r\n \\
large {_subject} % Adds another centered line below the main title\r\n}}\r\n\\author{{{_author}\\
thanks{{AcademyOfPhysics.}}}}\r\n\\date{{}}\r\n\\begin{{document}}\r\n\\maketitle\r\n\\end{{document}}");

return preamble;

Use two { to escape a brace

You might also like