@@ -178,8 +178,137 @@ public override async Task<Response> AddEntityAsync<T>(T entity, CancellationTok
178178
179179 public override Response UpsertEntity < T > ( T entity , TableUpdateMode mode = TableUpdateMode . Merge , CancellationToken cancellationToken = default )
180180 {
181+ if ( entity == null )
182+ throw new ArgumentNullException ( "entity" )
183+ {
184+ Source = "Azure.Data.Tables"
185+ } ;
186+
187+ if ( entity . PartitionKey == null )
188+ throw new ArgumentNullException ( "PartitionKey" )
189+ {
190+ Source = "Azure.Data.Tables"
191+ } ;
192+ if ( entity . RowKey == null )
193+ throw new ArgumentNullException ( "RowKey" )
194+ {
195+ Source = "Azure.Data.Tables"
196+ } ;
197+ if ( ! Enum . IsDefined ( mode ) )
198+ throw new ArgumentException ( $ "Unexpected value for mode: { mode } ")
199+ {
200+ Source = "Azure.Data.Tables"
201+ } ;
202+
181203 cancellationToken . ThrowIfCancellationRequested ( ) ;
182- throw new NotImplementedException ( ) ;
204+
205+ if ( entity . PartitionKey . Contains ( ( char ) 0 ) || entity . RowKey . Contains ( ( char ) 0 ) )
206+ throw TableStubResponseFactory . InvalidUriException (
207+ HttpStatusCode . BadRequest ,
208+ "<!DOCTYPE HTML PUBLIC \" -//W3C//DTD HTML 4.01//EN\" \" http://www.w3.org/TR/html4/strict.dtd\" >\r \n <HTML><HEAD><TITLE>Bad Request</TITLE>\r \n <META HTTP-EQUIV=\" Content-Type\" Content=\" text/html; charset=us-ascii\" ></HEAD>\r \n <BODY><h2>Bad Request - Invalid URL</h2>\r \n <hr><p>HTTP Error 400. The request URL is invalid.</p>\r \n </BODY></HTML>\r \n " ,
209+ new InvlaidUrlResponseHeaders
210+ {
211+ { "Connection" , "close" } ,
212+ { "Content-Length" , "324" }
213+ }
214+ ) ;
215+
216+ if ( entity . PartitionKey . Contains ( '/' ) || entity . PartitionKey . Contains ( '\\ ' ) || entity . RowKey . Contains ( '/' ) || entity . RowKey . Contains ( '\\ ' ) )
217+ throw TableStubResponseFactory . JsonRequestFailedException (
218+ HttpStatusCode . BadRequest ,
219+ "InvalidInput" ,
220+ "Bad Request - Error in query syntax." ,
221+ new DefaultResponseHeaders ( headers => headers . Remove ( "Cache-Control" ) )
222+ ) ;
223+
224+ if ( entity . PartitionKey . Any ( _IsInvalidUriCharacter ) || entity . RowKey . Any ( _IsInvalidUriCharacter ) )
225+ throw TableStubResponseFactory . InvalidUriException (
226+ HttpStatusCode . BadRequest ,
227+ "<!DOCTYPE HTML PUBLIC \" -//W3C//DTD HTML 4.01//EN\" \" http://www.w3.org/TR/html4/strict.dtd\" ><HTML><HEAD><TITLE>Bad Request</TITLE><META HTTP-EQUIV=\" Content-Type\" Content=\" text/html; charset=us-ascii\" ></HEAD><BODY><h2>Bad Request - Invalid URL</h2><hr><p>HTTP Error 400. The request URL is invalid.</p></BODY></HTML>" ,
228+ new InvlaidUrlResponseHeaders
229+ {
230+ { "Content-Length" , "312" }
231+ }
232+ ) ;
233+
234+ if ( entity . PartitionKey . Any ( TableRowStub . IsReservedKeyCharacter ) || entity . RowKey . Any ( TableRowStub . IsReservedKeyCharacter ) )
235+ throw TableStubResponseFactory . JsonRequestFailedException (
236+ HttpStatusCode . BadRequest ,
237+ "OutOfRangeInput" ,
238+ "One of the request inputs is out of range."
239+ ) ;
240+
241+ var mappedEntity = new ValidatedTableRowStub < T > ( entity ) ;
242+ if ( mappedEntity . NotSupportedDateTimeValue != null )
243+ throw new NotSupportedException ( $ "DateTime { mappedEntity . NotSupportedDateTimeValue } has a Kind of { mappedEntity . NotSupportedDateTimeValue ? . Kind } . Azure SDK requires it to be UTC. You can call DateTime.SpecifyKind to change Kind property value to DateTimeKind.Utc.")
244+ {
245+ Source = "Azure.Data.Tables"
246+ } ;
247+
248+ cancellationToken . ThrowIfCancellationRequested ( ) ;
249+
250+ if ( mappedEntity . IsPartitionKeyExceedingMaxLength || mappedEntity . IsRowKeyExceedingMaxLength || mappedEntity . IsStringPropertyExceedingMaxLength || mappedEntity . IsBinaryPropertyExceedingMaxLength )
251+ throw TableStubResponseFactory . JsonRequestFailedException (
252+ HttpStatusCode . BadRequest ,
253+ "PropertyValueTooLarge" ,
254+ "The property value exceeds the maximum allowed size (64KB). If the property value is a string, it is UTF-16 encoded and the maximum number of characters should be 32K or less." ,
255+ new DefaultResponseHeaders ( )
256+ ) ;
257+ if ( mappedEntity . InvalidDateTimeProperty != null )
258+ throw TableStubResponseFactory . JsonRequestFailedException (
259+ HttpStatusCode . BadRequest ,
260+ "OutOfRangeInput" ,
261+ $ "The '{ mappedEntity . InvalidDateTimeProperty ? . Key } ' parameter of value '{ mappedEntity . InvalidDateTimeProperty ? . Value : MM/dd/yyyy HH:mm:ss} ' is out of range.",
262+ new DefaultResponseHeaders ( headers => headers . Remove ( "Cache-Control" ) )
263+ ) ;
264+
265+ using ( _tableServiceClientStub . Tables . ReadLock ( ) )
266+ {
267+ if ( ! _tableServiceClientStub . Tables . TryGetValue ( _tableName , out var tableItem ) )
268+ throw TableStubResponseFactory . JsonRequestFailedException (
269+ HttpStatusCode . NotFound ,
270+ "TableNotFound" ,
271+ "The table specified does not exist." ,
272+ new DefaultResponseHeaders ( headers => headers . Remove ( "Cache-Control" ) )
273+ ) ;
274+
275+ using ( tableItem . WriteLock ( ) )
276+ {
277+ if ( ! tableItem . TryGetValue ( entity . PartitionKey , out var tablePartition ) )
278+ {
279+ tablePartition = new TablePartitionStub ( ) ;
280+ tableItem . Add ( entity . PartitionKey , tablePartition ) ;
281+ }
282+
283+ if ( ! tablePartition . TryGetValue ( entity . RowKey , out var existingEntity ) )
284+ tablePartition . Add ( entity . RowKey , mappedEntity ) ;
285+ else
286+ switch ( mode )
287+ {
288+ case TableUpdateMode . Merge :
289+ foreach ( var existingEntityProperty in existingEntity )
290+ if ( ! mappedEntity . ContainsKey ( existingEntityProperty . Key ) )
291+ mappedEntity . Add ( existingEntityProperty . Key , existingEntityProperty . Value ) ;
292+
293+ tablePartition [ entity . RowKey ] = mappedEntity ;
294+ break ;
295+
296+ case TableUpdateMode . Replace :
297+ tablePartition [ entity . RowKey ] = mappedEntity ;
298+ break ;
299+
300+ default :
301+ throw new InvalidOperationException ( $ "Unhandled '{ mode } ' mode.") ;
302+ }
303+ }
304+ }
305+
306+ return TableStubResponseFactory . NoContentResponse (
307+ new NoContentResponseHeaders ( )
308+ {
309+ { "ETag" , mappedEntity . ETag }
310+ }
311+ ) ;
183312 }
184313
185314 public override async Task < Response > UpsertEntityAsync < T > ( T entity , TableUpdateMode mode = TableUpdateMode . Merge , CancellationToken cancellationToken = default )
@@ -211,7 +340,11 @@ public override Response UpdateEntity<T>(T entity, ETag ifMatch, TableUpdateMode
211340 {
212341 Source = "Azure.Data.Tables"
213342 } ;
214-
343+ if ( ! Enum . IsDefined ( mode ) )
344+ throw new ArgumentException ( $ "Unexpected value for mode: { mode } ")
345+ {
346+ Source = "Azure.Data.Tables"
347+ } ;
215348
216349 cancellationToken . ThrowIfCancellationRequested ( ) ;
217350
@@ -318,7 +451,7 @@ public override Response UpdateEntity<T>(T entity, ETag ifMatch, TableUpdateMode
318451 break ;
319452
320453 default :
321- throw new NotImplementedException ( $ "Unhandled '{ mode } ' table update mode.") ;
454+ throw new InvalidOperationException ( $ "Unhandled '{ mode } ' mode.") ;
322455 }
323456 }
324457 }
0 commit comments