ASP.net Core mit Postgresql aus Heroku
Für die Datenhaltung setzt Heroku auf Postgresql. In dem Blogpost 5 Schritte um ASP.net Core mit postgresql zu verwenden habe ich gezeigt wie du mit Hilfe von Dapper in ASP.net Core auf eine Postgresql-Datenbank zugreifen kannst.
In einem ersten Schritt habe ich nun versucht die lokale Postgresql-Datenbank aus dem Projekt PostgresqlUndDapper mit einer Posgresql-Datenbank aus Heroku zu ersetzen. Mit folgenden Schritten kommst du zum Ziel:
1. Heroku Account einrichten und Postgresql-Datenbank erstellen
Als aller erstes benötigst du natürlich ein Heroku Account. Ich habe mit der Free-Version gearbeitet. Für dich wird dies wahrscheinlich auch ausreichen. Nachdem die Registrierung sowie die Aktivierung deines Accounts abgeschlossen hast, kannst du über das Heroku-Dashboard einfach eine neue Postgresql-Datenbank anlegen:
2. Entwicklungsumgebung für Heroku konfigurieren
Heroku liefert ein eigenes Toolset um von deiner Entwicklungsumgebung aus mit Heroku zu arbeiten. Dieses Toolset nennt sich Toolbelt. In einem nächsten Schritt gilt es also diesen Toolbelt auf deiner lokalen Entwicklungsumgebung zu installieren.
3. Zugriff auf Heroku Postgresql-Datenbank
In einem nächsten Schritt geht es darum, den Zugriff von der ASP.net 5.0 Applikation auf die Posgresql-Datenbank von Heroku sicherzustellen. Dabei sind verschiedene Schritte und Code-Anpassungen nötig. Du findest die Anpassungen zusätzlich in dem Github-Repoistory unter dem Branch postgresqlfromheroku
3.1 Zugriffsinformationen ermitteln
Damit du von der ASP.net 5.0 Applikation auf die Postgresql-Datenbank auf Heroku zugreifen kannst, werden die Zugangsinformationen benötigt. Dazu benötigen wir folgende Angaben:
- Server-Name
- Port
- Databasename
- User Id
- Passwort
Die Angaben erhälst du via dem Heroku Database-Dashboard. Wenn du dort deine neu erstellte Datenbank auswählst, siehst du unter dem Tab Connection Settings die benötigten Angaben:
3.2 Anpassen des ConnectionString
Als Nächstes müssen wir den ConnectionString unserer ASP.net 5.0 Applikation anpassen. Dies machst du in der Klasse Startup.cs
namespace PostgresqlUndDapper
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
// Set up configuration sources.
var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; set; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
var connection = @"Server = 127.0.0.1; Port = 5432; Database = RetschgisPostgresqlTest; User Id = postgres; Password = {your password};";
services.AddSingleton<IEmployeeRepository, EmployeeRepository>(parameter => new EmployeeRepository(connection));
}
...
}
}
Folgendermassen musst du die Informationen aus Heroku übertragen:
- Host -> Server
- Port -> Port
- Database -> Database
- User -> User Id
- Password -> Passwort
Hinweis: Heroku rät davon ab direkt mit dem ConnectionString zu arbeiten. Für unsere Testzwecke sollte diese Ausnahme aber OK sein.
Wenn du die Anpassungen gemacht hast und anschliessend deine Applikation startest und die View Employees aufrufst (http://localhost:50217/Employee ), wirst du folgende Fehlermeldung erhalten:
Npgsql.NpgsqlException was unhandled by user code
BaseMessage=no pg_hba.conf entry for host "XYZ", user "XYZ", database "XYZ", SSL off
Code=28000
ErrorCode=-2147467259
File=auth.c
HResult=-2147467259
InternalPosition=0
Line=474
Message=28000: no pg_hba.conf entry for host "XYZ", user "XYZ", database "XYZ", SSL off
MessageText=no pg_hba.conf entry for host "XYZ", user "XYZ", database "XYZ", SSL off
Position=0
Routine=ClientAuthentication
Severity=FATAL
Source=Npgsql
StackTrace:
bei Npgsql.NpgsqlConnector.DoReadSingleMessage(DataRowLoadingMode dataRowLoadingMode, Boolean returnNullForAsyncMessage, Boolean isPrependedMessage)
bei Npgsql.NpgsqlConnector.ReadSingleMessage(DataRowLoadingMode dataRowLoadingMode, Boolean returnNullForAsyncMessage)
bei Npgsql.NpgsqlConnector.HandleAuthentication()
bei Npgsql.NpgsqlConnector.Open()
bei Npgsql.NpgsqlConnectorPool.GetPooledConnector(NpgsqlConnection Connection)
bei Npgsql.NpgsqlConnectorPool.RequestConnector(NpgsqlConnection connection)
bei Npgsql.NpgsqlConnection.Open()
bei PostgresqlUndDapper.RepositoriesPostgresql.AbstractPostgresqlRepository`2.Get() in D:\git\ASPNET5PlayGround\PostgresqlUndDapper\src\PostgresqlUndDapper\RepositoriesPostgresql\AbstractPostgresqlRepository.cs:Zeile 34.
bei PostgresqlUndDapper.Controllers.EmployeeController.Index() in D:\git\ASPNET5PlayGround\PostgresqlUndDapper\src\PostgresqlUndDapper\Controllers\EmployeeController.cs:Zeile 21.
InnerException:
Interessant ist der Name der Routine ClientAuthentication. Wie du hier entnehmen kannst wird von Heroku den Zugriff via SSL verlangt. Gemäss den ConnectionString-Optionen von Npgsql muss somit der ConnectionString mit folgenden Einträgen ergänzt werden:
var connection = @"Server = 127.0.0.1; Port = 5432; Database = RetschgisPostgresqlTest; User Id = postgres;
Password = {your password}; SslMode=Require; UseSSLStream=true";
Nach dem du den ConnectionString ergänzt hast und die Applikation noch einmal startest, erhälst du wieder eine Fehlermeldung:
System.Security.Authentication.AuthenticationException was unhandled by user code
HResult=-2146233087
Message=The remote certificate is invalid according to the validation procedure
Source=System
StackTrace:
bei System.Net.Security.SslState.StartSendAuthResetSignal(ProtocolToken message, AsyncProtocolRequest asyncRequest, Exception exception)
bei System.Net.Security.SslState.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.ProcessReceivedBlob(Byte[] buffer, Int32 count, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartReadFrame(Byte[] buffer, Int32 readBytes, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartReceiveBlob(Byte[] buffer, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.CheckCompletionBeforeNextReceive(ProtocolToken message, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.StartSendBlob(Byte[] incoming, Int32 count, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.ForceAuthentication(Boolean receiveFirst, Byte[] buffer, AsyncProtocolRequest asyncRequest)
bei System.Net.Security.SslState.ProcessAuthentication(LazyAsyncResult lazyResult)
bei System.Net.Security.SslStream.AuthenticateAsClient(String targetHost, X509CertificateCollection clientCertificates, SslProtocols enabledSslProtocols, Boolean checkCertificateRevocation)
bei Npgsql.NpgsqlConnector.RawOpen(Int32 timeout)
bei Npgsql.NpgsqlConnector.Open()
bei Npgsql.NpgsqlConnectorPool.GetPooledConnector(NpgsqlConnection Connection)
bei Npgsql.NpgsqlConnectorPool.RequestConnector(NpgsqlConnection connection)
bei Npgsql.NpgsqlConnection.Open()
bei PostgresqlUndDapper.RepositoriesPostgresql.AbstractPostgresqlRepository`2.Get() in D:\git\ASPNET5PlayGround\PostgresqlUndDapper\src\PostgresqlUndDapper\RepositoriesPostgresql\AbstractPostgresqlRepository.cs:Zeile 34.
bei PostgresqlUndDapper.Controllers.EmployeeController.Index() in D:\git\ASPNET5PlayGround\PostgresqlUndDapper\src\PostgresqlUndDapper\Controllers\EmployeeController.cs:Zeile 21.
InnerException:
Hier gibt es zwei Lösungsansätze. Quick-and-Dirty ist, dass man beim Erstellen der NpgSqlConnection für UserCertificateValidationCallback ein Delegate übergibt, welcher immer true retourniert. Für unser Beispiel ausreichend, für eine echte Applikation wäre so ein Ansatz wie hier beschrieben wohl eher zu Verfolgen: http://www.limilabs.com/blog/the-remote-certificate-is-invalid-according-to-the-validation-procedure. Damit eine NpgSqlConnection immer gleich konfiguriert ist, bauen wir die Klasse AbstractPostgresqlRepository wie folgt um:
public abstract class AbstractPostgresqlRepository<TEntity,TPrimaryKey> : IRepository<TEntity, TPrimaryKey> where TEntity : BaseEntity<TPrimaryKey>
{
private string _connectionString;
public AbstractPostgresqlRepository(string connectionString)
{
_connectionString = connectionString;
}
protected NpgsqlConnection GetConnection()
{
NpgsqlConnection connection = new NpgsqlConnection(_connectionString);
connection.UserCertificateValidationCallback = delegate { return true; };
return connection;
}
public IEnumerable<TEntity> Get()
{
using (NpgsqlConnection connection = GetConnection())
{
connection.Open();
string query = string.Format("SELECT * FROM {0}", TableName);
return connection.Query<TEntity>(query);
}
}
public TEntity Get(TPrimaryKey id)
{
using (NpgsqlConnection connection = GetConnection())
{
connection.Open();
string query = string.Format("SELECT * FROM {0} WHERE Id = @Id LIMIT 1", TableName);
return connection.Query<TEntity>(query, new { Id = id }).First();;
}
}
public void Add(TEntity entity)
{
using (NpgsqlConnection connection = GetConnection())
{
connection.Open();
IEnumerable<KeyValuePair<string, string>> RowsAndValues = ResolveProperties(entity);
IEnumerable<string> keys = RowsAndValues.Select(c => c.Key);
IEnumerable<string> values = RowsAndValues.Select(c => c.Value);
string query = string.Format("INSERT INTO {0} ({1}) VALUES ({2});", TableName, string.Join(",",keys), string.Join(",", values));
connection.Execute(query);
}
}
public void Update(TEntity entity)
{
using (NpgsqlConnection connection = GetConnection())
{
connection.Open();
IEnumerable<KeyValuePair<string, string>> RowsAndValues = ResolveProperties(entity);
IEnumerable<string> keys = RowsAndValues.Select(c => c.Key);
IEnumerable<string> values = RowsAndValues.Select(c => c.Value);
string query = string.Format("UPDATE {0} SET ({1}) = ({2}) WHERE Id = @Id;", TableName, string.Join(",", keys), string.Join(",", values));
connection.Execute(query, new { Id = entity.Id });
}
}
public void Remove(TEntity entity)
{
using (NpgsqlConnection connection = GetConnection())
{
connection.Open();
string query = string.Format("DELETE FROM {0} WHERE Id = @Id", TableName);
connection.Execute(query, new { Id = entity.Id });
}
}
private IEnumerable<KeyValuePair<string, string>> ResolveProperties(TEntity entity)
{
List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();
PropertyInfo[] infos = entity.GetType().GetProperties();
foreach (PropertyInfo info in infos)
{
if(info.GetCustomAttribute<KeyAttribute>() == null)
{
result.Add(new KeyValuePair<string, string>(info.Name, string.Format("'{0}'", info.GetValue(entity))));
}
}
return result;
}
protected abstract string TableName { get; }
}
Nun solltest du die Applikation ohne Probleme starten können und auch beim Öffnen von http://localhost:50217/Employee keine Fehlermeldungen mehr erhalten.
4. Daten von lokaler Postgresql-Datenbank auf Heroku migrieren
Damit wir nun beim Öffnen von http://localhost:50217/Employee auch effektiv Employees angezeigt bekommen, müssen wir die Daten aus unserer lokalen Posgresql-Datenbank zu der Posgresql-Datenbank von Heroku migrieren.
Dazu gibt es zwei Möglichkeiten:
- Via Import / Export
- Via Datenbankscript
Die Variante via Backup hat Heroku selber hervorragend beschrieben: https://devcenter.heroku.com/articles/heroku-postgres-import-export
Wenn du es via Datenbankscript machen willst, kannst du wie folgt vorgehen:
1. Öffne PgAdmin und verbinde dich auf den Datenbankserver. Als Zugriffsinformationen nimmst du die gleichen wie wir sie unter 3.1 ermittelt haben
2. Suche dir deine Datenbank aus der Liste heraus
3. Führe das folgende Datenbankscript aus
CREATE TABLE Employees (
Id serial primary key not null,
Name varchar(255) not null,
YearOfContract integer not null
);
INSERT INTO Employees(Name, YearOfContract)
VALUES
('Hans Scheff', 2008),
('Fränzi Finanzi', 2009),
('Klaus Werber', '2011'),
('Fabienne Schaffer', 2009);
Wenn du die Applikation nun startest, solltest du unter der Employees-View alle erfassten Mitarbeiter sehen.