Skip to content

Commit 6b560f9

Browse files
committed
Use best-practices for Exception classes
Fixes #12285 Related #15214 It turns out that the code causing #15214 to happen is not due to normal serialization, and hence it is likely not possible to fix this in EF code. (See comments on the issue.) However, the issue (or similar) would still exist if the exception was serialized, and the fix is to use best practices for exceptions, so doing that. For reference, see https://docs.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions
1 parent 480bf51 commit 6b560f9

File tree

7 files changed

+393
-5
lines changed

7 files changed

+393
-5
lines changed

src/EFCore.Design/Design/OperationException.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,24 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Runtime.Serialization;
56
using JetBrains.Annotations;
67

78
namespace Microsoft.EntityFrameworkCore.Design
89
{
910
/// <summary>
1011
/// Represents an exception whose stack trace should, by default, not be reported by the commands.
1112
/// </summary>
13+
[Serializable]
1214
public class OperationException : Exception
1315
{
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="OperationException" /> class.
18+
/// </summary>
19+
public OperationException()
20+
{
21+
}
22+
1423
/// <summary>
1524
/// Initializes a new instance of the <see cref="OperationException" /> class.
1625
/// </summary>
@@ -29,5 +38,15 @@ public OperationException([NotNull] string message, [CanBeNull] Exception innerE
2938
: base(message, innerException)
3039
{
3140
}
41+
42+
/// <summary>
43+
/// Initializes a new instance of the <see cref="DbUpdateException" /> class from a serialized form.
44+
/// </summary>
45+
/// <param name="info"> The serialization info. </param>
46+
/// <param name="context"> The streaming context being used. </param>
47+
public OperationException(SerializationInfo info, StreamingContext context)
48+
: base(info, context)
49+
{
50+
}
3251
}
3352
}

src/EFCore/DbUpdateConcurrencyException.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
45
using System.Collections.Generic;
6+
using System.Runtime.Serialization;
57
using JetBrains.Annotations;
68
using Microsoft.EntityFrameworkCore.Update;
79

@@ -12,8 +14,49 @@ namespace Microsoft.EntityFrameworkCore
1214
/// occurs when an unexpected number of rows are affected during save. This is usually because the data in the database has
1315
/// been modified since it was loaded into memory.
1416
/// </summary>
17+
[Serializable]
1518
public class DbUpdateConcurrencyException : DbUpdateException
1619
{
20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="DbUpdateConcurrencyException" /> class.
22+
/// </summary>
23+
public DbUpdateConcurrencyException()
24+
{
25+
}
26+
27+
/// <summary>
28+
/// Initializes a new instance of the <see cref="DbUpdateConcurrencyException" /> class.
29+
/// </summary>
30+
/// <param name="message"> The error message that explains the reason for the exception. </param>
31+
public DbUpdateConcurrencyException([NotNull] string message)
32+
: base(message)
33+
{
34+
}
35+
36+
/// <summary>
37+
/// Initializes a new instance of the <see cref="DbUpdateConcurrencyException" /> class.
38+
/// </summary>
39+
/// <param name="message"> The error message that explains the reason for the exception. </param>
40+
/// <param name="innerException"> The exception that is the cause of the current exception. </param>
41+
public DbUpdateConcurrencyException([NotNull] string message, [CanBeNull] Exception innerException)
42+
: base(message, innerException)
43+
{
44+
}
45+
46+
/// <summary>
47+
/// Initializes a new instance of the <see cref="DbUpdateConcurrencyException" /> class.
48+
/// </summary>
49+
/// <param name="message"> The error message that explains the reason for the exception. </param>
50+
/// <param name="innerException"> The exception that is the cause of the current exception. </param>
51+
/// <param name="entries"> The entries that were involved in the error. </param>
52+
public DbUpdateConcurrencyException(
53+
[NotNull] string message,
54+
[CanBeNull] Exception innerException,
55+
[NotNull] IReadOnlyList<IUpdateEntry> entries)
56+
: base(message, innerException, entries)
57+
{
58+
}
59+
1760
/// <summary>
1861
/// Initializes a new instance of the <see cref="DbUpdateConcurrencyException" /> class.
1962
/// </summary>
@@ -25,5 +68,15 @@ public DbUpdateConcurrencyException(
2568
: base(message, entries)
2669
{
2770
}
71+
72+
/// <summary>
73+
/// Initializes a new instance of the <see cref="DbUpdateException" /> class from a serialized form.
74+
/// </summary>
75+
/// <param name="info"> The serialization info. </param>
76+
/// <param name="context"> The streaming context being used. </param>
77+
public DbUpdateConcurrencyException(SerializationInfo info, StreamingContext context)
78+
: base(info, context)
79+
{
80+
}
2881
}
2982
}

src/EFCore/DbUpdateException.cs

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Runtime.Serialization;
78
using JetBrains.Annotations;
89
using Microsoft.EntityFrameworkCore.ChangeTracking;
9-
using Microsoft.EntityFrameworkCore.Internal;
1010
using Microsoft.EntityFrameworkCore.Update;
1111
using Microsoft.EntityFrameworkCore.Utilities;
1212

@@ -15,8 +15,28 @@ namespace Microsoft.EntityFrameworkCore
1515
/// <summary>
1616
/// An exception that is thrown when an error is encountered while saving to the database.
1717
/// </summary>
18+
[Serializable]
1819
public class DbUpdateException : Exception
1920
{
21+
[NonSerialized]
22+
private IReadOnlyList<EntityEntry> _entries;
23+
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="DbUpdateException" /> class.
26+
/// </summary>
27+
public DbUpdateException()
28+
{
29+
}
30+
31+
/// <summary>
32+
/// Initializes a new instance of the <see cref="DbUpdateException" /> class.
33+
/// </summary>
34+
/// <param name="message"> The error message that explains the reason for the exception. </param>
35+
public DbUpdateException([NotNull] string message)
36+
: base(message)
37+
{
38+
}
39+
2040
/// <summary>
2141
/// Initializes a new instance of the <see cref="DbUpdateException" /> class.
2242
/// </summary>
@@ -25,7 +45,6 @@ public class DbUpdateException : Exception
2545
public DbUpdateException([NotNull] string message, [CanBeNull] Exception innerException)
2646
: base(message, innerException)
2747
{
28-
Entries = new List<EntityEntry>();
2948
}
3049

3150
/// <summary>
@@ -44,8 +63,8 @@ public DbUpdateException(
4463
/// Initializes a new instance of the <see cref="DbUpdateException" /> class.
4564
/// </summary>
4665
/// <param name="message"> The error message that explains the reason for the exception. </param>
47-
/// <param name="entries"> The entries that were involved in the error. </param>
4866
/// <param name="innerException"> The exception that is the cause of the current exception. </param>
67+
/// <param name="entries"> The entries that were involved in the error. </param>
4968
public DbUpdateException(
5069
[NotNull] string message,
5170
[CanBeNull] Exception innerException,
@@ -54,15 +73,25 @@ public DbUpdateException(
5473
{
5574
Check.NotEmpty(entries, nameof(entries));
5675

57-
Entries = entries
76+
_entries = entries
5877
.Where(e => e.EntityState != EntityState.Unchanged)
5978
.Select(e => e.ToEntityEntry()).ToList();
6079
}
6180

81+
/// <summary>
82+
/// Initializes a new instance of the <see cref="DbUpdateException" /> class from a serialized form.
83+
/// </summary>
84+
/// <param name="info"> The serialization info. </param>
85+
/// <param name="context"> The streaming context being used. </param>
86+
public DbUpdateException(SerializationInfo info, StreamingContext context)
87+
: base(info, context)
88+
{
89+
}
90+
6291
/// <summary>
6392
/// Gets the entries that were involved in the error. Typically this is a single entry, but in some cases it
6493
/// may be zero or multiple entries.
6594
/// </summary>
66-
public virtual IReadOnlyList<EntityEntry> Entries { get; }
95+
public virtual IReadOnlyList<EntityEntry> Entries => _entries ??= new List<EntityEntry>();
6796
}
6897
}

src/EFCore/Storage/RetryLimitExceededException.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,24 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Runtime.Serialization;
56
using JetBrains.Annotations;
67

78
namespace Microsoft.EntityFrameworkCore.Storage
89
{
910
/// <summary>
1011
/// The exception that is thrown when the action failed more times than the configured limit.
1112
/// </summary>
13+
[Serializable]
1214
public class RetryLimitExceededException : Exception
1315
{
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="RetryLimitExceededException" /> class.
18+
/// </summary>
19+
public RetryLimitExceededException()
20+
{
21+
}
22+
1423
/// <summary>
1524
/// Initializes a new instance of the <see cref="RetryLimitExceededException" /> class with a specified error message.
1625
/// </summary>
@@ -29,5 +38,15 @@ public RetryLimitExceededException([NotNull] string message, [NotNull] Exception
2938
: base(message, innerException)
3039
{
3140
}
41+
42+
/// <summary>
43+
/// Initializes a new instance of the <see cref="DbUpdateException" /> class from a serialized form.
44+
/// </summary>
45+
/// <param name="info"> The serialization info. </param>
46+
/// <param name="context"> The streaming context being used. </param>
47+
public RetryLimitExceededException(SerializationInfo info, StreamingContext context)
48+
: base(info, context)
49+
{
50+
}
3251
}
3352
}

test/EFCore.Design.Tests/ApiConsistencyTest.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Reflection;
7+
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
78
using Microsoft.EntityFrameworkCore.Design;
9+
using Microsoft.EntityFrameworkCore.Metadata;
10+
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
11+
using Microsoft.EntityFrameworkCore.TestUtilities;
12+
using Microsoft.EntityFrameworkCore.Update;
813
using Microsoft.Extensions.DependencyInjection;
914

1015
namespace Microsoft.EntityFrameworkCore
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.IO;
6+
using System.Runtime.Serialization.Formatters.Binary;
7+
using Microsoft.EntityFrameworkCore.Design;
8+
using Xunit;
9+
10+
namespace Microsoft.EntityFrameworkCore
11+
{
12+
public class DesignExceptionTest
13+
{
14+
[Fact]
15+
public void OperationException_exposes_public_empty_constructor()
16+
{
17+
new OperationException();
18+
}
19+
20+
[Fact]
21+
public void OperationException_exposes_public_string_constructor()
22+
{
23+
Assert.Equal("Foo", new OperationException("Foo").Message);
24+
}
25+
26+
[Fact]
27+
public void OperationException_exposes_public_string_and_inner_exception_constructor()
28+
{
29+
var inner = new Exception();
30+
31+
var ex = new OperationException("Foo", inner);
32+
33+
Assert.Equal("Foo", ex.Message);
34+
Assert.Same(inner, ex.InnerException);
35+
}
36+
37+
[Fact]
38+
public void Deserialized_OperationException_can_be_serialized_and_deserialized_again()
39+
{
40+
var transportedException = SerializeAndDeserialize(
41+
SerializeAndDeserialize(
42+
new OperationException(
43+
"But somehow the vital connection is made",
44+
new Exception("Bang!"))));
45+
46+
Assert.Equal("But somehow the vital connection is made", transportedException.Message);
47+
Assert.Equal("Bang!", transportedException.InnerException.Message);
48+
}
49+
50+
private TException SerializeAndDeserialize<TException>(TException exception) where TException : Exception
51+
{
52+
var stream = new MemoryStream();
53+
var formatter = new BinaryFormatter();
54+
55+
formatter.Serialize(stream, exception);
56+
stream.Seek(0, SeekOrigin.Begin);
57+
58+
return (TException)formatter.Deserialize(stream);
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)