Keskitymme tässä osassa siihen, kuinka tuotteet voidaan siirtää SQL Serverin Northwind-tietokannasta Cosmos DB:hen, hyödyntäen EF Corea ja JSON-pohjaisia dokumentteja. Koska alkuperäiset entiteettiluokat SQL Serverin Northwind-tietokannassa on suunniteltu normaalisoidulle tietorakenteelle, luomme uusia luokkia Cosmos DB:lle, jotka hyödyntävät sisäänrakennettuja tietoja JSON-dokumenteissa. Tässä käsittelemme muutamia keskeisiä vaiheita ja parhaimpia käytäntöjä, joita kannattaa noudattaa siirrettäessä tietoja Cosmos DB:hen.

Ensimmäinen askel on luoda uudet luokat, jotka kuvaavat tietoja Cosmos DB:ssä. Luomme kolme luokkaa: CategoryCosmos, SupplierCosmos ja ProductCosmos. Näiden luokkien kentät ja tietotyypit vastaavat Cosmos DB:n dokumenttirakennetta, ja ne noudattavat JSON-konventioita, sillä Cosmos DB tallentaa tiedot JSON-muodossa. Esimerkiksi:

csharp
namespace Northwind.CosmosDb.Items; public class CategoryCosmos { public int categoryId { get; set; }
public string categoryName { get; set; } = null!;
public string? description { get; set; } } public class SupplierCosmos { public int supplierId { get; set; }
public string companyName { get; set; } = null!;
public string? contactName { get; set; }
public string? contactTitle { get; set; }
public string? address { get; set; } public string? city { get; set; } public string? region { get; set; }
public string? postalCode { get; set; }
public string? country { get; set; } public string? phone { get; set; } public string? fax { get; set; }
public string? homePage { get; set; }
}
public class ProductCosmos { public string id { get; set; } = null!;
public string productId { get; set; } = null!;
public string productName { get; set; } = null!;
public string? quantityPerUnit { get; set; }
public decimal? unitPrice { get; set; }
public short? unitsInStock { get; set; }
public short? unitsOnOrder { get; set; }
public short? reorderLevel { get; set; }
public bool discontinued { get; set; } public CategoryCosmos? category { get; set; } public SupplierCosmos? supplier { get; set; } }

Kun luokat on luotu, voimme siirtyä siirtämään tietoja Northwind SQL-tietokannasta Cosmos DB:hen. Tämä tapahtuu luomalla uusi ohjelmointimenetelmä, joka ensin hakee kaikki tuotteet SQL-tietokannasta, mukaan lukien niiden liittyvät kategoriat ja toimittajat, ja siirtää ne sitten Cosmos DB:hen. Tätä varten hyödynnämme EF Core:n Include-metodia, jolla voimme hakea myös liittyvät tiedot, kuten kategoriat ja toimittajat. Esimerkki tästä:

csharp
static async Task CreateProductItems() {
double totalCharge = 0.0; try { using (CosmosClient client = new(accountEndpoint: endpointUri, authKeyOrResourceToken: primaryKey)) { Container container = client.GetContainer(databaseId: "Northwind", containerId: "Products"); using (NorthwindContext db = new()) { ProductCosmos[] products = db.Products .Include(p => p.Category) .Include(p => p.Supplier) .Where(p => p.Category != null && p.Supplier != null) .Select(p => new ProductCosmos { id = p.ProductId.ToString(), productId = p.ProductId.ToString(), productName = p.ProductName, category = new CategoryCosmos { categoryId = p.Category.CategoryId, categoryName = p.Category.CategoryName, description = p.Category.Description }, supplier = new SupplierCosmos { supplierId = p.Supplier.SupplierId, companyName = p.Supplier.CompanyName, contactName = p.Supplier.ContactName, contactTitle = p.Supplier.ContactTitle, address = p.Supplier.Address, city = p.Supplier.City, country = p.Supplier.Country, postalCode = p.Supplier.PostalCode, region = p.Supplier.Region, phone = p.Supplier.Phone, fax = p.Supplier.Fax, homePage = p.Supplier.HomePage }, unitPrice = p.UnitPrice, unitsInStock = p.UnitsInStock, reorderLevel = p.ReorderLevel, unitsOnOrder = p.UnitsOnOrder, discontinued = p.Discontinued, }) .ToArray(); foreach (ProductCosmos product in products) { try { ItemResponse productResponse = await container.ReadItemAsync(id: product.id, new PartitionKey(product.productId)); totalCharge += productResponse.RequestCharge; } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { ItemResponse productResponse = await container.CreateItemAsync(product); totalCharge += productResponse.RequestCharge; } catch (Exception ex) { WriteLine("Error: {0} says {1}", ex.GetType(), ex.Message); } } } } } catch (HttpRequestException ex) { WriteLine("Error: {0}", ex.Message); } catch (Exception ex) { WriteLine("Error: {0} says {1}", ex.GetType(), ex.Message); } WriteLine("Total requests charge: {0:N2} RUs", totalCharge); }

Tässä esimerkissä käytämme Cosmos DB:n ItemResponse-luokkaa, joka palauttaa vastauksena yksittäisten tuotteiden käsittelyn aikana käytetyt resurssit, kuten "Request Charge" -kulut. On tärkeää huomata, että tässä käytetty rakenne on yksinkertainen ja selkeä, mutta voidaan tarvita lisäoptimointia suurissa tietomäärissä.

Erityisesti on hyvä muistaa, että Cosmos DB:ssä kaikki dokumentit tarvitsevat id-kentän. Tämä kenttä toimii Cosmos DB:n identifioivana arvona. Jos tätä kenttää ei määritetä manuaalisesti, järjestelmä luo sen automaattisesti GUID-arvolla. Siksi on suositeltavaa määritellä id-kenttä itse, jotta voimme hallita sen arvoja tarkemmin ja vähentää riskiä mahdollisista konflikteista.

Yksi tärkeä käytäntö on myös huomioida, että dokumenttien lisääminen Cosmos DB:hen voi olla resursseja kuluttavaa. Erityisesti suurissa tietomäärissä on suositeltavaa seurata, kuinka paljon resursseja (Request Units, RUs) käytetään ja optimoida siirtoprosessia tarvittaessa. On myös tärkeää valita oikea jaoteltu avain, joka tukee tehokasta skaalautuvuutta ja suorituskykyä.

Miten estää samanaikaisen pääsyn ja kuolemanlukkautumisen monisäikeisissä sovelluksissa?

Kun ohjelmointi siirtyy monisäikeisiin ympäristöihin, on tärkeää ymmärtää, kuinka useat säikeet voivat käyttää ja muokata yhteisiä resursseja turvallisesti ilman ristiriitoja tai virheitä. Tämä on erityisen tärkeää, kun käsitellään samanaikaisia operaatioita, sillä useiden säikeiden pääsy samaan resurssiin voi johtaa odottamattomiin virheisiin ja ohjelman epäluotettavuuteen.

Oletetaan, että useampi säie käyttää ja muokkaa yhteistä resurssia samanaikaisesti. Tässä tilanteessa on mahdollista, että säikeet voivat vahingossa muokata resurssia yhtä aikaa, jolloin tilanne voi muuttua epäluotettavaksi ja synnyttää kilpailuolosuhteita (race conditions). Tällaisilta virheiltä voi välttyä, kun sovellukseen lisätään mekanismeja, jotka varmistavat, että vain yksi säie pääsee käsittelemään ja muokkaamaan resurssia kerrallaan.

Yksi tapa varmistaa, että vain yksi säie voi käyttää yhteistä resurssia kerrallaan, on käyttää ns. "lukkoa" (lock). Tämä lukko estää muiden säikeiden pääsyn ja muokkaamisen samanaikaisesti, kun toinen säie on jo saanut pääsyn resurssiin. Lukon käyttämiseen voidaan käyttää erityistä objektia, kuten "conch", joka toimii lukon välineenä.

Tässä on esimerkki siitä, kuinka lukko voidaan lisätä:

csharp
public static object Conch = new();

Seuraavaksi, kun haluamme lisätä lukon säikeen käsittelemiin metodeihin, käytämme seuraavaa koodia:

csharp
lock (SharedObjects.Conch) { for (int i = 0; i < 5; i++) { Thread.Sleep(Random.Shared.Next(2000)); SharedObjects.Message += "A"; Write("."); } }

Tässä tapauksessa "lock" varmistaa, että vain yksi säie voi muokata SharedObjects.Message-resurssia kerrallaan. Kun säie on suorittanut työnsä, lukko vapautetaan, ja toinen säie voi ottaa lukon ja jatkaa työtään.

Tärkeää on ymmärtää, että vaikka lukko estää kilpailevia säikeitä pääsemästä samanaikaisesti yhteiseen resurssiin, se ei automaattisesti estä kaikkia mahdollisia ongelmia. Yksi tällainen ongelma on ns. kuolemanlukkautuminen (deadlock). Kuolemanlukkautuminen syntyy, kun kaksi tai useampi säie odottaa toisiaan pääsemään resurssille, mutta eivät koskaan saa pääsyä, koska jokainen säie pitää toisen resurssin lukittuna.

Kuolemanlukkautumisen estämiseksi voidaan käyttää "aikakatkaisua" (timeout), joka rajoittaa, kuinka kauan säie odottaa lukkoa ennen kuin se luovuttaa ja jatkaa työskentelyään. Tämä voidaan toteuttaa seuraavasti:

csharp
try { if (Monitor.TryEnter(SharedObjects.Conch, TimeSpan.FromSeconds(15))) { for (int i = 0; i < 5; i++) { Thread.Sleep(Random.Shared.Next(2000)); SharedObjects.Message += "A"; Write("."); } } else { WriteLine("Method A timed out when entering a monitor on conch."); } } finally { Monitor.Exit(SharedObjects.Conch); }

Tässä esimerkissä Monitor.TryEnter antaa säikeelle rajoitetun ajan yrittää saada lukon, ja jos lukkoa ei voida saada tietyssä ajassa, säie luovuttaa ja vältetään kuolemanlukkautuminen. Tämä koodi on tehokas tapa varmistaa, että säie ei jää pysyvästi odottamaan resurssia, joka on jumiutunut.

On myös tärkeää huomata, että .NET-tilaisuudet eivät ole säieystävällisiä, ja niiden käyttö monisäikeisissä ympäristöissä voi aiheuttaa ongelmia. Joissain tapauksissa kehittäjät saattavat lisätä lukon tapahtumien käsittelyyn, mutta tämä ei ole aina paras käytäntö, ja voi olla ongelmallista monimutkaisempien tapahtumien käsittelyjen osalta.

Lopuksi, on tärkeää ymmärtää, mitä tarkoitetaan "atomisilla" operaatioilla. Atominen tarkoittaa sitä, että operaatio ei voi olla keskeytynyt – se suoritetaan kokonaan ilman keskeytyksiä. Esimerkiksi ++-operaattori C#:ssa ei ole atominen. Se koostuu kolmesta vaiheesta: arvon lataaminen, arvon lisääminen ja arvon tallentaminen takaisin muuttujaan. Näin ollen jos toinen säie keskeyttää operaatioiden välillä, se voi aiheuttaa virheitä. Atomisten operaatioiden varmistamiseksi C# tarjoaa Interlocked-luokan, joka mahdollistaa esimerkiksi Add, Increment, ja Exchange-operaatioiden suorittamisen atomisesti.

Tällaiset yksityiskohdat ovat olennaisia monisäikeisen ohjelmoinnin ymmärtämisessä, sillä ne auttavat estämään virheitä ja parantavat ohjelman luotettavuutta ja suorituskykyä monisäikeisissä ympäristöissä.

Miten ottaa käyttöön gRPC JSON -transkoodaus ASP.NET Core -palvelussa?

gRPC on tehokas ja skaalautuva tapa luoda palveluita, mutta sen käyttö web-selaimessa on usein rajoitettua, koska selaimet eivät suoraan tue gRPC-protokollaa. Microsoft on kehittänyt ratkaisun tähän ongelmaan nimeltään gRPC JSON -transkoodaus, joka mahdollistaa gRPC-palveluiden kutsumisen HTTP/1.1-protokollan yli JSON-muodossa. Tämä mahdollistaa gRPC-palveluiden käytön myös niille asiakkaille, jotka eivät tue gRPC:tä suoraan.

gRPC JSON -transkoodauksen käyttöönotto on yksinkertainen prosessi, joka vaatii muutaman askeleen. Ensimmäinen askel on lisätä tarvittavat paketit gRPC JSON -transkoodaukselle. Tämä tehdään lisäämällä gRPC JSON -transkoodauksen pakettiviite projektiin. Tämän jälkeen, ASP.NET Core -palvelu tulee konfiguroida lisäämällä JSON-transkoodaus gRPC:n perusasetusten jälkeen.

Käytännössä prosessi etenee seuraavasti:

  1. Lisää gRPC JSON -transkoodauksen paketti Northwind.Grpc.Service -projektiin.

  2. Konfiguroi gRPC JSON -transkoodaus ohjelman alustusvaiheessa lisäämällä rivi builder.Services.AddGrpc().AddJsonTranscoding();.

  3. Luo uusi kansio nimeltä "google" palvelun projektikansioon ja luo sinne apinäkymät, jotka mahdollistavat HTTP-pyyntöjen käsittelyn gRPC:n kautta.

  4. Lisää kaksi .proto-tiedostoa "http.proto" ja "annotations.proto" Google API:n määrittelyjen mukaisesti.

  5. Lisää nämä tiedostot palvelun määrittelyihin ja konfiguroi HTTP-pyyntöjen reitit, kuten /v1/greeter/{name}, joka kutsuu "SayHello"-metodia, ja /v1/shipper/{shipperId}, joka kutsuu "GetShipper"-metodia.

Kun kaikki on määritetty, voidaan gRPC-palvelu käynnistää ja kokeilla sen toimintaa selaimesta. Käyttäjä voi tehdä tavallisia HTTP GET -pyyntöjä selaimesta, ja palvelu palauttaa JSON-vastauksia gRPC-menetelmien sijaan. Esimerkiksi, kutsumalla URL-osoitteet https://localhost:5121/v1/greeter/Bob ja https://localhost:5121/v1/shipper/2, palvelu palauttaa JSON-vastaukset, jotka sisältävät gRPC-palveluiden tiedot.

gRPC JSON -transkoodauksen etu on, että se mahdollistaa gRPC-palveluiden käytön ilman, että asiakkaan tarvitsee tietää gRPC:n sisäisestä toiminnasta. Palvelu hoitaa kaiken transkoodauksen taustalla. Tämä on erityisen hyödyllistä silloin, kun asiakkaat eivät tue gRPC:tä suoraan, mutta he voivat käyttää perinteisiä HTTP-rajapintoja.

Toinen vaihtoehto gRPC-palveluiden käyttämiseen selaimesta on gRPC-Web. Tämä tekniikka mahdollistaa gRPC-palvelujen käytön suoraan selaimessa käyttämällä gRPC-Web-asiakasohjelmaa. Erityisesti gRPC-Web tarjoaa kaikki gRPC:n edut, kuten tehokkuuden ja skaalautuvuuden, mutta se vaatii hieman enemmän konfigurointia ja lisäkomponentteja verrattuna gRPC JSON -transkoodaukseen.

gRPC JSON -transkoodaus tapahtuu palvelinpuolella, jolloin kaikki HTTP-pyynnöt muuntuvat automaattisesti gRPC-pyynnöiksi. GPRC-Web taas toimii asiakaspuolella, jolloin selain kommunikoi suoraan gRPC-palvelun kanssa käyttämällä Protobufia. Tämä erottaa nämä kaksi lähestymistapaa ja määrittää niiden käytön tarpeen mukaan.

Hyvä käytäntö on lisätä gRPC JSON -transkoodauksen tuki kaikkiin ASP.NET Core -palveluihin, jotka isännöivät gRPC-palveluja. Tämä ratkaisu mahdollistaa sen, että ne asiakkaat, jotka eivät voi käyttää gRPC:tä suoraan, voivat silti käyttää palveluja HTTP-rajapintojen kautta JSON-muodossa, kun taas gRPC:n natiivisti tukevat asiakkaat voivat kommunikoida suoraan gRPC:n avulla. Tämä lähestymistapa yhdistää molempien maailmojen hyödyt.

On tärkeää huomioida, että gRPC JSON -transkoodauksen avulla voidaan helposti laajentaa palveluiden saavutettavuutta web-asiakkaille, mutta on myös ratkaisevan tärkeää ymmärtää, kuinka se eroaa gRPC-Webistä ja milloin kumpikin tekniikka on paras valinta. GPRC-Web tarjoaa nopean ja tehokkaan ratkaisun, mutta se vaatii enemmän asiakaspuolen konfigurointia. JSON-transkoodaus puolestaan yksinkertaistaa asiakaspuolen toteutusta ja tuo gRPC-palvelut helposti web-asiakkaille ilman syvällisempää teknistä tietämystä.