diff --git a/apps/docs/docs/ref/csharp/release-notes.mdx b/apps/docs/docs/ref/csharp/release-notes.mdx index 2f30b2cbed8..92c8fb7b3d9 100644 --- a/apps/docs/docs/ref/csharp/release-notes.mdx +++ b/apps/docs/docs/ref/csharp/release-notes.mdx @@ -3,6 +3,39 @@ id: release-notes title: Release Notes --- +## 0.8.0 - 2023-01-31 + +- Update dependency: realtime-csharp@5.0.0 + - Re: [#21](https://github.com/supabase-community/realtime-csharp/pull/21) Provide API for `presence`, `broadcast` and `postgres_changes` + - [Major, New] `Channel.PostgresChanges` event will receive the wildcard `*` changes event, not `Channel.OnMessage`. + - [Major] `Channel.OnInsert`, `Channel.OnUpdate`, and `Channel.OnDelete` now conform to the server's payload of `Response.Payload.**Data**` + - [Major] `Channel.OnInsert`, `Channel.OnUpdate`, and `Channel.OnDelete` now return `PostgresChangesEventArgs` + - [Minor] Rename `Channel` to `RealtimeChannel` + - Supports better handling of disconnects in `RealtimeSocket` and adds a `Client.OnReconnect` event. + - [Minor] Moves `ChannelOptions` to `Channel.ChannelOptions` + - [Minor] Moves `ChannelStateChangedEventArgs` to `Channel.ChannelStateChangedEventArgs` + - [Minor] Moves `Push` to `Channel.Push` + - [Minor] Moves `Channel.ChannelState` to `Constants.ChannelState` + - [Minor] Moves `SocketResponse`, `SocketRequest`, `SocketResponsePayload`, `SocketResponseEventArgs`, and `SocketStateChangedEventArgs` to `Socket` namespace. + - [New] Adds `RealtimeBroadcast` + - [New] Adds `RealtimePresence` + - [Improvement] Better handling of disconnection/reconnection +- Update dependency: postgrest-csharp@3.1.3 + - Another fix for [#61](https://github.com/supabase-community/postgrest-csharp/issues/61) which futher typechecks nullable values. + +## 0.7.2 - 2023-01-27 + +- Update dependency: gotrue-csharp@3.0.4 + - Makes `Session.CreatedAt` a publicly settable property, which should fix incorrect dates on retrieved `Session`s. +- Update dependency: postgrest-csharp@3.1.2 + - Fix [#61](https://github.com/supabase-community/postgrest-csharp/issues/61) which did not correctly parse Linq `Where` when encountering a nullable type. + - Add missing support for transforming for `== null` and `!= null` + +## 0.7.1 - 2023-01-17 + +- Update dependency: postgrest-csharp@3.1.1 + - Fix issue from supabase-community/supabase-csharp#48 where boolean model properties would not be evaluated in predicate expressions + ## 0.7.0 - 2023-01-16 - Update dependency: postgrest-csharp@3.1.0 diff --git a/spec/supabase_csharp_v0.yml b/spec/supabase_csharp_v0.yml index 4291bdfbb95..4a9209afe6b 100644 --- a/spec/supabase_csharp_v0.yml +++ b/spec/supabase_csharp_v0.yml @@ -13,7 +13,7 @@ info: libraries: - name: 'C#' id: 'csharp' - version: '0.0.1' + version: '0.8.0' functions: - id: sign-up @@ -283,7 +283,8 @@ functions: Performs vertical filtering with SELECT. title: 'Fetch data: Select()' notes: | - - **Column Names must match names in database, not names specified on model properties.** + - **LINQ expressions do not currently support parsing embedded resource columns. For these cases, `string` will need to be used.** + - **When using string Column Names to select, they must match names in database, not names specified on model properties.** - By default, Supabase projects will return a maximum of 1,000 rows. This setting can be changed in Project API Settings. It's recommended that you keep it low to limit the payload size of accidental or malicious requests. You can use `range()` queries to paginate through your data. - `From()` can be combined with [Modifiers](/docs/reference/csharp/using-modifiers) - `From()` can be combined with [Filters](/docs/reference/csharp/using-filters) @@ -313,7 +314,7 @@ functions: code: | ```c# var data = await supabase - .From() + .From() .Select("id, supplier:supplier_id(name), purchaser:purchaser_id(name)") .Get(); ``` @@ -432,14 +433,14 @@ functions: - `Update()` is typically called using a model as an argument or from a hydrated model. examples: - id: updating-your-data-with-filter - name: Update your data + name: Update your data using Filter isSpotlight: true code: | ```c# var update = await supabase .From() .Where(x => x.Name == "Auckland") - .Set(x => new KeyValuePair(x.Name, "Middle Earth")) + .Set(x => x.Name, "Middle Earth") .Update(); ``` - id: updating-your-data @@ -556,13 +557,68 @@ functions: ``` - id: subscribe + title: 'Realtime.Channel' description: | Subscribe to realtime changes in your database. - title: 'From().On()' notes: | - Realtime is disabled by default for new Projects for better database performance and security. You can turn it on by [managing replication](/docs/guides/api#managing-realtime). - If you want to receive the "previous" data for updates and deletes, you will need to set `REPLICA IDENTITY` to `FULL`, like this: `ALTER TABLE your_table REPLICA IDENTITY FULL;` examples: + - id: listen-to-broadcast + name: Listen to broadcast messages + isSpotlight: true + code: | + ```c# + class CursorBroadcast : BaseBroadcast + { + [JsonProperty("cursorX")] + public int CursorX {get; set;} + + [JsonProperty("cursorY")] + public int CursorY {get; set;} + } + + var channel = supabase.Realtime.Channel("any"); + var broadcast = channel.Register(); + + broadcast.OnBroadcast += (sender, _) => + { + var data = broadcast.Current(); + }; + + await channel.Subscribe(); + + // Send a broadcast + await broadcast.Send("cursor", new CursorBroadcast { CursorX = 123, CursorY = 456 }); + ``` + - id: listen-to-presence-sync + name: Listen to presence sync + isSpotlight: true + code: | + ```c# + class UserPresence : BasePresence + { + [JsonProperty("cursorX")] + public bool IsTyping {get; set;} + + [JsonProperty("onlineAt")] + public DateTime OnlineAt {get; set;} + } + + var channel = supabase.Realtime.Channel("any"); + var presenceKey = Guid.NewGuid().ToString(); + var presence = channel.Register(presenceKey); + + presence.OnSync += (sender, _) => + { + var state = presence.CurrentState; + }; + + await channel.Subscribe(); + + // Send a presence update + await presence.Track(new UserPresence { IsTyping = false, OnlineAt = DateTime.Now }); + ``` - id: listening-to-a-specific-table name: Listening to a specific table isSpotlight: true @@ -570,7 +626,7 @@ functions: ```c# await supabase.From().On(ChannelEventType.All, (sender, args) => { - Debug.WriteLine(args.Response.Payload); + Debug.WriteLine(args.Response.Payload.Data); }); ``` - id: listen-to-all-database-changes @@ -579,9 +635,9 @@ functions: ```c# var channel = supabase.Realtime.Channel("realtime", "public", "*"); - channel.OnMessage += (sender, args) => + channel.OnPostgresChanges += (sender, args) => { - Debug.WriteLine(args.Response.Payload); + Debug.WriteLine(args.Response.Payload.Data); }; await channel.Subscribe(); @@ -592,7 +648,7 @@ functions: ```c# await supabase.From().On(ChannelEventType.Insert, (sender, args) => { - Debug.WriteLine(args.Response.Payload); + Debug.WriteLine(args.Response.Payload.Data); }); ``` - id: listening-to-updates @@ -608,7 +664,7 @@ functions: ```c# await supabase.From().On(ChannelEventType.Update, (sender, args) => { - Debug.WriteLine(args.Response.Payload); + Debug.WriteLine(args.Response.Payload.Data); }); ``` - id: listening-to-deletes @@ -624,7 +680,7 @@ functions: ```c# await supabase.From().On(ChannelEventType.Delete, (sender, args) => { - Debug.WriteLine(args.Response.Payload); + Debug.WriteLine(args.Response.Payload.Data); }); ``` - id: listening-to-row-level-changes @@ -676,93 +732,6 @@ functions: var channels = supabase.Realtime.Subscriptions; ``` - # - id: stream - # description: | - # Notifies of data at the queried table. - # title: 'stream()' - # notes: | - # - `stream()` will emit the initial data as well as any further change on the database as `Stream` of `List>` by combining Postgrest and Realtime. - # - Takes a list of primary key columns as its argument. - # examples: - # - id: listening-to-a-specific-table - # name: Listening to a specific table - # isSpotlight: true - # code: | - # ```dart - # supabase.from('countries') - # .stream(primaryKey: ['id']) - # .listen((List> data) { - # // Do something awesome with the data - # }); - # ``` - # - id: listening-to-a-specific-rows-within-a-table - # name: Listening to a specific rows within a table - # description: | - # You can listen to individual rows using the format `{table}:{col}=eq.{val}` - where `{col}` is the column name, and `{val}` is the value which you want to match. - # This syntax is the as how you can filter data in Realtime - # code: | - # ```dart - # supabase.from('countries') - # .stream(primaryKey: ['id']) - # .eq('id', '120') - # .listen((List> data) { - # // Do something awesome with the data - # }); - # ``` - # - id: with-order - # name: With `order()` - # code: | - # ```dart - # supabase.from('countries') - # .stream(primaryKey: ['id']) - # .order('name', ascending: true) - # .listen((List> data) { - # // Do something awesome with the data - # }); - # ``` - # - id: with-limit - # name: With `limit()` - # code: | - # ```dart - # supabase.from('countries') - # .stream(primaryKey: ['id']) - # .order('name', ascending: true) - # .limit(10) - # .listen((List> data) { - # // Do something awesome with the data - # }); - # ``` - # - id: using-stream-with-stream-builder - # name: Using `stream()` with `StreamBuilder` - # description: | - # When using `stream()` with a `StreamBuilder` within your Flutter application, make sure to store your stream in a variable to prevent refetching upon rebuilding. - # code: | - # ```dart - # final supabase = Supabase.instance.client; - - # class MyWidget extends StatefulWidget { - # const MyWidget({Key? key}) : super(key: key); - - # @override - # State createState() => _MyWidgetState(); - # } - - # class _MyWidgetState extends State { - # // Persist the stream in a local variable to prevent refetching upon rebuilds - # final _stream = supabase.from('countries').stream(primaryKey: ['id']); - - # @override - # Widget build(BuildContext context) { - # return StreamBuilder( - # stream: _stream, - # builder: (context, snapshot) { - # // Return your widget with the data from the snapshot - # }, - # ); - # } - # } - # ``` - - id: list-buckets description: | Retrieves the details of all Storage buckets within an existing product. @@ -1043,7 +1012,7 @@ functions: code: | ```c# var result = await supabase.From() - .Select("name, country_id") + .Select(x => new object[] { x.Name, x.CountryId }) .Limit(10) .Get(); ``` @@ -1069,8 +1038,8 @@ functions: code: | ```c# var result = await supabase.From() - .Select("name, country_id") - .Order("id", Ordering.Descending) + .Select(x => new object[] { x.Name, x.CountryId }) + .Order(x => x.Id, Ordering.Descending) .Get(); ``` - id: with-embedded-resources @@ -1079,7 +1048,7 @@ functions: ```c# var result = await supabase.From() .Select("name, cities(name)") - .Filter("name", Operator.Equals, "United States") + .Filter(x => x.Name == "United States") .Order("cities", "name", Ordering.Descending) .Get(); ``` @@ -1111,7 +1080,7 @@ functions: code: | ```c# var result = await supabase.From() - .Select("name, country_id") + .Select(x => new object[] { x.Name, x.CountryId }) .Offset(25) .Get(); ``` @@ -1127,7 +1096,7 @@ functions: code: | ```c# var result = await supabase.From() - .Select("name, country_id") + .Select(x => new object[] { x.Name, x.CountryId }) .Single(); ``` @@ -1138,19 +1107,15 @@ functions: Filters can be used on `Select()`, `Update()`, and `Delete()` queries. - If a Database function returns a table response, you can also apply filters. + **Note: LINQ expressions do not currently support parsing embedded resource columns. For these cases, `string` will need to be used.** examples: - id: applying-filters name: Applying Filters - description: | - Filters must be applied after any of `Select()`, `Update()`, `Upsert()`, - `Delete()`, and `rpc()` and before - [modifiers](/docs/reference/csharp/using-modifiers). code: | ```c# var result = await supabase.From() - .Select("name, country_id") - .Filter("name", Operator.Equals, "The Shire") + .Select(x => new object[] { x.Name, x.CountryId }) + .Where(x => x.Name == "The Shire") .Single(); ``` - id: filter-by-value-within-json-column @@ -1171,26 +1136,9 @@ functions: (1, 'Michael', '{ "postcode": 90210 }'), (2, 'Jane', null); ``` - response: | - ```json - { - "data": [ - { - "id": 1, - "name": "Michael", - "address": { - "postcode": 90210 - } - } - ], - "status": 200, - "statusText": "OK" - } - ``` code: | ```c# var result = await supabase.From() - .Select("name, country_id") .Filter("address->postcode", Operator.Equals, 90210) .Get(); ``` @@ -1203,46 +1151,7 @@ functions: .Filter("cities.name", Operator.Equals, "Bali") .Get(); ``` - data: - sql: | - ```sql - create table - countries (id int8 primary key, name text); - create table - cities ( - id int8 primary key, - country_id int8 not null references countries, - name text - ); - insert into - countries (id, name) - values - (1, 'Germany'), - (2, 'Indonesia'); - insert into - cities (id, country_id, name) - values - (1, 2, 'Bali'), - (2, 1, 'Munich'); - ``` - response: | - ```json - { - "data": [ - { - "name": "Indonesia", - "cities": [ - { - "name": "Bali" - } - ] - } - ], - "status": 200, - "statusText": "OK" - } - ``` - id: or title: Or() description: | @@ -1253,33 +1162,17 @@ functions: isSpotlight: true code: | ```c# - var filters = new List - { - new QueryFilter("id", Operator.Equals, 20), - new QueryFilter("id", Operator.Equals, 30) - }; - var result = await supabase.From() - .Or(filters) + .Where(x => x.Id == 20 || x.Id == 30) .Get(); ``` - id: use-or-with-and name: Use `or` with `and` code: | ```c# - var filters = new List - { - new QueryFilter("id", Operator.Equals, 20), - new QueryFilter("id", Operator.Equals, 30), - new QueryFilter(Operator.And, new List - { - new QueryFilter("name", Operator.Equals, "New Zealand"), - new QueryFilter("name", Operator.Equals, "France") - }) - }; - var result = await supabase.From() - .Or(filters) + .Where(x => x.Population > 300000 || x.BirthRate < 0.6) + .Where(x => x.Name != "Mordor") .Get(); ``` @@ -1294,8 +1187,8 @@ functions: code: | ```c# var result = await supabase.From() - .Select("id, name") - .Not("name", Operator.Equals, "Paris") + .Select(x => new object[] { x.Name, x.CountryId }) + .Where(x => x.Name != "Paris") .Get(); ``` @@ -1342,17 +1235,9 @@ functions: code: | ```c# var result = await supabase.From() - .Filter("name", Operator.Equals, "Bali") + .Where(x => x.Name == "Bali") .Get(); ``` - - id: with-delete - name: With `Delete()` - code: | - ```dart - await supabase.From() - .Filter("name", Operator.Equals, "Bali") - .Delete(); - ``` - id: neq title: Operator.NotEqual @@ -1365,18 +1250,10 @@ functions: code: | ```c# var result = await supabase.From() - .Select("name, country_id") - .Filter("name", Operator.NotEqual, "Bali") + .Select(x => new object[] { x.Name, x.CountryId }) + .Where(x => x.Name != "Bali") .Get(); ``` - - id: with-delete - name: With `Delete()` - code: | - ```c# - await supabase.From() - .Filter("name", Operator.NotEqual, "Bali") - .Delete(); - ``` - id: gt title: Operator.GreaterThan @@ -1389,18 +1266,10 @@ functions: code: | ```c# var result = await supabase.From() - .Select("name, country_id") - .Filter("country_id", Operator.GreaterThan, 250) + .Select(x => new object[] { x.Name, x.CountryId }) + .Where(x => x.CountryId > 250) .Get(); ``` - - id: with-delete - name: With `Delete()` - code: | - ```c# - await supabase.From() - .Filter("country_id", Operator.GreaterThan, 250) - .Delete(); - ``` - id: gte title: Operator.GreaterThanOrEqual @@ -1413,18 +1282,10 @@ functions: code: | ```c# var result = await supabase.From() - .Select("name, country_id") - .Filter("country_id", Operator.GreaterThanOrEqual, 250) + .Select(x => new object[] { x.Name, x.CountryId }) + .Where(x => x.CountryId >= 250) .Get(); ``` - - id: with-delete - name: With `Delete()` - code: | - ```c# - await supabase.From() - .Filter("country_id", Operator.GreaterThanOrEqual, 250) - .Delete(); - ``` - id: lt title: Operator.LessThan @@ -1438,23 +1299,14 @@ functions: ```c# var result = await supabase.From() .Select("name, country_id") - .Filter("country_id", Operator.LessThan, 250) + .Where(x => x.CountryId < 250) .Get(); ``` - - id: with-delete - name: With `Delete()` - code: | - ```c# - await supabase.From() - .Filter("country_id", Operator.LessThan, 250) - .Delete(); - ``` - id: lte title: Operator.LessThanOrEqual description: | Finds all rows whose value on the stated `column` is less than or equal to the specified `value`. - $ref: '@supabase/postgrest-js.PostgrestFilterBuilder.lte' examples: - id: with-select name: With `Select()` @@ -1462,18 +1314,9 @@ functions: code: | ```c# var result = await supabase.From() - .Select("name, country_id") - .Filter("country_id", Operator.LessThanOrEqual, 250) + .Where(x => x.CountryId <= 250) .Get(); ``` - - id: with-delete - name: With `Delete()` - code: | - ```c# - await supabase.From() - .Filter("country_id", Operator.LessThanOrEqual, 250) - .Delete(); - ``` - id: like title: Operator.Like @@ -1486,18 +1329,9 @@ functions: code: | ```c# var result = await supabase.From() - .Select("name, country_id") - .Filter("name", Operator.Like, "%la%") + .Filter(x => x.Name, Operator.Like, "%la%") .Get(); ``` - - id: with-delete - name: With `Delete()` - code: | - ```c# - await supabase.From() - .Filter("name", Operator.Like, "%la%") - .Delete(); - ``` - id: ilike title: Operator.ILike @@ -1510,16 +1344,8 @@ functions: code: | ```c# await supabase.From() - .Filter("name", Operator.ILike, "%la%") - .Delete(); - ``` - - id: with-delete - name: With `Delete()` - code: | - ```c# - await supabase.From() - .Filter("name", Operator.ILike, "%la%") - .Delete(); + .Filter(x => x.Name, Operator.ILike, "%la%") + .Get(); ``` - id: is @@ -1533,18 +1359,9 @@ functions: code: | ```c# var result = await supabase.From() - .Select("name, country_id") - .Filter("name", Operator.Is, null) + .Where(x => x.Name == null .Get(); ``` - - id: with-delete - name: With `Delete()` - code: | - ```c# - await supabase.From() - .Filter("name", Operator.Is, null) - .Delete(); - ``` - id: in title: Operator.In @@ -1557,18 +1374,9 @@ functions: code: | ```c# var result = await supabase.From() - .Select("name, country_id") - .Filter("name", Operator.In, new List { "Rio de Janiero", "San Francisco" }) + .Filter(x => x.Name, Operator.In, new List { "Rio de Janiero", "San Francisco" }) .Get(); ``` - - id: with-delete - name: With `Delete()` - code: | - ```c# - await supabase.From() - .Filter("name", Operator.In, new List { "Rio de Janiero", "San Francisco" }) - .Delete(); - ``` - id: contains title: Operator.Contains @@ -1579,18 +1387,9 @@ functions: code: | ```c# var result = await supabase.From() - .Select("name, country_id, main_exports") - .Filter("main_exports", Operator.Contains, new List { "oil", "fish" }) + .Filter(x => x.MainExports, Operator.Contains, new List { "oil", "fish" }) .Get(); ``` - - id: with-delete - name: With `Delete()` - code: | - ```c# - await supabase.From() - .Filter("main_exports", Operator.Contains, new List { "oil", "fish" }) - .Delete(); - ``` - id: contained-by title: Operator.ContainedIn @@ -1601,18 +1400,9 @@ functions: code: | ```c# var result = await supabase.From() - .Select("name, country_id, main_exports") - .Filter("main_exports", Operator.ContainedIn, new List { "oil", "fish" }) + .Filter(x => x.MainExports, Operator.ContainedIn, new List { "oil", "fish" }) .Get(); ``` - - id: with-delete - name: With `Delete()` - code: | - ```c# - await supabase.From() - .Filter("main_exports", Operator.ContainedIn, new List { "oil", "fish" }) - .Delete(); - ``` - id: text-search title: Operator.[FTS,PLFTS,PHFTS,WFTS] (Full Text Search) @@ -1624,8 +1414,8 @@ functions: code: | ```c# var result = await supabase.From() - .Select("catchphrase") - .Filter("catchphrase", Operator.FTS, new FullTextSearchConfig("'fat' & 'cat", "english")) + .Select(x => x.Catchphrase) + .Filter(x => x.Catchphrase, Operator.FTS, new FullTextSearchConfig("'fat' & 'cat", "english")) .Get(); ``` - id: basic-normalization @@ -1634,8 +1424,8 @@ functions: code: | ```c# var result = await supabase.From() - .Select("catchphrase") - .Filter("catchphrase", Operator.PLFTS, new FullTextSearchConfig("'fat' & 'cat", "english")) + .Select(x => x.Catchphrase) + .Filter(x => x.Catchphrase, Operator.PLFTS, new FullTextSearchConfig("'fat' & 'cat", "english")) .Get(); ``` - id: full-normalization @@ -1644,8 +1434,8 @@ functions: code: | ```c# var result = await supabase.From() - .Select("catchphrase") - .Filter("catchphrase", Operator.PHFTS, new FullTextSearchConfig("'fat' & 'cat", "english")) + .Select(x => x.Catchphrase) + .Filter(x => x.Catchphrase, Operator.PHFTS, new FullTextSearchConfig("'fat' & 'cat", "english")) .Get(); ``` - id: web-search @@ -1663,7 +1453,7 @@ functions: code: | ```c# var result = await supabase.From() - .Select("catchphrase") - .Filter("catchphrase", Operator.WFTS, new FullTextSearchConfig("'fat' & 'cat", "english")) + .Select(x => x.Catchphrase) + .Filter(x => x.Catchphrase, Operator.WFTS, new FullTextSearchConfig("'fat' & 'cat", "english")) .Get(); ```