Dataset e relazioni

giovedì 18 dicembre 2008 - 16.07

simbla79 Profilo | Junior Member

Ciao non sono molto esperto di ve.net e avrei bisogno di qualche consiglio su come sviluppare un programma.

Sto cercando di fare un programma per creare delle campagnie di spedizione email e voglio salvare i dati dentro diverse tabelle. 1 Tabella gestisce il nome della campagna e il progressivo, 1 i dati relativi all'oggeto della mail il body etc...., l'ultima ai dati ovvero gli indirizzi email

Ho creato 3 tabelle in SQL Express

CAMPAIN_COUNTER
CAMPAIN_DETAIL
CAMPIAN_DATA

le tre tabelle sono relazionate tra loro da un campo IDCAMPAIN che è un id univoco autoincrementate generato nella tabella CAMPAIN_COUNTER, nelle altre 2 il campo IDCAMPAIN è settato su NOT NULL

Ho creato un dataset in visual studio collegando le tre tabelle e in una form voglio visualizzare i dati contenuti della tabella CAMPAIN_DETAIL collegando delle textbox a una bindingsource.

Premendo un bottone "Nuova campagna" faccio Bindingsource.AddNew per aggiungere 1 riga nella tabella CAMPAIN_COUNTER e 1 nella CAMPAIN_DETAIL.

Il problema è che quando faccio l'update sul dataset mi dice che il campo IDCAMPAIN non può contenere il valore NULL. Volevo sapere quindi come funzionano le relazioni tra le tabelle, se aggiungo una riga nella tabella COUNTER non dovrei Avere una riga anche nella tabella DETAIL e DATA con lo stesso IDCAMPAIN?

Spero di essere stato chiaro e di ricevere qualche dritta che mi possa far capire meglio come funziona il tutto.

Ciao

Jeremy Profilo | Guru

>se aggiungo una riga nella tabella COUNTER non dovrei Avere una
>riga anche nella tabella DETAIL e DATA con lo stesso IDCAMPAIN?
>
Assolutamente NO....sei tu che devi inserire i dati in una tabella e nell'altra.
la relazione ti serve solo per associare una gruppo di record di una tabella ad un'altra in base all'indice.....ma solo in lettura dei dati non in inserimento.
per esempio:

Tabella Clienti.
IdCliente---Cliente---IdCitta----etc....

Tabella Citta
IdCitta---Citta---Cap---Etc...

Come vedi, nella tabella Clienti, ho inserito l'IdCittà che conterrà il valore, di tipo contatore,della citta corrispondente alla Citta di residenza del cliente.


In questo modo ho evitato di creare Rindondanza dei dati.
La relazione tra i due campi IdCitta mi permettera di visualizzare in una ipotetica griglia il cliente in che città è residente.
Ovviamente i due campi IdCitta devono essere dello stesso Tipo(interi in questo caso)

simbla79 Profilo | Junior Member

Grazie x la risposta!!!

Quindi nel mio caso per inserire i dati nella tabella DETAIL devo prima recuperare l'IDCAMPAIN dalla tabella che me lo genera per poi portarmelo a dientro nelle tabelle dove mi serve?

Jeremy Profilo | Guru

Sinceramente on ho capito bene lo scenario della tua applicazione, quindi non saprei darti un consiglio
Se magari spieghi meglio lo scenario, si potrebbe vedere insieme la soluzione migliore per come strutturare le tabelle.
Per esempio:
Cosa intendi per campagna di spedizione mail??
Come pensi di gestirla??
Effettivamente, qual'è il lavoro che deve svolgere la tua applicazione??.

Ciao.

simbla79 Profilo | Junior Member

ok niente il programma deve inviare delle email a delle liste che l'utente carica. Io vorrei fare in modo di creare delle campagnie, ovvero salvare sul db alcuni dati in modo che ogni volta l'utente non debba riscriverli (esempio il corpo del messaggio, il soggetto, gli allegati etc...),e se l'utente lo vuole di salvare anche la lista caricata.

io ho strutturato 3 tabelle appunto, una è CAMPAIN_COUNTER che contiene il nome della campagnia, il contatore (autoincrementate), e una descrizione. CAMPAIN_DETAILS sarebbe la tabella che contiene le informazioni che ti ho riportato sopra ovevero l'oggetto della email, il corpo, il from etc..., poi c'è la tabella CAMPAIN_DATA che contiene i dati importati (queste soono le colonne IDCAMPAIN (int), DATA (campo xml)).

Nella tabella CAMPAIN_DATA utilizzo un campo XML perchè l'utente può importate o un file di excel o un file di testo, che può contenere anche altre informazioni oltre che all'indirizzo email che mi potrebbero servire, a richiesta dell'utenete, da inserire in diversi parti dell'email. Ma questa parte già mi funziona.

Ti faccio un esempio io carico in un nuovo datase il contenuto dell'XML e poi l'utente digitando {nome campo} può inserire il valore in un qualsiasi punto della email.

spero di aver fornito maggiori informazioni...

grazie ancora x la disponibilità


Jeremy Profilo | Guru

Ho capito solo in parte, comunque sembra che le tabelle, così strutturate, vadano bene.
Non mi spiego comunque il perchè della tabella Details che contiene dei campi che potresti integrare nella tabella Campain, dato che, fanno sempre comunque riferimento alla stessa Campagna.....o per la stessa campagna puoi avere degli oggetti del messaggio diversi?????

Comunque, tornando all'errore..... IDCAMPAIN non può contenere il valore NULL
Vuol dire che, probabilmente, nella Tabella Details, hai un campo IDCAMPAIN che è impostato in modo tale da obbligarti ad inserirci un valore(come è normale che sia altrimenti, addio relazione), quindi devi valorizzare anche quel campo con L'IdCampain al quale appartiene l'entità Oggetto.

Spero di esserti stato utile comunque...
Ciao

simbla79 Profilo | Junior Member

si lo sei stato!!!

effettivamente hai ragione posso risparmiare una tabella e inserire tutto nella COUNTER...

volevo chiedere dove posso trovare dei tutorial su come usare i dataset perchè vorrei approfondire un pò l'utilizzo, capire come utilizzare le storeprocedure nei table adapter etc...


grazie ancora

Jeremy Profilo | Guru

Prova a vedere questo articolo....comunque in rete trovi molto.

http://www.visual-basic.it/articoli/acAdoNet.htm

se poi va su google libri....trovi anche di più.

simbla79 Profilo | Junior Member

ma se io aggiungo una riga dal datate, dove ho un campo contatore, e nel frattempo un altro utente fa la stessa cosa, quando chiamo l'update cosa succede? avrò un errore perchè potrebbe capitare di inserire lo stesso record?

Jeremy Profilo | Guru

>ma se io aggiungo una riga dal datate, dove ho un campo contatore,
>e nel frattempo un altro utente fa la stessa cosa, quando chiamo
>l'update cosa succede? avrò un errore perchè potrebbe capitare
>di inserire lo stesso record?

I datasets (non i datate ) prevedono questo tipo di situazione(concorrenza ottimistica) ma, il discorso, è un pò complesso da spiegare.
Puoi comunque cercare in rete e credimi che troverai un bordello di roba.
Prova sul blog di Luciano Bastianello, mi pare che tratti proprio questo argomento.

http://community.visual-basic.it/LucianoB/

Rimango comunque a disposizione se posso aiutari in altro.

Ciao.

Jeremy Profilo | Guru

Io,comunque, non avendo molta voglia di preoccuparmi di concorrenza ottimistica o quant'altro, ho dichiarato Shared una variabile pubblica che metto a True quando è in esecuzione il metodo che gestisce l'aggiornamento dei dati, in modo che una qualsiasi altra istanza della classe, qualora volesse richiamare anch'essa lo stesso metodo, trova la variabile a True e quindi gliene viene negato l'accesso.

Però, probabilmente, questo è solo un workaround o 'accrocchio' che dir si voglia.

simbla79 Profilo | Junior Member

ma questo funziona se girano più programmi in contemporanea sullo stesso pc giusto?

Jeremy Profilo | Guru

Nel mio caso parlo di due o più pc che fanno riferimento ad un webservice che espone i metodi per la gestione dell'accesso ai dati.

simbla79 Profilo | Junior Member

per me robo nuova !!! qualche esempio?

Jeremy Profilo | Guru

Il discorso non è che si possa affrontare così in quattro righe e comunque in questo caso non ti risolverebbe il problema(a meno che tu non voglia decidere di intraprendere questa strada).

Ti posso magari dire che il WebService è, appunto, un servizio che viene creato come applicazione Asp.net e che, nel mio caso, mi permette di accedere in lettura e scrittura ad un database residente sul mio sito attraverso dei WebMethod e al quale possono accederci tutti i pc che ne fanno riferimento.

Se ti dovesse servire fammi pure un fischio ma, se è solo per scopi didattici, ti consiglio di acquistare un buon libro su Asp.Net o cercare in rete qualche articolo(ce ne sono molti).
Ti farai sicuramente un'idea più chiara piuttosto che la confusione che ti potrei creare io spiegandotelo grossomodo...un pezzo alla volta.

Rimango comunque a disposizioni per eventuali chiarimenti

simbla79 Profilo | Junior Member

per il momento ti ringrazio eventualmente ti faccio un fischio!!!

grazie ancora

simbla79 Profilo | Junior Member

ciao ho ancora bisogno del tuo aiuto...

allora il ho il dataset con 3 con i loro tableadapter etc...

La sitauazione è questa il DB è vuoto non ci sono ancora righe dalla mia maschera creo la nuova "campagna" quindi faccio bindingsource.addnew per aggiunger una nuova riga nella tabella principale CAMPAIN_COUNTER, ma siccome il dataset è vuoto l'idcampain ritorna 0, questo valore mi serve anche per inserire altri dati in un altra tabella. bhe il problema è che quando poi vado a fare l'aggiornamento dei dati sul database mi dice

"The INSERT statement conflicted with the FOREIGN KEY constraint "FK_CAMPAIN_ATTACHMENTS_CAMPAIN_COUNTER". The conflict occurred in database "DBEMAILS", table "dbo.CAMPAIN_COUNTER", column 'IDCAMPAIN'.
The statement has been terminated."

qualche suggerimento?

Jeremy Profilo | Guru

Il campo che nella tabella Db è contatore, tu...non devi valorizzarlo.
Il campo contatore è autoincrementante, ci pensa il Db a valorizzarlo.

simbla79 Profilo | Junior Member

si ok nella tabella che lo genera ma nella tabella che in cui deveo poi inserirlo li non è contatore

TABELLA1
IDCAMPAIN(CONTATORE)

TABELLA2
IDCAMPAIN

nel db ho impostato una reazione tra le 2 tabelle con deleteaction = cascade

quando il dataset ha delle righe quando faccio bindingsource.addnew nella TABELLA1 il contatore mi ritorna il numero successivo a quello presente ma se il dataset è vuoto la addnew mi ritorna come contatore 0, quando poi vado a fare la addnew nella TABELLA2 devo passargli il contatore?

Jeremy Profilo | Guru

Dammi il tempo di verificare ma comunque ti posso già dire che il contatore parte da 1.....

simbla79 Profilo | Junior Member

ma non so se mi sono spiegato bene hai capito qul'è il mio problema?

Jeremy Profilo | Guru

Se posti un pò di codice relativo al problema...sicuramente ti capisco meglio!!!

simbla79 Profilo | Junior Member

ok eccolo

La funzione mi ritorna l'id della campagna

Private Function NuovaCampagna(ByVal inName As String) As Integer Dim retVal As Integer Me.CAMPAIN_COUNTERTableAdapter.Fill(Me.DB.CAMPAIN_COUNTER) Me.CAMPAIN_ATTACHMENTSTableAdapter.Fill(Me.DB.CAMPAIN_ATTACHMENTS) Try Dim tRv As DataRowView = Me.CAMPAINCOUNTERBindingSource.AddNew() tRv("CMPNAME") = inName retVal = tRv("IDCAMPAIN") Return retVal Catch ex As Exception Throw ex End Try End Function

Poi un un altro punto chiamo questo pezzo di codice

Dim tRv As DataRowView = Me.CAMPAIN_ATTACHMENTSBindingSource.AddNew tRv("IDCAMPAIN") = idCampain tRv("ATTFILE") = theContent

per inserire i dati in un altra tabella

Jeremy Profilo | Guru

Se hai 2 tabelle con una relazione uno a molti. In fase di insert o di update sulla tabella 'principale' la FOREIGN KEY potrebbe essere valorizzata a 0 .
Visto che sulla tabella 'secondaria' i record partono da 1 hai chiaramente un errore di vincolo violato.
Quindi, secondo me, devi usare la proprietà count del bindingsource e, se è 0, la sostituisci con 1...


fammi sapere
Ciao.

simbla79 Profilo | Junior Member

boh mi sfugge ancora qlc. ti chiedo un'altra cosa x capire meglio. Allora sempre sto benedetto dataset nella query di select del tablaadapter gli metto un filtro con IDCAMPAIN = @IDCAMPAIN, quando chiamo tableadapter.fill(tabella, numeroid) le tabelle relazionate non vengono riempite di conseguenza?

scuasami ma sono un pò ingnorante in materia voglio iniziare a capire come funzionano di cavolo di cosi....

grazie x la disponibilità ti farò il regalo di natale

Jeremy Profilo | Guru

Allora.....se mi dai tempo fino a domani sera, vediamo di fare un pò di chiarezza....
ti chiedo di aspettare fino a domani sera perchè stasera sono davvero cotto e domani sono in viaggio di ritorno verso casa....
Ok????
Ciao a Domani

simbla79 Profilo | Junior Member

certo ci mancherebbe quando hai tempo

Jeremy Profilo | Guru

Allora.....vediamo di fare un pò di ordine...
Tu hai bisogno di gestire dei dati.
Cominciamo a capire i dati che devi gestire e come strutturare la/le Tablella/e il quale li conterranno.
Campain, qualunque cosa sia, ha bisogno innanzitutto di un indice Id, che sarà di tipo contatore, e da qui non ci schioda nessuno.
Poi abbiamo:
DescrizioneCampagna--->Uno per ogni Id---->Tipo String
OggettoCampagna----->Uno per ogni Id???---->Tipo String
Poi che altro????prosegui indicando i dettagli che comporranno l'entità campagna.
Poi penseremo dove metterli.

simbla79 Profilo | Junior Member

allora ecco i dettagli delle tabelle:

tabella COUNTER

IDCAMPAIN int autoincrementante indicizzato e chiave univoca
CAMPAINNAME varchar
EMAILTO varchar
EMAILFROM varchar
EMAILBODY varchar
EMAILSUBJECT varchar

tabella ATTACHS

IDCAMPAIN int
IDATTACH int autoincrementante
ATTFILE varbinary
ATTNAME varchar

tabella DATA

IDACAMPAIN int
SENDATA xml

Queste sono le tre tabelle sul DB e nel diagramma ho disegnato le relazioni tra COUNTER E ATTACHS e COUNTER DATA con il campo IDCAMPAIN impostando le regole della deleteaction = cascade.

Questo tipo di struttura è corretto? sembra "girare" bene?

Jeremy Profilo | Guru

Ok ......perchè vuoi creare la relazione tra le tabelle????
Dimentichiamola un attimo.
Inserisci un datagridview nel tuo form.
Inserisci una textbox chiamata txtIdCampain.
Inserisci anche un pulsante chiamato "Aggiorna"

Nell'evento click del button scrivi questo codice:

Dim objCmd As New OleDb.OleDbCommand
Dim ds as new Dataset
Dim par(0) As OleDb.OleDbParameter
objCmd.Connection = Conn<----Questa è la tua variabile string di connessione al Db
Dim SqlString="SELECT * FROM COUNTER,ATTACHS,DATA WHERE IDCAMPAIN=?"
objCmd.CommandText = SqlString
Dim index As Integer = Array.IndexOf(item.Parameters, Parameter)
par(0) = New OleDb.OleDbParameter With { .ParameterName = "@IDCAMPAIN",.DbType =DbType.String,.Value = txtIdCampain.text}
objCmd.Parameters.Add(par(0))
Dim da As New Data.OleDb.OleDbDataAdapter(objCmd)
da.Fill(ds, "TuttiICampi")
da.Dispose()
objCmd.Dispose()
DataGridView1.Datasource=ds.Tables(0)
DataGridView1.Refresh

Poi lo perfezioniamo in base a quello che tu hai davvero bisogno.
Questo è solo per farti capire che la relazione in questo caso non è per forza necessaria.
Fammi sapere quando sei pronto.




simbla79 Profilo | Junior Member

ciao e grazie per la risposta...

premetto una cosa io il mio progetto l'ho sviluppato con il degigner quindi tutte le varie query x insert e update sonon generate in automatico...

cmq ho fatto un nuovo procetto inserendo il codice che mi hai passato non capisco cosa voglia dire la dicitura indexof(item.parameter) per item cosa intendi?

io mi collego a un db sql 2005 express quindi uso sqlcommand e non l'oledb ma per questo nn ci dovrebbero essere problemi.

Al posto della datagrdiview vorrei poi usare delle textbox e combobox.

Cmq ho copiato il codice che mi ha dato e mi da ambiguous column name presumo che devo specificare quale sia la tabella "sorgente"

attendo altre istruzioni...

Jeremy Profilo | Guru

Scusami questa riga toglila......il codice che ti ho riportato, è un estratto di una mia applicazione dove, avevo bisogno di ottenere l'indice di un parametro presente in un array di parametri.

>Dim index As Integer = Array.IndexOf(item.Parameters, Parameter)

>premetto una cosa io il mio progetto l'ho sviluppato con il degigner quindi tutte le varie query x insert e update sonon generate in automatico...

Nel tuo primo post, hai specificato che non sei molto pratico di programmazione.....per questo mi permetto di dirti che, secondo me, e solo secondo me, (comunque sfido chiunque a smentirmi), se vuoi imparare veramente devi un pò abbandonare(ovviamente piano piano) tutto ciò che è Designer o Wizard..
Questo perchè, il Designer o Wizard che sia, non fanno nient'altro che creare codice al posto tuo (sicuramente funzionante), ma tu non saprai mai perchè funziona.
Solo scrivendo tu, il tuo codice, potrai capitre cosa stai scrivendo e perchè???
Questo, ovviamente, è solo un consiglio da parte mia che tu, potrai decidere se prendere in considerazione o meno..

Tornando al codice....prova ad eliminare la riga che non serve e fammi sapere se il resto funziona....poi ti commento tutto il codice che ti ho passato.

Ciao a presto

simbla79 Profilo | Junior Member

ciao anche io sono d'accordo con te ad imparare a scrivere un pò di codice nn fa mai male, pensavo che centrava qlc con il nostro problema.

cmq ho scritto il tuo codice togliendo quello che mi hai detto e specificando la tabella sorgente da cui filtrare l'idcampain e il tutto funziona...

Jeremy Profilo | Guru

Ok.....vediamo cosa ho scritto e perchè.....

--qui dichiaro un nuovo oggetto OledbCommand
Dim objCmd As New OleDb.OleDbCommand

--qui dichiaro un nuovo Dataset
Dim ds as new Dataset

--qui dichiaro un vettore di oggetto OleDbParameter che, in questa caso, è lungo 1 oggetto ( (0) Indice base 0)
Dim par(0) As OleDb.OleDbParameter

--qui imposto la stringa di connessione al db
objCmd.Connection = Conn<----Questa è la tua variabile string di connessione al Db

--qui preparo la stringa Sql(sbagliata....menomale che l'hai corretta tu)
Dim SqlString="SELECT * FROM COUNTER,ATTACHS,DATA WHERE IDCAMPAIN=?"

--qui assegno la stringa Sql al command
objCmd.CommandText = SqlString

--qui istanzio un nuovo oggetto parameter e, grazie alla parola chiave "With", assegno il nome del parametro, il tipo e il valore
par(0) = New OleDb.OleDbParameter With { .ParameterName = "@IDCAMPAIN",.DbType =DbType.String,.Value = txtIdCampain.text}

--qui aggiungo il parametro, appena creato, al command
objCmd.Parameters.Add(par(0))

--qui dichiaro un nuovo dataadapter e, come parametro al costruttore, gli passo il command
Dim da As New Data.OleDb.OleDbDataAdapter(objCmd)

--riempio il dataset
da.Fill(ds, "TuttiICampi")

--anniento gli oggetti
da.Dispose()
objCmd.Dispose()

--assegno, come fonte dati del datagridview, la tabella (0) del dataset
DataGridView1.Datasource=ds.Tables(0)

--rinfresco la griglia
DataGridView1.Refresh


A te, però, non servono tutti i campi.....quindi la select diventerà così.


SELECT COUNTER.IDCAMPAIN,COUNTER.CAMPAINNAME,COUNTER.EMAILFROM,COUNTER.EMAILTO,COUNTER.EMAILBODY,COUNTER.EMAILSUBJECT,
ATTACHS.ATTFILE,ATTACHS.ATTNAME
FROM COUNTER,ATTACHS,DATA
WHERE COUNTER.IDCAMPAIN=? AND COUNTER.IDCAMPAIN=ATTACHS.IDCAMPAIN

Se ti è chiaro quanto fatto fin ora.....vado avanti con l'inserimento dei dati nelle tabelle.




simbla79 Profilo | Junior Member

per la query si possono usare anche delle join?

ok per ora tutto chiaro.

Jeremy Profilo | Guru

>per la query si possono usare anche delle join?
Certo che si.....

Dammi tempo e ti posto un'altra parte di codice per l'inserimento dei dati ........
Per stasera te la mando.
Ciao.

simbla79 Profilo | Junior Member

sempre quando sei + comodo tu

grazie ancora


Jeremy Profilo | Guru

Grazie a te per la pazienza.

Adesso dobbiamo creare un nuovo record nella tabella COUNTER ed nuovo record nella tabella ATTACH...
Però....nella tabella COUNTER l'IdCampain è di tipo contatore, quindi autoincrementante, mentre nella tabella ATTACH dobbiamo valorizzare noi il campo IdCampain.
Quindi, per prima cosa, dobbiamo ottenere l'ultimo valore IdCampain della tabella COUNTER per poi valorizzare l'IdCampain nella tabella ATTACH.

Inserisci nel form un nuovo button chiamato NuovoRecord
Inserisci nel form una nuova textbox chiamata IdCampain(o forse l'hai già)

Nell'evento click del button scrivi questo codice.

--qui dichiaro un nuovo oggetto OledbCommand
Dim objCmd As New OleDb.OleDbCommand

--qui dichiaro un nuovo Dataset
Dim ds as new Dataset

--qui imposto la stringa di connessione al db
objCmd.Connection = Conn<----Questa è la tua variabile string di connessione al Db

--qui preparo la stringa Sql(sbagliata....menomale che l'hai corretta tu)
Dim SqlString="SELECT IdCampain FROM COUNTER"

--qui assegno la stringa Sql al command
objCmd.CommandText = SqlString

--qui dichiaro un nuovo dataadapter e, come parametro al costruttore, gli passo il command
Dim da As New Data.OleDb.OleDbDataAdapter(objCmd)

--riempio il dataset
da.Fill(ds, "COUNTER")

--anniento gli oggetti
da.Dispose()
objCmd.Dispose()

Dim bs as new bindingsource
bs.datasource=Ds
bs.movelast
txtIdCampain.text=cstr(cint(directcast(bs.current,datarowview).rows(0)) + 1)

Così abbiamo ottenuto l'ultimo valore contatore presente nel campo IdCampain e lo abbiamo incrementato di 1.

bs.dispose----oppure bs=nothing (non ricordo se bs implementa l'interfaccia IDisposable)

Adesso ti scriverò il resto, intanto, ci tenevo a precisare che il codice che ti sto scrivendo io, è solo uno dei tanti modi per raggiungere lo stesso obbiettivo, sta a te, poi capire come snellirlo e utilizzare oggetti funzioni e metodi che più ti garbano.
Tieni presente, comunque, che io, non scriverei mai tutto questo codice per fare quello che stiamo facendo, ma piuttosto creerei una classe ad-hoc alle mie esigenze e ne utilizzerei metodi e funzioni (parametrizzati a dovere).
Ovviamente ciò non sarebbe possibile farlo attraverso questo forum se non argomentando a dovere ogni oggetto del post.
Ma, se sei d'accordo, potremmo trovare il modo di farlo privatamente.
Non ti nascondo che così sono un pò scomodo.
Ciao
Al prossimo post.




simbla79 Profilo | Junior Member

ciao ok proverò a scrivere queste righe se ti è più comodo magari potresti inviarmi una tua classe cosi posi vedere come gestisci tu l'accesso e posso prendere spunto.

nel frattempo buon natale e buone feste.

al prox ciao
Partecipa anche tu! Registrati!
Hai bisogno di aiuto ?
Perchè non ti registri subito?

Dopo esserti registrato potrai chiedere
aiuto sul nostro Forum oppure aiutare gli altri

Consulta le Stanze disponibili.

Registrati ora !
Copyright © dotNetHell.it 2002-2025
Running on Windows Server 2008 R2 Standard, SQL Server 2012 & ASP.NET 3.5