zurück zum Artikel

Ferris Talk #13: Rust-Web-APIs und Mocking mit Axum

Rainer Stropek

Das jüngste Release von Axum verbessert das State-Handling und vermeidet auf diese Weise Laufzeitfehler.

Komponenten aus dem Tokio-Stack waren bereits häufiger Thema im Ferris Talk. Zu ihnen gehört Axum, ein Framework für die Entwicklung von Web-APIs. Im November ist Axum in Version 0.6 erschienen [1]. Eine wichtige Änderung ist die Verbesserung der Typsicherheit bezüglich des Shared State. Bisher galt die Empfehlung, die Extension-Middleware und den Extension-Extraktor zu verwenden [2], um den State zu teilen.

Ferris Talks – die Kolumne für Rustaceans

Diese Komponenten stellten jedoch die Konsistenz der Typen nicht zur Übersetzungszeit sicher. Hatte jemand die Middleware vergessen oder die falschen Typen bereitgestellt, kam es beim Extraktor zu einem Laufzeitfehler. In Axum 0.6 wurde der State-Extraktor eingeführt [18], der diese Schwäche beseitigt.

Diese Folge des Ferris Talk zeigt, wie das verbesserte State-Handling in Axum aussieht und zwar über triviale Anwendungsfälle hinaus. Die Beispiele demonstrieren, wie sich mit Rust-Traits und dem Mocking-Framework mockall [19] Unittests für die Axum-API umsetzen lassen, bei denen Mock-Objekte zugrundeliegende Schichten wie Datenbankzugriff während des Tests ersetzen.

Das folgende Beispiel illustriert zunächst die Axum-Neuerungen und zeigt anschließend die Entkopplung der Schichten mit Rust-Traits Schritt für Schritt. Die im Text eingebetteten Codeausschnitte gehen nur auf die Codeteile ein, die für das Thema relevant sind. Der gesamte, lauffähige Code findet sich auf GitHub [20].

Der Beispielcode stellt den Prototyp einer einfachen Web-API zum Verwalten von Superhelden mit Rust und Axum dar. Er soll zunächst die Suche nach Helden anhand ihrer Namen umsetzen. Weitere API-Funktionen würden demselben Schema folgen. Eine Einschränkung des Prototyps besteht darin, dass er nicht tatsächlich auf eine Datenbank zugreift. Dennoch soll der der Code alle Vorkehrungen enthalten, um den asynchronen Datenbankzugriff beispielsweise mit der sqlx-Crate hinzuzufügen [21].

Der Code der Web-API und der für den Datenbankzugriff sollten unbedingt voneinander getrennt sein. Das ist im Beispiel für den Einsatz von Mock-Objekten und Unittests unerlässlich, wäre aber auch für eine reguläre Anwendung sinnvoll. Der folgende Code zeigt die Datenzugriffsschicht des Prototyps. Details zur Implementierung finden sich in den Kommentaren.

/// The model for our API. We are maintaining heroes.
#[derive(Serialize)]
pub struct Hero {
  pub id: &'static str,
  pub name: &'static str,
}

/// Represents an error that happened during data access.
enum DataAccessError {
  NotFound,
  #[allow(dead_code)]
  TechnicalError,
  #[allow(dead_code)]
  OtherError,
}

/// Dummy implementation for our repository
/// 
/// In real life, this repository would access a 
/// database with persisted heroes. However, for
/// the sake of simplicity, we just simulate 
/// database access by using an in-memory
/// collection of heroes.
struct HeroesRepository();

impl HeroesRepository {
  async fn get_by_name(&self, name: &str) 
    -> Result<Vec<Hero>, DataAccessError> {
    // We don't use a database in this example,
    // but simulate one.
    // Our data is just a const array of heroes:
    const HEROES: [Hero; 2] = [
      Hero {
        id: "1",
        name: "Wonder Woman",
      },
      Hero {
        id: "2",
        name: "Deadpool",
      },
    ];

    // Simulate database access by doing 
    // an async wait operation.
    time::sleep(Duration::from_millis(100)).await;

    // As we do not have a real database, we just 
    // filter the heroes array in memory. Note
    // that we simulate the DB behaviour of `LIKE`
    // and the % wildcard at the end of the name 
    // filter by using the `starts_with` method.
    let found_heroes = HEROES
      .into_iter()
      .filter(|hero| {
        if let Some(stripped_name) = 
          name.strip_suffix('%') {
          // We have a % at the end of the name 
          // filter. This means that we have to use
          // the `starts_with` method to simulate
          // the `LIKE` behaviour of the DB.
          hero.name.starts_with(stripped_name)
        } 
        else {
          // No % at the end, so we can use 
          // the `==` operator.
          hero.name == name
        }
      })
      .collect::<Vec<Hero>>();

    if found_heroes.is_empty() {
      // We did not find any heroes. This means 
      // that we have to return a "not found" error.
      Err(DataAccessError::NotFound)
    } 
    else {
      // We found some heroes. Return them.
      Ok(found_heroes)
    }
  }
}

Besonders zu beachten ist, dass die Methode get_by_name als Rückgabetyp Result<vec, DataAccessError> verwendet, also Fehlerzustände an den Aufrufer der Methode zurückmelden kann. Es wird Aufgabe der API-Handler-Funktion sein, diese Fehlerzustände in passende HTTP Response Messages umzuwandeln. Das spielt für das automatisierte Testen später eine wichtige Rolle.

Auf der Datenzugriffsschicht aufbauend lässt sich als nächstes die API-Handler-Funktion entwickeln. Zu beachten ist im folgenden Codeausschnitt der in Axum 0.6 neu hinzugekommene State-Extraktor, den die Handler-Funktion get_heroes verwendet. Er verbindet die Datenzugriffsschicht HeroesRepository mit der Handler-Funktion.

/// Helper type for the repository state.
type HeroesRepositoryState = Arc<HeroesRepository>;

/// The filter struct for the get_heroes 
/// handler function.
#[derive(Deserialize)]
pub struct GetHeroFilter {
  name: Option<String>,
  // In practice, add additional query parameters here
}

/// The handler function for the get_heroes endpoint.
#[debug_handler]
async fn get_heroes(
  State(repo): State<HeroesRepositoryState>,
  filter: Query<GetHeroFilter>,
) -> impl IntoResponse {
  // Use "%" as the default filter if none was provided.
  let mut name_filter = 
    filter.name.to_owned().unwrap_or("%".to_string());

  // Append % wildcard to the filter if it does
  // not already end with %.
  if !name_filter.ends_with('%') {
    name_filter.push('%');
  }

  // Call the repository to get the heroes from the DB.
  let result = repo.get_by_name(name_filter.as_str()).await;

  // Here comes the core logic of our API handler.
  // It translates the result of the DB access into
  // the correct HTTP status codes. In practice,
  // you would do more error handling and probably
  // some logging here. However, in our example we
  // want to keep it simple and focus on the basic
  // principles of mocking and testing.
  match result {
    // We did not find any heroes. Return a 404.
    Err(DataAccessError::NotFound) 
      => StatusCode::NOT_FOUND.into_response(),

    // We found some heroes. Return them as JSON.
    Ok(heroes) => Json(heroes).into_response(),

    // We got any other error from the DB. Return a 500.
    Err(_) 
      => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
  }
}

Am Ende der Handler-Funktion findet die Umwandlung der Rückgabewerte der Datenzugriffsschicht in die HTTP Response Message statt. Später prüfen Unittests dieses Vorgehen.

In der Praxis besteht eine API nicht nur aus einer einzelnen Handler-Funktion. Folgende Hilfsfunktion enthält die Routen aller Heroes-bezogenen Handler-Funktionen. Eine größere API, die unterschiedliche Themenbereiche abdeckt, würde mehrere solcher Routing-Funktionen enthalten.

/// Setup the sub-router for the heroes endpoint.
/// 
/// Note that this method uses the new type-safe
/// routing feature of axum. 
fn heroes_routes() -> Router<HeroesRepositoryState> {
  Router::new().route("/", get(get_heroes))
}

Wichtig bei der Funktion heroes_routes ist der Rückgabetyp Router<HeroesRepositoryState>. Wie man sieht, ist der Shared State HeroesRepository beziehungsweise der zugehörige Alias HeroesRepositoryState im Rückgabetyp enthalten.

Axum würde das Kompilieren des Codes verhindern, falls die Handler-Funktion get_heroes einen anderen State verlangen würde. Eine solche Inkonsistenz bei der Dependency Injection kann nicht zu einem Laufzeitfehler führen. Das unterscheidet Rust und Axum von anderen API-Plattformen, die nicht in der Lage sind, solche Fehler schon zur Übersetzungszeit zu erkennen.

Jetzt sind alle Komponenten beisammen, um den Server konfigurieren und starten zu können. Im folgenden Codeausschnitt ist die in Axum 0.6 neue Funktion with_state zu beachten, die den Shared State an den Router bindet. Auch dabei findet die Konsistenzprüfung zur Übersetzungzeit statt. Würde man den Aufruf von with_state vergessen oder einen falschen Datentyp bereitstellen, ließe sich das Programm nicht kompilieren.

#[tokio::main]
async fn main() {
  // Create the repository for our API.
  let repo: HeroesRepositoryState = 
    Arc::new(HeroesRepository());

  // Setup the top-level router. Here we only nest
  // one sub-router (heroes). In practice, you would
  // nest more sub-routers here. Note that we create 
  // the router with state. The previously created
  // repository is our state.
  let app = Router::new()
    .nest("/heroes", heroes_routes())
    .with_state(repo);

  // Start the server. Note that for brevity, we do 
  // not add logging, graceful shutdown, etc.
  let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
  axum::Server::bind(&addr)
    .serve(app.into_make_service())
    .await
    .unwrap();
}

Als nächstes steht das automatisierte Testen des Codes an. Axum bietet unterschiedliche Methoden, um Web-APIs zu testen, und in dem Gebiet hat sich in Version 0.6 nichts verändert. Anders sieht es beim Umgang mit Shared State aus: Axum 0.6 erfordert auch für Tests die Bereitstellung des State mit with_state. Dabei ist erneut die Typsicherheit vom Router zur Handler-Funktionen zur Übersetzungszeit sichergestellt. Folgender Codeausschnitt zeigt zwei einfache, automatisierte Tests für die Handler-Funktion in Verbindung mit der Datenzugriffsschicht.

/// Helper function to create a GET request for 
/// a given URI.
fn send_get_request(uri: &str) -> Request<Body> {
  Request::builder()
    .uri(uri)
    .method("GET")
    .body(Body::empty())
    .unwrap()
}

/// Test function for the successful execution of 
/// the get_heroes handler function.
///
/// This test function uses the rstest crate to
/// test the handler function with different query
/// parameters. The test function is executed for
///  each test case.
#[rstest]
// Verify that % is appended to the filter
#[case("/?name=Wonder")] 
// Verify that % is not appended to the filter if
// it already ends with %
#[case("/?name=Wonder%")] 
// Verify that % is used as the default filter
#[case("/")] 
#[tokio::test]
async fn 
  get_by_name_success(#[case] uri: &'static str) {
  // Create repository. Note that this would turn 
  // out test into an integration test if we would
  // use a real database here. We would need to 
  // prepare a well-defined test database and
  // clean it up after the test.
  let repo: HeroesRepositoryState = 
    Arc::new(HeroesRepository());

  // Create the app with the repository as state.
  let app = heroes_routes().with_state(repo);

  // Call the app with a GET request to the 
  // get_heroes endpoint.
  let response = 
    app.oneshot(send_get_request(uri)).await.unwrap();

  // Check the response status code.
  assert_eq!(response.status(), StatusCode::OK);

  // Check the response body.
  let body =
    hyper::body::to_bytes(response.into_body())
    .await
    .unwrap();
  let body: Value = 
    serde_json::from_slice(&body).unwrap();

  // Here we just verify that we get back an array. 
  // In real life, you would probably/ verify 
  // the content of the array as well. To do this,
  // you need the well-defined test database 
  // mentioned above.
  assert!(matches!(body, Value::Array { .. }));
}

/// Test DB error handling in get_heroes 
/// handler function.
#[tokio::test]
async fn get_by_name_failure() {
  // Create repository. Note that this would
  // turn out test into an integration test
  // if we would use a real database here. 
  // We would need to prepare a well-defined
  // test database and clean it up after the test.
  let repo: HeroesRepositoryState = 
    Arc::new(HeroesRepository());

  // Create the app with the repository as state.
  let app = heroes_routes().with_state(repo);

  // Call the app with a GET request to the 
  // get_heroes endpoint. Note that we need
  // to specifiy a filter from which we know 
  // that no heroes will be returned.
  // To be able to do this, you need the 
  // well-defined test database mentioned above.
  let response = app
    .oneshot(send_get_request("/?name=Spider"))
    .await
    .unwrap();

  // Check the response status code.
  assert_eq!(response.status(), 
             StatusCode::NOT_FOUND);
}

// Note that we cannot write a test simulating 
// proper handling of technical errors 
// (e.g. DB crash) in our API handler. This is 
// because we cannot mock DB access in our test.

Das größte Problem an der Umsetzung ist, dass es keine Möglichkeit gibt, die Handler-Funktionen isoliert ohne echten Datenzugriff zu testen. Für Tests wäre eine befüllte Testdatenbank erforderlich. Das mag für Integrationstests sinnvoll sein, aber für das Beispiel sollen die Tests schlank bleiben. Dazu muss man im Code die Datenzugriffsschicht in den Tests der Handler-Funktionen durch Mock-Objekte ersetzen.

Im oben gezeigten Code ist das nicht möglich, da über den Typ des State-Extraktors die Handler-Funktion fest an die Datenzugriffskomponente HeroesRepository gebunden ist. Der Versuch, mit with_state ein Mock-Objekt bereitzustellen, führt zu einem Fehler, da Axum die Konsistenz der Typen beim Kompilieren sicherstellt.

Um die Datenzugriffsschicht austauschen zu können, muss man sie mit Traits entkoppeln. Traits in Rust sind ähnlich zu Interfaces in anderen Programmiersprachen, bieten jedoch weit mehr Möglichkeiten. Wir haben Traits in Ferris Talk #2 [22] genauer vorgestellt.

Der folgende Codeausschnitt zeigt den Trait HeroesRepositoryTrait zum Entkoppeln des Datenzugriffs. Die Methode get_by_name, die bisher Teil der HeroesRepository-Struktur war, siedelt in den Trait um. Zur Laufzeit erfolgt das Bereitstellen der echten Implementierung HeroesRepository über Axum. Für die Tests kommen Mock-Objekte zum Einsatz, die sich mit der mockall-Crate erstellen lassen. Das Makro automock generiert eine Mock-Implementierung des Trait namens MockHeroesRepositoryTrait, die sich später in den Unit Tests verwenden lässt.

/// The repository trait for heroes
///
/// This trait is used to abstract the data access
/// layer. At runtime, the trait is implemented by
/// the HeroesRepository struct. During unit
/// testing, the trait is mocked by the 
/// MockHeroesRepositoryTrait.
/// This allows us to test the handler functions 
/// without having to access the database.
/// MockHeroesRepositoryTrait is generated by 
/// the automock macro (mockall crate).

#[cfg_attr(test, automock)]
#[async_trait]
trait HeroesRepositoryTrait {
  /// Gets a list of heroes from the DB filted by name
  async fn get_by_name(&self, name: &str) 
    -> Result<Vec<Hero>, DataAccessError>;
}

/// Dummy implementation for our repository
/// 
/// In real life, this repository would access 
/// a database with persisted heroes. However, 
/// for the sake of simplicity, we just simulate 
/// database access by using an in-memory
/// collection of heroes.
struct HeroesRepository();

#[async_trait]
impl HeroesRepositoryTrait for HeroesRepository {
  async fn get_by_name(&self, name: &str) 
    -> Result<Vec<Hero>, DataAccessError> {
    // Same code as shown before when we did not 
    // have the HeroesRepositoryTrait trait yet
        ...
  }
}

Für das Zusammenspiel zwischen Axum und dem Trait sind Trait Objects erforderlich. Sie implementieren eine Rust-typische Form von Polymorphismus, mit dem sich unterschiedliche Strukturen – in der Beispielanwendung HeroesRepository und die generierte Struktur MockHeroesRepositoryTrait – über ein einzelnes Trait-Objekt referenzieren lassen. Letzteres enthält eine Referenz auf das jeweilige Objekt, das den Trait implementiert, und eine Tabelle mit Referenzen auf die Methoden des Traits, über die zur Laufzeit der dynamische Methodenaufruf erfolgt.

Der folgende Codeausschnitt zeigt den für Axum erforderlichen Typalias. Zu beachten ist das Schlüsselwort dyn, das den Einsatz eines Trait-Objekts und damit dynamische Methodenaufrufe anstößt. Die Traits Send und Sync sind erforderlich, weil die Handler-Funktionen im Beispiel asynchron sind und damit die beiden Traits voraussetzen. Nähere Informationen zu Send und Sync findet sich in der Tokio-Dokumentation.

/// The repository trait for the state of our router.
type DynHeroesRepository = 
  Arc<dyn HeroesRepositoryTrait + Send + Sync>;

Der Trait ist nun fertig und lässt sich im Code mit der Handler-Funktion einsetzen. Im folgenden Codeausschnitt ist zu beachten, dass der State-Extraktor von Axum nicht mehr das HeroesRepository verwendet, sondern das Trait DynHeroesRepository. Die Handler-Funktion kann somit ebenso mit anderen Implementierungen des HeroesRepositoryTrait wie der generierten Mock-Struktur MockHeroesRepositoryTrait umgehen.

/// The handler function for the get_heroes endpoint.
async fn get_heroes(
  State(repo): State<DynHeroesRepository>,
  filter: Query<GetHeroFilter>,
) -> impl IntoResponse {
    // Same code as shown before when we did 
    //not have the DynHeroesRepository trait yet
    ...
}

Der Code zum Konfigurieren der Routen und zum Starten des Servers ändert sich kaum. Im Wesentlichen wird nur HeroesRepository durch DynHeroesRepository ausgetauscht. Axum ist damit zufrieden, da die Typsicherheit sichergestellt ist.

#[tokio::main]
async fn main() {
  // Create the repository for our API. 
  // Note that the respository is a trait object.
  // It implements the trait DynHeroesRepository. 
  // This is done to allow mocking the repository
  // during unit testing of our handler functions.
  let repo = Arc::new(HeroesRepository()) 
    as DynHeroesRepository;

  // Setup the top-level router. Here we only nest
  // one sub-router (heroes). In practice, you would
  // nest more sub-routers here. Note that we create
  // the router with state. The previously created
  // repository is our state.
  let app = Router::new()
      .nest("/heroes", heroes_routes())
      .with_state(repo);

  // Start the server. Note that for brevity, we 
  // do not add logging, graceful shutdown, etc.
  let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
  axum::Server::bind(&addr)
      .serve(app.into_make_service())
      .await
      .unwrap();
}

/// Setup the sub-router for the heroes endpoint.
fn heroes_routes() -> Router<DynHeroesRepository> {
  Router::new().route("/", get(get_heroes))
}

Die großen Änderungen durch den Trait ergeben sich beim Testen. Die generierte Mock-Struktur MockHeroesRepositoryTrait ermöglicht es, echte Unittests für die Handler-Funktion zu schreiben. Dabei kann man eine Datenzugriffsschicht simulieren und die richtige Behandlung jedes Fehlerzustands sicherstellen. In folgendem Codeausschnitt ist insbesondere das Konfigurieren der Mock-Objekte mit der Funktion expect_get_by_name zu beachten:

#[cfg(test)]
mod tests {
  use super::*;
  use axum::{body::Body, http::Request};
  use mockall::predicate::*;
  use rstest::rstest;
  use serde_json::{json, Value};
  use tower::ServiceExt;

  /// Helper function to create a GET request 
  /// for a given URI.
  fn send_get_request(uri: &str) -> Request<Body> {
    Request::builder()
      .uri(uri)
      .method("GET")
      .body(Body::empty())
      .unwrap()
  }

  /// Test function for the successful execution 
  /// of the get_heroes handler function.
  ///
  /// This test function uses the rstest crate 
  /// to test the handler function with different
  /// query parameters. The test function is executed
  ///  for each test case.
  #[rstest]
  // Verify that % is appended to the filter:
  #[case("/?name=Wonder", "Wonder%")] 
  // Verify that % is not appended to the filter 
  // if it already ends with %:
  #[case("/?name=Wonder%", "Wonder%")] 
  // Verify that % is used as the default filter:
  #[case("/", "%")] 
  #[tokio::test]
  async fn  get_by_name_success(
    #[case] uri: &'static str,
    #[case] expected_filter: &'static str) {
    // Create a vector of dummy heroes to return 
    // from the mock repository.
    let dummy_heroes = vec![Default::default()];

    // Create a mock repository and set the 
    // expectations for the get_by_name method.
    // Note that we are filtering the expectation 
    // by the expected filter string.
    let mut repo_mock = 
      MockHeroesRepositoryTrait::new();
    let result = Ok(dummy_heroes.clone());
    repo_mock
      .expect_get_by_name()
      .with(eq(expected_filter))
      .return_once(move |_| result);

    // Create mock repository
    let repo = Arc::new(repo_mock) 
      as DynHeroesRepository;

    // Create the app with the mock repository 
    // as state.
    let app = heroes_routes().with_state(repo);

    // Call the app with a GET request to the 
    // get_heroes endpoint.
    let response =
      app.oneshot(send_get_request(uri))
        .await
        .unwrap();

    // Check the response status code.
    assert_eq!(response.status(), StatusCode::OK);

    // Check the response body.
    let body = 
      hyper::body::to_bytes(response.into_body())
        .await
        .unwrap();
    let body: Value = 
      serde_json::from_slice(&body).unwrap();
    assert_eq!(body, json!(&dummy_heroes));
  }

  /// Test DB error handling in get_heroes 
  /// handler function.
  #[rstest]
  #[case(DataAccessError::NotFound, StatusCode::NOT_FOUND)]
  #[case(DataAccessError::TechnicalError, 
         StatusCode::INTERNAL_SERVER_ERROR)]
  #[tokio::test]
  async fn get_by_name_failure(
    #[case] db_result: DataAccessError,
    #[case] expected_status: StatusCode,
  ) {
    // Create a mock repository and set the 
    // expectations for the get_by_name method.
    // Note that this time, we are returning 
    // an error.
    let mut repo_mock = 
      MockHeroesRepositoryTrait::new();
    repo_mock
      .expect_get_by_name()
      .with(eq("Spider%"))
      .return_once(|_| Err(db_result));

    // Create mock repository
    let repo = Arc::new(repo_mock) 
      as DynHeroesRepository;

    // Create the app with the mock repository 
    // as state.
    let app = heroes_routes().with_state(repo);

    // Call the app with a GET request to the
    // get_heroes endpoint.
    let response = app
      .oneshot(send_get_request("/?name=Spider"))
      .await
      .unwrap();

    // Check the response status code.
    assert_eq!(response.status(), expected_status);
  }
}

Axum 0.6 hat das State Handling deutlich zum Besseren verändert. Dass der Compiler die Typsicherheit bezüglich Shared State beim Übersetzen prüft, verringert die Laufzeitfehler weiter. Der Schritt passt gut ins generelle Bild von Rust, das für Stabilität und Sicherheit bekannt ist.

Typsicherheit hat aber ihre Tücken, wenn es darum geht, Schichten zu entkoppeln. Man kann nicht wie beispielsweise in JavaScript einfach Mock-Objekte bereitstellen, die mehr oder weniger willkürlich die gleichen Methoden anbieten wie die eigentliche Implementierung der jeweiligen Strukturen, und sich darauf verlassen, dass die Laufzeitumgebung damit umgehen kann. Typsicherheit erfordert den Einsatz von Traits, die aber für sich noch nicht zum Erfolg führen: Ein dynamisches Binding über Rust Trait Objects ist erforderlich, damit Axum mit der Typsicherheit zufrieden ist und man mit Mocking-Frameworks wie mockall Mock-Objekte generieren kann.

Wer neu in Rust ist, findet das Thema Trait Objects oft etwas abschreckend. Die richtige Syntax ist angesichts der komplexen Typkonstrukte mit dyn nicht einfach. Asynchrone Programmierung tut das ihre dazu, da für Traits mit asynchronen Methoden besondere Vorkehrungen erforderlich sind. Der gezeigte Beispielcode soll beim Einstieg helfen, indem er Schritt für Schritt die Schichten der API entkoppelt und dabei den Einsatz der Traits zeigt.

Ferris Talk – Neuigkeiten zu Rust. Kolumnist:
Rainer Stropek, timecockpit.com, Rust Meetup Linz, Autor der Ferris Talks, der Kolumne über die Programmiersprache Rust bei Heise Developer

Rainer Stropek

ist Softwareentwickler, Trainer, Autor und Vortragender im Microsoft-Umfeld und seit über 25 Jahren als Unternehmer in der IT-Industrie tätig.

Er gründete und führte in dieser Zeit mehrere IT-Dienstleistungsunternehmen. Neben der Tätigkeit als Trainer und Berater in seiner Firma software architects [23] entwickelt er mit seinem Team die preisgekrönte Software time cockpit [24]. Rainer hat Abschlüsse der höheren technischen Schule für Informatik Leonding (AT) sowie der University of Derby (UK). Er ist Autor mehrerer Fachbücher und Artikel in Magazinen im Umfeld von Microsoft .NET und C#, Azure, Go und Rust. Seine technischen Schwerpunkte sind Cloud Computing, die Entwicklung verteilter Systeme sowie Datenbanksysteme.

Regelmäßig tritt er als Speaker und Trainer auf namhaften Konferenzen in Europa und den USA auf. 2010 wurde er von Microsoft zu einem der ersten MVPs (Most Valuable Professionals) für die Azure-Plattform ernannt. Seit 2015 ist er Microsoft Regional Director. 2016 hat er zudem den MVP Award für Visual Studio und Developer Technologies erhalten.

(rme [25])


URL dieses Artikels:
https://www.heise.de/-7457143

Links in diesem Artikel:
[1] https://tokio.rs/blog/2022-11-25-announcing-axum-0-6-0
[2] https://docs.rs/axum/latest/axum/#using-request-extensions
[3] https://www.heise.de/hintergrund/Ferris-Talk-1-Iteratoren-in-Rust-6175409.html
[4] https://www.heise.de/hintergrund/Ferris-Talk-2-Abstraktionen-ohne-Mehraufwand-Traits-in-Rust-6185053.html
[5] https://www.heise.de/hintergrund/Ferris-Talk-3-Neue-Rust-Edition-2021-ist-da-mit-Disjoint-Capture-in-Closures-6222248.html
[6] https://www.heise.de/hintergrund/Ferris-Talk-4-Asynchrone-Programmierung-in-Rust-6299096.html
[7] https://www.heise.de/hintergrund/Ferris-Talk-5-Tokio-als-asynchrone-Laufzeitumgebung-ist-ein-Fast-Alleskoenner-6341018.html
[8] https://www.heise.de/hintergrund/Ferris-Talk-6-Ein-neuer-Trick-fuer-die-Formatstrings-in-Rust-6505377.html
[9] https://www.heise.de/hintergrund/Ferris-Talk-7-Vom-Ungetuem-zur-Goldrose-eine-kleine-Rust-Refactoring-Story-6658167.html
[10] https://www.heise.de/hintergrund/Ferris-Talk-8-Wasm-loves-Rust-WebAssembly-und-Rust-jenseits-des-Browsers-7064040.html
[11] https://www.heise.de/hintergrund/Ferris-Talk-9-Vom-Builder-Pattern-und-anderen-Typestate-Abenteuern-7134143.html
[12] https://www.heise.de/hintergrund/Ferris-Talk-10-Constant-Fun-mit-Rust-const-fn-7162074.html
[13] https://www.heise.de/hintergrund/Ferris-Talk-11-Memory-Management-Speichermanagement-in-Rust-mit-Ownership-7195773.html
[14] https://www.heise.de/hintergrund/Ferris-Talk-12-Web-APIs-mit-Rust-erstellen-7321340.html
[15] https://www.heise.de/hintergrund/Ferris-Talk-13-Rust-Web-APIs-und-Mocking-mit-Axum-7457143.html
[16] https://www.heise.de/hintergrund/Ferris-Talk-14-Rust-bekommt-endlich-asynchrone-Methoden-in-Traits-8929334.html
[17] https://www.heise.de/hintergrund/Ferris-Talk-15-Bedingte-Kompilierung-in-Rust-9337115.html
[18] https://docs.rs/axum/latest/axum/#using-the-state-extractor
[19] https://crates.io/crates/mockall
[20] https://github.com/rstropek/rust-samples/tree/master/axum-di-testing
[21] https://crates.io/crates/sqlx
[22] https://www.heise.de/hintergrund/Ferris-Talk-2-Abstraktionen-ohne-Mehraufwand-Traits-in-Rust-6185053.html
[23] https://www.linkedin.com/company/software-architects-og/about/
[24] https://www.timecockpit.com/
[25] mailto:rme@ix.de